Add configurable tools support to Anthropic provider

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
braginini
2026-04-03 16:21:34 +02:00
co-authored by Claude Opus 4.6
parent d10539bf96
commit 33bc9fb789
6 changed files with 1257 additions and 47 deletions
+1128 -4
View File
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@netbirdio/explain",
"version": "0.1.3",
"version": "0.1.5",
"description": "Full-stack AI assistant library with React frontend components and Node.js backend handler",
"license": "BSD-3-Clause",
"main": "./dist/client/index.js",
@@ -17,11 +17,18 @@
},
"typesVersions": {
"*": {
"client": ["./dist/client/index.d.ts"],
"server": ["./dist/server/index.d.ts"]
"client": [
"./dist/client/index.d.ts"
],
"server": [
"./dist/server/index.d.ts"
]
}
},
"files": ["dist", "src"],
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
@@ -31,6 +38,7 @@
"react-dom": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"lucide-react": ">=0.300.0"
},
"repository": {
@@ -42,4 +50,4 @@
"@types/react-dom": "^18.0.0",
"typescript": "^5.0.0"
}
}
}
+3 -2
View File
@@ -2,7 +2,7 @@ import type { Message } from "../types";
import { AnthropicProvider } from "./providers/anthropic";
import { DifyProvider } from "./providers/dify";
import { OpenAIProvider } from "./providers/openai";
import type { LLMProvider, Middleware } from "./providers/types";
import type { LLMProvider, Middleware, Tool } from "./providers/types";
export type AssistantConfig = {
provider: "anthropic" | "openai" | "dify" | LLMProvider;
@@ -11,6 +11,7 @@ export type AssistantConfig = {
baseUrl?: string;
systemPrompt?: string;
middleware?: Middleware[];
tools?: Tool[];
};
type HandlerOptions = {
@@ -132,7 +133,7 @@ export function createAssistant(config: AssistantConfig) {
systemPrompt = result.systemPrompt;
}
const reply = await provider.chat(messages, systemPrompt);
const reply = await provider.chat(messages, systemPrompt, config.tools);
return { reply };
}
+1 -1
View File
@@ -3,4 +3,4 @@ export type { AssistantConfig } from "./handler";
export { AnthropicProvider } from "./providers/anthropic";
export { DifyProvider } from "./providers/dify";
export { OpenAIProvider } from "./providers/openai";
export type { LLMProvider, ProviderConfig, Middleware } from "./providers/types";
export type { LLMProvider, ProviderConfig, Middleware, Tool } from "./providers/types";
+104 -34
View File
@@ -1,5 +1,15 @@
import type { Message } from "../../types";
import type { LLMProvider, ProviderConfig } from "./types";
import type { LLMProvider, ProviderConfig, Tool } from "./types";
type AnthropicMessage = {
role: "user" | "assistant";
content: string | AnthropicContentBlock[];
};
type AnthropicContentBlock =
| { type: "text"; text: string }
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
| { type: "tool_result"; tool_use_id: string; content: string };
export class AnthropicProvider implements LLMProvider {
private apiKey: string;
@@ -10,44 +20,104 @@ export class AnthropicProvider implements LLMProvider {
this.model = config.model || "claude-sonnet-4-20250514";
}
async chat(messages: Message[], systemPrompt?: string): Promise<string> {
const anthropicMessages = messages.map((m) => ({
async chat(messages: Message[], systemPrompt?: string, tools?: Tool[]): Promise<string> {
const anthropicMessages: AnthropicMessage[] = messages.map((m) => ({
role: m.role === "context" || m.role === "system" ? ("user" as const) : m.role,
content: m.role === "context" ? `[Context]: ${m.content}` : m.content,
}));
const body: Record<string, unknown> = {
model: this.model,
max_tokens: 4096,
messages: anthropicMessages,
};
const toolDefs = tools?.map((t) => ({
name: t.name,
description: t.description,
input_schema: t.input_schema,
}));
if (systemPrompt) {
body.system = systemPrompt;
const toolsByName = new Map(tools?.map((t) => [t.name, t]));
let lastTextResponse = "";
// eslint-disable-next-line no-constant-condition
while (true) {
const body: Record<string, unknown> = {
model: this.model,
max_tokens: 4096,
messages: anthropicMessages,
};
if (systemPrompt) {
body.system = systemPrompt;
}
if (toolDefs && toolDefs.length > 0) {
body.tools = toolDefs;
}
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": this.apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Anthropic API error ${response.status}: ${text}`);
}
const data = await response.json();
const textBlock = data.content?.find(
(block: { type: string }) => block.type === "text",
);
if (textBlock) {
lastTextResponse = textBlock.text;
}
if (data.stop_reason !== "tool_use") {
if (!lastTextResponse) {
throw new Error("No text content in Anthropic response");
}
return lastTextResponse;
}
// Handle tool calls
const toolUseBlocks = data.content.filter(
(block: { type: string }) => block.type === "tool_use",
);
anthropicMessages.push({ role: "assistant", content: data.content });
const toolResults: AnthropicContentBlock[] = [];
for (const toolUse of toolUseBlocks) {
const tool = toolsByName.get(toolUse.name);
if (!tool) {
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: `Error: unknown tool "${toolUse.name}"`,
});
continue;
}
try {
const result = await tool.execute(toolUse.input);
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: result,
});
} catch (err) {
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
});
}
}
anthropicMessages.push({ role: "user", content: toolResults });
}
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": this.apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Anthropic API error ${response.status}: ${text}`);
}
const data = await response.json();
const textBlock = data.content?.find(
(block: { type: string }) => block.type === "text",
);
if (!textBlock) {
throw new Error("No text content in Anthropic response");
}
return textBlock.text;
}
}
+8 -1
View File
@@ -1,7 +1,14 @@
import type { Message } from "../../types";
export type Tool = {
name: string;
description: string;
input_schema: Record<string, unknown>;
execute: (input: Record<string, unknown>) => Promise<string>;
};
export interface LLMProvider {
chat(messages: Message[], systemPrompt?: string): Promise<string>;
chat(messages: Message[], systemPrompt?: string, tools?: Tool[]): Promise<string>;
}
export type ProviderConfig = {