From 55ba41bb8bbdf53975f706bed1c2dc041b5297f5 Mon Sep 17 00:00:00 2001 From: aakash srivastav Date: Sat, 18 Apr 2026 20:39:22 +0530 Subject: [PATCH] feat: add Claude LLM integration and update dependencies --- core/package.json | 21 +-- core/src/common.ts | 2 + core/src/models/claude_llm.ts | 304 ++++++++++++++++++++++++++++++++++ core/src/models/registry.ts | 2 + 4 files changed, 319 insertions(+), 10 deletions(-) create mode 100644 core/src/models/claude_llm.ts diff --git a/core/package.json b/core/package.json index 43d8613f..ec557572 100644 --- a/core/package.json +++ b/core/package.json @@ -41,19 +41,14 @@ }, "dependencies": { "@a2a-js/sdk": "^0.3.10", + "@anthropic-ai/sdk": "^0.90.0", + "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0", + "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", + "@google-cloud/storage": "^7.17.1", "@google/genai": "^1.37.0", "@mikro-orm/core": "^6.6.10", "@mikro-orm/reflection": "^6.6.6", "@modelcontextprotocol/sdk": "^1.26.0", - "express": "^4.22.1", - "google-auth-library": "^10.3.0", - "lodash-es": "^4.18.1", - "winston": "^3.19.0", - "zod": "^4.2.1", - "zod-to-json-schema": "^3.25.1", - "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0", - "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", - "@google-cloud/storage": "^7.17.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "^0.205.0", "@opentelemetry/exporter-logs-otlp-http": "^0.205.0", @@ -64,7 +59,13 @@ "@opentelemetry/sdk-logs": "^0.205.0", "@opentelemetry/sdk-metrics": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", - "@opentelemetry/sdk-trace-node": "^2.1.0" + "@opentelemetry/sdk-trace-node": "^2.1.0", + "express": "^4.22.1", + "google-auth-library": "^10.3.0", + "lodash-es": "^4.18.1", + "winston": "^3.19.0", + "zod": "^4.2.1", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@mikro-orm/sqlite": "^6.6.6", diff --git a/core/src/common.ts b/core/src/common.ts index b8ccf135..21b06fec 100644 --- a/core/src/common.ts +++ b/core/src/common.ts @@ -148,6 +148,8 @@ export {ApigeeLlm} from './models/apigee_llm.js'; export type {ApigeeLlmParams} from './models/apigee_llm.js'; export {BaseLlm, isBaseLlm} from './models/base_llm.js'; export type {BaseLlmConnection} from './models/base_llm_connection.js'; +export {Claude} from './models/claude_llm.js'; +export type {ClaudeParams} from './models/claude_llm.js'; export {Gemini, geminiInitParams} from './models/google_llm.js'; export type {GeminiParams} from './models/google_llm.js'; export type {LlmRequest} from './models/llm_request.js'; diff --git a/core/src/models/claude_llm.ts b/core/src/models/claude_llm.ts new file mode 100644 index 00000000..65e597d4 --- /dev/null +++ b/core/src/models/claude_llm.ts @@ -0,0 +1,304 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import Anthropic from '@anthropic-ai/sdk'; +import {Content, FinishReason, FunctionDeclaration, Part} from '@google/genai'; + +import {logger} from '../utils/logger.js'; + +import {BaseLlm} from './base_llm.js'; +import {BaseLlmConnection} from './base_llm_connection.js'; +import {LlmRequest} from './llm_request.js'; +import {LlmResponse} from './llm_response.js'; + +export interface ClaudeParams { + model: string; + apiKey?: string; + maxTokens?: number; + client?: Anthropic; +} + +/** + * Represents the Claude Generative AI model by Anthropic. + * + *

This class provides methods for interacting with Claude models. Streaming and live connections + * are not currently supported for Claude. + */ +export class Claude extends BaseLlm { + private readonly client: Anthropic; + private readonly maxTokens: number; + + static override readonly supportedModels: Array = [ + /claude-.*/, + ]; + + constructor(params: ClaudeParams) { + super({model: params.model}); + this.maxTokens = params.maxTokens ?? 8192; + if (params.client) { + this.client = params.client; + } else { + this.client = new Anthropic({ + apiKey: params.apiKey ?? process.env['ANTHROPIC_API_KEY'] ?? '', + }); + } + } + + override async *generateContentAsync( + llmRequest: LlmRequest, + stream = false, + ): AsyncGenerator { + if (stream) { + logger.warn( + 'Streaming is not currently supported for Claude models. Falling back to non-streaming.', + ); + } + + this.maybeAppendUserContent(llmRequest); + + const messages = this.contentsToAnthropicMessages(llmRequest.contents); + const tools = this.toolsToAnthropicTools(llmRequest); + + let systemText = ''; + if (llmRequest.config?.systemInstruction) { + const sysInst = llmRequest.config.systemInstruction as unknown as Record< + string, + unknown + >; + if (typeof sysInst === 'string') { + systemText = sysInst; + } else if (Array.isArray(sysInst.parts)) { + systemText = sysInst.parts + .map((p: Record) => + typeof p.text === 'string' ? p.text : '', + ) + .filter((t: string) => t.length > 0) + .join('\n'); + } else if (typeof sysInst.text === 'string') { + systemText = sysInst.text; + } + } + + const requestParams: Anthropic.MessageCreateParamsNonStreaming = { + model: llmRequest.model ?? this.model, + max_tokens: this.maxTokens, + messages, + }; + + if (systemText) { + requestParams.system = systemText; + } + + if (tools.length > 0) { + requestParams.tools = tools; + requestParams.tool_choice = { + type: 'auto', + disable_parallel_tool_use: true, + }; + } + + const response = await this.client.messages.create(requestParams); + logger.debug(`Claude response: ${JSON.stringify(response)}`); + + yield this.convertAnthropicResponseToLlmResponse(response); + } + + override async connect(_llmRequest: LlmRequest): Promise { + throw new Error('Live connection is not supported for Claude models.'); + } + + private contentsToAnthropicMessages( + contents: Content[], + ): Anthropic.MessageParam[] { + return contents.map((c) => { + const role: 'user' | 'assistant' = + c.role === 'model' || c.role === 'assistant' ? 'assistant' : 'user'; + const content = c.parts + ?.map((p) => this.partToAnthropicMessageBlock(p)) + .filter(Boolean) as Array< + | Anthropic.TextBlockParam + | Anthropic.ImageBlockParam + | Anthropic.ToolUseBlockParam + | Anthropic.ToolResultBlockParam + >; + + return { + role, + content: content && content.length > 0 ? content : '', + }; + }); + } + + private partToAnthropicMessageBlock( + part: Part, + ): + | Anthropic.TextBlockParam + | Anthropic.ToolUseBlockParam + | Anthropic.ToolResultBlockParam + | undefined { + if (part.text !== undefined && part.text !== null) { + return {type: 'text', text: part.text}; + } else if (part.functionCall) { + return { + type: 'tool_use', + id: part.functionCall.id ?? '', // ID is required by Anthropic + name: part.functionCall.name ?? '', + input: part.functionCall.args ?? {}, + }; + } else if (part.functionResponse) { + let content = ''; + if (part.functionResponse.response) { + const respObj = part.functionResponse.response as Record< + string, + unknown + >; + if ( + respObj && + typeof respObj === 'object' && + 'result' in respObj && + respObj.result !== null && + respObj.result !== undefined + ) { + content = String(respObj.result); + } else { + content = JSON.stringify(part.functionResponse.response); + } + } + return { + type: 'tool_result', + tool_use_id: part.functionResponse.id ?? '', + content, + is_error: false, + }; + } + return undefined; + } + + private toolsToAnthropicTools(llmRequest: LlmRequest): Anthropic.Tool[] { + const tools: Anthropic.Tool[] = []; + if (!llmRequest.config?.tools) { + return tools; + } + + for (const toolObj of llmRequest.config.tools) { + if ('functionDeclarations' in toolObj && toolObj.functionDeclarations) { + for (const funcDecl of toolObj.functionDeclarations) { + tools.push(this.functionDeclarationToAnthropicTool(funcDecl)); + } + } + } + return tools; + } + + private functionDeclarationToAnthropicTool( + funcDecl: FunctionDeclaration, + ): Anthropic.Tool { + const inputSchema: Anthropic.Tool.InputSchema = { + type: 'object', + properties: {}, + }; + + if (funcDecl.parameters) { + const fullSchema = structuredClone(funcDecl.parameters); + this.updateTypeString(fullSchema); + + if (fullSchema.properties) { + inputSchema.properties = fullSchema.properties as Record< + string, + unknown + >; + } + if (fullSchema.required) { + inputSchema.required = fullSchema.required; + } + } + + return { + name: funcDecl.name ?? '', + description: funcDecl.description ?? '', + input_schema: inputSchema, + }; + } + + private updateTypeString(schemaObj: unknown) { + if (!schemaObj || typeof schemaObj !== 'object') { + return; + } + + const obj = schemaObj as Record; + + if (typeof obj.type === 'string') { + obj.type = obj.type.toLowerCase(); + } + if (obj.items) { + this.updateTypeString(obj.items); + } + if (obj.properties && typeof obj.properties === 'object') { + const props = obj.properties as Record; + for (const key in props) { + this.updateTypeString(props[key]); + } + } + } + + private convertAnthropicResponseToLlmResponse( + message: Anthropic.Messages.Message, + ): LlmResponse { + const parts: Part[] = []; + + for (const block of message.content) { + const part = this.anthropicContentBlockToPart(block); + if (part) { + parts.push(part); + } + } + + let finishReason: FinishReason | undefined; + switch (message.stop_reason) { + case 'end_turn': + case 'stop_sequence': + finishReason = FinishReason.STOP; + break; + case 'max_tokens': + finishReason = FinishReason.MAX_TOKENS; + break; + case 'tool_use': + finishReason = FinishReason.STOP; + break; + } + + return { + content: { + role: 'model', + parts, + }, + usageMetadata: { + promptTokenCount: message.usage.input_tokens, + candidatesTokenCount: message.usage.output_tokens, + totalTokenCount: + message.usage.input_tokens + message.usage.output_tokens, + }, + finishReason, + }; + } + + private anthropicContentBlockToPart( + block: Anthropic.ContentBlock, + ): Part | undefined { + if (block.type === 'text') { + return {text: block.text}; + } else if (block.type === 'tool_use') { + return { + functionCall: { + id: block.id, + name: block.name, + args: block.input as Record, + }, + }; + } + return undefined; + } +} diff --git a/core/src/models/registry.ts b/core/src/models/registry.ts index 18a1333d..b359f233 100644 --- a/core/src/models/registry.ts +++ b/core/src/models/registry.ts @@ -8,6 +8,7 @@ import {logger} from '../utils/logger.js'; import {ApigeeLlm} from './apigee_llm.js'; import {BaseLlm} from './base_llm.js'; +import {Claude} from './claude_llm.js'; import {Gemini} from './google_llm.js'; /** @@ -131,3 +132,4 @@ export class LLMRegistry { /** Registers default LLM factories, e.g. for Gemini models. */ LLMRegistry.register(Gemini); LLMRegistry.register(ApigeeLlm); +LLMRegistry.register(Claude);