feat(llm): Added Anthropic Claude provider

This commit is contained in:
2026-07-16 13:21:00 +05:30
parent d6ab076b09
commit 622fdfe2f1
9 changed files with 300 additions and 7 deletions

View File

@@ -258,6 +258,27 @@ When no active row is found for the requested type:
Also exports `GeminiEmbeddingProvider` (implements `IEmbeddingProvider`) using the same key resolution pattern but querying for the `"embedding"` type.
### Anthropic Claude — `AnthropicProvider`
| Property | Value |
| --------------------------- | ------------------------------------------------------ |
| **File** | [`providers/anthropic.ts`](src/providers/anthropic.ts) |
| **Provider ID** | `anthropic` |
| **SDK** | `@langchain/anthropic` (`ChatAnthropic`) |
| **Default Model** | `claude-3-5-sonnet-latest` |
| **Default Embedding Model** | _(none)_ |
| **Default Max Context** | `200000` |
| **Type** | Generative only (no embedding provider) |
**Key resolution** in the constructor follows this cascade:
```
1. Explicit apiKey argument → use it
2. ProviderManager.getActive() → if providerName matches "anthropic"
3. ANTHROPIC_API_KEY env var → final fallback
4. None found → throw Error
```
### OpenRouter — `OpenRouterProvider`
| Property | Value |
@@ -284,21 +305,21 @@ Same three-step key resolution as Gemini (`explicit → ProviderManager → env
| **Default Max Context** | `32768` |
| **Type** | Generative + Embedding |
Ollama runs **locally** — no API key is required. The `apiKey` field in `ModelProviderInstance` is repurposed to store the Ollama server base URL (default: `http://localhost:11434`).
Ollama runs **locally** — no API key is required. The `endpointUrl` field in `ModelProviderInstance` stores the Ollama server base URL (default: `http://localhost:11434`).
**Key resolution** in the constructor:
```
1. Explicit baseUrl argument → use it
2. ProviderManager.getActive() → if providerName matches "ollama"
(apiKey field = base URL)
(endpointUrl field = base URL)
3. Default → http://localhost:11434
```
Also exports `OllamaEmbeddingProvider` (implements `IEmbeddingProvider`), which uses the same resolution pattern against the `"embedding"` type instance. The default embedding model is `nomic-embed-text`.
> [!TIP]
> To get started: `ollama pull llama3.1` and `ollama pull nomic-embed-text`. Then create a provider instance with `apiKey` = `http://localhost:11434`.
> To get started: `ollama pull llama3.1` and `ollama pull nomic-embed-text`. Then create a provider instance with `endpointUrl` = `http://localhost:11434`.
### Mock — `MockLLMProvider`
@@ -358,6 +379,7 @@ The `buildLLMProvider()` and `buildEmbeddingProvider()` functions perform the fi
| `"google-genai"` | `GeminiProvider` | `GeminiEmbeddingProvider` |
| `"openrouter"` | `OpenRouterProvider` | _(falls through to mock)_ |
| `"ollama"` | `OllamaProvider` | `OllamaEmbeddingProvider` |
| `"anthropic"` | `AnthropicProvider` | _(falls through to mock)_ |
| _(anything else)_ | `MockLLMProvider` | `MockEmbeddingProvider` |
## Structured Output
@@ -380,10 +402,11 @@ This sends the Zod schema to the model as a structured output constraint. The re
[`config.ts`](src/config.ts) parses environment variables using Zod:
| Variable | Required | Description |
| -------------------- | -------- | --------------------- |
| `GOOGLE_API_KEY` | No | Google Gemini API key |
| `OPENROUTER_API_KEY` | No | OpenRouter API key |
| Variable | Required | Description |
| -------------------- | -------- | ------------------------ |
| `GOOGLE_API_KEY` | No | Google Gemini API key |
| `OPENROUTER_API_KEY` | No | OpenRouter API key |
| `ANTHROPIC_API_KEY` | No | Anthropic Claude API key |
Both are optional because providers can also be configured through the database via the GUI settings page.
@@ -400,6 +423,7 @@ packages/llm/
│ ├── google-genai.ts # GeminiProvider + GeminiEmbeddingProvider
│ ├── ollama.ts # OllamaProvider + OllamaEmbeddingProvider
│ ├── openrouter.ts # OpenRouterProvider
│ ├── anthropic.ts # AnthropicProvider
│ └── mock.ts # MockLLMProvider + MockEmbeddingProvider
├── tests/
│ ├── mock.test.ts

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
const LLMConfigSchema = z.object({
GOOGLE_API_KEY: z.string().optional(),
OPENROUTER_API_KEY: z.string().optional(),
ANTHROPIC_API_KEY: z.string().optional(),
});
export const llmConfig = LLMConfigSchema.parse(process.env);

View File

@@ -4,4 +4,5 @@ export * from "./providers/google-genai.js";
export * from "./providers/mock.js";
export * from "./providers/ollama.js";
export * from "./providers/openrouter.js";
export * from "./providers/anthropic.js";
export * from "./provider-manager.js";

View File

@@ -76,6 +76,13 @@ export const AVAILABLE_PROVIDERS: ModelProviderMeta[] = [
defaultModel: "gemini-2.5-flash",
defaultEmbeddingModel: "gemini-embedding-001",
},
{
id: "anthropic",
displayName: "Anthropic Claude",
description: "Official Claude integration using @langchain/anthropic SDK",
defaultModel: "claude-3-5-sonnet-latest",
defaultEmbeddingModel: "",
},
{
id: "openrouter",
displayName: "OpenRouter",

View File

@@ -99,6 +99,7 @@ function getSettingsDb() {
if (totalCount.count === 0) {
const googleKey = process.env.GOOGLE_API_KEY;
const openRouterKey = process.env.OPENROUTER_API_KEY;
const anthropicKey = process.env.ANTHROPIC_API_KEY;
let hasInsertedGenerative = false;
if (googleKey && googleKey.trim()) {
@@ -138,6 +139,29 @@ function getSettingsDb() {
);
}
if (anthropicKey && anthropicKey.trim()) {
const id = "provider-default-anthropic";
const isActive = hasInsertedGenerative ? 0 : 1;
db.prepare(
`
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
id,
"Anthropic (Env)",
"anthropic",
anthropicKey.trim(),
isActive,
"claude-3-5-sonnet-latest",
"generative",
200000,
);
if (isActive === 1) {
hasInsertedGenerative = true;
}
}
if (openRouterKey && openRouterKey.trim()) {
const id = "provider-default-openrouter";
const isActive = hasInsertedGenerative ? 0 : 1;
@@ -391,6 +415,7 @@ export class ProviderManager {
if (totalCount.count === 0) {
const googleKey = process.env.GOOGLE_API_KEY;
const openRouterKey = process.env.OPENROUTER_API_KEY;
const anthropicKey = process.env.ANTHROPIC_API_KEY;
let hasInsertedGenerative = false;
if (googleKey && googleKey.trim()) {
@@ -430,6 +455,29 @@ export class ProviderManager {
);
}
if (anthropicKey && anthropicKey.trim()) {
const id = "provider-default-anthropic";
const isActive = hasInsertedGenerative ? 0 : 1;
db.prepare(
`
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
id,
"Anthropic (Env)",
"anthropic",
anthropicKey.trim(),
isActive,
"claude-3-5-sonnet-latest",
"generative",
200000,
);
if (isActive === 1) {
hasInsertedGenerative = true;
}
}
if (openRouterKey && openRouterKey.trim()) {
const id = "provider-default-openrouter";
const isActive = hasInsertedGenerative ? 0 : 1;
@@ -576,6 +624,19 @@ export class ProviderManager {
maxContext: 32768,
};
}
const anthropicKey = process.env.ANTHROPIC_API_KEY;
if (anthropicKey && anthropicKey.trim()) {
return {
id: "provider-default-env-fallback",
name: "Anthropic (Env Fallback)",
providerName: "anthropic",
apiKey: anthropicKey.trim(),
isActive: true,
modelName: "claude-3-5-sonnet-latest",
type: "generative",
maxContext: 200000,
};
}
const openRouterKey = process.env.OPENROUTER_API_KEY;
if (openRouterKey && openRouterKey.trim()) {
return {

View File

@@ -0,0 +1,114 @@
import { z } from "zod";
import { ChatAnthropic } from "@langchain/anthropic";
import {
ILLMProvider,
LLMRequest,
LLMResponse,
LLMCallRecord,
} from "../llm.js";
import { llmConfig } from "../config.js";
import { ProviderManager } from "../provider-manager.js";
export class AnthropicProvider implements ILLMProvider {
static readonly providerId = "anthropic";
static readonly displayName = "Anthropic Claude";
static readonly description =
"Official Claude integration using @langchain/anthropic SDK";
static readonly defaultModel = "claude-3-5-sonnet-latest";
providerName = "Anthropic";
private model: ChatAnthropic;
private modelNameUsed: string;
private providerInstanceName?: string;
private maxContextUsed?: number;
lastCalls: LLMCallRecord[] = [];
constructor(
apiKey?: string,
modelName?: string,
providerInstanceName?: string,
maxContext?: number,
) {
let key = apiKey;
let model = modelName;
this.providerInstanceName = providerInstanceName;
this.maxContextUsed = maxContext;
if (!key) {
const active = ProviderManager.getActive("generative");
if (active && active.providerName === AnthropicProvider.providerId) {
key = active.apiKey;
if (!model) {
model = active.modelName;
}
if (!this.providerInstanceName) {
this.providerInstanceName = active.name;
}
if (this.maxContextUsed === undefined) {
this.maxContextUsed = active.maxContext;
}
}
}
if (!key) {
key = llmConfig.ANTHROPIC_API_KEY;
if (!this.providerInstanceName && key) {
this.providerInstanceName = "Environment Variable";
}
}
if (!key) {
throw new Error(
"ANTHROPIC_API_KEY is required to initialize AnthropicProvider",
);
}
this.modelNameUsed = model || AnthropicProvider.defaultModel;
this.model = new ChatAnthropic({
apiKey: key,
model: this.modelNameUsed,
});
}
async generateStructuredResponse<T extends z.ZodTypeAny>(
request: LLMRequest<T>,
): Promise<LLMResponse<z.infer<T>>> {
const structuredModel = this.model.withStructuredOutput(request.schema, {
includeRaw: true,
});
const result = (await structuredModel.invoke([
{ role: "system", content: request.systemPrompt },
{ role: "user", content: request.userContext },
])) as unknown as {
parsed?: z.infer<T>;
raw?: {
usage_metadata?: {
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
};
};
};
const parsed = result?.parsed;
const raw = result?.raw;
const usage = {
inputTokens: raw?.usage_metadata?.input_tokens || 0,
outputTokens: raw?.usage_metadata?.output_tokens || 0,
totalTokens: raw?.usage_metadata?.total_tokens || 0,
modelName: this.modelNameUsed,
providerInstanceName: this.providerInstanceName || "Default",
maxContext:
this.maxContextUsed !== undefined ? this.maxContextUsed : 200000,
};
this.lastCalls.push({
systemPrompt: request.systemPrompt,
userContext: request.userContext,
usage,
});
return { success: true, data: parsed, usage };
}
}