@livekit/agents-plugin-anthropic 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/llm.cjs +5 -1
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +3 -0
- package/dist/llm.d.ts +3 -0
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +5 -1
- package/dist/llm.js.map +1 -1
- package/dist/llm.test.cjs +85 -0
- package/dist/llm.test.cjs.map +1 -1
- package/dist/llm.test.js +64 -1
- package/dist/llm.test.js.map +1 -1
- package/package.json +6 -6
- package/src/llm.test.ts +80 -2
- package/src/llm.ts +7 -0
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/llm.cjs
CHANGED
|
@@ -53,7 +53,8 @@ class LLM extends import_agents.llm.LLM {
|
|
|
53
53
|
}
|
|
54
54
|
this.#client = this.#opts.client || new import_sdk.default({
|
|
55
55
|
baseURL: this.#opts.baseURL,
|
|
56
|
-
apiKey: this.#opts.apiKey
|
|
56
|
+
apiKey: this.#opts.apiKey,
|
|
57
|
+
maxRetries: this.#opts.maxRetries ?? 0
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
/** @returns Human-readable label for logging. */
|
|
@@ -73,6 +74,9 @@ class LLM extends import_agents.llm.LLM {
|
|
|
73
74
|
return "api.anthropic.com";
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
async _prewarmImpl(signal) {
|
|
78
|
+
await this.#client.models.list({ limit: 1 }, { signal });
|
|
79
|
+
}
|
|
76
80
|
/**
|
|
77
81
|
* Converts a framework ChatContext into Anthropic's message format.
|
|
78
82
|
*
|
package/dist/llm.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport Anthropic from '@anthropic-ai/sdk';\nimport type { APIConnectOptions } from '@livekit/agents';\nimport {\n APIConnectionError,\n APIStatusError,\n APITimeoutError,\n DEFAULT_API_CONNECT_OPTIONS,\n llm,\n} from '@livekit/agents';\nimport type { ChatModels } from './models.js';\n\n/** Configuration options for the Anthropic LLM plugin. */\nexport interface LLMOptions {\n /** The model identifier to use. */\n model: string | ChatModels;\n /** Anthropic API key. Falls back to the `ANTHROPIC_API_KEY` environment variable. */\n apiKey?: string;\n /** Custom base URL for the Anthropic API. */\n baseURL?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Pre-configured Anthropic client instance. */\n client?: Anthropic;\n /** Tool selection strategy. */\n toolChoice?: llm.ToolChoice;\n /** Whether to allow parallel tool calls. */\n parallelToolCalls?: boolean;\n /** Maximum number of tokens in the response. Defaults to 4096. */\n maxTokens?: number;\n}\n\nconst defaultLLMOptions: LLMOptions = {\n model: 'claude-sonnet-4-6',\n /* eslint-disable-next-line turbo/no-undeclared-env-vars */\n apiKey: process.env.ANTHROPIC_API_KEY,\n parallelToolCalls: true,\n};\n\n/**\n * Anthropic LLM provider for LiveKit Agents.\n *\n * @remarks\n * Implements the {@link llm.LLM} interface using the Anthropic Messages API.\n * Supports streaming, tool calling, and system prompt isolation required by\n * Claude 3.5+ models.\n */\nexport class LLM extends llm.LLM {\n #opts: LLMOptions;\n #client: Anthropic;\n\n constructor(opts: Partial<LLMOptions> = defaultLLMOptions) {\n super();\n\n this.#opts = { ...defaultLLMOptions, ...opts };\n if (!this.#opts.apiKey && !this.#opts.client) {\n throw new Error(\n 'Anthropic API key is required, whether as an argument or as $ANTHROPIC_API_KEY',\n );\n }\n\n this.#client =\n this.#opts.client ||\n new Anthropic({\n baseURL: this.#opts.baseURL,\n apiKey: this.#opts.apiKey,\n });\n }\n\n /** @returns Human-readable label for logging. */\n label(): string {\n return 'anthropic.LLM';\n }\n\n /** @returns The model identifier being used. */\n get model(): string {\n return this.#opts.model;\n }\n\n /** @returns The API provider host. */\n get provider(): string {\n try {\n const url = new URL(this.#client.baseURL);\n return url.host;\n } catch {\n return 'api.anthropic.com';\n }\n }\n\n /**\n * Converts a framework ChatContext into Anthropic's message format.\n *\n * @remarks\n * - System prompts are isolated into a separate `TextBlockParam[]` array\n * (required by Claude 3.5+).\n * - `function_call` items are mapped to Anthropic `tool_use` content blocks.\n * - `function_call_output` items are mapped to `tool_result` content blocks.\n * - Consecutive same-role messages are merged to satisfy Anthropic's\n * strict alternating-turn requirement.\n * - A dummy `(empty)` user message is injected if the conversation doesn't\n * start with a user turn.\n * - A dummy user message is appended if the conversation ends with an\n * assistant turn, since Claude 4.6+ does not support prefilling.\n */\n protected _buildAnthropicContext(chatCtx: llm.ChatContext): {\n system: Anthropic.TextBlockParam[];\n messages: Anthropic.MessageParam[];\n } {\n const system: Anthropic.TextBlockParam[] = [];\n const rawMessages: Anthropic.MessageParam[] = [];\n\n for (const msg of chatCtx.items) {\n if (msg.type === 'message') {\n const textContent = msg.rawTextContent || '';\n if (msg.role === 'system' || msg.role === 'developer') {\n system.push({ type: 'text', text: textContent });\n } else if (msg.role === 'user' || msg.role === 'assistant') {\n rawMessages.push({\n role: msg.role,\n content: textContent,\n });\n }\n } else if (msg.type === 'function_call') {\n // Map to Anthropic's tool_use content block (assistant role)\n rawMessages.push({\n role: 'assistant',\n content: [\n {\n type: 'tool_use',\n id: msg.callId,\n name: msg.name,\n input: JSON.parse(msg.args || '{}'),\n },\n ],\n });\n } else if (msg.type === 'function_call_output') {\n // Map to Anthropic's tool_result content block (user role)\n rawMessages.push({\n role: 'user',\n content: [\n {\n type: 'tool_result',\n tool_use_id: msg.callId,\n content: msg.output,\n is_error: msg.isError,\n },\n ],\n });\n }\n }\n\n // Merge consecutive same-role messages (Anthropic requires alternating turns)\n const messages: Anthropic.MessageParam[] = [];\n for (const msg of rawMessages) {\n const prev = messages[messages.length - 1];\n if (prev && prev.role === msg.role) {\n // Merge content into a single content array\n const prevContent = Array.isArray(prev.content)\n ? prev.content\n : [{ type: 'text' as const, text: prev.content as string }];\n const curContent = Array.isArray(msg.content)\n ? msg.content\n : [{ type: 'text' as const, text: msg.content as string }];\n prev.content = [...prevContent, ...curContent] as Anthropic.ContentBlockParam[];\n } else {\n messages.push({ ...msg });\n }\n }\n\n // Anthropic requires conversations to start with a user turn\n if (messages.length === 0 || messages[0]!.role !== 'user') {\n messages.unshift({ role: 'user', content: '(empty)' });\n }\n\n // Claude 4.6+ does not support prefilling (trailing assistant messages).\n if (messages.length > 0 && messages[messages.length - 1]!.role === 'assistant') {\n messages.push({ role: 'user', content: [{ type: 'text', text: '.' }] });\n }\n\n return { system, messages };\n }\n\n /**\n * Creates a streaming chat completion.\n *\n * @remarks\n * Maps `toolChoice` to Anthropic's format:\n * - `\"required\"` → `{ type: \"any\" }`\n * - `\"none\"` → clears tools\n * - `{ type: \"function\", function: { name } }` → `{ type: \"tool\", name }`\n */\n chat({\n chatCtx,\n toolCtx: toolCtxInput,\n connOptions = DEFAULT_API_CONNECT_OPTIONS,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContextLike;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: llm.ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream {\n const extras: Record<string, unknown> = { ...extraKwargs };\n\n if (this.#opts.temperature !== undefined) extras.temperature = this.#opts.temperature;\n\n const { system, messages } = this._buildAnthropicContext(chatCtx);\n\n // Build Anthropic tool schemas\n const toolCtx = llm.toToolContext(toolCtxInput);\n const anthropicTools: Anthropic.Tool[] = [];\n if (toolCtx) {\n for (const [name, tool] of llm.sortedToolEntries(toolCtx)) {\n anthropicTools.push({\n name: name,\n description: tool.description || '',\n input_schema: (tool.parameters\n ? llm.toJsonSchema(tool.parameters, false)\n : { type: 'object', properties: {} }) as Anthropic.Tool.InputSchema,\n });\n }\n }\n\n // Map toolChoice and parallelToolCalls to Anthropic format\n const resolvedToolChoice = toolChoice ?? this.#opts.toolChoice;\n const resolvedParallel = parallelToolCalls ?? this.#opts.parallelToolCalls;\n\n if ((resolvedToolChoice || resolvedParallel !== undefined) && anthropicTools.length > 0) {\n let anthropicToolChoice: Record<string, unknown> | undefined = { type: 'auto' };\n\n if (typeof resolvedToolChoice === 'string') {\n if (resolvedToolChoice === 'required') {\n anthropicToolChoice = { type: 'any' };\n } else if (resolvedToolChoice === 'none') {\n // Clear tools entirely when none is requested\n anthropicTools.length = 0;\n anthropicToolChoice = undefined;\n }\n } else if (\n typeof resolvedToolChoice === 'object' &&\n 'type' in resolvedToolChoice &&\n resolvedToolChoice.type === 'function'\n ) {\n const fn = (resolvedToolChoice as { function: { name: string } }).function;\n anthropicToolChoice = { type: 'tool', name: fn.name };\n }\n\n if (anthropicToolChoice) {\n // Map parallelToolCalls\n if (resolvedParallel !== undefined) {\n anthropicToolChoice.disable_parallel_tool_use = !resolvedParallel;\n }\n extras.tool_choice = anthropicToolChoice;\n }\n }\n\n const requestParams: Anthropic.MessageCreateParamsStreaming = {\n model: this.#opts.model,\n messages: messages,\n system: system.length > 0 ? system : undefined,\n tools: anthropicTools.length > 0 ? anthropicTools : undefined,\n stream: true,\n max_tokens: this.#opts.maxTokens || 4096,\n ...extras,\n };\n\n return new LLMStream(this, this.#client, requestParams, chatCtx, toolCtx, connOptions);\n }\n}\n\n/**\n * Streaming response handler for Anthropic Messages API.\n *\n * @remarks\n * Parses SSE events including:\n * - `content_block_start` / `content_block_delta` / `content_block_stop` for text and tool use\n * - `message_start` / `message_delta` for token usage tracking\n * - Chain-of-thought `<thinking>` block filtering when tools are active\n */\nexport class LLMStream extends llm.LLMStream {\n #client: Anthropic;\n #requestParams: Anthropic.MessageCreateParamsStreaming;\n #toolCallId?: string;\n #fncName?: string;\n #fncRawArgs?: string;\n #requestId = '';\n #ignoringCoT = false;\n #inputTokens = 0;\n #outputTokens = 0;\n #cacheCreationTokens = 0;\n #cacheReadTokens = 0;\n #toolCtx?: llm.ToolContext;\n\n constructor(\n llmInst: LLM,\n client: Anthropic,\n requestParams: Anthropic.MessageCreateParamsStreaming,\n chatCtx: llm.ChatContext,\n toolCtx: llm.ToolContext | undefined,\n connOptions: APIConnectOptions,\n ) {\n super(llmInst, { chatCtx, toolCtx, connOptions });\n this.#client = client;\n this.#requestParams = requestParams;\n this.#toolCtx = toolCtx;\n }\n\n protected async run(): Promise<void> {\n let retryable = true;\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n this.#requestId = '';\n this.#ignoringCoT = false;\n this.#inputTokens = 0;\n this.#outputTokens = 0;\n this.#cacheCreationTokens = 0;\n this.#cacheReadTokens = 0;\n\n try {\n const stream = await this.#client.messages.create(this.#requestParams, {\n timeout: this.connOptions.timeoutMs,\n });\n for await (const event of stream) {\n if (event.type === 'message_start') {\n this.#requestId = event.message.id;\n this.#inputTokens = event.message.usage.input_tokens;\n this.#outputTokens = event.message.usage.output_tokens;\n this.#cacheCreationTokens = event.message.usage.cache_creation_input_tokens ?? 0;\n this.#cacheReadTokens = event.message.usage.cache_read_input_tokens ?? 0;\n } else if (event.type === 'message_delta') {\n this.#outputTokens = event.usage.output_tokens;\n } else if (\n event.type === 'content_block_start' &&\n event.content_block.type === 'tool_use'\n ) {\n this.#toolCallId = event.content_block.id;\n this.#fncName = event.content_block.name;\n this.#fncRawArgs = '';\n } else if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {\n let text = event.delta.text;\n\n // Filter chain-of-thought <thinking> blocks when tools are active\n if (this.#toolCtx) {\n const thinkingStart = text.indexOf('<thinking>');\n if (thinkingStart >= 0) {\n const preThinking = text.slice(0, thinkingStart);\n if (preThinking) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: preThinking },\n });\n retryable = false;\n }\n text = text.slice(thinkingStart + '<thinking>'.length);\n this.#ignoringCoT = true;\n }\n if (this.#ignoringCoT) {\n const thinkingEnd = text.indexOf('</thinking>');\n if (thinkingEnd >= 0) {\n text = text.slice(thinkingEnd + '</thinking>'.length);\n this.#ignoringCoT = false;\n }\n }\n }\n\n if (this.#ignoringCoT) {\n continue;\n }\n\n // A delta can be reduced to an empty string when it was entirely a\n // <thinking> block; don't emit it or mark the stream non-retryable.\n if (text) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: text },\n });\n retryable = false;\n }\n } else if (\n event.type === 'content_block_delta' &&\n event.delta.type === 'input_json_delta'\n ) {\n this.#fncRawArgs += event.delta.partial_json;\n } else if (event.type === 'content_block_stop' && this.#toolCallId) {\n this.queue.put({\n id: this.#requestId,\n delta: {\n role: 'assistant',\n toolCalls: [\n llm.FunctionCall.create({\n callId: this.#toolCallId,\n name: this.#fncName || '',\n args: this.#fncRawArgs || '',\n }),\n ],\n },\n });\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n retryable = false;\n }\n }\n\n // Emit final usage chunk. Anthropic reports cached tokens separately from\n // input_tokens, so fold them into promptTokens like the Python plugin does.\n const promptTokens = this.#inputTokens + this.#cacheCreationTokens + this.#cacheReadTokens;\n this.queue.put({\n id: this.#requestId,\n usage: {\n completionTokens: this.#outputTokens,\n promptTokens,\n totalTokens: promptTokens + this.#outputTokens,\n promptCachedTokens: this.#cacheReadTokens,\n },\n });\n } catch (e: unknown) {\n if (e instanceof Anthropic.APIError) {\n if (e.status === 408) {\n throw new APITimeoutError({\n message: e.message,\n options: { retryable },\n });\n }\n throw new APIStatusError({\n message: e.message,\n options: {\n statusCode: e.status,\n body: e.error as object,\n retryable: retryable && (e.status === 429 || e.status >= 500),\n },\n });\n }\n throw new APIConnectionError({\n message: e instanceof Error ? e.message : String(e),\n options: { retryable },\n });\n }\n }\n}\n"],"mappings":"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","names":["Anthropic"]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport Anthropic from '@anthropic-ai/sdk';\nimport type { APIConnectOptions } from '@livekit/agents';\nimport {\n APIConnectionError,\n APIStatusError,\n APITimeoutError,\n DEFAULT_API_CONNECT_OPTIONS,\n llm,\n} from '@livekit/agents';\nimport type { ChatModels } from './models.js';\n\n/** Configuration options for the Anthropic LLM plugin. */\nexport interface LLMOptions {\n /** The model identifier to use. */\n model: string | ChatModels;\n /** Anthropic API key. Falls back to the `ANTHROPIC_API_KEY` environment variable. */\n apiKey?: string;\n /** Custom base URL for the Anthropic API. */\n baseURL?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Pre-configured Anthropic client instance. */\n client?: Anthropic;\n /** Vendor client retries. Defaults to 0 because the framework owns retries. */\n maxRetries?: number;\n /** Tool selection strategy. */\n toolChoice?: llm.ToolChoice;\n /** Whether to allow parallel tool calls. */\n parallelToolCalls?: boolean;\n /** Maximum number of tokens in the response. Defaults to 4096. */\n maxTokens?: number;\n}\n\nconst defaultLLMOptions: LLMOptions = {\n model: 'claude-sonnet-4-6',\n /* eslint-disable-next-line turbo/no-undeclared-env-vars */\n apiKey: process.env.ANTHROPIC_API_KEY,\n parallelToolCalls: true,\n};\n\n/**\n * Anthropic LLM provider for LiveKit Agents.\n *\n * @remarks\n * Implements the {@link llm.LLM} interface using the Anthropic Messages API.\n * Supports streaming, tool calling, and system prompt isolation required by\n * Claude 3.5+ models.\n */\nexport class LLM extends llm.LLM {\n #opts: LLMOptions;\n #client: Anthropic;\n\n constructor(opts: Partial<LLMOptions> = defaultLLMOptions) {\n super();\n\n this.#opts = { ...defaultLLMOptions, ...opts };\n if (!this.#opts.apiKey && !this.#opts.client) {\n throw new Error(\n 'Anthropic API key is required, whether as an argument or as $ANTHROPIC_API_KEY',\n );\n }\n\n this.#client =\n this.#opts.client ||\n new Anthropic({\n baseURL: this.#opts.baseURL,\n apiKey: this.#opts.apiKey,\n maxRetries: this.#opts.maxRetries ?? 0,\n });\n }\n\n /** @returns Human-readable label for logging. */\n label(): string {\n return 'anthropic.LLM';\n }\n\n /** @returns The model identifier being used. */\n get model(): string {\n return this.#opts.model;\n }\n\n /** @returns The API provider host. */\n get provider(): string {\n try {\n const url = new URL(this.#client.baseURL);\n return url.host;\n } catch {\n return 'api.anthropic.com';\n }\n }\n\n protected override async _prewarmImpl(signal: AbortSignal): Promise<void> {\n await this.#client.models.list({ limit: 1 }, { signal });\n }\n\n /**\n * Converts a framework ChatContext into Anthropic's message format.\n *\n * @remarks\n * - System prompts are isolated into a separate `TextBlockParam[]` array\n * (required by Claude 3.5+).\n * - `function_call` items are mapped to Anthropic `tool_use` content blocks.\n * - `function_call_output` items are mapped to `tool_result` content blocks.\n * - Consecutive same-role messages are merged to satisfy Anthropic's\n * strict alternating-turn requirement.\n * - A dummy `(empty)` user message is injected if the conversation doesn't\n * start with a user turn.\n * - A dummy user message is appended if the conversation ends with an\n * assistant turn, since Claude 4.6+ does not support prefilling.\n */\n protected _buildAnthropicContext(chatCtx: llm.ChatContext): {\n system: Anthropic.TextBlockParam[];\n messages: Anthropic.MessageParam[];\n } {\n const system: Anthropic.TextBlockParam[] = [];\n const rawMessages: Anthropic.MessageParam[] = [];\n\n for (const msg of chatCtx.items) {\n if (msg.type === 'message') {\n const textContent = msg.rawTextContent || '';\n if (msg.role === 'system' || msg.role === 'developer') {\n system.push({ type: 'text', text: textContent });\n } else if (msg.role === 'user' || msg.role === 'assistant') {\n rawMessages.push({\n role: msg.role,\n content: textContent,\n });\n }\n } else if (msg.type === 'function_call') {\n // Map to Anthropic's tool_use content block (assistant role)\n rawMessages.push({\n role: 'assistant',\n content: [\n {\n type: 'tool_use',\n id: msg.callId,\n name: msg.name,\n input: JSON.parse(msg.args || '{}'),\n },\n ],\n });\n } else if (msg.type === 'function_call_output') {\n // Map to Anthropic's tool_result content block (user role)\n rawMessages.push({\n role: 'user',\n content: [\n {\n type: 'tool_result',\n tool_use_id: msg.callId,\n content: msg.output,\n is_error: msg.isError,\n },\n ],\n });\n }\n }\n\n // Merge consecutive same-role messages (Anthropic requires alternating turns)\n const messages: Anthropic.MessageParam[] = [];\n for (const msg of rawMessages) {\n const prev = messages[messages.length - 1];\n if (prev && prev.role === msg.role) {\n // Merge content into a single content array\n const prevContent = Array.isArray(prev.content)\n ? prev.content\n : [{ type: 'text' as const, text: prev.content as string }];\n const curContent = Array.isArray(msg.content)\n ? msg.content\n : [{ type: 'text' as const, text: msg.content as string }];\n prev.content = [...prevContent, ...curContent] as Anthropic.ContentBlockParam[];\n } else {\n messages.push({ ...msg });\n }\n }\n\n // Anthropic requires conversations to start with a user turn\n if (messages.length === 0 || messages[0]!.role !== 'user') {\n messages.unshift({ role: 'user', content: '(empty)' });\n }\n\n // Claude 4.6+ does not support prefilling (trailing assistant messages).\n if (messages.length > 0 && messages[messages.length - 1]!.role === 'assistant') {\n messages.push({ role: 'user', content: [{ type: 'text', text: '.' }] });\n }\n\n return { system, messages };\n }\n\n /**\n * Creates a streaming chat completion.\n *\n * @remarks\n * Maps `toolChoice` to Anthropic's format:\n * - `\"required\"` → `{ type: \"any\" }`\n * - `\"none\"` → clears tools\n * - `{ type: \"function\", function: { name } }` → `{ type: \"tool\", name }`\n */\n chat({\n chatCtx,\n toolCtx: toolCtxInput,\n connOptions = DEFAULT_API_CONNECT_OPTIONS,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContextLike;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: llm.ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream {\n const extras: Record<string, unknown> = { ...extraKwargs };\n\n if (this.#opts.temperature !== undefined) extras.temperature = this.#opts.temperature;\n\n const { system, messages } = this._buildAnthropicContext(chatCtx);\n\n // Build Anthropic tool schemas\n const toolCtx = llm.toToolContext(toolCtxInput);\n const anthropicTools: Anthropic.Tool[] = [];\n if (toolCtx) {\n for (const [name, tool] of llm.sortedToolEntries(toolCtx)) {\n anthropicTools.push({\n name: name,\n description: tool.description || '',\n input_schema: (tool.parameters\n ? llm.toJsonSchema(tool.parameters, false)\n : { type: 'object', properties: {} }) as Anthropic.Tool.InputSchema,\n });\n }\n }\n\n // Map toolChoice and parallelToolCalls to Anthropic format\n const resolvedToolChoice = toolChoice ?? this.#opts.toolChoice;\n const resolvedParallel = parallelToolCalls ?? this.#opts.parallelToolCalls;\n\n if ((resolvedToolChoice || resolvedParallel !== undefined) && anthropicTools.length > 0) {\n let anthropicToolChoice: Record<string, unknown> | undefined = { type: 'auto' };\n\n if (typeof resolvedToolChoice === 'string') {\n if (resolvedToolChoice === 'required') {\n anthropicToolChoice = { type: 'any' };\n } else if (resolvedToolChoice === 'none') {\n // Clear tools entirely when none is requested\n anthropicTools.length = 0;\n anthropicToolChoice = undefined;\n }\n } else if (\n typeof resolvedToolChoice === 'object' &&\n 'type' in resolvedToolChoice &&\n resolvedToolChoice.type === 'function'\n ) {\n const fn = (resolvedToolChoice as { function: { name: string } }).function;\n anthropicToolChoice = { type: 'tool', name: fn.name };\n }\n\n if (anthropicToolChoice) {\n // Map parallelToolCalls\n if (resolvedParallel !== undefined) {\n anthropicToolChoice.disable_parallel_tool_use = !resolvedParallel;\n }\n extras.tool_choice = anthropicToolChoice;\n }\n }\n\n const requestParams: Anthropic.MessageCreateParamsStreaming = {\n model: this.#opts.model,\n messages: messages,\n system: system.length > 0 ? system : undefined,\n tools: anthropicTools.length > 0 ? anthropicTools : undefined,\n stream: true,\n max_tokens: this.#opts.maxTokens || 4096,\n ...extras,\n };\n\n return new LLMStream(this, this.#client, requestParams, chatCtx, toolCtx, connOptions);\n }\n}\n\n/**\n * Streaming response handler for Anthropic Messages API.\n *\n * @remarks\n * Parses SSE events including:\n * - `content_block_start` / `content_block_delta` / `content_block_stop` for text and tool use\n * - `message_start` / `message_delta` for token usage tracking\n * - Chain-of-thought `<thinking>` block filtering when tools are active\n */\nexport class LLMStream extends llm.LLMStream {\n #client: Anthropic;\n #requestParams: Anthropic.MessageCreateParamsStreaming;\n #toolCallId?: string;\n #fncName?: string;\n #fncRawArgs?: string;\n #requestId = '';\n #ignoringCoT = false;\n #inputTokens = 0;\n #outputTokens = 0;\n #cacheCreationTokens = 0;\n #cacheReadTokens = 0;\n #toolCtx?: llm.ToolContext;\n\n constructor(\n llmInst: LLM,\n client: Anthropic,\n requestParams: Anthropic.MessageCreateParamsStreaming,\n chatCtx: llm.ChatContext,\n toolCtx: llm.ToolContext | undefined,\n connOptions: APIConnectOptions,\n ) {\n super(llmInst, { chatCtx, toolCtx, connOptions });\n this.#client = client;\n this.#requestParams = requestParams;\n this.#toolCtx = toolCtx;\n }\n\n protected async run(): Promise<void> {\n let retryable = true;\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n this.#requestId = '';\n this.#ignoringCoT = false;\n this.#inputTokens = 0;\n this.#outputTokens = 0;\n this.#cacheCreationTokens = 0;\n this.#cacheReadTokens = 0;\n\n try {\n const stream = await this.#client.messages.create(this.#requestParams, {\n timeout: this.connOptions.timeoutMs,\n });\n for await (const event of stream) {\n if (event.type === 'message_start') {\n this.#requestId = event.message.id;\n this.#inputTokens = event.message.usage.input_tokens;\n this.#outputTokens = event.message.usage.output_tokens;\n this.#cacheCreationTokens = event.message.usage.cache_creation_input_tokens ?? 0;\n this.#cacheReadTokens = event.message.usage.cache_read_input_tokens ?? 0;\n } else if (event.type === 'message_delta') {\n this.#outputTokens = event.usage.output_tokens;\n } else if (\n event.type === 'content_block_start' &&\n event.content_block.type === 'tool_use'\n ) {\n this.#toolCallId = event.content_block.id;\n this.#fncName = event.content_block.name;\n this.#fncRawArgs = '';\n } else if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {\n let text = event.delta.text;\n\n // Filter chain-of-thought <thinking> blocks when tools are active\n if (this.#toolCtx) {\n const thinkingStart = text.indexOf('<thinking>');\n if (thinkingStart >= 0) {\n const preThinking = text.slice(0, thinkingStart);\n if (preThinking) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: preThinking },\n });\n retryable = false;\n }\n text = text.slice(thinkingStart + '<thinking>'.length);\n this.#ignoringCoT = true;\n }\n if (this.#ignoringCoT) {\n const thinkingEnd = text.indexOf('</thinking>');\n if (thinkingEnd >= 0) {\n text = text.slice(thinkingEnd + '</thinking>'.length);\n this.#ignoringCoT = false;\n }\n }\n }\n\n if (this.#ignoringCoT) {\n continue;\n }\n\n // A delta can be reduced to an empty string when it was entirely a\n // <thinking> block; don't emit it or mark the stream non-retryable.\n if (text) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: text },\n });\n retryable = false;\n }\n } else if (\n event.type === 'content_block_delta' &&\n event.delta.type === 'input_json_delta'\n ) {\n this.#fncRawArgs += event.delta.partial_json;\n } else if (event.type === 'content_block_stop' && this.#toolCallId) {\n this.queue.put({\n id: this.#requestId,\n delta: {\n role: 'assistant',\n toolCalls: [\n llm.FunctionCall.create({\n callId: this.#toolCallId,\n name: this.#fncName || '',\n args: this.#fncRawArgs || '',\n }),\n ],\n },\n });\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n retryable = false;\n }\n }\n\n // Emit final usage chunk. Anthropic reports cached tokens separately from\n // input_tokens, so fold them into promptTokens like the Python plugin does.\n const promptTokens = this.#inputTokens + this.#cacheCreationTokens + this.#cacheReadTokens;\n this.queue.put({\n id: this.#requestId,\n usage: {\n completionTokens: this.#outputTokens,\n promptTokens,\n totalTokens: promptTokens + this.#outputTokens,\n promptCachedTokens: this.#cacheReadTokens,\n },\n });\n } catch (e: unknown) {\n if (e instanceof Anthropic.APIError) {\n if (e.status === 408) {\n throw new APITimeoutError({\n message: e.message,\n options: { retryable },\n });\n }\n throw new APIStatusError({\n message: e.message,\n options: {\n statusCode: e.status,\n body: e.error as object,\n retryable: retryable && (e.status === 429 || e.status >= 500),\n },\n });\n }\n throw new APIConnectionError({\n message: e instanceof Error ? e.message : String(e),\n options: { retryable },\n });\n }\n }\n}\n"],"mappings":"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","names":["Anthropic"]}
|
package/dist/llm.d.cts
CHANGED
|
@@ -14,6 +14,8 @@ export interface LLMOptions {
|
|
|
14
14
|
temperature?: number;
|
|
15
15
|
/** Pre-configured Anthropic client instance. */
|
|
16
16
|
client?: Anthropic;
|
|
17
|
+
/** Vendor client retries. Defaults to 0 because the framework owns retries. */
|
|
18
|
+
maxRetries?: number;
|
|
17
19
|
/** Tool selection strategy. */
|
|
18
20
|
toolChoice?: llm.ToolChoice;
|
|
19
21
|
/** Whether to allow parallel tool calls. */
|
|
@@ -38,6 +40,7 @@ export declare class LLM extends llm.LLM {
|
|
|
38
40
|
get model(): string;
|
|
39
41
|
/** @returns The API provider host. */
|
|
40
42
|
get provider(): string;
|
|
43
|
+
protected _prewarmImpl(signal: AbortSignal): Promise<void>;
|
|
41
44
|
/**
|
|
42
45
|
* Converts a framework ChatContext into Anthropic's message format.
|
|
43
46
|
*
|
package/dist/llm.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface LLMOptions {
|
|
|
14
14
|
temperature?: number;
|
|
15
15
|
/** Pre-configured Anthropic client instance. */
|
|
16
16
|
client?: Anthropic;
|
|
17
|
+
/** Vendor client retries. Defaults to 0 because the framework owns retries. */
|
|
18
|
+
maxRetries?: number;
|
|
17
19
|
/** Tool selection strategy. */
|
|
18
20
|
toolChoice?: llm.ToolChoice;
|
|
19
21
|
/** Whether to allow parallel tool calls. */
|
|
@@ -38,6 +40,7 @@ export declare class LLM extends llm.LLM {
|
|
|
38
40
|
get model(): string;
|
|
39
41
|
/** @returns The API provider host. */
|
|
40
42
|
get provider(): string;
|
|
43
|
+
protected _prewarmImpl(signal: AbortSignal): Promise<void>;
|
|
41
44
|
/**
|
|
42
45
|
* Converts a framework ChatContext into Anthropic's message format.
|
|
43
46
|
*
|
package/dist/llm.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../src/llm.ts"],"names":[],"mappings":"AAGA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAKL,GAAG,EACJ,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,mCAAmC;IACnC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,+BAA+B;IAC/B,UAAU,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC;IAC5B,4CAA4C;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD;;;;;;;GAOG;AACH,qBAAa,GAAI,SAAQ,GAAG,CAAC,GAAG;;gBAIlB,IAAI,GAAE,OAAO,CAAC,UAAU,CAAqB;
|
|
1
|
+
{"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../src/llm.ts"],"names":[],"mappings":"AAGA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAKL,GAAG,EACJ,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,mCAAmC;IACnC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,UAAU,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC;IAC5B,4CAA4C;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD;;;;;;;GAOG;AACH,qBAAa,GAAI,SAAQ,GAAG,CAAC,GAAG;;gBAIlB,IAAI,GAAE,OAAO,CAAC,UAAU,CAAqB;IAmBzD,iDAAiD;IACjD,KAAK,IAAI,MAAM;IAIf,gDAAgD;IAChD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,sCAAsC;IACtC,IAAI,QAAQ,IAAI,MAAM,CAOrB;cAEwB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzE;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,sBAAsB,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,GAAG;QAC1D,MAAM,EAAE,SAAS,CAAC,cAAc,EAAE,CAAC;QACnC,QAAQ,EAAE,SAAS,CAAC,YAAY,EAAE,CAAC;KACpC;IA2ED;;;;;;;;OAQG;IACH,IAAI,CAAC,EACH,OAAO,EACP,OAAO,EAAE,YAAY,EACrB,WAAyC,EACzC,iBAAiB,EACjB,UAAU,EACV,WAAW,GACZ,EAAE;QACD,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC;QACzB,OAAO,CAAC,EAAE,GAAG,CAAC,eAAe,CAAC;QAC9B,WAAW,CAAC,EAAE,iBAAiB,CAAC;QAChC,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,UAAU,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC;QAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACvC,GAAG,SAAS;CAmEd;AAED;;;;;;;;GAQG;AACH,qBAAa,SAAU,SAAQ,GAAG,CAAC,SAAS;;gBAexC,OAAO,EAAE,GAAG,EACZ,MAAM,EAAE,SAAS,EACjB,aAAa,EAAE,SAAS,CAAC,4BAA4B,EACrD,OAAO,EAAE,GAAG,CAAC,WAAW,EACxB,OAAO,EAAE,GAAG,CAAC,WAAW,GAAG,SAAS,EACpC,WAAW,EAAE,iBAAiB;cAQhB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAqIrC"}
|
package/dist/llm.js
CHANGED
|
@@ -25,7 +25,8 @@ class LLM extends llm.LLM {
|
|
|
25
25
|
}
|
|
26
26
|
this.#client = this.#opts.client || new Anthropic({
|
|
27
27
|
baseURL: this.#opts.baseURL,
|
|
28
|
-
apiKey: this.#opts.apiKey
|
|
28
|
+
apiKey: this.#opts.apiKey,
|
|
29
|
+
maxRetries: this.#opts.maxRetries ?? 0
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
/** @returns Human-readable label for logging. */
|
|
@@ -45,6 +46,9 @@ class LLM extends llm.LLM {
|
|
|
45
46
|
return "api.anthropic.com";
|
|
46
47
|
}
|
|
47
48
|
}
|
|
49
|
+
async _prewarmImpl(signal) {
|
|
50
|
+
await this.#client.models.list({ limit: 1 }, { signal });
|
|
51
|
+
}
|
|
48
52
|
/**
|
|
49
53
|
* Converts a framework ChatContext into Anthropic's message format.
|
|
50
54
|
*
|
package/dist/llm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport Anthropic from '@anthropic-ai/sdk';\nimport type { APIConnectOptions } from '@livekit/agents';\nimport {\n APIConnectionError,\n APIStatusError,\n APITimeoutError,\n DEFAULT_API_CONNECT_OPTIONS,\n llm,\n} from '@livekit/agents';\nimport type { ChatModels } from './models.js';\n\n/** Configuration options for the Anthropic LLM plugin. */\nexport interface LLMOptions {\n /** The model identifier to use. */\n model: string | ChatModels;\n /** Anthropic API key. Falls back to the `ANTHROPIC_API_KEY` environment variable. */\n apiKey?: string;\n /** Custom base URL for the Anthropic API. */\n baseURL?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Pre-configured Anthropic client instance. */\n client?: Anthropic;\n /** Tool selection strategy. */\n toolChoice?: llm.ToolChoice;\n /** Whether to allow parallel tool calls. */\n parallelToolCalls?: boolean;\n /** Maximum number of tokens in the response. Defaults to 4096. */\n maxTokens?: number;\n}\n\nconst defaultLLMOptions: LLMOptions = {\n model: 'claude-sonnet-4-6',\n /* eslint-disable-next-line turbo/no-undeclared-env-vars */\n apiKey: process.env.ANTHROPIC_API_KEY,\n parallelToolCalls: true,\n};\n\n/**\n * Anthropic LLM provider for LiveKit Agents.\n *\n * @remarks\n * Implements the {@link llm.LLM} interface using the Anthropic Messages API.\n * Supports streaming, tool calling, and system prompt isolation required by\n * Claude 3.5+ models.\n */\nexport class LLM extends llm.LLM {\n #opts: LLMOptions;\n #client: Anthropic;\n\n constructor(opts: Partial<LLMOptions> = defaultLLMOptions) {\n super();\n\n this.#opts = { ...defaultLLMOptions, ...opts };\n if (!this.#opts.apiKey && !this.#opts.client) {\n throw new Error(\n 'Anthropic API key is required, whether as an argument or as $ANTHROPIC_API_KEY',\n );\n }\n\n this.#client =\n this.#opts.client ||\n new Anthropic({\n baseURL: this.#opts.baseURL,\n apiKey: this.#opts.apiKey,\n });\n }\n\n /** @returns Human-readable label for logging. */\n label(): string {\n return 'anthropic.LLM';\n }\n\n /** @returns The model identifier being used. */\n get model(): string {\n return this.#opts.model;\n }\n\n /** @returns The API provider host. */\n get provider(): string {\n try {\n const url = new URL(this.#client.baseURL);\n return url.host;\n } catch {\n return 'api.anthropic.com';\n }\n }\n\n /**\n * Converts a framework ChatContext into Anthropic's message format.\n *\n * @remarks\n * - System prompts are isolated into a separate `TextBlockParam[]` array\n * (required by Claude 3.5+).\n * - `function_call` items are mapped to Anthropic `tool_use` content blocks.\n * - `function_call_output` items are mapped to `tool_result` content blocks.\n * - Consecutive same-role messages are merged to satisfy Anthropic's\n * strict alternating-turn requirement.\n * - A dummy `(empty)` user message is injected if the conversation doesn't\n * start with a user turn.\n * - A dummy user message is appended if the conversation ends with an\n * assistant turn, since Claude 4.6+ does not support prefilling.\n */\n protected _buildAnthropicContext(chatCtx: llm.ChatContext): {\n system: Anthropic.TextBlockParam[];\n messages: Anthropic.MessageParam[];\n } {\n const system: Anthropic.TextBlockParam[] = [];\n const rawMessages: Anthropic.MessageParam[] = [];\n\n for (const msg of chatCtx.items) {\n if (msg.type === 'message') {\n const textContent = msg.rawTextContent || '';\n if (msg.role === 'system' || msg.role === 'developer') {\n system.push({ type: 'text', text: textContent });\n } else if (msg.role === 'user' || msg.role === 'assistant') {\n rawMessages.push({\n role: msg.role,\n content: textContent,\n });\n }\n } else if (msg.type === 'function_call') {\n // Map to Anthropic's tool_use content block (assistant role)\n rawMessages.push({\n role: 'assistant',\n content: [\n {\n type: 'tool_use',\n id: msg.callId,\n name: msg.name,\n input: JSON.parse(msg.args || '{}'),\n },\n ],\n });\n } else if (msg.type === 'function_call_output') {\n // Map to Anthropic's tool_result content block (user role)\n rawMessages.push({\n role: 'user',\n content: [\n {\n type: 'tool_result',\n tool_use_id: msg.callId,\n content: msg.output,\n is_error: msg.isError,\n },\n ],\n });\n }\n }\n\n // Merge consecutive same-role messages (Anthropic requires alternating turns)\n const messages: Anthropic.MessageParam[] = [];\n for (const msg of rawMessages) {\n const prev = messages[messages.length - 1];\n if (prev && prev.role === msg.role) {\n // Merge content into a single content array\n const prevContent = Array.isArray(prev.content)\n ? prev.content\n : [{ type: 'text' as const, text: prev.content as string }];\n const curContent = Array.isArray(msg.content)\n ? msg.content\n : [{ type: 'text' as const, text: msg.content as string }];\n prev.content = [...prevContent, ...curContent] as Anthropic.ContentBlockParam[];\n } else {\n messages.push({ ...msg });\n }\n }\n\n // Anthropic requires conversations to start with a user turn\n if (messages.length === 0 || messages[0]!.role !== 'user') {\n messages.unshift({ role: 'user', content: '(empty)' });\n }\n\n // Claude 4.6+ does not support prefilling (trailing assistant messages).\n if (messages.length > 0 && messages[messages.length - 1]!.role === 'assistant') {\n messages.push({ role: 'user', content: [{ type: 'text', text: '.' }] });\n }\n\n return { system, messages };\n }\n\n /**\n * Creates a streaming chat completion.\n *\n * @remarks\n * Maps `toolChoice` to Anthropic's format:\n * - `\"required\"` → `{ type: \"any\" }`\n * - `\"none\"` → clears tools\n * - `{ type: \"function\", function: { name } }` → `{ type: \"tool\", name }`\n */\n chat({\n chatCtx,\n toolCtx: toolCtxInput,\n connOptions = DEFAULT_API_CONNECT_OPTIONS,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContextLike;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: llm.ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream {\n const extras: Record<string, unknown> = { ...extraKwargs };\n\n if (this.#opts.temperature !== undefined) extras.temperature = this.#opts.temperature;\n\n const { system, messages } = this._buildAnthropicContext(chatCtx);\n\n // Build Anthropic tool schemas\n const toolCtx = llm.toToolContext(toolCtxInput);\n const anthropicTools: Anthropic.Tool[] = [];\n if (toolCtx) {\n for (const [name, tool] of llm.sortedToolEntries(toolCtx)) {\n anthropicTools.push({\n name: name,\n description: tool.description || '',\n input_schema: (tool.parameters\n ? llm.toJsonSchema(tool.parameters, false)\n : { type: 'object', properties: {} }) as Anthropic.Tool.InputSchema,\n });\n }\n }\n\n // Map toolChoice and parallelToolCalls to Anthropic format\n const resolvedToolChoice = toolChoice ?? this.#opts.toolChoice;\n const resolvedParallel = parallelToolCalls ?? this.#opts.parallelToolCalls;\n\n if ((resolvedToolChoice || resolvedParallel !== undefined) && anthropicTools.length > 0) {\n let anthropicToolChoice: Record<string, unknown> | undefined = { type: 'auto' };\n\n if (typeof resolvedToolChoice === 'string') {\n if (resolvedToolChoice === 'required') {\n anthropicToolChoice = { type: 'any' };\n } else if (resolvedToolChoice === 'none') {\n // Clear tools entirely when none is requested\n anthropicTools.length = 0;\n anthropicToolChoice = undefined;\n }\n } else if (\n typeof resolvedToolChoice === 'object' &&\n 'type' in resolvedToolChoice &&\n resolvedToolChoice.type === 'function'\n ) {\n const fn = (resolvedToolChoice as { function: { name: string } }).function;\n anthropicToolChoice = { type: 'tool', name: fn.name };\n }\n\n if (anthropicToolChoice) {\n // Map parallelToolCalls\n if (resolvedParallel !== undefined) {\n anthropicToolChoice.disable_parallel_tool_use = !resolvedParallel;\n }\n extras.tool_choice = anthropicToolChoice;\n }\n }\n\n const requestParams: Anthropic.MessageCreateParamsStreaming = {\n model: this.#opts.model,\n messages: messages,\n system: system.length > 0 ? system : undefined,\n tools: anthropicTools.length > 0 ? anthropicTools : undefined,\n stream: true,\n max_tokens: this.#opts.maxTokens || 4096,\n ...extras,\n };\n\n return new LLMStream(this, this.#client, requestParams, chatCtx, toolCtx, connOptions);\n }\n}\n\n/**\n * Streaming response handler for Anthropic Messages API.\n *\n * @remarks\n * Parses SSE events including:\n * - `content_block_start` / `content_block_delta` / `content_block_stop` for text and tool use\n * - `message_start` / `message_delta` for token usage tracking\n * - Chain-of-thought `<thinking>` block filtering when tools are active\n */\nexport class LLMStream extends llm.LLMStream {\n #client: Anthropic;\n #requestParams: Anthropic.MessageCreateParamsStreaming;\n #toolCallId?: string;\n #fncName?: string;\n #fncRawArgs?: string;\n #requestId = '';\n #ignoringCoT = false;\n #inputTokens = 0;\n #outputTokens = 0;\n #cacheCreationTokens = 0;\n #cacheReadTokens = 0;\n #toolCtx?: llm.ToolContext;\n\n constructor(\n llmInst: LLM,\n client: Anthropic,\n requestParams: Anthropic.MessageCreateParamsStreaming,\n chatCtx: llm.ChatContext,\n toolCtx: llm.ToolContext | undefined,\n connOptions: APIConnectOptions,\n ) {\n super(llmInst, { chatCtx, toolCtx, connOptions });\n this.#client = client;\n this.#requestParams = requestParams;\n this.#toolCtx = toolCtx;\n }\n\n protected async run(): Promise<void> {\n let retryable = true;\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n this.#requestId = '';\n this.#ignoringCoT = false;\n this.#inputTokens = 0;\n this.#outputTokens = 0;\n this.#cacheCreationTokens = 0;\n this.#cacheReadTokens = 0;\n\n try {\n const stream = await this.#client.messages.create(this.#requestParams, {\n timeout: this.connOptions.timeoutMs,\n });\n for await (const event of stream) {\n if (event.type === 'message_start') {\n this.#requestId = event.message.id;\n this.#inputTokens = event.message.usage.input_tokens;\n this.#outputTokens = event.message.usage.output_tokens;\n this.#cacheCreationTokens = event.message.usage.cache_creation_input_tokens ?? 0;\n this.#cacheReadTokens = event.message.usage.cache_read_input_tokens ?? 0;\n } else if (event.type === 'message_delta') {\n this.#outputTokens = event.usage.output_tokens;\n } else if (\n event.type === 'content_block_start' &&\n event.content_block.type === 'tool_use'\n ) {\n this.#toolCallId = event.content_block.id;\n this.#fncName = event.content_block.name;\n this.#fncRawArgs = '';\n } else if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {\n let text = event.delta.text;\n\n // Filter chain-of-thought <thinking> blocks when tools are active\n if (this.#toolCtx) {\n const thinkingStart = text.indexOf('<thinking>');\n if (thinkingStart >= 0) {\n const preThinking = text.slice(0, thinkingStart);\n if (preThinking) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: preThinking },\n });\n retryable = false;\n }\n text = text.slice(thinkingStart + '<thinking>'.length);\n this.#ignoringCoT = true;\n }\n if (this.#ignoringCoT) {\n const thinkingEnd = text.indexOf('</thinking>');\n if (thinkingEnd >= 0) {\n text = text.slice(thinkingEnd + '</thinking>'.length);\n this.#ignoringCoT = false;\n }\n }\n }\n\n if (this.#ignoringCoT) {\n continue;\n }\n\n // A delta can be reduced to an empty string when it was entirely a\n // <thinking> block; don't emit it or mark the stream non-retryable.\n if (text) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: text },\n });\n retryable = false;\n }\n } else if (\n event.type === 'content_block_delta' &&\n event.delta.type === 'input_json_delta'\n ) {\n this.#fncRawArgs += event.delta.partial_json;\n } else if (event.type === 'content_block_stop' && this.#toolCallId) {\n this.queue.put({\n id: this.#requestId,\n delta: {\n role: 'assistant',\n toolCalls: [\n llm.FunctionCall.create({\n callId: this.#toolCallId,\n name: this.#fncName || '',\n args: this.#fncRawArgs || '',\n }),\n ],\n },\n });\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n retryable = false;\n }\n }\n\n // Emit final usage chunk. Anthropic reports cached tokens separately from\n // input_tokens, so fold them into promptTokens like the Python plugin does.\n const promptTokens = this.#inputTokens + this.#cacheCreationTokens + this.#cacheReadTokens;\n this.queue.put({\n id: this.#requestId,\n usage: {\n completionTokens: this.#outputTokens,\n promptTokens,\n totalTokens: promptTokens + this.#outputTokens,\n promptCachedTokens: this.#cacheReadTokens,\n },\n });\n } catch (e: unknown) {\n if (e instanceof Anthropic.APIError) {\n if (e.status === 408) {\n throw new APITimeoutError({\n message: e.message,\n options: { retryable },\n });\n }\n throw new APIStatusError({\n message: e.message,\n options: {\n statusCode: e.status,\n body: e.error as object,\n retryable: retryable && (e.status === 429 || e.status >= 500),\n },\n });\n }\n throw new APIConnectionError({\n message: e instanceof Error ? e.message : String(e),\n options: { retryable },\n });\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport Anthropic from '@anthropic-ai/sdk';\nimport type { APIConnectOptions } from '@livekit/agents';\nimport {\n APIConnectionError,\n APIStatusError,\n APITimeoutError,\n DEFAULT_API_CONNECT_OPTIONS,\n llm,\n} from '@livekit/agents';\nimport type { ChatModels } from './models.js';\n\n/** Configuration options for the Anthropic LLM plugin. */\nexport interface LLMOptions {\n /** The model identifier to use. */\n model: string | ChatModels;\n /** Anthropic API key. Falls back to the `ANTHROPIC_API_KEY` environment variable. */\n apiKey?: string;\n /** Custom base URL for the Anthropic API. */\n baseURL?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Pre-configured Anthropic client instance. */\n client?: Anthropic;\n /** Vendor client retries. Defaults to 0 because the framework owns retries. */\n maxRetries?: number;\n /** Tool selection strategy. */\n toolChoice?: llm.ToolChoice;\n /** Whether to allow parallel tool calls. */\n parallelToolCalls?: boolean;\n /** Maximum number of tokens in the response. Defaults to 4096. */\n maxTokens?: number;\n}\n\nconst defaultLLMOptions: LLMOptions = {\n model: 'claude-sonnet-4-6',\n /* eslint-disable-next-line turbo/no-undeclared-env-vars */\n apiKey: process.env.ANTHROPIC_API_KEY,\n parallelToolCalls: true,\n};\n\n/**\n * Anthropic LLM provider for LiveKit Agents.\n *\n * @remarks\n * Implements the {@link llm.LLM} interface using the Anthropic Messages API.\n * Supports streaming, tool calling, and system prompt isolation required by\n * Claude 3.5+ models.\n */\nexport class LLM extends llm.LLM {\n #opts: LLMOptions;\n #client: Anthropic;\n\n constructor(opts: Partial<LLMOptions> = defaultLLMOptions) {\n super();\n\n this.#opts = { ...defaultLLMOptions, ...opts };\n if (!this.#opts.apiKey && !this.#opts.client) {\n throw new Error(\n 'Anthropic API key is required, whether as an argument or as $ANTHROPIC_API_KEY',\n );\n }\n\n this.#client =\n this.#opts.client ||\n new Anthropic({\n baseURL: this.#opts.baseURL,\n apiKey: this.#opts.apiKey,\n maxRetries: this.#opts.maxRetries ?? 0,\n });\n }\n\n /** @returns Human-readable label for logging. */\n label(): string {\n return 'anthropic.LLM';\n }\n\n /** @returns The model identifier being used. */\n get model(): string {\n return this.#opts.model;\n }\n\n /** @returns The API provider host. */\n get provider(): string {\n try {\n const url = new URL(this.#client.baseURL);\n return url.host;\n } catch {\n return 'api.anthropic.com';\n }\n }\n\n protected override async _prewarmImpl(signal: AbortSignal): Promise<void> {\n await this.#client.models.list({ limit: 1 }, { signal });\n }\n\n /**\n * Converts a framework ChatContext into Anthropic's message format.\n *\n * @remarks\n * - System prompts are isolated into a separate `TextBlockParam[]` array\n * (required by Claude 3.5+).\n * - `function_call` items are mapped to Anthropic `tool_use` content blocks.\n * - `function_call_output` items are mapped to `tool_result` content blocks.\n * - Consecutive same-role messages are merged to satisfy Anthropic's\n * strict alternating-turn requirement.\n * - A dummy `(empty)` user message is injected if the conversation doesn't\n * start with a user turn.\n * - A dummy user message is appended if the conversation ends with an\n * assistant turn, since Claude 4.6+ does not support prefilling.\n */\n protected _buildAnthropicContext(chatCtx: llm.ChatContext): {\n system: Anthropic.TextBlockParam[];\n messages: Anthropic.MessageParam[];\n } {\n const system: Anthropic.TextBlockParam[] = [];\n const rawMessages: Anthropic.MessageParam[] = [];\n\n for (const msg of chatCtx.items) {\n if (msg.type === 'message') {\n const textContent = msg.rawTextContent || '';\n if (msg.role === 'system' || msg.role === 'developer') {\n system.push({ type: 'text', text: textContent });\n } else if (msg.role === 'user' || msg.role === 'assistant') {\n rawMessages.push({\n role: msg.role,\n content: textContent,\n });\n }\n } else if (msg.type === 'function_call') {\n // Map to Anthropic's tool_use content block (assistant role)\n rawMessages.push({\n role: 'assistant',\n content: [\n {\n type: 'tool_use',\n id: msg.callId,\n name: msg.name,\n input: JSON.parse(msg.args || '{}'),\n },\n ],\n });\n } else if (msg.type === 'function_call_output') {\n // Map to Anthropic's tool_result content block (user role)\n rawMessages.push({\n role: 'user',\n content: [\n {\n type: 'tool_result',\n tool_use_id: msg.callId,\n content: msg.output,\n is_error: msg.isError,\n },\n ],\n });\n }\n }\n\n // Merge consecutive same-role messages (Anthropic requires alternating turns)\n const messages: Anthropic.MessageParam[] = [];\n for (const msg of rawMessages) {\n const prev = messages[messages.length - 1];\n if (prev && prev.role === msg.role) {\n // Merge content into a single content array\n const prevContent = Array.isArray(prev.content)\n ? prev.content\n : [{ type: 'text' as const, text: prev.content as string }];\n const curContent = Array.isArray(msg.content)\n ? msg.content\n : [{ type: 'text' as const, text: msg.content as string }];\n prev.content = [...prevContent, ...curContent] as Anthropic.ContentBlockParam[];\n } else {\n messages.push({ ...msg });\n }\n }\n\n // Anthropic requires conversations to start with a user turn\n if (messages.length === 0 || messages[0]!.role !== 'user') {\n messages.unshift({ role: 'user', content: '(empty)' });\n }\n\n // Claude 4.6+ does not support prefilling (trailing assistant messages).\n if (messages.length > 0 && messages[messages.length - 1]!.role === 'assistant') {\n messages.push({ role: 'user', content: [{ type: 'text', text: '.' }] });\n }\n\n return { system, messages };\n }\n\n /**\n * Creates a streaming chat completion.\n *\n * @remarks\n * Maps `toolChoice` to Anthropic's format:\n * - `\"required\"` → `{ type: \"any\" }`\n * - `\"none\"` → clears tools\n * - `{ type: \"function\", function: { name } }` → `{ type: \"tool\", name }`\n */\n chat({\n chatCtx,\n toolCtx: toolCtxInput,\n connOptions = DEFAULT_API_CONNECT_OPTIONS,\n parallelToolCalls,\n toolChoice,\n extraKwargs,\n }: {\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContextLike;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: llm.ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): LLMStream {\n const extras: Record<string, unknown> = { ...extraKwargs };\n\n if (this.#opts.temperature !== undefined) extras.temperature = this.#opts.temperature;\n\n const { system, messages } = this._buildAnthropicContext(chatCtx);\n\n // Build Anthropic tool schemas\n const toolCtx = llm.toToolContext(toolCtxInput);\n const anthropicTools: Anthropic.Tool[] = [];\n if (toolCtx) {\n for (const [name, tool] of llm.sortedToolEntries(toolCtx)) {\n anthropicTools.push({\n name: name,\n description: tool.description || '',\n input_schema: (tool.parameters\n ? llm.toJsonSchema(tool.parameters, false)\n : { type: 'object', properties: {} }) as Anthropic.Tool.InputSchema,\n });\n }\n }\n\n // Map toolChoice and parallelToolCalls to Anthropic format\n const resolvedToolChoice = toolChoice ?? this.#opts.toolChoice;\n const resolvedParallel = parallelToolCalls ?? this.#opts.parallelToolCalls;\n\n if ((resolvedToolChoice || resolvedParallel !== undefined) && anthropicTools.length > 0) {\n let anthropicToolChoice: Record<string, unknown> | undefined = { type: 'auto' };\n\n if (typeof resolvedToolChoice === 'string') {\n if (resolvedToolChoice === 'required') {\n anthropicToolChoice = { type: 'any' };\n } else if (resolvedToolChoice === 'none') {\n // Clear tools entirely when none is requested\n anthropicTools.length = 0;\n anthropicToolChoice = undefined;\n }\n } else if (\n typeof resolvedToolChoice === 'object' &&\n 'type' in resolvedToolChoice &&\n resolvedToolChoice.type === 'function'\n ) {\n const fn = (resolvedToolChoice as { function: { name: string } }).function;\n anthropicToolChoice = { type: 'tool', name: fn.name };\n }\n\n if (anthropicToolChoice) {\n // Map parallelToolCalls\n if (resolvedParallel !== undefined) {\n anthropicToolChoice.disable_parallel_tool_use = !resolvedParallel;\n }\n extras.tool_choice = anthropicToolChoice;\n }\n }\n\n const requestParams: Anthropic.MessageCreateParamsStreaming = {\n model: this.#opts.model,\n messages: messages,\n system: system.length > 0 ? system : undefined,\n tools: anthropicTools.length > 0 ? anthropicTools : undefined,\n stream: true,\n max_tokens: this.#opts.maxTokens || 4096,\n ...extras,\n };\n\n return new LLMStream(this, this.#client, requestParams, chatCtx, toolCtx, connOptions);\n }\n}\n\n/**\n * Streaming response handler for Anthropic Messages API.\n *\n * @remarks\n * Parses SSE events including:\n * - `content_block_start` / `content_block_delta` / `content_block_stop` for text and tool use\n * - `message_start` / `message_delta` for token usage tracking\n * - Chain-of-thought `<thinking>` block filtering when tools are active\n */\nexport class LLMStream extends llm.LLMStream {\n #client: Anthropic;\n #requestParams: Anthropic.MessageCreateParamsStreaming;\n #toolCallId?: string;\n #fncName?: string;\n #fncRawArgs?: string;\n #requestId = '';\n #ignoringCoT = false;\n #inputTokens = 0;\n #outputTokens = 0;\n #cacheCreationTokens = 0;\n #cacheReadTokens = 0;\n #toolCtx?: llm.ToolContext;\n\n constructor(\n llmInst: LLM,\n client: Anthropic,\n requestParams: Anthropic.MessageCreateParamsStreaming,\n chatCtx: llm.ChatContext,\n toolCtx: llm.ToolContext | undefined,\n connOptions: APIConnectOptions,\n ) {\n super(llmInst, { chatCtx, toolCtx, connOptions });\n this.#client = client;\n this.#requestParams = requestParams;\n this.#toolCtx = toolCtx;\n }\n\n protected async run(): Promise<void> {\n let retryable = true;\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n this.#requestId = '';\n this.#ignoringCoT = false;\n this.#inputTokens = 0;\n this.#outputTokens = 0;\n this.#cacheCreationTokens = 0;\n this.#cacheReadTokens = 0;\n\n try {\n const stream = await this.#client.messages.create(this.#requestParams, {\n timeout: this.connOptions.timeoutMs,\n });\n for await (const event of stream) {\n if (event.type === 'message_start') {\n this.#requestId = event.message.id;\n this.#inputTokens = event.message.usage.input_tokens;\n this.#outputTokens = event.message.usage.output_tokens;\n this.#cacheCreationTokens = event.message.usage.cache_creation_input_tokens ?? 0;\n this.#cacheReadTokens = event.message.usage.cache_read_input_tokens ?? 0;\n } else if (event.type === 'message_delta') {\n this.#outputTokens = event.usage.output_tokens;\n } else if (\n event.type === 'content_block_start' &&\n event.content_block.type === 'tool_use'\n ) {\n this.#toolCallId = event.content_block.id;\n this.#fncName = event.content_block.name;\n this.#fncRawArgs = '';\n } else if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {\n let text = event.delta.text;\n\n // Filter chain-of-thought <thinking> blocks when tools are active\n if (this.#toolCtx) {\n const thinkingStart = text.indexOf('<thinking>');\n if (thinkingStart >= 0) {\n const preThinking = text.slice(0, thinkingStart);\n if (preThinking) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: preThinking },\n });\n retryable = false;\n }\n text = text.slice(thinkingStart + '<thinking>'.length);\n this.#ignoringCoT = true;\n }\n if (this.#ignoringCoT) {\n const thinkingEnd = text.indexOf('</thinking>');\n if (thinkingEnd >= 0) {\n text = text.slice(thinkingEnd + '</thinking>'.length);\n this.#ignoringCoT = false;\n }\n }\n }\n\n if (this.#ignoringCoT) {\n continue;\n }\n\n // A delta can be reduced to an empty string when it was entirely a\n // <thinking> block; don't emit it or mark the stream non-retryable.\n if (text) {\n this.queue.put({\n id: this.#requestId,\n delta: { role: 'assistant', content: text },\n });\n retryable = false;\n }\n } else if (\n event.type === 'content_block_delta' &&\n event.delta.type === 'input_json_delta'\n ) {\n this.#fncRawArgs += event.delta.partial_json;\n } else if (event.type === 'content_block_stop' && this.#toolCallId) {\n this.queue.put({\n id: this.#requestId,\n delta: {\n role: 'assistant',\n toolCalls: [\n llm.FunctionCall.create({\n callId: this.#toolCallId,\n name: this.#fncName || '',\n args: this.#fncRawArgs || '',\n }),\n ],\n },\n });\n this.#toolCallId = undefined;\n this.#fncName = undefined;\n this.#fncRawArgs = undefined;\n retryable = false;\n }\n }\n\n // Emit final usage chunk. Anthropic reports cached tokens separately from\n // input_tokens, so fold them into promptTokens like the Python plugin does.\n const promptTokens = this.#inputTokens + this.#cacheCreationTokens + this.#cacheReadTokens;\n this.queue.put({\n id: this.#requestId,\n usage: {\n completionTokens: this.#outputTokens,\n promptTokens,\n totalTokens: promptTokens + this.#outputTokens,\n promptCachedTokens: this.#cacheReadTokens,\n },\n });\n } catch (e: unknown) {\n if (e instanceof Anthropic.APIError) {\n if (e.status === 408) {\n throw new APITimeoutError({\n message: e.message,\n options: { retryable },\n });\n }\n throw new APIStatusError({\n message: e.message,\n options: {\n statusCode: e.status,\n body: e.error as object,\n retryable: retryable && (e.status === 429 || e.status >= 500),\n },\n });\n }\n throw new APIConnectionError({\n message: e instanceof Error ? e.message : String(e),\n options: { retryable },\n });\n }\n }\n}\n"],"mappings":"AAGA,OAAO,eAAe;AAEtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAyBP,MAAM,oBAAgC;AAAA,EACpC,OAAO;AAAA;AAAA,EAEP,QAAQ,QAAQ,IAAI;AAAA,EACpB,mBAAmB;AACrB;AAUO,MAAM,YAAY,IAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EAEA,YAAY,OAA4B,mBAAmB;AACzD,UAAM;AAEN,SAAK,QAAQ,EAAE,GAAG,mBAAmB,GAAG,KAAK;AAC7C,QAAI,CAAC,KAAK,MAAM,UAAU,CAAC,KAAK,MAAM,QAAQ;AAC5C,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,SAAK,UACH,KAAK,MAAM,UACX,IAAI,UAAU;AAAA,MACZ,SAAS,KAAK,MAAM;AAAA,MACpB,QAAQ,KAAK,MAAM;AAAA,MACnB,YAAY,KAAK,MAAM,cAAc;AAAA,IACvC,CAAC;AAAA,EACL;AAAA;AAAA,EAGA,QAAgB;AACd,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,QAAI;AACF,YAAM,MAAM,IAAI,IAAI,KAAK,QAAQ,OAAO;AACxC,aAAO,IAAI;AAAA,IACb,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAyB,aAAa,QAAoC;AACxE,UAAM,KAAK,QAAQ,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBU,uBAAuB,SAG/B;AACA,UAAM,SAAqC,CAAC;AAC5C,UAAM,cAAwC,CAAC;AAE/C,eAAW,OAAO,QAAQ,OAAO;AAC/B,UAAI,IAAI,SAAS,WAAW;AAC1B,cAAM,cAAc,IAAI,kBAAkB;AAC1C,YAAI,IAAI,SAAS,YAAY,IAAI,SAAS,aAAa;AACrD,iBAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,YAAY,CAAC;AAAA,QACjD,WAAW,IAAI,SAAS,UAAU,IAAI,SAAS,aAAa;AAC1D,sBAAY,KAAK;AAAA,YACf,MAAM,IAAI;AAAA,YACV,SAAS;AAAA,UACX,CAAC;AAAA,QACH;AAAA,MACF,WAAW,IAAI,SAAS,iBAAiB;AAEvC,oBAAY,KAAK;AAAA,UACf,MAAM;AAAA,UACN,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,IAAI,IAAI;AAAA,cACR,MAAM,IAAI;AAAA,cACV,OAAO,KAAK,MAAM,IAAI,QAAQ,IAAI;AAAA,YACpC;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,WAAW,IAAI,SAAS,wBAAwB;AAE9C,oBAAY,KAAK;AAAA,UACf,MAAM;AAAA,UACN,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,aAAa,IAAI;AAAA,cACjB,SAAS,IAAI;AAAA,cACb,UAAU,IAAI;AAAA,YAChB;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,WAAqC,CAAC;AAC5C,eAAW,OAAO,aAAa;AAC7B,YAAM,OAAO,SAAS,SAAS,SAAS,CAAC;AACzC,UAAI,QAAQ,KAAK,SAAS,IAAI,MAAM;AAElC,cAAM,cAAc,MAAM,QAAQ,KAAK,OAAO,IAC1C,KAAK,UACL,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,QAAkB,CAAC;AAC5D,cAAM,aAAa,MAAM,QAAQ,IAAI,OAAO,IACxC,IAAI,UACJ,CAAC,EAAE,MAAM,QAAiB,MAAM,IAAI,QAAkB,CAAC;AAC3D,aAAK,UAAU,CAAC,GAAG,aAAa,GAAG,UAAU;AAAA,MAC/C,OAAO;AACL,iBAAS,KAAK,EAAE,GAAG,IAAI,CAAC;AAAA,MAC1B;AAAA,IACF;AAGA,QAAI,SAAS,WAAW,KAAK,SAAS,CAAC,EAAG,SAAS,QAAQ;AACzD,eAAS,QAAQ,EAAE,MAAM,QAAQ,SAAS,UAAU,CAAC;AAAA,IACvD;AAGA,QAAI,SAAS,SAAS,KAAK,SAAS,SAAS,SAAS,CAAC,EAAG,SAAS,aAAa;AAC9E,eAAS,KAAK,EAAE,MAAM,QAAQ,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,IAAI,CAAC,EAAE,CAAC;AAAA,IACxE;AAEA,WAAO,EAAE,QAAQ,SAAS;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,KAAK;AAAA,IACH;AAAA,IACA,SAAS;AAAA,IACT,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAOc;AACZ,UAAM,SAAkC,EAAE,GAAG,YAAY;AAEzD,QAAI,KAAK,MAAM,gBAAgB,OAAW,QAAO,cAAc,KAAK,MAAM;AAE1E,UAAM,EAAE,QAAQ,SAAS,IAAI,KAAK,uBAAuB,OAAO;AAGhE,UAAM,UAAU,IAAI,cAAc,YAAY;AAC9C,UAAM,iBAAmC,CAAC;AAC1C,QAAI,SAAS;AACX,iBAAW,CAAC,MAAM,IAAI,KAAK,IAAI,kBAAkB,OAAO,GAAG;AACzD,uBAAe,KAAK;AAAA,UAClB;AAAA,UACA,aAAa,KAAK,eAAe;AAAA,UACjC,cAAe,KAAK,aAChB,IAAI,aAAa,KAAK,YAAY,KAAK,IACvC,EAAE,MAAM,UAAU,YAAY,CAAC,EAAE;AAAA,QACvC,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,qBAAqB,cAAc,KAAK,MAAM;AACpD,UAAM,mBAAmB,qBAAqB,KAAK,MAAM;AAEzD,SAAK,sBAAsB,qBAAqB,WAAc,eAAe,SAAS,GAAG;AACvF,UAAI,sBAA2D,EAAE,MAAM,OAAO;AAE9E,UAAI,OAAO,uBAAuB,UAAU;AAC1C,YAAI,uBAAuB,YAAY;AACrC,gCAAsB,EAAE,MAAM,MAAM;AAAA,QACtC,WAAW,uBAAuB,QAAQ;AAExC,yBAAe,SAAS;AACxB,gCAAsB;AAAA,QACxB;AAAA,MACF,WACE,OAAO,uBAAuB,YAC9B,UAAU,sBACV,mBAAmB,SAAS,YAC5B;AACA,cAAM,KAAM,mBAAsD;AAClE,8BAAsB,EAAE,MAAM,QAAQ,MAAM,GAAG,KAAK;AAAA,MACtD;AAEA,UAAI,qBAAqB;AAEvB,YAAI,qBAAqB,QAAW;AAClC,8BAAoB,4BAA4B,CAAC;AAAA,QACnD;AACA,eAAO,cAAc;AAAA,MACvB;AAAA,IACF;AAEA,UAAM,gBAAwD;AAAA,MAC5D,OAAO,KAAK,MAAM;AAAA,MAClB;AAAA,MACA,QAAQ,OAAO,SAAS,IAAI,SAAS;AAAA,MACrC,OAAO,eAAe,SAAS,IAAI,iBAAiB;AAAA,MACpD,QAAQ;AAAA,MACR,YAAY,KAAK,MAAM,aAAa;AAAA,MACpC,GAAG;AAAA,IACL;AAEA,WAAO,IAAI,UAAU,MAAM,KAAK,SAAS,eAAe,SAAS,SAAS,WAAW;AAAA,EACvF;AACF;AAWO,MAAM,kBAAkB,IAAI,UAAU;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,eAAe;AAAA,EACf,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,uBAAuB;AAAA,EACvB,mBAAmB;AAAA,EACnB;AAAA,EAEA,YACE,SACA,QACA,eACA,SACA,SACA,aACA;AACA,UAAM,SAAS,EAAE,SAAS,SAAS,YAAY,CAAC;AAChD,SAAK,UAAU;AACf,SAAK,iBAAiB;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAgB,MAAqB;AACnC,QAAI,YAAY;AAChB,SAAK,cAAc;AACnB,SAAK,WAAW;AAChB,SAAK,cAAc;AACnB,SAAK,aAAa;AAClB,SAAK,eAAe;AACpB,SAAK,eAAe;AACpB,SAAK,gBAAgB;AACrB,SAAK,uBAAuB;AAC5B,SAAK,mBAAmB;AAExB,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,QAAQ,SAAS,OAAO,KAAK,gBAAgB;AAAA,QACrE,SAAS,KAAK,YAAY;AAAA,MAC5B,CAAC;AACD,uBAAiB,SAAS,QAAQ;AAChC,YAAI,MAAM,SAAS,iBAAiB;AAClC,eAAK,aAAa,MAAM,QAAQ;AAChC,eAAK,eAAe,MAAM,QAAQ,MAAM;AACxC,eAAK,gBAAgB,MAAM,QAAQ,MAAM;AACzC,eAAK,uBAAuB,MAAM,QAAQ,MAAM,+BAA+B;AAC/E,eAAK,mBAAmB,MAAM,QAAQ,MAAM,2BAA2B;AAAA,QACzE,WAAW,MAAM,SAAS,iBAAiB;AACzC,eAAK,gBAAgB,MAAM,MAAM;AAAA,QACnC,WACE,MAAM,SAAS,yBACf,MAAM,cAAc,SAAS,YAC7B;AACA,eAAK,cAAc,MAAM,cAAc;AACvC,eAAK,WAAW,MAAM,cAAc;AACpC,eAAK,cAAc;AAAA,QACrB,WAAW,MAAM,SAAS,yBAAyB,MAAM,MAAM,SAAS,cAAc;AACpF,cAAI,OAAO,MAAM,MAAM;AAGvB,cAAI,KAAK,UAAU;AACjB,kBAAM,gBAAgB,KAAK,QAAQ,YAAY;AAC/C,gBAAI,iBAAiB,GAAG;AACtB,oBAAM,cAAc,KAAK,MAAM,GAAG,aAAa;AAC/C,kBAAI,aAAa;AACf,qBAAK,MAAM,IAAI;AAAA,kBACb,IAAI,KAAK;AAAA,kBACT,OAAO,EAAE,MAAM,aAAa,SAAS,YAAY;AAAA,gBACnD,CAAC;AACD,4BAAY;AAAA,cACd;AACA,qBAAO,KAAK,MAAM,gBAAgB,aAAa,MAAM;AACrD,mBAAK,eAAe;AAAA,YACtB;AACA,gBAAI,KAAK,cAAc;AACrB,oBAAM,cAAc,KAAK,QAAQ,aAAa;AAC9C,kBAAI,eAAe,GAAG;AACpB,uBAAO,KAAK,MAAM,cAAc,cAAc,MAAM;AACpD,qBAAK,eAAe;AAAA,cACtB;AAAA,YACF;AAAA,UACF;AAEA,cAAI,KAAK,cAAc;AACrB;AAAA,UACF;AAIA,cAAI,MAAM;AACR,iBAAK,MAAM,IAAI;AAAA,cACb,IAAI,KAAK;AAAA,cACT,OAAO,EAAE,MAAM,aAAa,SAAS,KAAK;AAAA,YAC5C,CAAC;AACD,wBAAY;AAAA,UACd;AAAA,QACF,WACE,MAAM,SAAS,yBACf,MAAM,MAAM,SAAS,oBACrB;AACA,eAAK,eAAe,MAAM,MAAM;AAAA,QAClC,WAAW,MAAM,SAAS,wBAAwB,KAAK,aAAa;AAClE,eAAK,MAAM,IAAI;AAAA,YACb,IAAI,KAAK;AAAA,YACT,OAAO;AAAA,cACL,MAAM;AAAA,cACN,WAAW;AAAA,gBACT,IAAI,aAAa,OAAO;AAAA,kBACtB,QAAQ,KAAK;AAAA,kBACb,MAAM,KAAK,YAAY;AAAA,kBACvB,MAAM,KAAK,eAAe;AAAA,gBAC5B,CAAC;AAAA,cACH;AAAA,YACF;AAAA,UACF,CAAC;AACD,eAAK,cAAc;AACnB,eAAK,WAAW;AAChB,eAAK,cAAc;AACnB,sBAAY;AAAA,QACd;AAAA,MACF;AAIA,YAAM,eAAe,KAAK,eAAe,KAAK,uBAAuB,KAAK;AAC1E,WAAK,MAAM,IAAI;AAAA,QACb,IAAI,KAAK;AAAA,QACT,OAAO;AAAA,UACL,kBAAkB,KAAK;AAAA,UACvB;AAAA,UACA,aAAa,eAAe,KAAK;AAAA,UACjC,oBAAoB,KAAK;AAAA,QAC3B;AAAA,MACF,CAAC;AAAA,IACH,SAAS,GAAY;AACnB,UAAI,aAAa,UAAU,UAAU;AACnC,YAAI,EAAE,WAAW,KAAK;AACpB,gBAAM,IAAI,gBAAgB;AAAA,YACxB,SAAS,EAAE;AAAA,YACX,SAAS,EAAE,UAAU;AAAA,UACvB,CAAC;AAAA,QACH;AACA,cAAM,IAAI,eAAe;AAAA,UACvB,SAAS,EAAE;AAAA,UACX,SAAS;AAAA,YACP,YAAY,EAAE;AAAA,YACd,MAAM,EAAE;AAAA,YACR,WAAW,cAAc,EAAE,WAAW,OAAO,EAAE,UAAU;AAAA,UAC3D;AAAA,QACF,CAAC;AAAA,MACH;AACA,YAAM,IAAI,mBAAmB;AAAA,QAC3B,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,QAClD,SAAS,EAAE,UAAU;AAAA,MACvB,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|
package/dist/llm.test.cjs
CHANGED
|
@@ -1,8 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
2
25
|
var import_agents = require("@livekit/agents");
|
|
3
26
|
var import_vitest = require("vitest");
|
|
4
27
|
var import_zod = require("zod");
|
|
5
28
|
var import_llm = require("./llm.cjs");
|
|
29
|
+
const { anthropicConstructor } = import_vitest.vi.hoisted(() => ({
|
|
30
|
+
anthropicConstructor: import_vitest.vi.fn()
|
|
31
|
+
}));
|
|
32
|
+
import_vitest.vi.mock("@anthropic-ai/sdk", async (importOriginal) => {
|
|
33
|
+
const { default: AnthropicClient } = await importOriginal();
|
|
34
|
+
return {
|
|
35
|
+
default: class extends AnthropicClient {
|
|
36
|
+
constructor(opts) {
|
|
37
|
+
super(opts);
|
|
38
|
+
anthropicConstructor(opts);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
(0, import_vitest.describe)("Anthropic LLM prewarm", () => {
|
|
44
|
+
(0, import_vitest.it)("lists one model with the prewarm cancellation signal", async () => {
|
|
45
|
+
let prewarmSignal;
|
|
46
|
+
const modelsList = import_vitest.vi.fn(
|
|
47
|
+
async (_params, options) => {
|
|
48
|
+
prewarmSignal = options.signal;
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
const client = {
|
|
52
|
+
baseURL: "https://api.anthropic.test",
|
|
53
|
+
models: { list: modelsList }
|
|
54
|
+
};
|
|
55
|
+
const llm2 = new import_llm.LLM({ model: "claude-sonnet-4-6", client });
|
|
56
|
+
llm2.prewarm();
|
|
57
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
58
|
+
(0, import_vitest.expect)(modelsList).toHaveBeenCalledWith({ limit: 1 }, { signal: import_vitest.expect.any(AbortSignal) });
|
|
59
|
+
(0, import_vitest.expect)(prewarmSignal == null ? void 0 : prewarmSignal.aborted).toBe(false);
|
|
60
|
+
await llm2.aclose();
|
|
61
|
+
(0, import_vitest.expect)(prewarmSignal == null ? void 0 : prewarmSignal.aborted).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
6
64
|
function messageStartEvent() {
|
|
7
65
|
return {
|
|
8
66
|
type: "message_start",
|
|
@@ -44,6 +102,33 @@ async function collectTextFromEvents(events, toolCtx) {
|
|
|
44
102
|
return textChunks.join("");
|
|
45
103
|
}
|
|
46
104
|
(0, import_vitest.describe)("Anthropic LLM", () => {
|
|
105
|
+
(0, import_vitest.it)("disables Anthropic SDK retries by default", () => {
|
|
106
|
+
var _a;
|
|
107
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
108
|
+
new import_llm.LLM({ apiKey: "dummy", model: "claude-3-5-sonnet-20241022" });
|
|
109
|
+
(0, import_vitest.expect)((_a = anthropicConstructor.mock.calls[callCount]) == null ? void 0 : _a[0]).toEqual(
|
|
110
|
+
import_vitest.expect.objectContaining({ maxRetries: 0 })
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
(0, import_vitest.it)("leaves an injected client and its retry policy untouched", () => {
|
|
114
|
+
const client = {
|
|
115
|
+
baseURL: "https://api.anthropic.test",
|
|
116
|
+
maxRetries: 7
|
|
117
|
+
};
|
|
118
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
119
|
+
const anthropicLlm = new import_llm.LLM({ client, model: "claude-3-5-sonnet-20241022" });
|
|
120
|
+
(0, import_vitest.expect)(anthropicConstructor.mock.calls).toHaveLength(callCount);
|
|
121
|
+
(0, import_vitest.expect)(anthropicLlm.provider).toBe("api.anthropic.test");
|
|
122
|
+
(0, import_vitest.expect)(client.maxRetries).toBe(7);
|
|
123
|
+
});
|
|
124
|
+
(0, import_vitest.it)("forwards an explicit maxRetries to the Anthropic SDK", () => {
|
|
125
|
+
var _a;
|
|
126
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
127
|
+
new import_llm.LLM({ apiKey: "dummy", model: "claude-3-5-sonnet-20241022", maxRetries: 2 });
|
|
128
|
+
(0, import_vitest.expect)((_a = anthropicConstructor.mock.calls[callCount]) == null ? void 0 : _a[0]).toEqual(
|
|
129
|
+
import_vitest.expect.objectContaining({ maxRetries: 2 })
|
|
130
|
+
);
|
|
131
|
+
});
|
|
47
132
|
(0, import_vitest.it)("correctly maps ChatContext to Anthropic system and messages arrays", () => {
|
|
48
133
|
const anthropicLlm = new import_llm.LLM({ apiKey: "dummy", model: "claude-3-5-sonnet-20241022" });
|
|
49
134
|
const chatCtx = new import_agents.llm.ChatContext();
|
package/dist/llm.test.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type Anthropic from '@anthropic-ai/sdk';\nimport { llm } from '@livekit/agents';\nimport { describe, expect, it } from 'vitest';\nimport { z } from 'zod';\nimport { LLM } from './llm.js';\n\nfunction messageStartEvent(): Anthropic.MessageStreamEvent {\n return {\n type: 'message_start',\n message: {\n id: 'msg_123',\n usage: { input_tokens: 0, output_tokens: 0 },\n },\n } as Anthropic.MessageStreamEvent;\n}\n\nfunction textDeltaEvent(text: string): Anthropic.MessageStreamEvent {\n return {\n type: 'content_block_delta',\n delta: { type: 'text_delta', text },\n } as Anthropic.MessageStreamEvent;\n}\n\nasync function collectTextFromEvents(\n events: Anthropic.MessageStreamEvent[],\n toolCtx?: llm.ToolContextLike,\n): Promise<string> {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield* events;\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n const textChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content) {\n textChunks.push(chunk.delta.content);\n }\n }\n return textChunks.join('');\n}\n\ndescribe('Anthropic LLM', () => {\n it('correctly maps ChatContext to Anthropic system and messages arrays', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({\n role: 'system',\n content: 'You are a mock agent.',\n });\n chatCtx.addMessage({\n role: 'user',\n content: 'Hello, world!',\n });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { system, messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Assert that system prompts were correctly isolated\n expect(system).toHaveLength(1);\n expect(system[0].text).toBe('You are a mock agent.');\n\n // Assert that strictly user messages ended up in the messages payload\n expect(messages).toHaveLength(1);\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('Hello, world!');\n });\n\n it('merges consecutive same-role messages', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'First message' });\n chatCtx.addMessage({ role: 'user', content: 'Second message' });\n chatCtx.addMessage({ role: 'assistant', content: 'Reply' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Two user messages should be merged into one with array content, followed by a\n // trailing dummy user turn for Claude 4.6+.\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(Array.isArray(messages[0].content)).toBe(true);\n expect(messages[0].content).toHaveLength(2);\n expect(messages[1].role).toBe('assistant');\n expect(messages[2].role).toBe('user');\n expect(messages[2].content).toEqual([{ type: 'text', text: '.' }]);\n });\n\n it('injects a dummy user message if conversation starts with assistant', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'system', content: 'System prompt' });\n chatCtx.addMessage({ role: 'assistant', content: 'I start talking' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Should have injected a dummy user message at the start\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('(empty)');\n expect(messages[1].role).toBe('assistant');\n });\n\n it('handles function_call and function_call_output items', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather?' });\n\n // Simulate a function call from the assistant\n chatCtx.items.push(\n new llm.FunctionCall({\n callId: 'call_123',\n name: 'get_weather',\n args: '{\"city\":\"London\"}',\n }),\n );\n\n // Simulate the tool result\n chatCtx.items.push(\n new llm.FunctionCallOutput({\n callId: 'call_123',\n name: 'get_weather',\n output: '{\"temp\": 20}',\n isError: false,\n }),\n );\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // user message, then assistant tool_use, then user tool_result\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(messages[1].role).toBe('assistant');\n expect(Array.isArray(messages[1].content)).toBe(true);\n expect(messages[1].content[0].type).toBe('tool_use');\n expect(messages[1].content[0].id).toBe('call_123');\n expect(messages[2].role).toBe('user');\n expect(Array.isArray(messages[2].content)).toBe(true);\n expect(messages[2].content[0].type).toBe('tool_result');\n });\n\n it('creates a fresh stream on retry', async () => {\n let calls = 0;\n const client = {\n messages: {\n create: async () => {\n calls += 1;\n if (calls === 1) {\n throw new Error('transient connect failure');\n }\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {})();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n anthropicLlm.on('error', () => {});\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({\n chatCtx,\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const chunks: llm.ChatChunk[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk);\n }\n\n expect(calls).toBe(2);\n expect(chunks.at(-1)?.usage).toEqual({\n completionTokens: 0,\n promptTokens: 0,\n promptCachedTokens: 0,\n totalTokens: 0,\n });\n });\n\n it('sends function tool schemas from a real ToolContext', async () => {\n let capturedParams: Anthropic.MessageCreateParamsStreaming | undefined;\n const client = {\n messages: {\n create: async (params: Anthropic.MessageCreateParamsStreaming) => {\n capturedParams = params;\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n })();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather in Tokyo?' });\n\n const toolCtx = llm.toToolContext({\n getWeather: llm.tool({\n description: 'Get the weather for a given location.',\n parameters: z.object({ location: z.string() }),\n execute: async () => 'sunny',\n }),\n });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n for await (const _ of stream) {\n // drain\n }\n\n expect(capturedParams?.tools).toHaveLength(1);\n const toolSchema = capturedParams!.tools![0] as Anthropic.Tool;\n expect(toolSchema.name).toBe('getWeather');\n expect(toolSchema.description).toBe('Get the weather for a given location.');\n expect(toolSchema.input_schema.type).toBe('object');\n expect(Object.keys(toolSchema.input_schema.properties ?? {})).toEqual(['location']);\n });\n\n it('does not emit an empty delta for a fully-filtered thinking block', async () => {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n yield textDeltaEvent('<thinking>only reasoning</thinking>');\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx: llm.ToolContext.empty() });\n const contentChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content !== undefined) {\n contentChunks.push(chunk.delta.content);\n }\n }\n\n // The whole delta was a thinking block: nothing (not even '') is emitted.\n expect(contentChunks).toEqual([]);\n });\n\n it('filters thinking blocks when tools are active', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('<thinking>hidden'),\n textDeltaEvent('still hidden</thinking>visible'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('visible');\n });\n\n it('does not filter thinking blocks without tools', async () => {\n const text = await collectTextFromEvents([\n messageStartEvent(),\n textDeltaEvent('<thinking>visible without tools</thinking>'),\n ]);\n\n expect(text).toBe('<thinking>visible without tools</thinking>');\n });\n\n it('preserves text around same-delta thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [messageStartEvent(), textDeltaEvent('before <thinking>hidden</thinking> after')],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n\n it('preserves text before split thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('before <thinking>hidden'),\n textDeltaEvent('still hidden</thinking> after'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nimport Anthropic from '@anthropic-ai/sdk';\nimport { llm } from '@livekit/agents';\nimport { describe, expect, it, vi } from 'vitest';\nimport { z } from 'zod';\nimport { LLM } from './llm.js';\n\nconst { anthropicConstructor } = vi.hoisted(() => ({\n anthropicConstructor: vi.fn<(opts?: { maxRetries?: number }) => void>(),\n}));\n\nvi.mock('@anthropic-ai/sdk', async (importOriginal) => {\n const { default: AnthropicClient } = await importOriginal<{ default: typeof Anthropic }>();\n\n return {\n default: class extends AnthropicClient {\n constructor(opts: ConstructorParameters<typeof AnthropicClient>[0]) {\n super(opts);\n anthropicConstructor(opts);\n }\n },\n };\n});\n\ndescribe('Anthropic LLM prewarm', () => {\n it('lists one model with the prewarm cancellation signal', async () => {\n let prewarmSignal: AbortSignal | undefined;\n const modelsList = vi.fn(\n async (_params: { limit: number }, options: { signal?: AbortSignal }) => {\n prewarmSignal = options.signal;\n },\n );\n const client = {\n baseURL: 'https://api.anthropic.test',\n models: { list: modelsList },\n } as unknown as Anthropic;\n const llm = new LLM({ model: 'claude-sonnet-4-6', client });\n\n llm.prewarm();\n await new Promise<void>((resolve) => setImmediate(resolve));\n\n expect(modelsList).toHaveBeenCalledWith({ limit: 1 }, { signal: expect.any(AbortSignal) });\n expect(prewarmSignal?.aborted).toBe(false);\n\n await llm.aclose();\n expect(prewarmSignal?.aborted).toBe(true);\n });\n});\n\nfunction messageStartEvent(): Anthropic.MessageStreamEvent {\n return {\n type: 'message_start',\n message: {\n id: 'msg_123',\n usage: { input_tokens: 0, output_tokens: 0 },\n },\n } as Anthropic.MessageStreamEvent;\n}\n\nfunction textDeltaEvent(text: string): Anthropic.MessageStreamEvent {\n return {\n type: 'content_block_delta',\n delta: { type: 'text_delta', text },\n } as Anthropic.MessageStreamEvent;\n}\n\nasync function collectTextFromEvents(\n events: Anthropic.MessageStreamEvent[],\n toolCtx?: llm.ToolContextLike,\n): Promise<string> {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield* events;\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n const textChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content) {\n textChunks.push(chunk.delta.content);\n }\n }\n return textChunks.join('');\n}\n\ndescribe('Anthropic LLM', () => {\n it('disables Anthropic SDK retries by default', () => {\n const callCount = anthropicConstructor.mock.calls.length;\n\n new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n expect(anthropicConstructor.mock.calls[callCount]?.[0]).toEqual(\n expect.objectContaining({ maxRetries: 0 }),\n );\n });\n\n it('leaves an injected client and its retry policy untouched', () => {\n const client = {\n baseURL: 'https://api.anthropic.test',\n maxRetries: 7,\n } as unknown as Anthropic;\n const callCount = anthropicConstructor.mock.calls.length;\n\n const anthropicLlm = new LLM({ client, model: 'claude-3-5-sonnet-20241022' });\n\n // The caller owns the client: no SDK client is constructed on its behalf.\n expect(anthropicConstructor.mock.calls).toHaveLength(callCount);\n expect(anthropicLlm.provider).toBe('api.anthropic.test');\n expect(client.maxRetries).toBe(7);\n });\n\n it('forwards an explicit maxRetries to the Anthropic SDK', () => {\n const callCount = anthropicConstructor.mock.calls.length;\n\n new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022', maxRetries: 2 });\n\n expect(anthropicConstructor.mock.calls[callCount]?.[0]).toEqual(\n expect.objectContaining({ maxRetries: 2 }),\n );\n });\n\n it('correctly maps ChatContext to Anthropic system and messages arrays', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({\n role: 'system',\n content: 'You are a mock agent.',\n });\n chatCtx.addMessage({\n role: 'user',\n content: 'Hello, world!',\n });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { system, messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Assert that system prompts were correctly isolated\n expect(system).toHaveLength(1);\n expect(system[0].text).toBe('You are a mock agent.');\n\n // Assert that strictly user messages ended up in the messages payload\n expect(messages).toHaveLength(1);\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('Hello, world!');\n });\n\n it('merges consecutive same-role messages', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'First message' });\n chatCtx.addMessage({ role: 'user', content: 'Second message' });\n chatCtx.addMessage({ role: 'assistant', content: 'Reply' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Two user messages should be merged into one with array content, followed by a\n // trailing dummy user turn for Claude 4.6+.\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(Array.isArray(messages[0].content)).toBe(true);\n expect(messages[0].content).toHaveLength(2);\n expect(messages[1].role).toBe('assistant');\n expect(messages[2].role).toBe('user');\n expect(messages[2].content).toEqual([{ type: 'text', text: '.' }]);\n });\n\n it('injects a dummy user message if conversation starts with assistant', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'system', content: 'System prompt' });\n chatCtx.addMessage({ role: 'assistant', content: 'I start talking' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Should have injected a dummy user message at the start\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('(empty)');\n expect(messages[1].role).toBe('assistant');\n });\n\n it('handles function_call and function_call_output items', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather?' });\n\n // Simulate a function call from the assistant\n chatCtx.items.push(\n new llm.FunctionCall({\n callId: 'call_123',\n name: 'get_weather',\n args: '{\"city\":\"London\"}',\n }),\n );\n\n // Simulate the tool result\n chatCtx.items.push(\n new llm.FunctionCallOutput({\n callId: 'call_123',\n name: 'get_weather',\n output: '{\"temp\": 20}',\n isError: false,\n }),\n );\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // user message, then assistant tool_use, then user tool_result\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(messages[1].role).toBe('assistant');\n expect(Array.isArray(messages[1].content)).toBe(true);\n expect(messages[1].content[0].type).toBe('tool_use');\n expect(messages[1].content[0].id).toBe('call_123');\n expect(messages[2].role).toBe('user');\n expect(Array.isArray(messages[2].content)).toBe(true);\n expect(messages[2].content[0].type).toBe('tool_result');\n });\n\n it('creates a fresh stream on retry', async () => {\n let calls = 0;\n const client = {\n messages: {\n create: async () => {\n calls += 1;\n if (calls === 1) {\n throw new Error('transient connect failure');\n }\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {})();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n anthropicLlm.on('error', () => {});\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({\n chatCtx,\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const chunks: llm.ChatChunk[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk);\n }\n\n expect(calls).toBe(2);\n expect(chunks.at(-1)?.usage).toEqual({\n completionTokens: 0,\n promptTokens: 0,\n promptCachedTokens: 0,\n totalTokens: 0,\n });\n });\n\n it('sends function tool schemas from a real ToolContext', async () => {\n let capturedParams: Anthropic.MessageCreateParamsStreaming | undefined;\n const client = {\n messages: {\n create: async (params: Anthropic.MessageCreateParamsStreaming) => {\n capturedParams = params;\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n })();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather in Tokyo?' });\n\n const toolCtx = llm.toToolContext({\n getWeather: llm.tool({\n description: 'Get the weather for a given location.',\n parameters: z.object({ location: z.string() }),\n execute: async () => 'sunny',\n }),\n });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n for await (const _ of stream) {\n // drain\n }\n\n expect(capturedParams?.tools).toHaveLength(1);\n const toolSchema = capturedParams!.tools![0] as Anthropic.Tool;\n expect(toolSchema.name).toBe('getWeather');\n expect(toolSchema.description).toBe('Get the weather for a given location.');\n expect(toolSchema.input_schema.type).toBe('object');\n expect(Object.keys(toolSchema.input_schema.properties ?? {})).toEqual(['location']);\n });\n\n it('does not emit an empty delta for a fully-filtered thinking block', async () => {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n yield textDeltaEvent('<thinking>only reasoning</thinking>');\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx: llm.ToolContext.empty() });\n const contentChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content !== undefined) {\n contentChunks.push(chunk.delta.content);\n }\n }\n\n // The whole delta was a thinking block: nothing (not even '') is emitted.\n expect(contentChunks).toEqual([]);\n });\n\n it('filters thinking blocks when tools are active', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('<thinking>hidden'),\n textDeltaEvent('still hidden</thinking>visible'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('visible');\n });\n\n it('does not filter thinking blocks without tools', async () => {\n const text = await collectTextFromEvents([\n messageStartEvent(),\n textDeltaEvent('<thinking>visible without tools</thinking>'),\n ]);\n\n expect(text).toBe('<thinking>visible without tools</thinking>');\n });\n\n it('preserves text around same-delta thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [messageStartEvent(), textDeltaEvent('before <thinking>hidden</thinking> after')],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n\n it('preserves text before split thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('before <thinking>hidden'),\n textDeltaEvent('still hidden</thinking> after'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n});\n"],"mappings":"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","names":["llm"]}
|
package/dist/llm.test.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
1
2
|
import { llm } from "@livekit/agents";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { LLM } from "./llm.js";
|
|
6
|
+
const { anthropicConstructor } = vi.hoisted(() => ({
|
|
7
|
+
anthropicConstructor: vi.fn()
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("@anthropic-ai/sdk", async (importOriginal) => {
|
|
10
|
+
const { default: AnthropicClient } = await importOriginal();
|
|
11
|
+
return {
|
|
12
|
+
default: class extends AnthropicClient {
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
super(opts);
|
|
15
|
+
anthropicConstructor(opts);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
describe("Anthropic LLM prewarm", () => {
|
|
21
|
+
it("lists one model with the prewarm cancellation signal", async () => {
|
|
22
|
+
let prewarmSignal;
|
|
23
|
+
const modelsList = vi.fn(
|
|
24
|
+
async (_params, options) => {
|
|
25
|
+
prewarmSignal = options.signal;
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
const client = {
|
|
29
|
+
baseURL: "https://api.anthropic.test",
|
|
30
|
+
models: { list: modelsList }
|
|
31
|
+
};
|
|
32
|
+
const llm2 = new LLM({ model: "claude-sonnet-4-6", client });
|
|
33
|
+
llm2.prewarm();
|
|
34
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
35
|
+
expect(modelsList).toHaveBeenCalledWith({ limit: 1 }, { signal: expect.any(AbortSignal) });
|
|
36
|
+
expect(prewarmSignal == null ? void 0 : prewarmSignal.aborted).toBe(false);
|
|
37
|
+
await llm2.aclose();
|
|
38
|
+
expect(prewarmSignal == null ? void 0 : prewarmSignal.aborted).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
5
41
|
function messageStartEvent() {
|
|
6
42
|
return {
|
|
7
43
|
type: "message_start",
|
|
@@ -43,6 +79,33 @@ async function collectTextFromEvents(events, toolCtx) {
|
|
|
43
79
|
return textChunks.join("");
|
|
44
80
|
}
|
|
45
81
|
describe("Anthropic LLM", () => {
|
|
82
|
+
it("disables Anthropic SDK retries by default", () => {
|
|
83
|
+
var _a;
|
|
84
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
85
|
+
new LLM({ apiKey: "dummy", model: "claude-3-5-sonnet-20241022" });
|
|
86
|
+
expect((_a = anthropicConstructor.mock.calls[callCount]) == null ? void 0 : _a[0]).toEqual(
|
|
87
|
+
expect.objectContaining({ maxRetries: 0 })
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
it("leaves an injected client and its retry policy untouched", () => {
|
|
91
|
+
const client = {
|
|
92
|
+
baseURL: "https://api.anthropic.test",
|
|
93
|
+
maxRetries: 7
|
|
94
|
+
};
|
|
95
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
96
|
+
const anthropicLlm = new LLM({ client, model: "claude-3-5-sonnet-20241022" });
|
|
97
|
+
expect(anthropicConstructor.mock.calls).toHaveLength(callCount);
|
|
98
|
+
expect(anthropicLlm.provider).toBe("api.anthropic.test");
|
|
99
|
+
expect(client.maxRetries).toBe(7);
|
|
100
|
+
});
|
|
101
|
+
it("forwards an explicit maxRetries to the Anthropic SDK", () => {
|
|
102
|
+
var _a;
|
|
103
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
104
|
+
new LLM({ apiKey: "dummy", model: "claude-3-5-sonnet-20241022", maxRetries: 2 });
|
|
105
|
+
expect((_a = anthropicConstructor.mock.calls[callCount]) == null ? void 0 : _a[0]).toEqual(
|
|
106
|
+
expect.objectContaining({ maxRetries: 2 })
|
|
107
|
+
);
|
|
108
|
+
});
|
|
46
109
|
it("correctly maps ChatContext to Anthropic system and messages arrays", () => {
|
|
47
110
|
const anthropicLlm = new LLM({ apiKey: "dummy", model: "claude-3-5-sonnet-20241022" });
|
|
48
111
|
const chatCtx = new llm.ChatContext();
|
package/dist/llm.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type Anthropic from '@anthropic-ai/sdk';\nimport { llm } from '@livekit/agents';\nimport { describe, expect, it } from 'vitest';\nimport { z } from 'zod';\nimport { LLM } from './llm.js';\n\nfunction messageStartEvent(): Anthropic.MessageStreamEvent {\n return {\n type: 'message_start',\n message: {\n id: 'msg_123',\n usage: { input_tokens: 0, output_tokens: 0 },\n },\n } as Anthropic.MessageStreamEvent;\n}\n\nfunction textDeltaEvent(text: string): Anthropic.MessageStreamEvent {\n return {\n type: 'content_block_delta',\n delta: { type: 'text_delta', text },\n } as Anthropic.MessageStreamEvent;\n}\n\nasync function collectTextFromEvents(\n events: Anthropic.MessageStreamEvent[],\n toolCtx?: llm.ToolContextLike,\n): Promise<string> {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield* events;\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n const textChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content) {\n textChunks.push(chunk.delta.content);\n }\n }\n return textChunks.join('');\n}\n\ndescribe('Anthropic LLM', () => {\n it('correctly maps ChatContext to Anthropic system and messages arrays', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({\n role: 'system',\n content: 'You are a mock agent.',\n });\n chatCtx.addMessage({\n role: 'user',\n content: 'Hello, world!',\n });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { system, messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Assert that system prompts were correctly isolated\n expect(system).toHaveLength(1);\n expect(system[0].text).toBe('You are a mock agent.');\n\n // Assert that strictly user messages ended up in the messages payload\n expect(messages).toHaveLength(1);\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('Hello, world!');\n });\n\n it('merges consecutive same-role messages', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'First message' });\n chatCtx.addMessage({ role: 'user', content: 'Second message' });\n chatCtx.addMessage({ role: 'assistant', content: 'Reply' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Two user messages should be merged into one with array content, followed by a\n // trailing dummy user turn for Claude 4.6+.\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(Array.isArray(messages[0].content)).toBe(true);\n expect(messages[0].content).toHaveLength(2);\n expect(messages[1].role).toBe('assistant');\n expect(messages[2].role).toBe('user');\n expect(messages[2].content).toEqual([{ type: 'text', text: '.' }]);\n });\n\n it('injects a dummy user message if conversation starts with assistant', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'system', content: 'System prompt' });\n chatCtx.addMessage({ role: 'assistant', content: 'I start talking' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Should have injected a dummy user message at the start\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('(empty)');\n expect(messages[1].role).toBe('assistant');\n });\n\n it('handles function_call and function_call_output items', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather?' });\n\n // Simulate a function call from the assistant\n chatCtx.items.push(\n new llm.FunctionCall({\n callId: 'call_123',\n name: 'get_weather',\n args: '{\"city\":\"London\"}',\n }),\n );\n\n // Simulate the tool result\n chatCtx.items.push(\n new llm.FunctionCallOutput({\n callId: 'call_123',\n name: 'get_weather',\n output: '{\"temp\": 20}',\n isError: false,\n }),\n );\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // user message, then assistant tool_use, then user tool_result\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(messages[1].role).toBe('assistant');\n expect(Array.isArray(messages[1].content)).toBe(true);\n expect(messages[1].content[0].type).toBe('tool_use');\n expect(messages[1].content[0].id).toBe('call_123');\n expect(messages[2].role).toBe('user');\n expect(Array.isArray(messages[2].content)).toBe(true);\n expect(messages[2].content[0].type).toBe('tool_result');\n });\n\n it('creates a fresh stream on retry', async () => {\n let calls = 0;\n const client = {\n messages: {\n create: async () => {\n calls += 1;\n if (calls === 1) {\n throw new Error('transient connect failure');\n }\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {})();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n anthropicLlm.on('error', () => {});\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({\n chatCtx,\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const chunks: llm.ChatChunk[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk);\n }\n\n expect(calls).toBe(2);\n expect(chunks.at(-1)?.usage).toEqual({\n completionTokens: 0,\n promptTokens: 0,\n promptCachedTokens: 0,\n totalTokens: 0,\n });\n });\n\n it('sends function tool schemas from a real ToolContext', async () => {\n let capturedParams: Anthropic.MessageCreateParamsStreaming | undefined;\n const client = {\n messages: {\n create: async (params: Anthropic.MessageCreateParamsStreaming) => {\n capturedParams = params;\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n })();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather in Tokyo?' });\n\n const toolCtx = llm.toToolContext({\n getWeather: llm.tool({\n description: 'Get the weather for a given location.',\n parameters: z.object({ location: z.string() }),\n execute: async () => 'sunny',\n }),\n });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n for await (const _ of stream) {\n // drain\n }\n\n expect(capturedParams?.tools).toHaveLength(1);\n const toolSchema = capturedParams!.tools![0] as Anthropic.Tool;\n expect(toolSchema.name).toBe('getWeather');\n expect(toolSchema.description).toBe('Get the weather for a given location.');\n expect(toolSchema.input_schema.type).toBe('object');\n expect(Object.keys(toolSchema.input_schema.properties ?? {})).toEqual(['location']);\n });\n\n it('does not emit an empty delta for a fully-filtered thinking block', async () => {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n yield textDeltaEvent('<thinking>only reasoning</thinking>');\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx: llm.ToolContext.empty() });\n const contentChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content !== undefined) {\n contentChunks.push(chunk.delta.content);\n }\n }\n\n // The whole delta was a thinking block: nothing (not even '') is emitted.\n expect(contentChunks).toEqual([]);\n });\n\n it('filters thinking blocks when tools are active', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('<thinking>hidden'),\n textDeltaEvent('still hidden</thinking>visible'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('visible');\n });\n\n it('does not filter thinking blocks without tools', async () => {\n const text = await collectTextFromEvents([\n messageStartEvent(),\n textDeltaEvent('<thinking>visible without tools</thinking>'),\n ]);\n\n expect(text).toBe('<thinking>visible without tools</thinking>');\n });\n\n it('preserves text around same-delta thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [messageStartEvent(), textDeltaEvent('before <thinking>hidden</thinking> after')],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n\n it('preserves text before split thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('before <thinking>hidden'),\n textDeltaEvent('still hidden</thinking> after'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\nimport Anthropic from '@anthropic-ai/sdk';\nimport { llm } from '@livekit/agents';\nimport { describe, expect, it, vi } from 'vitest';\nimport { z } from 'zod';\nimport { LLM } from './llm.js';\n\nconst { anthropicConstructor } = vi.hoisted(() => ({\n anthropicConstructor: vi.fn<(opts?: { maxRetries?: number }) => void>(),\n}));\n\nvi.mock('@anthropic-ai/sdk', async (importOriginal) => {\n const { default: AnthropicClient } = await importOriginal<{ default: typeof Anthropic }>();\n\n return {\n default: class extends AnthropicClient {\n constructor(opts: ConstructorParameters<typeof AnthropicClient>[0]) {\n super(opts);\n anthropicConstructor(opts);\n }\n },\n };\n});\n\ndescribe('Anthropic LLM prewarm', () => {\n it('lists one model with the prewarm cancellation signal', async () => {\n let prewarmSignal: AbortSignal | undefined;\n const modelsList = vi.fn(\n async (_params: { limit: number }, options: { signal?: AbortSignal }) => {\n prewarmSignal = options.signal;\n },\n );\n const client = {\n baseURL: 'https://api.anthropic.test',\n models: { list: modelsList },\n } as unknown as Anthropic;\n const llm = new LLM({ model: 'claude-sonnet-4-6', client });\n\n llm.prewarm();\n await new Promise<void>((resolve) => setImmediate(resolve));\n\n expect(modelsList).toHaveBeenCalledWith({ limit: 1 }, { signal: expect.any(AbortSignal) });\n expect(prewarmSignal?.aborted).toBe(false);\n\n await llm.aclose();\n expect(prewarmSignal?.aborted).toBe(true);\n });\n});\n\nfunction messageStartEvent(): Anthropic.MessageStreamEvent {\n return {\n type: 'message_start',\n message: {\n id: 'msg_123',\n usage: { input_tokens: 0, output_tokens: 0 },\n },\n } as Anthropic.MessageStreamEvent;\n}\n\nfunction textDeltaEvent(text: string): Anthropic.MessageStreamEvent {\n return {\n type: 'content_block_delta',\n delta: { type: 'text_delta', text },\n } as Anthropic.MessageStreamEvent;\n}\n\nasync function collectTextFromEvents(\n events: Anthropic.MessageStreamEvent[],\n toolCtx?: llm.ToolContextLike,\n): Promise<string> {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield* events;\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n const textChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content) {\n textChunks.push(chunk.delta.content);\n }\n }\n return textChunks.join('');\n}\n\ndescribe('Anthropic LLM', () => {\n it('disables Anthropic SDK retries by default', () => {\n const callCount = anthropicConstructor.mock.calls.length;\n\n new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n expect(anthropicConstructor.mock.calls[callCount]?.[0]).toEqual(\n expect.objectContaining({ maxRetries: 0 }),\n );\n });\n\n it('leaves an injected client and its retry policy untouched', () => {\n const client = {\n baseURL: 'https://api.anthropic.test',\n maxRetries: 7,\n } as unknown as Anthropic;\n const callCount = anthropicConstructor.mock.calls.length;\n\n const anthropicLlm = new LLM({ client, model: 'claude-3-5-sonnet-20241022' });\n\n // The caller owns the client: no SDK client is constructed on its behalf.\n expect(anthropicConstructor.mock.calls).toHaveLength(callCount);\n expect(anthropicLlm.provider).toBe('api.anthropic.test');\n expect(client.maxRetries).toBe(7);\n });\n\n it('forwards an explicit maxRetries to the Anthropic SDK', () => {\n const callCount = anthropicConstructor.mock.calls.length;\n\n new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022', maxRetries: 2 });\n\n expect(anthropicConstructor.mock.calls[callCount]?.[0]).toEqual(\n expect.objectContaining({ maxRetries: 2 }),\n );\n });\n\n it('correctly maps ChatContext to Anthropic system and messages arrays', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({\n role: 'system',\n content: 'You are a mock agent.',\n });\n chatCtx.addMessage({\n role: 'user',\n content: 'Hello, world!',\n });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { system, messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Assert that system prompts were correctly isolated\n expect(system).toHaveLength(1);\n expect(system[0].text).toBe('You are a mock agent.');\n\n // Assert that strictly user messages ended up in the messages payload\n expect(messages).toHaveLength(1);\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('Hello, world!');\n });\n\n it('merges consecutive same-role messages', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'First message' });\n chatCtx.addMessage({ role: 'user', content: 'Second message' });\n chatCtx.addMessage({ role: 'assistant', content: 'Reply' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Two user messages should be merged into one with array content, followed by a\n // trailing dummy user turn for Claude 4.6+.\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(Array.isArray(messages[0].content)).toBe(true);\n expect(messages[0].content).toHaveLength(2);\n expect(messages[1].role).toBe('assistant');\n expect(messages[2].role).toBe('user');\n expect(messages[2].content).toEqual([{ type: 'text', text: '.' }]);\n });\n\n it('injects a dummy user message if conversation starts with assistant', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'system', content: 'System prompt' });\n chatCtx.addMessage({ role: 'assistant', content: 'I start talking' });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // Should have injected a dummy user message at the start\n expect(messages[0].role).toBe('user');\n expect(messages[0].content).toBe('(empty)');\n expect(messages[1].role).toBe('assistant');\n });\n\n it('handles function_call and function_call_output items', () => {\n const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });\n\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather?' });\n\n // Simulate a function call from the assistant\n chatCtx.items.push(\n new llm.FunctionCall({\n callId: 'call_123',\n name: 'get_weather',\n args: '{\"city\":\"London\"}',\n }),\n );\n\n // Simulate the tool result\n chatCtx.items.push(\n new llm.FunctionCallOutput({\n callId: 'call_123',\n name: 'get_weather',\n output: '{\"temp\": 20}',\n isError: false,\n }),\n );\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { messages } = (anthropicLlm as any)._buildAnthropicContext(chatCtx);\n\n // user message, then assistant tool_use, then user tool_result\n expect(messages).toHaveLength(3);\n expect(messages[0].role).toBe('user');\n expect(messages[1].role).toBe('assistant');\n expect(Array.isArray(messages[1].content)).toBe(true);\n expect(messages[1].content[0].type).toBe('tool_use');\n expect(messages[1].content[0].id).toBe('call_123');\n expect(messages[2].role).toBe('user');\n expect(Array.isArray(messages[2].content)).toBe(true);\n expect(messages[2].content[0].type).toBe('tool_result');\n });\n\n it('creates a fresh stream on retry', async () => {\n let calls = 0;\n const client = {\n messages: {\n create: async () => {\n calls += 1;\n if (calls === 1) {\n throw new Error('transient connect failure');\n }\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {})();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n anthropicLlm.on('error', () => {});\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({\n chatCtx,\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const chunks: llm.ChatChunk[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk);\n }\n\n expect(calls).toBe(2);\n expect(chunks.at(-1)?.usage).toEqual({\n completionTokens: 0,\n promptTokens: 0,\n promptCachedTokens: 0,\n totalTokens: 0,\n });\n });\n\n it('sends function tool schemas from a real ToolContext', async () => {\n let capturedParams: Anthropic.MessageCreateParamsStreaming | undefined;\n const client = {\n messages: {\n create: async (params: Anthropic.MessageCreateParamsStreaming) => {\n capturedParams = params;\n return (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n })();\n },\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'What is the weather in Tokyo?' });\n\n const toolCtx = llm.toToolContext({\n getWeather: llm.tool({\n description: 'Get the weather for a given location.',\n parameters: z.object({ location: z.string() }),\n execute: async () => 'sunny',\n }),\n });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx });\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n for await (const _ of stream) {\n // drain\n }\n\n expect(capturedParams?.tools).toHaveLength(1);\n const toolSchema = capturedParams!.tools![0] as Anthropic.Tool;\n expect(toolSchema.name).toBe('getWeather');\n expect(toolSchema.description).toBe('Get the weather for a given location.');\n expect(toolSchema.input_schema.type).toBe('object');\n expect(Object.keys(toolSchema.input_schema.properties ?? {})).toEqual(['location']);\n });\n\n it('does not emit an empty delta for a fully-filtered thinking block', async () => {\n const client = {\n messages: {\n create: async () =>\n (async function* (): AsyncGenerator<Anthropic.MessageStreamEvent> {\n yield messageStartEvent();\n yield textDeltaEvent('<thinking>only reasoning</thinking>');\n })(),\n },\n } as unknown as Anthropic;\n const anthropicLlm = new LLM({\n apiKey: 'dummy',\n client,\n model: 'claude-3-5-sonnet-20241022',\n });\n const chatCtx = new llm.ChatContext();\n chatCtx.addMessage({ role: 'user', content: 'Hello, world!' });\n\n const stream = anthropicLlm.chat({ chatCtx, toolCtx: llm.ToolContext.empty() });\n const contentChunks: string[] = [];\n for await (const chunk of stream) {\n if (chunk.delta?.content !== undefined) {\n contentChunks.push(chunk.delta.content);\n }\n }\n\n // The whole delta was a thinking block: nothing (not even '') is emitted.\n expect(contentChunks).toEqual([]);\n });\n\n it('filters thinking blocks when tools are active', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('<thinking>hidden'),\n textDeltaEvent('still hidden</thinking>visible'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('visible');\n });\n\n it('does not filter thinking blocks without tools', async () => {\n const text = await collectTextFromEvents([\n messageStartEvent(),\n textDeltaEvent('<thinking>visible without tools</thinking>'),\n ]);\n\n expect(text).toBe('<thinking>visible without tools</thinking>');\n });\n\n it('preserves text around same-delta thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [messageStartEvent(), textDeltaEvent('before <thinking>hidden</thinking> after')],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n\n it('preserves text before split thinking blocks', async () => {\n const text = await collectTextFromEvents(\n [\n messageStartEvent(),\n textDeltaEvent('before <thinking>hidden'),\n textDeltaEvent('still hidden</thinking> after'),\n ],\n llm.ToolContext.empty(),\n );\n\n expect(text).toBe('before after');\n });\n});\n"],"mappings":"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","names":["llm"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livekit/agents-plugin-anthropic",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "Anthropic plugin for LiveKit Node Agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"require": "dist/index.cjs",
|
|
@@ -28,20 +28,20 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@livekit/rtc-node": "^0.13.
|
|
31
|
+
"@livekit/rtc-node": "^0.13.33",
|
|
32
32
|
"@microsoft/api-extractor": "^7.35.0",
|
|
33
33
|
"tsup": "^8.3.5",
|
|
34
34
|
"typescript": "^5.0.0",
|
|
35
35
|
"zod": "^3.25.76 || ^4.1.8",
|
|
36
|
-
"@livekit/agents": "1.6.
|
|
37
|
-
"@livekit/agents-plugins-test": "1.6.
|
|
36
|
+
"@livekit/agents": "1.6.2",
|
|
37
|
+
"@livekit/agents-plugins-test": "1.6.2"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@anthropic-ai/sdk": "^0.33.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@livekit/rtc-node": "^0.13.
|
|
44
|
-
"@livekit/agents": "1.6.
|
|
43
|
+
"@livekit/rtc-node": "^0.13.33",
|
|
44
|
+
"@livekit/agents": "1.6.2"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsup --onSuccess \"pnpm build:types\"",
|
package/src/llm.test.ts
CHANGED
|
@@ -1,12 +1,55 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
5
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
6
|
import { llm } from '@livekit/agents';
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
8
|
import { z } from 'zod';
|
|
8
9
|
import { LLM } from './llm.js';
|
|
9
10
|
|
|
11
|
+
const { anthropicConstructor } = vi.hoisted(() => ({
|
|
12
|
+
anthropicConstructor: vi.fn<(opts?: { maxRetries?: number }) => void>(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@anthropic-ai/sdk', async (importOriginal) => {
|
|
16
|
+
const { default: AnthropicClient } = await importOriginal<{ default: typeof Anthropic }>();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
default: class extends AnthropicClient {
|
|
20
|
+
constructor(opts: ConstructorParameters<typeof AnthropicClient>[0]) {
|
|
21
|
+
super(opts);
|
|
22
|
+
anthropicConstructor(opts);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('Anthropic LLM prewarm', () => {
|
|
29
|
+
it('lists one model with the prewarm cancellation signal', async () => {
|
|
30
|
+
let prewarmSignal: AbortSignal | undefined;
|
|
31
|
+
const modelsList = vi.fn(
|
|
32
|
+
async (_params: { limit: number }, options: { signal?: AbortSignal }) => {
|
|
33
|
+
prewarmSignal = options.signal;
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
const client = {
|
|
37
|
+
baseURL: 'https://api.anthropic.test',
|
|
38
|
+
models: { list: modelsList },
|
|
39
|
+
} as unknown as Anthropic;
|
|
40
|
+
const llm = new LLM({ model: 'claude-sonnet-4-6', client });
|
|
41
|
+
|
|
42
|
+
llm.prewarm();
|
|
43
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
44
|
+
|
|
45
|
+
expect(modelsList).toHaveBeenCalledWith({ limit: 1 }, { signal: expect.any(AbortSignal) });
|
|
46
|
+
expect(prewarmSignal?.aborted).toBe(false);
|
|
47
|
+
|
|
48
|
+
await llm.aclose();
|
|
49
|
+
expect(prewarmSignal?.aborted).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
10
53
|
function messageStartEvent(): Anthropic.MessageStreamEvent {
|
|
11
54
|
return {
|
|
12
55
|
type: 'message_start',
|
|
@@ -55,6 +98,41 @@ async function collectTextFromEvents(
|
|
|
55
98
|
}
|
|
56
99
|
|
|
57
100
|
describe('Anthropic LLM', () => {
|
|
101
|
+
it('disables Anthropic SDK retries by default', () => {
|
|
102
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
103
|
+
|
|
104
|
+
new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });
|
|
105
|
+
|
|
106
|
+
expect(anthropicConstructor.mock.calls[callCount]?.[0]).toEqual(
|
|
107
|
+
expect.objectContaining({ maxRetries: 0 }),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('leaves an injected client and its retry policy untouched', () => {
|
|
112
|
+
const client = {
|
|
113
|
+
baseURL: 'https://api.anthropic.test',
|
|
114
|
+
maxRetries: 7,
|
|
115
|
+
} as unknown as Anthropic;
|
|
116
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
117
|
+
|
|
118
|
+
const anthropicLlm = new LLM({ client, model: 'claude-3-5-sonnet-20241022' });
|
|
119
|
+
|
|
120
|
+
// The caller owns the client: no SDK client is constructed on its behalf.
|
|
121
|
+
expect(anthropicConstructor.mock.calls).toHaveLength(callCount);
|
|
122
|
+
expect(anthropicLlm.provider).toBe('api.anthropic.test');
|
|
123
|
+
expect(client.maxRetries).toBe(7);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('forwards an explicit maxRetries to the Anthropic SDK', () => {
|
|
127
|
+
const callCount = anthropicConstructor.mock.calls.length;
|
|
128
|
+
|
|
129
|
+
new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022', maxRetries: 2 });
|
|
130
|
+
|
|
131
|
+
expect(anthropicConstructor.mock.calls[callCount]?.[0]).toEqual(
|
|
132
|
+
expect.objectContaining({ maxRetries: 2 }),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
58
136
|
it('correctly maps ChatContext to Anthropic system and messages arrays', () => {
|
|
59
137
|
const anthropicLlm = new LLM({ apiKey: 'dummy', model: 'claude-3-5-sonnet-20241022' });
|
|
60
138
|
|
package/src/llm.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface LLMOptions {
|
|
|
24
24
|
temperature?: number;
|
|
25
25
|
/** Pre-configured Anthropic client instance. */
|
|
26
26
|
client?: Anthropic;
|
|
27
|
+
/** Vendor client retries. Defaults to 0 because the framework owns retries. */
|
|
28
|
+
maxRetries?: number;
|
|
27
29
|
/** Tool selection strategy. */
|
|
28
30
|
toolChoice?: llm.ToolChoice;
|
|
29
31
|
/** Whether to allow parallel tool calls. */
|
|
@@ -66,6 +68,7 @@ export class LLM extends llm.LLM {
|
|
|
66
68
|
new Anthropic({
|
|
67
69
|
baseURL: this.#opts.baseURL,
|
|
68
70
|
apiKey: this.#opts.apiKey,
|
|
71
|
+
maxRetries: this.#opts.maxRetries ?? 0,
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -89,6 +92,10 @@ export class LLM extends llm.LLM {
|
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
protected override async _prewarmImpl(signal: AbortSignal): Promise<void> {
|
|
96
|
+
await this.#client.models.list({ limit: 1 }, { signal });
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
/**
|
|
93
100
|
* Converts a framework ChatContext into Anthropic's message format.
|
|
94
101
|
*
|