From 9185e9c0a02364233d433562407f83433457ecbf Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 17 Dec 2025 15:00:32 +0000 Subject: [PATCH 1/9] refactor(anthropic): parts seperated from runners --- .../anthropic/src/parts/input_json_part.ts | 47 ++++++ .../anthropic/src/parts/mcp_tool_use.ts | 46 ++++++ js/plugins/anthropic/src/parts/part.ts | 81 ++++++++++ .../src/parts/redacted_thinking_part.ts | 62 +++++++ .../src/parts/server_tool_use_part.ts | 85 ++++++++++ js/plugins/anthropic/src/parts/text_part.ts | 80 ++++++++++ .../anthropic/src/parts/thinking_part.ts | 105 ++++++++++++ .../anthropic/src/parts/tool_use_part.ts | 74 +++++++++ .../src/parts/web_search_tool_result_part.ts | 89 +++++++++++ js/plugins/anthropic/src/runner/base.ts | 136 +++++++++++----- js/plugins/anthropic/src/runner/beta.ts | 109 +++++-------- js/plugins/anthropic/src/runner/stable.ts | 151 ++---------------- js/plugins/anthropic/src/utils.ts | 18 +++ .../anthropic/tests/beta_runner_test.ts | 12 +- 14 files changed, 847 insertions(+), 248 deletions(-) create mode 100644 js/plugins/anthropic/src/parts/input_json_part.ts create mode 100644 js/plugins/anthropic/src/parts/mcp_tool_use.ts create mode 100644 js/plugins/anthropic/src/parts/part.ts create mode 100644 js/plugins/anthropic/src/parts/redacted_thinking_part.ts create mode 100644 js/plugins/anthropic/src/parts/server_tool_use_part.ts create mode 100644 js/plugins/anthropic/src/parts/text_part.ts create mode 100644 js/plugins/anthropic/src/parts/thinking_part.ts create mode 100644 js/plugins/anthropic/src/parts/tool_use_part.ts create mode 100644 js/plugins/anthropic/src/parts/web_search_tool_result_part.ts create mode 100644 js/plugins/anthropic/src/utils.ts diff --git a/js/plugins/anthropic/src/parts/input_json_part.ts b/js/plugins/anthropic/src/parts/input_json_part.ts new file mode 100644 index 0000000000..e88fbeb2ab --- /dev/null +++ b/js/plugins/anthropic/src/parts/input_json_part.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID_DELTA = 'input_json_delta'; + +export const InputJsonPart: SupportedPart = { + abilities: [ + { + id: ID_DELTA, + when: SupportedPartWhen.StreamDelta, + what: SupportedPartWhat.ContentBlock, + func: (delta) => { + if (delta.type !== ID_DELTA) { + throwErrorWrongTypeForAbility( + ID_DELTA, + SupportedPartWhen.StreamDelta, + SupportedPartWhat.ContentBlock + ); + } + + throw new Error( + `Anthropic streaming tool input (${ID_DELTA}) is not yet supported. Please disable streaming or upgrade this plugin.` + ); + }, + }, + ], +}; diff --git a/js/plugins/anthropic/src/parts/mcp_tool_use.ts b/js/plugins/anthropic/src/parts/mcp_tool_use.ts new file mode 100644 index 0000000000..3dc66673d8 --- /dev/null +++ b/js/plugins/anthropic/src/parts/mcp_tool_use.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { unsupportedBetaServerToolError } from '../utils'; +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID = 'mcp_tool_use'; + +export const McpToolUsePart: SupportedPart = { + abilities: [ + { + id: ID, + when: SupportedPartWhen.NonStream, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + } + + throw new Error(unsupportedBetaServerToolError(contentBlock.type)); + }, + }, + ], +}; diff --git a/js/plugins/anthropic/src/parts/part.ts b/js/plugins/anthropic/src/parts/part.ts new file mode 100644 index 0000000000..bfa2cd4d34 --- /dev/null +++ b/js/plugins/anthropic/src/parts/part.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ContentBlock, + RawContentBlockDelta, +} from '@anthropic-ai/sdk/resources'; +import { + BetaContentBlock, + BetaRawContentBlockDelta, + BetaRawMessageStreamEvent, +} from '@anthropic-ai/sdk/resources/beta.js'; +import { MessageStreamEvent } from '@anthropic-ai/sdk/resources/messages.js'; + +export interface SupportedPart { + abilities: Ability[]; +} + +export interface Ability { + id: string; + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen]; + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat]; + func: ( + chunk: + | MessageStreamEvent + | BetaRawMessageStreamEvent + | ContentBlock + | BetaContentBlock + | RawContentBlockDelta + | BetaRawContentBlockDelta + ) => any; +} + +export const SupportedPartWhen = { + StreamStart: 'stream_start' as const, + StreamDelta: 'stream_delta' as const, + StreamEnd: 'stream_end' as const, + NonStream: 'non_stream' as const, +}; + +export const SupportedPartWhat = { + ContentBlock: 'content_block' as const, +}; + +export function throwErrorWrongTypeForAbility( + partId: string, + chunk: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat] +): never { + switch (chunk) { + case SupportedPartWhen.StreamStart: + throw new Error( + `Part '${partId}' is not supported for stream start in ${what}` + ); + case SupportedPartWhen.StreamDelta: + throw new Error( + `Part '${partId}' is not supported for stream delta in ${what}` + ); + case SupportedPartWhen.StreamEnd: + throw new Error( + `Part '${partId}' is not supported for stream end in ${what}` + ); + case SupportedPartWhen.NonStream: + throw new Error( + `Part '${partId}' is not supported for non stream in ${what}` + ); + } +} diff --git a/js/plugins/anthropic/src/parts/redacted_thinking_part.ts b/js/plugins/anthropic/src/parts/redacted_thinking_part.ts new file mode 100644 index 0000000000..b80c47421a --- /dev/null +++ b/js/plugins/anthropic/src/parts/redacted_thinking_part.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID = 'redacted_thinking'; + +export const RedactedThinkingPart: SupportedPart = { + abilities: [ + { + id: ID, + when: SupportedPartWhen.NonStream, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + } + + return { custom: { redactedThinking: contentBlock.data } }; + }, + }, + + { + id: ID, + when: SupportedPartWhen.StreamStart, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock + ); + } + + return { custom: { redactedThinking: contentBlock.data } }; + }, + }, + ], +}; diff --git a/js/plugins/anthropic/src/parts/server_tool_use_part.ts b/js/plugins/anthropic/src/parts/server_tool_use_part.ts new file mode 100644 index 0000000000..e2baeac61d --- /dev/null +++ b/js/plugins/anthropic/src/parts/server_tool_use_part.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID = 'server_tool_use'; + +export const ServerToolUsePart: SupportedPart = { + abilities: [ + { + id: ID, + when: SupportedPartWhen.NonStream, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + } + + const baseName = contentBlock.name ?? 'unknown_tool'; + const serverToolName = + 'server_name' in contentBlock && contentBlock.server_name + ? `${contentBlock.server_name}/${baseName}` + : baseName; + + return { + text: `[Anthropic server tool ${serverToolName}] input: ${JSON.stringify(contentBlock.input)}`, + custom: { + anthropicServerToolUse: { + id: contentBlock.id, + name: serverToolName, + input: contentBlock.input, + }, + }, + }; + }, + }, + + { + id: ID, + when: SupportedPartWhen.StreamStart, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock + ); + } + return { + text: `[Anthropic server tool ${contentBlock.name}] input: ${JSON.stringify(contentBlock.input)}`, + custom: { + anthropicServerToolUse: { + id: contentBlock.id, + name: contentBlock.name ?? 'unknown_tool', + input: contentBlock.input, + }, + }, + }; + }, + }, + ], +}; diff --git a/js/plugins/anthropic/src/parts/text_part.ts b/js/plugins/anthropic/src/parts/text_part.ts new file mode 100644 index 0000000000..17b8903b3c --- /dev/null +++ b/js/plugins/anthropic/src/parts/text_part.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID = 'text'; +const ID_DELTA = 'text_delta'; + +export const TextPart: SupportedPart = { + abilities: [ + { + id: ID, + when: SupportedPartWhen.NonStream, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + } + + return { text: contentBlock.text }; + }, + }, + + { + id: ID_DELTA, + when: SupportedPartWhen.StreamDelta, + what: SupportedPartWhat.ContentBlock, + func: (delta) => { + if (delta.type !== ID_DELTA) { + throwErrorWrongTypeForAbility( + ID_DELTA, + SupportedPartWhen.StreamDelta, + SupportedPartWhat.ContentBlock + ); + } + + return { text: delta.text }; + }, + }, + + { + id: ID, + when: SupportedPartWhen.StreamStart, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock + ); + } + + return { text: contentBlock.text }; + }, + }, + ], +}; diff --git a/js/plugins/anthropic/src/parts/thinking_part.ts b/js/plugins/anthropic/src/parts/thinking_part.ts new file mode 100644 index 0000000000..2bf873ef23 --- /dev/null +++ b/js/plugins/anthropic/src/parts/thinking_part.ts @@ -0,0 +1,105 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Part } from 'genkit'; +import { ANTHROPIC_THINKING_CUSTOM_KEY } from '../runner/base'; +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID = 'thinking'; +const ID_DELTA = 'thinking_delta'; + +export const ThinkingPart: SupportedPart = { + abilities: [ + { + id: ID, + when: SupportedPartWhen.NonStream, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + } + + return createThinkingPart( + contentBlock.thinking, + contentBlock.signature + ); + }, + }, + + { + id: ID_DELTA, + when: SupportedPartWhen.StreamDelta, + what: SupportedPartWhat.ContentBlock, + func: (delta) => { + if (delta.type !== ID_DELTA) { + throwErrorWrongTypeForAbility( + ID_DELTA, + SupportedPartWhen.StreamDelta, + SupportedPartWhat.ContentBlock + ); + } + + return { reasoning: delta.thinking }; + }, + }, + + { + id: ID, + when: SupportedPartWhen.StreamStart, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock + ); + } + + return createThinkingPart( + contentBlock.thinking, + contentBlock.signature + ); + }, + }, + ], +}; + +function createThinkingPart(thinking: string, signature?: string): Part { + const custom = + signature !== undefined + ? { + [ANTHROPIC_THINKING_CUSTOM_KEY]: { signature }, + } + : undefined; + return custom + ? { + reasoning: thinking, + custom, + } + : { + reasoning: thinking, + }; +} diff --git a/js/plugins/anthropic/src/parts/tool_use_part.ts b/js/plugins/anthropic/src/parts/tool_use_part.ts new file mode 100644 index 0000000000..8d65bc4228 --- /dev/null +++ b/js/plugins/anthropic/src/parts/tool_use_part.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID = 'tool_use'; + +export const ToolUsePart: SupportedPart = { + abilities: [ + { + id: ID, + when: SupportedPartWhen.NonStream, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + } + + return { + toolRequest: { + ref: contentBlock.id, + name: contentBlock.name ?? 'unknown_tool', + input: contentBlock.input, + }, + }; + }, + }, + + { + id: ID, + when: SupportedPartWhen.StreamStart, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock + ); + } + + return { + toolRequest: { + ref: contentBlock.id, + name: contentBlock.name ?? 'unknown_tool', + input: contentBlock.input, + }, + }; + }, + }, + ], +}; diff --git a/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts b/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts new file mode 100644 index 0000000000..a71dfd4dc2 --- /dev/null +++ b/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Part } from 'genkit'; +import { + SupportedPart, + SupportedPartWhat, + SupportedPartWhen, + throwErrorWrongTypeForAbility, +} from './part'; + +const ID = 'web_search_tool_result'; + +export const WebSearchToolResultPart: SupportedPart = { + abilities: [ + { + id: ID, + when: SupportedPartWhen.NonStream, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + } + + return toWebSearchToolResultPart({ + type: contentBlock.type, + toolUseId: contentBlock.tool_use_id, + content: contentBlock.content, + }); + }, + }, + + { + id: ID, + when: SupportedPartWhen.StreamStart, + what: SupportedPartWhat.ContentBlock, + func: (contentBlock) => { + if (contentBlock.type !== ID) { + throwErrorWrongTypeForAbility( + ID, + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock + ); + } + + return toWebSearchToolResultPart({ + type: contentBlock.type, + toolUseId: contentBlock.tool_use_id, + content: contentBlock.content, + }); + }, + }, + ], +}; + +function toWebSearchToolResultPart(params: { + toolUseId: string; + content: unknown; + type: string; +}): Part { + const { toolUseId, content, type } = params; + return { + text: `[Anthropic server tool result ${toolUseId}] ${JSON.stringify(content)}`, + custom: { + anthropicServerToolResult: { + type, + toolUseId, + content, + }, + }, + }; +} diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index e6b7132e28..b9abb36716 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -15,7 +15,10 @@ */ import { Anthropic } from '@anthropic-ai/sdk'; -import type { DocumentBlockParam } from '@anthropic-ai/sdk/resources/messages'; +import type { + DocumentBlockParam, + MessageStreamEvent, +} from '@anthropic-ai/sdk/resources/messages'; import type { GenerateRequest, GenerateResponseChunkData, @@ -27,6 +30,18 @@ import type { import { Message as GenkitMessage } from 'genkit'; import type { ToolDefinition } from 'genkit/model'; +import { ContentBlock } from '@anthropic-ai/sdk/resources'; +import { + BetaContentBlock, + BetaRawMessageStreamEvent, +} from '@anthropic-ai/sdk/resources/beta.js'; +import { logger } from 'genkit/logging'; +import { + Ability, + SupportedPart as PluginPart, + SupportedPartWhat, + SupportedPartWhen, +} from '../parts/part.js'; import { AnthropicConfigSchema, Media, @@ -36,7 +51,6 @@ import { type ClaudeRunnerParams, type ThinkingConfig, } from '../types.js'; - import { RunnerContentBlockParam, RunnerMessage, @@ -50,7 +64,7 @@ import { RunnerTypes, } from './types.js'; -const ANTHROPIC_THINKING_CUSTOM_KEY = 'anthropicThinking'; +export const ANTHROPIC_THINKING_CUSTOM_KEY = 'anthropicThinking'; /** * Shared runner logic for Anthropic SDK integrations. @@ -63,6 +77,7 @@ export abstract class BaseRunner { protected name: string; protected client: Anthropic; protected cacheSystemPrompt?: boolean; + protected supportedParts: PluginPart[] = []; /** * Default maximum output tokens for Claude models when not specified in the request. @@ -298,23 +313,6 @@ export abstract class BaseRunner { }; } - protected createThinkingPart(thinking: string, signature?: string): Part { - const custom = - signature !== undefined - ? { - [ANTHROPIC_THINKING_CUSTOM_KEY]: { signature }, - } - : undefined; - return custom - ? { - reasoning: thinking, - custom, - } - : { - reasoning: thinking, - }; - } - protected getThinkingSignature(part: Part): string | undefined { const custom = part.custom as Record | undefined; const thinkingValue = custom?.[ANTHROPIC_THINKING_CUSTOM_KEY]; @@ -363,24 +361,6 @@ export abstract class BaseRunner { return undefined; } - protected toWebSearchToolResultPart(params: { - toolUseId: string; - content: unknown; - type: string; - }): Part { - const { toolUseId, content, type } = params; - return { - text: `[Anthropic server tool result ${toolUseId}] ${JSON.stringify(content)}`, - custom: { - anthropicServerToolResult: { - type, - toolUseId, - content, - }, - }, - }; - } - /** * Converts a Genkit Part to the corresponding Anthropic content block. * Each runner implements this to return its specific API type. @@ -452,6 +432,67 @@ export abstract class BaseRunner { return { system, messages: anthropicMsgs }; } + protected fromAnthropicContentBlockChunk( + event: MessageStreamEvent | BetaRawMessageStreamEvent + ): Part | undefined { + // Handle content_block_start events + if (event.type === 'content_block_start') { + const contentBlock = event.content_block; + + const foundSupportedPartAbility = this.findSupportedPartAbility( + contentBlock.type, + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock + ); + if (foundSupportedPartAbility) { + return foundSupportedPartAbility.func(contentBlock); + } + + const unknownType = (contentBlock as { type: string }).type; + logger.warn( + `Unexpected Anthropic content block type in stream: ${unknownType}. Returning undefined. Content block: ${JSON.stringify(contentBlock)}` + ); + return undefined; + } + + // Handle content_block_delta events + if (event.type === 'content_block_delta') { + const foundSupportedPartAbility = this.findSupportedPartAbility( + event.delta.type, + SupportedPartWhen.StreamDelta, + SupportedPartWhat.ContentBlock + ); + if (foundSupportedPartAbility) { + return foundSupportedPartAbility.func(event.delta); + } + + // signature_delta - ignore + return undefined; + } + + // Other event types (message_start, message_delta, etc.) - ignore + return undefined; + } + + protected fromAnthropicContentBlock( + contentBlock: ContentBlock | BetaContentBlock + ): Part { + const foundSupportedPartAbility = this.findSupportedPartAbility( + contentBlock.type, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + if (foundSupportedPartAbility) { + return foundSupportedPartAbility.func(contentBlock); + } + + const unknownType = (contentBlock as { type: string }).type; + logger.warn( + `Unexpected Anthropic content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify(contentBlock)}` + ); + return { text: '' }; + } + /** * Converts a Genkit ToolDefinition to an Anthropic Tool object. */ @@ -463,6 +504,25 @@ export abstract class BaseRunner { } as RunnerTool; } + protected findSupportedPartAbility( + type: string, + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat] + ): Ability | undefined { + for (const part of this.supportedParts) { + for (const ability of part.abilities) { + if ( + ability.when === when && + ability.what === what && + ability.id === type + ) { + return ability; + } + } + } + return undefined; + } + /** * Converts an Anthropic request to a non-streaming Anthropic API request body. * @param modelName The name of the Anthropic model to use. diff --git a/js/plugins/anthropic/src/runner/beta.ts b/js/plugins/anthropic/src/runner/beta.ts index 6a71fa71d5..3ed27c0e30 100644 --- a/js/plugins/anthropic/src/runner/beta.ts +++ b/js/plugins/anthropic/src/runner/beta.ts @@ -43,7 +43,16 @@ import type { import { logger } from 'genkit/logging'; import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js'; +import { McpToolUsePart } from '../parts/mcp_tool_use.js'; +import { SupportedPartWhat, SupportedPartWhen } from '../parts/part.js'; +import { RedactedThinkingPart } from '../parts/redacted_thinking_part.js'; +import { ServerToolUsePart } from '../parts/server_tool_use_part.js'; +import { TextPart } from '../parts/text_part.js'; +import { ThinkingPart } from '../parts/thinking_part.js'; +import { ToolUsePart } from '../parts/tool_use_part.js'; +import { WebSearchToolResultPart } from '../parts/web_search_tool_result_part.js'; import { AnthropicConfigSchema, type ClaudeRunnerParams } from '../types.js'; +import { unsupportedBetaServerToolError } from '../utils.js'; import { BaseRunner } from './base.js'; import { RunnerTypes } from './types.js'; @@ -66,9 +75,6 @@ const BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES = new Set([ 'container_upload', ]); -const unsupportedServerToolError = (blockType: string): string => - `Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`; - interface BetaRunnerTypes extends RunnerTypes { Message: BetaMessage; Stream: BetaMessageStream; @@ -94,6 +100,16 @@ interface BetaRunnerTypes extends RunnerTypes { export class BetaRunner extends BaseRunner { constructor(params: ClaudeRunnerParams) { super(params); + + this.supportedParts = [ + RedactedThinkingPart, + ServerToolUsePart, + TextPart, + ThinkingPart, + ToolUsePart, + WebSearchToolResultPart, + McpToolUsePart, + ]; } /** @@ -367,7 +383,7 @@ export class BetaRunner extends BaseRunner { message: { role: 'model', content: message.content.map((block) => - this.fromBetaContentBlock(block) + this.fromAnthropicContentBlock(block) ), }, }, @@ -387,9 +403,9 @@ export class BetaRunner extends BaseRunner { blockType && BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(blockType) ) { - throw new Error(unsupportedServerToolError(blockType)); + throw new Error(unsupportedBetaServerToolError(blockType)); } - return this.fromBetaContentBlock(event.content_block); + return this.fromAnthropicContentBlock(event.content_block); } if (event.type === 'content_block_delta') { if (event.delta.type === 'text_delta') { @@ -404,71 +420,26 @@ export class BetaRunner extends BaseRunner { return undefined; } - private fromBetaContentBlock(contentBlock: BetaContentBlock): Part { - switch (contentBlock.type) { - case 'tool_use': { - return { - toolRequest: { - ref: contentBlock.id, - name: contentBlock.name ?? 'unknown_tool', - input: contentBlock.input, - }, - }; - } - - case 'mcp_tool_use': - throw new Error(unsupportedServerToolError(contentBlock.type)); - - case 'server_tool_use': { - const baseName = contentBlock.name ?? 'unknown_tool'; - const serverToolName = - 'server_name' in contentBlock && contentBlock.server_name - ? `${contentBlock.server_name}/${baseName}` - : baseName; - return { - text: `[Anthropic server tool ${serverToolName}] input: ${JSON.stringify(contentBlock.input)}`, - custom: { - anthropicServerToolUse: { - id: contentBlock.id, - name: serverToolName, - input: contentBlock.input, - }, - }, - }; - } - - case 'web_search_tool_result': - return this.toWebSearchToolResultPart({ - type: contentBlock.type, - toolUseId: contentBlock.tool_use_id, - content: contentBlock.content, - }); - - case 'text': - return { text: contentBlock.text }; - - case 'thinking': - return this.createThinkingPart( - contentBlock.thinking, - contentBlock.signature - ); + override fromAnthropicContentBlock(contentBlock: BetaContentBlock): Part { + const foundSupportedPartAbility = this.findSupportedPartAbility( + contentBlock.type, + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock + ); + if (foundSupportedPartAbility) { + return foundSupportedPartAbility.func(contentBlock); + } - case 'redacted_thinking': - return { custom: { redactedThinking: contentBlock.data } }; - - default: { - if (BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(contentBlock.type)) { - throw new Error(unsupportedServerToolError(contentBlock.type)); - } - const unknownType = (contentBlock as { type: string }).type; - logger.warn( - `Unexpected Anthropic beta content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify( - contentBlock - )}` - ); - return { text: '' }; - } + if (BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(contentBlock.type)) { + throw new Error(unsupportedBetaServerToolError(contentBlock.type)); } + const unknownType = (contentBlock as { type: string }).type; + logger.warn( + `Unexpected Anthropic beta content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify( + contentBlock + )}` + ); + return { text: '' }; } private fromBetaStopReason( diff --git a/js/plugins/anthropic/src/runner/stable.ts b/js/plugins/anthropic/src/runner/stable.ts index 0c8f7ffc4f..b5c6bef63f 100644 --- a/js/plugins/anthropic/src/runner/stable.ts +++ b/js/plugins/anthropic/src/runner/stable.ts @@ -16,7 +16,6 @@ import { MessageStream } from '@anthropic-ai/sdk/lib/MessageStream.js'; import type { - ContentBlock, DocumentBlockParam, ImageBlockParam, Message, @@ -38,9 +37,15 @@ import type { ModelResponseData, Part, } from 'genkit'; -import { logger } from 'genkit/logging'; import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js'; +import { InputJsonPart } from '../parts/input_json_part.js'; +import { RedactedThinkingPart } from '../parts/redacted_thinking_part.js'; +import { ServerToolUsePart } from '../parts/server_tool_use_part.js'; +import { TextPart } from '../parts/text_part.js'; +import { ThinkingPart } from '../parts/thinking_part.js'; +import { ToolUsePart } from '../parts/tool_use_part.js'; +import { WebSearchToolResultPart } from '../parts/web_search_tool_result_part.js'; import { AnthropicConfigSchema, type ClaudeRunnerParams } from '../types.js'; import { BaseRunner } from './base.js'; import { RunnerTypes as BaseRunnerTypes } from './types.js'; @@ -66,6 +71,15 @@ interface RunnerTypes extends BaseRunnerTypes { export class Runner extends BaseRunner { constructor(params: ClaudeRunnerParams) { super(params); + this.supportedParts = [ + InputJsonPart, + RedactedThinkingPart, + ServerToolUsePart, + TextPart, + ThinkingPart, + ToolUsePart, + WebSearchToolResultPart, + ]; } protected toAnthropicMessageContent( @@ -338,139 +352,6 @@ export class Runner extends BaseRunner { return this.fromAnthropicContentBlockChunk(event); } - protected fromAnthropicContentBlockChunk( - event: MessageStreamEvent - ): Part | undefined { - // Handle content_block_delta events - if (event.type === 'content_block_delta') { - const delta = event.delta; - - if (delta.type === 'input_json_delta') { - throw new Error( - 'Anthropic streaming tool input (input_json_delta) is not yet supported. Please disable streaming or upgrade this plugin.' - ); - } - - if (delta.type === 'text_delta') { - return { text: delta.text }; - } - - if (delta.type === 'thinking_delta') { - return { reasoning: delta.thinking }; - } - - // signature_delta - ignore - return undefined; - } - - // Handle content_block_start events - if (event.type === 'content_block_start') { - const block = event.content_block; - - switch (block.type) { - case 'server_tool_use': - return { - text: `[Anthropic server tool ${block.name}] input: ${JSON.stringify(block.input)}`, - custom: { - anthropicServerToolUse: { - id: block.id, - name: block.name, - input: block.input, - }, - }, - }; - - case 'web_search_tool_result': - return this.toWebSearchToolResultPart({ - type: block.type, - toolUseId: block.tool_use_id, - content: block.content, - }); - - case 'text': - return { text: block.text }; - - case 'thinking': - return this.createThinkingPart(block.thinking, block.signature); - - case 'redacted_thinking': - return { custom: { redactedThinking: block.data } }; - - case 'tool_use': - return { - toolRequest: { - ref: block.id, - name: block.name, - input: block.input, - }, - }; - - default: { - const unknownType = (block as { type: string }).type; - logger.warn( - `Unexpected Anthropic content block type in stream: ${unknownType}. Returning undefined. Content block: ${JSON.stringify(block)}` - ); - return undefined; - } - } - } - - // Other event types (message_start, message_delta, etc.) - ignore - return undefined; - } - - protected fromAnthropicContentBlock(contentBlock: ContentBlock): Part { - switch (contentBlock.type) { - case 'server_tool_use': - return { - text: `[Anthropic server tool ${contentBlock.name}] input: ${JSON.stringify(contentBlock.input)}`, - custom: { - anthropicServerToolUse: { - id: contentBlock.id, - name: contentBlock.name, - input: contentBlock.input, - }, - }, - }; - - case 'web_search_tool_result': - return this.toWebSearchToolResultPart({ - type: contentBlock.type, - toolUseId: contentBlock.tool_use_id, - content: contentBlock.content, - }); - - case 'tool_use': - return { - toolRequest: { - ref: contentBlock.id, - name: contentBlock.name, - input: contentBlock.input, - }, - }; - - case 'text': - return { text: contentBlock.text }; - - case 'thinking': - return this.createThinkingPart( - contentBlock.thinking, - contentBlock.signature - ); - - case 'redacted_thinking': - return { custom: { redactedThinking: contentBlock.data } }; - - default: { - const unknownType = (contentBlock as { type: string }).type; - logger.warn( - `Unexpected Anthropic content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify(contentBlock)}` - ); - return { text: '' }; - } - } - } - protected fromAnthropicStopReason( reason: Message['stop_reason'] ): ModelResponseData['finishReason'] { diff --git a/js/plugins/anthropic/src/utils.ts b/js/plugins/anthropic/src/utils.ts new file mode 100644 index 0000000000..c8b0f24419 --- /dev/null +++ b/js/plugins/anthropic/src/utils.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const unsupportedBetaServerToolError = (blockType: string): string => + `Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`; diff --git a/js/plugins/anthropic/tests/beta_runner_test.ts b/js/plugins/anthropic/tests/beta_runner_test.ts index 655bfc599e..6262d75cc7 100644 --- a/js/plugins/anthropic/tests/beta_runner_test.ts +++ b/js/plugins/anthropic/tests/beta_runner_test.ts @@ -709,7 +709,7 @@ describe('BetaRunner', () => { assert.throws( () => - exposed.fromBetaContentBlock({ + exposed.fromAnthropicContentBlock({ type: 'mcp_tool_use', id: 'toolu_unknown', input: {}, @@ -725,7 +725,7 @@ describe('BetaRunner', () => { client: mockClient as Anthropic, }); - const thinkingPart = (runner as any).fromBetaContentBlock({ + const thinkingPart = (runner as any).fromAnthropicContentBlock({ type: 'thinking', thinking: 'pondering', signature: 'sig_456', @@ -735,7 +735,7 @@ describe('BetaRunner', () => { custom: { anthropicThinking: { signature: 'sig_456' } }, }); - const redactedPart = (runner as any).fromBetaContentBlock({ + const redactedPart = (runner as any).fromAnthropicContentBlock({ type: 'redacted_thinking', data: '[redacted]', }); @@ -743,7 +743,7 @@ describe('BetaRunner', () => { custom: { redactedThinking: '[redacted]' }, }); - const toolPart = (runner as any).fromBetaContentBlock({ + const toolPart = (runner as any).fromAnthropicContentBlock({ type: 'tool_use', id: 'toolu_x', name: 'plainTool', @@ -757,7 +757,7 @@ describe('BetaRunner', () => { }, }); - const serverToolPart = (runner as any).fromBetaContentBlock({ + const serverToolPart = (runner as any).fromAnthropicContentBlock({ type: 'server_tool_use', id: 'srv_tool_1', name: 'serverTool', @@ -776,7 +776,7 @@ describe('BetaRunner', () => { }); const warnMock = mock.method(console, 'warn', () => {}); - const fallbackPart = (runner as any).fromBetaContentBlock({ + const fallbackPart = (runner as any).fromAnthropicContentBlock({ type: 'mystery', }); assert.deepStrictEqual(fallbackPart, { text: '' }); From e3aef68dca89a265fa20920c2a50098ceadc84c9 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Mon, 22 Dec 2025 12:37:42 +0000 Subject: [PATCH 2/9] fix(anthropic): remove unused func --- js/plugins/anthropic/src/runner/beta.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/js/plugins/anthropic/src/runner/beta.ts b/js/plugins/anthropic/src/runner/beta.ts index 86bdcd19e1..35f226cb6c 100644 --- a/js/plugins/anthropic/src/runner/beta.ts +++ b/js/plugins/anthropic/src/runner/beta.ts @@ -126,9 +126,6 @@ function toAnthropicSchema( return out; } -const unsupportedServerToolError = (blockType: string): string => - `Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`; - interface BetaRunnerTypes extends RunnerTypes { Message: BetaMessage; Stream: BetaMessageStream; From db39c7b87bb6d2e5c2762b656796f8f74919875e Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Mon, 5 Jan 2026 16:43:54 +0000 Subject: [PATCH 3/9] refactor(anthropic): improvements --- .../anthropic/src/parts/mcp_tool_use.ts | 46 ----------- js/plugins/anthropic/src/runner/base.ts | 20 ++--- js/plugins/anthropic/src/runner/beta.ts | 79 ++----------------- js/plugins/anthropic/src/runner/stable.ts | 48 +++++------ js/plugins/anthropic/src/utils.ts | 4 +- 5 files changed, 42 insertions(+), 155 deletions(-) delete mode 100644 js/plugins/anthropic/src/parts/mcp_tool_use.ts diff --git a/js/plugins/anthropic/src/parts/mcp_tool_use.ts b/js/plugins/anthropic/src/parts/mcp_tool_use.ts deleted file mode 100644 index 3dc66673d8..0000000000 --- a/js/plugins/anthropic/src/parts/mcp_tool_use.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { unsupportedBetaServerToolError } from '../utils'; -import { - SupportedPart, - SupportedPartWhat, - SupportedPartWhen, - throwErrorWrongTypeForAbility, -} from './part'; - -const ID = 'mcp_tool_use'; - -export const McpToolUsePart: SupportedPart = { - abilities: [ - { - id: ID, - when: SupportedPartWhen.NonStream, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); - } - - throw new Error(unsupportedBetaServerToolError(contentBlock.type)); - }, - }, - ], -}; diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index b9abb36716..eb8f20618e 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -57,12 +57,12 @@ import { RunnerMessageParam, RunnerRequestBody, RunnerStream, - RunnerStreamEvent, RunnerStreamingRequestBody, RunnerTool, RunnerToolResponseContent, RunnerTypes, } from './types.js'; +import { unsupportedServerToolError } from '../utils.js'; export const ANTHROPIC_THINKING_CUSTOM_KEY = 'anthropicThinking'; @@ -76,8 +76,10 @@ export const ANTHROPIC_THINKING_CUSTOM_KEY = 'anthropicThinking'; export abstract class BaseRunner { protected name: string; protected client: Anthropic; + protected isBeta?: boolean; protected cacheSystemPrompt?: boolean; protected supportedParts: PluginPart[] = []; + protected unsupportedServerToolBlockTypes: Set = new Set(); /** * Default maximum output tokens for Claude models when not specified in the request. @@ -432,12 +434,17 @@ export abstract class BaseRunner { return { system, messages: anthropicMsgs }; } - protected fromAnthropicContentBlockChunk( + protected toGenkitPart( event: MessageStreamEvent | BetaRawMessageStreamEvent ): Part | undefined { // Handle content_block_start events if (event.type === 'content_block_start') { const contentBlock = event.content_block; + const contentBlockType = (event.content_block as { type?: string }).type; + + if (contentBlockType && this.unsupportedServerToolBlockTypes.has(contentBlockType)) { + throw new Error(unsupportedServerToolError(contentBlockType, this.isBeta ?? false)); + } const foundSupportedPartAbility = this.findSupportedPartAbility( contentBlock.type, @@ -448,9 +455,8 @@ export abstract class BaseRunner { return foundSupportedPartAbility.func(contentBlock); } - const unknownType = (contentBlock as { type: string }).type; logger.warn( - `Unexpected Anthropic content block type in stream: ${unknownType}. Returning undefined. Content block: ${JSON.stringify(contentBlock)}` + `Unexpected Anthropic content block type in stream: ${contentBlockType}. Returning undefined. Content block: ${JSON.stringify(contentBlock)}` ); return undefined; } @@ -565,10 +571,6 @@ export abstract class BaseRunner { message: RunnerMessage ): GenerateResponseData; - protected abstract toGenkitPart( - event: RunnerStreamEvent - ): Part | undefined; - public async run( request: GenerateRequest, options: { @@ -587,7 +589,7 @@ export abstract class BaseRunner { ); const stream = this.streamMessages(body, abortSignal); for await (const event of stream) { - const part = this.toGenkitPart(event); + const part = this.toGenkitPart(event as MessageStreamEvent | BetaRawMessageStreamEvent); if (part) { sendChunk({ index: 0, diff --git a/js/plugins/anthropic/src/runner/beta.ts b/js/plugins/anthropic/src/runner/beta.ts index 35f226cb6c..382d8366e1 100644 --- a/js/plugins/anthropic/src/runner/beta.ts +++ b/js/plugins/anthropic/src/runner/beta.ts @@ -16,7 +16,6 @@ import { BetaMessageStream } from '@anthropic-ai/sdk/lib/BetaMessageStream.js'; import type { - BetaContentBlock, BetaImageBlockParam, BetaMessage, MessageCreateParams as BetaMessageCreateParams, @@ -40,11 +39,8 @@ import type { ModelResponseData, Part, } from 'genkit'; -import { logger } from 'genkit/logging'; import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js'; -import { McpToolUsePart } from '../parts/mcp_tool_use.js'; -import { SupportedPartWhat, SupportedPartWhen } from '../parts/part.js'; import { RedactedThinkingPart } from '../parts/redacted_thinking_part.js'; import { ServerToolUsePart } from '../parts/server_tool_use_part.js'; import { TextPart } from '../parts/text_part.js'; @@ -52,29 +48,9 @@ import { ThinkingPart } from '../parts/thinking_part.js'; import { ToolUsePart } from '../parts/tool_use_part.js'; import { WebSearchToolResultPart } from '../parts/web_search_tool_result_part.js'; import { AnthropicConfigSchema, type ClaudeRunnerParams } from '../types.js'; -import { unsupportedBetaServerToolError } from '../utils.js'; import { BaseRunner } from './base.js'; import { RunnerTypes } from './types.js'; -/** - * Server-managed tool blocks emitted by the beta API that Genkit cannot yet - * interpret. We fail fast on these so callers do not accidentally treat them as - * locally executable tool invocations. - */ -/** - * Server tool types that exist in beta but are not yet supported. - * Note: server_tool_use and web_search_tool_result ARE supported (same as stable API). - */ -const BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES = new Set([ - 'web_fetch_tool_result', - 'code_execution_tool_result', - 'bash_code_execution_tool_result', - 'text_editor_code_execution_tool_result', - 'mcp_tool_result', - 'mcp_tool_use', - 'container_upload', -]); - const BETA_APIS = [ // 'message-batches-2024-09-24', // 'prompt-caching-2024-07-31', @@ -152,6 +128,7 @@ export class BetaRunner extends BaseRunner { constructor(params: ClaudeRunnerParams) { super(params); + this.isBeta = true; this.supportedParts = [ RedactedThinkingPart, ServerToolUsePart, @@ -159,8 +136,14 @@ export class BetaRunner extends BaseRunner { ThinkingPart, ToolUsePart, WebSearchToolResultPart, - McpToolUsePart, ]; + this.unsupportedServerToolBlockTypes.add('web_fetch_tool_result'); + this.unsupportedServerToolBlockTypes.add('code_execution_tool_result'); + this.unsupportedServerToolBlockTypes.add('bash_code_execution_tool_result'); + this.unsupportedServerToolBlockTypes.add('text_editor_code_execution_tool_result'); + this.unsupportedServerToolBlockTypes.add('mcp_tool_result'); + this.unsupportedServerToolBlockTypes.add('mcp_tool_use'); + this.unsupportedServerToolBlockTypes.add('container_upload'); } /** @@ -452,52 +435,6 @@ export class BetaRunner extends BaseRunner { }; } - protected toGenkitPart(event: BetaRawMessageStreamEvent): Part | undefined { - if (event.type === 'content_block_start') { - const blockType = (event.content_block as { type?: string }).type; - if ( - blockType && - BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(blockType) - ) { - throw new Error(unsupportedBetaServerToolError(blockType)); - } - return this.fromAnthropicContentBlock(event.content_block); - } - if (event.type === 'content_block_delta') { - if (event.delta.type === 'text_delta') { - return { text: event.delta.text }; - } - if (event.delta.type === 'thinking_delta') { - return { reasoning: event.delta.thinking }; - } - // server/client tool input_json_delta not supported yet - return undefined; - } - return undefined; - } - - override fromAnthropicContentBlock(contentBlock: BetaContentBlock): Part { - const foundSupportedPartAbility = this.findSupportedPartAbility( - contentBlock.type, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); - if (foundSupportedPartAbility) { - return foundSupportedPartAbility.func(contentBlock); - } - - if (BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(contentBlock.type)) { - throw new Error(unsupportedBetaServerToolError(contentBlock.type)); - } - const unknownType = (contentBlock as { type: string }).type; - logger.warn( - `Unexpected Anthropic beta content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify( - contentBlock - )}` - ); - return { text: '' }; - } - private fromBetaStopReason( reason: BetaStopReason | null ): ModelResponseData['finishReason'] { diff --git a/js/plugins/anthropic/src/runner/stable.ts b/js/plugins/anthropic/src/runner/stable.ts index b5c6bef63f..c5413c2e70 100644 --- a/js/plugins/anthropic/src/runner/stable.ts +++ b/js/plugins/anthropic/src/runner/stable.ts @@ -71,6 +71,7 @@ interface RunnerTypes extends BaseRunnerTypes { export class Runner extends BaseRunner { constructor(params: ClaudeRunnerParams) { super(params); + this.isBeta = false; this.supportedParts = [ InputJsonPart, RedactedThinkingPart, @@ -80,6 +81,7 @@ export class Runner extends BaseRunner { ToolUsePart, WebSearchToolResultPart, ]; + this.unsupportedServerToolBlockTypes.add('mcp_tool_use'); } protected toAnthropicMessageContent( @@ -345,11 +347,25 @@ export class Runner extends BaseRunner { } protected toGenkitResponse(message: Message): GenerateResponseData { - return this.fromAnthropicResponse(message); - } - - protected toGenkitPart(event: MessageStreamEvent): Part | undefined { - return this.fromAnthropicContentBlockChunk(event); + return { + candidates: [ + { + index: 0, + finishReason: this.fromAnthropicStopReason(message.stop_reason), + message: { + role: 'model', + content: message.content.map((block) => + this.fromAnthropicContentBlock(block) + ), + }, + }, + ], + usage: { + inputTokens: message.usage.input_tokens, + outputTokens: message.usage.output_tokens, + }, + custom: message, + }; } protected fromAnthropicStopReason( @@ -370,26 +386,4 @@ export class Runner extends BaseRunner { return 'other'; } } - - protected fromAnthropicResponse(response: Message): GenerateResponseData { - return { - candidates: [ - { - index: 0, - finishReason: this.fromAnthropicStopReason(response.stop_reason), - message: { - role: 'model', - content: response.content.map((block) => - this.fromAnthropicContentBlock(block) - ), - }, - }, - ], - usage: { - inputTokens: response.usage.input_tokens, - outputTokens: response.usage.output_tokens, - }, - custom: response, - }; - } } diff --git a/js/plugins/anthropic/src/utils.ts b/js/plugins/anthropic/src/utils.ts index c8b0f24419..0f3f9758e6 100644 --- a/js/plugins/anthropic/src/utils.ts +++ b/js/plugins/anthropic/src/utils.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export const unsupportedBetaServerToolError = (blockType: string): string => - `Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`; +export const unsupportedServerToolError = (blockType: string, isBeta: boolean): string => + `Anthropic ${isBeta ? 'beta' : 'stable'} runner does not yet support server-managed tool block '${blockType}'.`; From ca9cb6d719f4a3fbdd843da693b66917b08737d4 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Mon, 5 Jan 2026 16:46:18 +0000 Subject: [PATCH 4/9] chore: format --- js/plugins/anthropic/src/runner/base.ts | 15 +++++++++++---- js/plugins/anthropic/src/runner/beta.ts | 4 +++- js/plugins/anthropic/src/utils.ts | 5 ++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index eb8f20618e..114af34f2f 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -51,6 +51,7 @@ import { type ClaudeRunnerParams, type ThinkingConfig, } from '../types.js'; +import { unsupportedServerToolError } from '../utils.js'; import { RunnerContentBlockParam, RunnerMessage, @@ -62,7 +63,6 @@ import { RunnerToolResponseContent, RunnerTypes, } from './types.js'; -import { unsupportedServerToolError } from '../utils.js'; export const ANTHROPIC_THINKING_CUSTOM_KEY = 'anthropicThinking'; @@ -442,8 +442,13 @@ export abstract class BaseRunner { const contentBlock = event.content_block; const contentBlockType = (event.content_block as { type?: string }).type; - if (contentBlockType && this.unsupportedServerToolBlockTypes.has(contentBlockType)) { - throw new Error(unsupportedServerToolError(contentBlockType, this.isBeta ?? false)); + if ( + contentBlockType && + this.unsupportedServerToolBlockTypes.has(contentBlockType) + ) { + throw new Error( + unsupportedServerToolError(contentBlockType, this.isBeta ?? false) + ); } const foundSupportedPartAbility = this.findSupportedPartAbility( @@ -589,7 +594,9 @@ export abstract class BaseRunner { ); const stream = this.streamMessages(body, abortSignal); for await (const event of stream) { - const part = this.toGenkitPart(event as MessageStreamEvent | BetaRawMessageStreamEvent); + const part = this.toGenkitPart( + event as MessageStreamEvent | BetaRawMessageStreamEvent + ); if (part) { sendChunk({ index: 0, diff --git a/js/plugins/anthropic/src/runner/beta.ts b/js/plugins/anthropic/src/runner/beta.ts index 382d8366e1..2dbdf437e2 100644 --- a/js/plugins/anthropic/src/runner/beta.ts +++ b/js/plugins/anthropic/src/runner/beta.ts @@ -140,7 +140,9 @@ export class BetaRunner extends BaseRunner { this.unsupportedServerToolBlockTypes.add('web_fetch_tool_result'); this.unsupportedServerToolBlockTypes.add('code_execution_tool_result'); this.unsupportedServerToolBlockTypes.add('bash_code_execution_tool_result'); - this.unsupportedServerToolBlockTypes.add('text_editor_code_execution_tool_result'); + this.unsupportedServerToolBlockTypes.add( + 'text_editor_code_execution_tool_result' + ); this.unsupportedServerToolBlockTypes.add('mcp_tool_result'); this.unsupportedServerToolBlockTypes.add('mcp_tool_use'); this.unsupportedServerToolBlockTypes.add('container_upload'); diff --git a/js/plugins/anthropic/src/utils.ts b/js/plugins/anthropic/src/utils.ts index 0f3f9758e6..1bc7640fdd 100644 --- a/js/plugins/anthropic/src/utils.ts +++ b/js/plugins/anthropic/src/utils.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export const unsupportedServerToolError = (blockType: string, isBeta: boolean): string => +export const unsupportedServerToolError = ( + blockType: string, + isBeta: boolean +): string => `Anthropic ${isBeta ? 'beta' : 'stable'} runner does not yet support server-managed tool block '${blockType}'.`; From 02c35ff808e8286cffae335da81df9a47f37e08a Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Mon, 5 Jan 2026 19:03:37 +0000 Subject: [PATCH 5/9] fix(anthropic): tests --- .../anthropic/src/parts/server_tool_use_part.ts | 11 +++++++++-- js/plugins/anthropic/src/runner/base.ts | 11 +++++++++++ .../anthropic/tests/stable_runner_test.ts | 17 +++++++---------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/js/plugins/anthropic/src/parts/server_tool_use_part.ts b/js/plugins/anthropic/src/parts/server_tool_use_part.ts index e2baeac61d..50878caf6b 100644 --- a/js/plugins/anthropic/src/parts/server_tool_use_part.ts +++ b/js/plugins/anthropic/src/parts/server_tool_use_part.ts @@ -69,12 +69,19 @@ export const ServerToolUsePart: SupportedPart = { SupportedPartWhat.ContentBlock ); } + + const baseName = contentBlock.name ?? 'unknown_tool'; + const serverToolName = + 'server_name' in contentBlock && contentBlock.server_name + ? `${contentBlock.server_name}/${baseName}` + : baseName; + return { - text: `[Anthropic server tool ${contentBlock.name}] input: ${JSON.stringify(contentBlock.input)}`, + text: `[Anthropic server tool ${serverToolName}] input: ${JSON.stringify(contentBlock.input)}`, custom: { anthropicServerToolUse: { id: contentBlock.id, - name: contentBlock.name ?? 'unknown_tool', + name: serverToolName, input: contentBlock.input, }, }, diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index 114af34f2f..a594b46584 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -488,6 +488,17 @@ export abstract class BaseRunner { protected fromAnthropicContentBlock( contentBlock: ContentBlock | BetaContentBlock ): Part { + const contentBlockType = (contentBlock as { type?: string }).type; + + if ( + contentBlockType && + this.unsupportedServerToolBlockTypes.has(contentBlockType) + ) { + throw new Error( + unsupportedServerToolError(contentBlockType, this.isBeta ?? false) + ); + } + const foundSupportedPartAbility = this.findSupportedPartAbility( contentBlock.type, SupportedPartWhen.NonStream, diff --git a/js/plugins/anthropic/tests/stable_runner_test.ts b/js/plugins/anthropic/tests/stable_runner_test.ts index 72797251b9..cd3cb9b55c 100644 --- a/js/plugins/anthropic/tests/stable_runner_test.ts +++ b/js/plugins/anthropic/tests/stable_runner_test.ts @@ -65,11 +65,10 @@ type RunnerProtectedMethods = { request: GenerateRequest, cacheSystemPrompt?: boolean ) => any; - fromAnthropicContentBlockChunk: ( - event: MessageStreamEvent - ) => Part | undefined; fromAnthropicStopReason: (reason: Message['stop_reason']) => any; fromAnthropicResponse: (message: Message) => GenerateResponseData; + toGenkitResponse: (message: Message) => GenerateResponseData; + toGenkitPart: (event: MessageStreamEvent) => Part | undefined; }; const mockClient = createMockAnthropicClient(); @@ -651,7 +650,7 @@ describe('toAnthropicTool', () => { }); }); -describe('fromAnthropicContentBlockChunk', () => { +describe('toGenkitPart', () => { const testCases: { should: string; event: MessageStreamEvent; @@ -755,9 +754,7 @@ describe('fromAnthropicContentBlockChunk', () => { for (const test of testCases) { it(test.should, () => { - const actualOutput = testRunner.fromAnthropicContentBlockChunk( - test.event - ); + const actualOutput = testRunner.toGenkitPart(test.event); assert.deepStrictEqual(actualOutput, test.expectedOutput); }); } @@ -765,7 +762,7 @@ describe('fromAnthropicContentBlockChunk', () => { it('should throw for unsupported tool input streaming deltas', () => { assert.throws( () => - testRunner.fromAnthropicContentBlockChunk({ + testRunner.toGenkitPart({ index: 0, type: 'content_block_delta', delta: { @@ -819,7 +816,7 @@ describe('fromAnthropicStopReason', () => { } }); -describe('fromAnthropicResponse', () => { +describe('toGenkitResponse', () => { const testCases: { should: string; message: Message; @@ -918,7 +915,7 @@ describe('fromAnthropicResponse', () => { for (const test of testCases) { it(test.should, () => { - const actualOutput = testRunner.fromAnthropicResponse(test.message); + const actualOutput = testRunner.toGenkitResponse(test.message); // Check custom field exists and is the message assert.ok(actualOutput.custom); assert.strictEqual(actualOutput.custom, test.message); From 4a16f93e0f655a90b4f5a70aad2387ae30541511 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 6 Jan 2026 09:48:23 +0000 Subject: [PATCH 6/9] refactor(anthropic): reduce duplication --- .../anthropic/src/parts/input_json_part.ts | 14 ++--- js/plugins/anthropic/src/parts/part.ts | 11 ++-- .../src/parts/redacted_thinking_part.ts | 31 ++-------- .../src/parts/server_tool_use_part.ts | 46 ++------------- js/plugins/anthropic/src/parts/text_part.ts | 56 ++++--------------- .../anthropic/src/parts/thinking_part.ts | 48 ++++------------ .../anthropic/src/parts/tool_use_part.ts | 37 ++---------- .../src/parts/web_search_tool_result_part.ts | 35 ++---------- js/plugins/anthropic/src/runner/base.ts | 24 ++++++-- 9 files changed, 71 insertions(+), 231 deletions(-) diff --git a/js/plugins/anthropic/src/parts/input_json_part.ts b/js/plugins/anthropic/src/parts/input_json_part.ts index e88fbeb2ab..7d9483a908 100644 --- a/js/plugins/anthropic/src/parts/input_json_part.ts +++ b/js/plugins/anthropic/src/parts/input_json_part.ts @@ -26,16 +26,12 @@ const ID_DELTA = 'input_json_delta'; export const InputJsonPart: SupportedPart = { abilities: [ { - id: ID_DELTA, - when: SupportedPartWhen.StreamDelta, - what: SupportedPartWhat.ContentBlock, - func: (delta) => { + id: [ID_DELTA], + when: [SupportedPartWhen.StreamDelta], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, delta) => { if (delta.type !== ID_DELTA) { - throwErrorWrongTypeForAbility( - ID_DELTA, - SupportedPartWhen.StreamDelta, - SupportedPartWhat.ContentBlock - ); + throwErrorWrongTypeForAbility(ID_DELTA, when, what); } throw new Error( diff --git a/js/plugins/anthropic/src/parts/part.ts b/js/plugins/anthropic/src/parts/part.ts index bfa2cd4d34..188701f5f8 100644 --- a/js/plugins/anthropic/src/parts/part.ts +++ b/js/plugins/anthropic/src/parts/part.ts @@ -24,16 +24,19 @@ import { BetaRawMessageStreamEvent, } from '@anthropic-ai/sdk/resources/beta.js'; import { MessageStreamEvent } from '@anthropic-ai/sdk/resources/messages.js'; +import type { Part } from 'genkit'; export interface SupportedPart { abilities: Ability[]; } export interface Ability { - id: string; - when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen]; - what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat]; + id: string[]; + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen][]; + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat][]; func: ( + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat], chunk: | MessageStreamEvent | BetaRawMessageStreamEvent @@ -41,7 +44,7 @@ export interface Ability { | BetaContentBlock | RawContentBlockDelta | BetaRawContentBlockDelta - ) => any; + ) => Part; } export const SupportedPartWhen = { diff --git a/js/plugins/anthropic/src/parts/redacted_thinking_part.ts b/js/plugins/anthropic/src/parts/redacted_thinking_part.ts index b80c47421a..20ceaabd2a 100644 --- a/js/plugins/anthropic/src/parts/redacted_thinking_part.ts +++ b/js/plugins/anthropic/src/parts/redacted_thinking_part.ts @@ -26,33 +26,12 @@ const ID = 'redacted_thinking'; export const RedactedThinkingPart: SupportedPart = { abilities: [ { - id: ID, - when: SupportedPartWhen.NonStream, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { + id: [ID], + when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, contentBlock) => { if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); - } - - return { custom: { redactedThinking: contentBlock.data } }; - }, - }, - - { - id: ID, - when: SupportedPartWhen.StreamStart, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock - ); + throwErrorWrongTypeForAbility(ID, when, what); } return { custom: { redactedThinking: contentBlock.data } }; diff --git a/js/plugins/anthropic/src/parts/server_tool_use_part.ts b/js/plugins/anthropic/src/parts/server_tool_use_part.ts index 50878caf6b..504077d8a7 100644 --- a/js/plugins/anthropic/src/parts/server_tool_use_part.ts +++ b/js/plugins/anthropic/src/parts/server_tool_use_part.ts @@ -26,48 +26,12 @@ const ID = 'server_tool_use'; export const ServerToolUsePart: SupportedPart = { abilities: [ { - id: ID, - when: SupportedPartWhen.NonStream, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { + id: [ID], + when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, contentBlock) => { if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); - } - - const baseName = contentBlock.name ?? 'unknown_tool'; - const serverToolName = - 'server_name' in contentBlock && contentBlock.server_name - ? `${contentBlock.server_name}/${baseName}` - : baseName; - - return { - text: `[Anthropic server tool ${serverToolName}] input: ${JSON.stringify(contentBlock.input)}`, - custom: { - anthropicServerToolUse: { - id: contentBlock.id, - name: serverToolName, - input: contentBlock.input, - }, - }, - }; - }, - }, - - { - id: ID, - when: SupportedPartWhen.StreamStart, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock - ); + throwErrorWrongTypeForAbility(ID, when, what); } const baseName = contentBlock.name ?? 'unknown_tool'; diff --git a/js/plugins/anthropic/src/parts/text_part.ts b/js/plugins/anthropic/src/parts/text_part.ts index 17b8903b3c..e3b3afab7d 100644 --- a/js/plugins/anthropic/src/parts/text_part.ts +++ b/js/plugins/anthropic/src/parts/text_part.ts @@ -27,53 +27,19 @@ const ID_DELTA = 'text_delta'; export const TextPart: SupportedPart = { abilities: [ { - id: ID, - when: SupportedPartWhen.NonStream, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); + id: [ID, ID_DELTA], + when: [ + SupportedPartWhen.NonStream, + SupportedPartWhen.StreamDelta, + SupportedPartWhen.StreamStart, + ], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, content) => { + if (content.type !== ID && content.type !== ID_DELTA) { + throwErrorWrongTypeForAbility(ID, when, what); } - return { text: contentBlock.text }; - }, - }, - - { - id: ID_DELTA, - when: SupportedPartWhen.StreamDelta, - what: SupportedPartWhat.ContentBlock, - func: (delta) => { - if (delta.type !== ID_DELTA) { - throwErrorWrongTypeForAbility( - ID_DELTA, - SupportedPartWhen.StreamDelta, - SupportedPartWhat.ContentBlock - ); - } - - return { text: delta.text }; - }, - }, - - { - id: ID, - when: SupportedPartWhen.StreamStart, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock - ); - } - - return { text: contentBlock.text }; + return { text: content.text }; }, }, ], diff --git a/js/plugins/anthropic/src/parts/thinking_part.ts b/js/plugins/anthropic/src/parts/thinking_part.ts index 2bf873ef23..75ee94a6b4 100644 --- a/js/plugins/anthropic/src/parts/thinking_part.ts +++ b/js/plugins/anthropic/src/parts/thinking_part.ts @@ -29,16 +29,12 @@ const ID_DELTA = 'thinking_delta'; export const ThinkingPart: SupportedPart = { abilities: [ { - id: ID, - when: SupportedPartWhen.NonStream, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { + id: [ID], + when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, contentBlock) => { if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); + throwErrorWrongTypeForAbility(ID, when, what); } return createThinkingPart( @@ -49,41 +45,17 @@ export const ThinkingPart: SupportedPart = { }, { - id: ID_DELTA, - when: SupportedPartWhen.StreamDelta, - what: SupportedPartWhat.ContentBlock, - func: (delta) => { + id: [ID_DELTA], + when: [SupportedPartWhen.StreamDelta], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, delta) => { if (delta.type !== ID_DELTA) { - throwErrorWrongTypeForAbility( - ID_DELTA, - SupportedPartWhen.StreamDelta, - SupportedPartWhat.ContentBlock - ); + throwErrorWrongTypeForAbility(ID_DELTA, when, what); } return { reasoning: delta.thinking }; }, }, - - { - id: ID, - when: SupportedPartWhen.StreamStart, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock - ); - } - - return createThinkingPart( - contentBlock.thinking, - contentBlock.signature - ); - }, - }, ], }; diff --git a/js/plugins/anthropic/src/parts/tool_use_part.ts b/js/plugins/anthropic/src/parts/tool_use_part.ts index 8d65bc4228..9ff8cde59c 100644 --- a/js/plugins/anthropic/src/parts/tool_use_part.ts +++ b/js/plugins/anthropic/src/parts/tool_use_part.ts @@ -26,39 +26,12 @@ const ID = 'tool_use'; export const ToolUsePart: SupportedPart = { abilities: [ { - id: ID, - when: SupportedPartWhen.NonStream, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { + id: [ID], + when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, contentBlock) => { if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); - } - - return { - toolRequest: { - ref: contentBlock.id, - name: contentBlock.name ?? 'unknown_tool', - input: contentBlock.input, - }, - }; - }, - }, - - { - id: ID, - when: SupportedPartWhen.StreamStart, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock - ); + throwErrorWrongTypeForAbility(ID, when, what); } return { diff --git a/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts b/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts index a71dfd4dc2..cacddde9d0 100644 --- a/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts +++ b/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts @@ -27,37 +27,12 @@ const ID = 'web_search_tool_result'; export const WebSearchToolResultPart: SupportedPart = { abilities: [ { - id: ID, - when: SupportedPartWhen.NonStream, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { + id: [ID], + when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], + what: [SupportedPartWhat.ContentBlock], + func: (when, what, contentBlock) => { if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock - ); - } - - return toWebSearchToolResultPart({ - type: contentBlock.type, - toolUseId: contentBlock.tool_use_id, - content: contentBlock.content, - }); - }, - }, - - { - id: ID, - when: SupportedPartWhen.StreamStart, - what: SupportedPartWhat.ContentBlock, - func: (contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility( - ID, - SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock - ); + throwErrorWrongTypeForAbility(ID, when, what); } return toWebSearchToolResultPart({ diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index a594b46584..cd326c21a9 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -457,7 +457,11 @@ export abstract class BaseRunner { SupportedPartWhat.ContentBlock ); if (foundSupportedPartAbility) { - return foundSupportedPartAbility.func(contentBlock); + return foundSupportedPartAbility.func( + SupportedPartWhen.StreamStart, + SupportedPartWhat.ContentBlock, + contentBlock + ); } logger.warn( @@ -474,7 +478,11 @@ export abstract class BaseRunner { SupportedPartWhat.ContentBlock ); if (foundSupportedPartAbility) { - return foundSupportedPartAbility.func(event.delta); + return foundSupportedPartAbility.func( + SupportedPartWhen.StreamDelta, + SupportedPartWhat.ContentBlock, + event.delta + ); } // signature_delta - ignore @@ -505,7 +513,11 @@ export abstract class BaseRunner { SupportedPartWhat.ContentBlock ); if (foundSupportedPartAbility) { - return foundSupportedPartAbility.func(contentBlock); + return foundSupportedPartAbility.func( + SupportedPartWhen.NonStream, + SupportedPartWhat.ContentBlock, + contentBlock + ); } const unknownType = (contentBlock as { type: string }).type; @@ -534,9 +546,9 @@ export abstract class BaseRunner { for (const part of this.supportedParts) { for (const ability of part.abilities) { if ( - ability.when === when && - ability.what === what && - ability.id === type + ability.when.includes(when) && + ability.what.includes(what) && + ability.id.includes(type) ) { return ability; } From 9cd3b047de61031f0d8b41d56097e0e6d431b376 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 6 Jan 2026 10:33:49 +0000 Subject: [PATCH 7/9] refactor(anthropic): reduce duplication further --- .../anthropic/src/parts/input_json_part.ts | 13 +-- js/plugins/anthropic/src/parts/part.ts | 57 +++++++---- .../src/parts/redacted_thinking_part.ts | 13 +-- .../src/parts/server_tool_use_part.ts | 13 +-- js/plugins/anthropic/src/parts/text_part.ts | 13 +-- .../anthropic/src/parts/thinking_part.ts | 23 ++--- .../anthropic/src/parts/tool_use_part.ts | 13 +-- .../src/parts/web_search_tool_result_part.ts | 13 +-- js/plugins/anthropic/src/runner/base.ts | 99 +++++++------------ js/plugins/anthropic/src/utils.ts | 21 ---- 10 files changed, 114 insertions(+), 164 deletions(-) delete mode 100644 js/plugins/anthropic/src/utils.ts diff --git a/js/plugins/anthropic/src/parts/input_json_part.ts b/js/plugins/anthropic/src/parts/input_json_part.ts index 7d9483a908..acc253a295 100644 --- a/js/plugins/anthropic/src/parts/input_json_part.ts +++ b/js/plugins/anthropic/src/parts/input_json_part.ts @@ -14,30 +14,27 @@ * limitations under the License. */ +import { InputJSONDelta } from '@anthropic-ai/sdk/resources'; import { + createAbility, SupportedPart, SupportedPartWhat, SupportedPartWhen, - throwErrorWrongTypeForAbility, } from './part'; const ID_DELTA = 'input_json_delta'; export const InputJsonPart: SupportedPart = { abilities: [ - { + createAbility({ id: [ID_DELTA], when: [SupportedPartWhen.StreamDelta], what: [SupportedPartWhat.ContentBlock], - func: (when, what, delta) => { - if (delta.type !== ID_DELTA) { - throwErrorWrongTypeForAbility(ID_DELTA, when, what); - } - + func: (_when, _what, _delta) => { throw new Error( `Anthropic streaming tool input (${ID_DELTA}) is not yet supported. Please disable streaming or upgrade this plugin.` ); }, - }, + }), ], }; diff --git a/js/plugins/anthropic/src/parts/part.ts b/js/plugins/anthropic/src/parts/part.ts index 188701f5f8..94ffad1463 100644 --- a/js/plugins/anthropic/src/parts/part.ts +++ b/js/plugins/anthropic/src/parts/part.ts @@ -60,25 +60,46 @@ export const SupportedPartWhat = { export function throwErrorWrongTypeForAbility( partId: string, - chunk: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat] ): never { - switch (chunk) { - case SupportedPartWhen.StreamStart: - throw new Error( - `Part '${partId}' is not supported for stream start in ${what}` - ); - case SupportedPartWhen.StreamDelta: - throw new Error( - `Part '${partId}' is not supported for stream delta in ${what}` - ); - case SupportedPartWhen.StreamEnd: - throw new Error( - `Part '${partId}' is not supported for stream end in ${what}` - ); - case SupportedPartWhen.NonStream: - throw new Error( - `Part '${partId}' is not supported for non stream in ${what}` - ); + throw new Error( + `Part '${partId}' is not supported for ${String(when) + .replace(/([A-Z])/g, ' $1') + .toLowerCase()} in ${what}` + ); +} + +function validatePartType( + expectedTypes: string | string[], + chunk: unknown, + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat] +): asserts chunk is T { + const types = Array.isArray(expectedTypes) ? expectedTypes : [expectedTypes]; + const chunkWithType = chunk as { type: string }; + if (!types.includes(chunkWithType.type)) { + throwErrorWrongTypeForAbility(types[0], when, what); } } + +export function createAbility(config: { + id: string[]; + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen][]; + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat][]; + func: ( + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], + what: (typeof SupportedPartWhat)[keyof typeof SupportedPartWhat], + chunk: T + ) => Part; +}): Ability { + return { + id: config.id, + when: config.when, + what: config.what, + func: (when, what, chunk) => { + validatePartType(config.id, chunk, when, what); + return config.func(when, what, chunk); + }, + }; +} diff --git a/js/plugins/anthropic/src/parts/redacted_thinking_part.ts b/js/plugins/anthropic/src/parts/redacted_thinking_part.ts index 20ceaabd2a..f93c64861d 100644 --- a/js/plugins/anthropic/src/parts/redacted_thinking_part.ts +++ b/js/plugins/anthropic/src/parts/redacted_thinking_part.ts @@ -14,28 +14,25 @@ * limitations under the License. */ +import { RedactedThinkingBlock } from '@anthropic-ai/sdk/resources'; import { + createAbility, SupportedPart, SupportedPartWhat, SupportedPartWhen, - throwErrorWrongTypeForAbility, } from './part'; const ID = 'redacted_thinking'; export const RedactedThinkingPart: SupportedPart = { abilities: [ - { + createAbility({ id: [ID], when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], what: [SupportedPartWhat.ContentBlock], - func: (when, what, contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility(ID, when, what); - } - + func: (_when, _what, contentBlock) => { return { custom: { redactedThinking: contentBlock.data } }; }, - }, + }), ], }; diff --git a/js/plugins/anthropic/src/parts/server_tool_use_part.ts b/js/plugins/anthropic/src/parts/server_tool_use_part.ts index 504077d8a7..90dbda3390 100644 --- a/js/plugins/anthropic/src/parts/server_tool_use_part.ts +++ b/js/plugins/anthropic/src/parts/server_tool_use_part.ts @@ -14,26 +14,23 @@ * limitations under the License. */ +import { ServerToolUseBlock } from '@anthropic-ai/sdk/resources'; import { + createAbility, SupportedPart, SupportedPartWhat, SupportedPartWhen, - throwErrorWrongTypeForAbility, } from './part'; const ID = 'server_tool_use'; export const ServerToolUsePart: SupportedPart = { abilities: [ - { + createAbility({ id: [ID], when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], what: [SupportedPartWhat.ContentBlock], - func: (when, what, contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility(ID, when, what); - } - + func: (_when, _what, contentBlock) => { const baseName = contentBlock.name ?? 'unknown_tool'; const serverToolName = 'server_name' in contentBlock && contentBlock.server_name @@ -51,6 +48,6 @@ export const ServerToolUsePart: SupportedPart = { }, }; }, - }, + }), ], }; diff --git a/js/plugins/anthropic/src/parts/text_part.ts b/js/plugins/anthropic/src/parts/text_part.ts index e3b3afab7d..542c953115 100644 --- a/js/plugins/anthropic/src/parts/text_part.ts +++ b/js/plugins/anthropic/src/parts/text_part.ts @@ -14,11 +14,12 @@ * limitations under the License. */ +import { TextBlock, TextDelta } from '@anthropic-ai/sdk/resources'; import { SupportedPart, SupportedPartWhat, SupportedPartWhen, - throwErrorWrongTypeForAbility, + createAbility, } from './part'; const ID = 'text'; @@ -26,7 +27,7 @@ const ID_DELTA = 'text_delta'; export const TextPart: SupportedPart = { abilities: [ - { + createAbility({ id: [ID, ID_DELTA], when: [ SupportedPartWhen.NonStream, @@ -34,13 +35,9 @@ export const TextPart: SupportedPart = { SupportedPartWhen.StreamStart, ], what: [SupportedPartWhat.ContentBlock], - func: (when, what, content) => { - if (content.type !== ID && content.type !== ID_DELTA) { - throwErrorWrongTypeForAbility(ID, when, what); - } - + func: (_when, _what, content) => { return { text: content.text }; }, - }, + }), ], }; diff --git a/js/plugins/anthropic/src/parts/thinking_part.ts b/js/plugins/anthropic/src/parts/thinking_part.ts index 75ee94a6b4..0221162a36 100644 --- a/js/plugins/anthropic/src/parts/thinking_part.ts +++ b/js/plugins/anthropic/src/parts/thinking_part.ts @@ -14,13 +14,14 @@ * limitations under the License. */ +import { ThinkingBlock, ThinkingDelta } from '@anthropic-ai/sdk/resources'; import { Part } from 'genkit'; import { ANTHROPIC_THINKING_CUSTOM_KEY } from '../runner/base'; import { SupportedPart, SupportedPartWhat, SupportedPartWhen, - throwErrorWrongTypeForAbility, + createAbility, } from './part'; const ID = 'thinking'; @@ -28,34 +29,26 @@ const ID_DELTA = 'thinking_delta'; export const ThinkingPart: SupportedPart = { abilities: [ - { + createAbility({ id: [ID], when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], what: [SupportedPartWhat.ContentBlock], - func: (when, what, contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility(ID, when, what); - } - + func: (_when, _what, contentBlock) => { return createThinkingPart( contentBlock.thinking, contentBlock.signature ); }, - }, + }), - { + createAbility({ id: [ID_DELTA], when: [SupportedPartWhen.StreamDelta], what: [SupportedPartWhat.ContentBlock], - func: (when, what, delta) => { - if (delta.type !== ID_DELTA) { - throwErrorWrongTypeForAbility(ID_DELTA, when, what); - } - + func: (_when, _what, delta) => { return { reasoning: delta.thinking }; }, - }, + }), ], }; diff --git a/js/plugins/anthropic/src/parts/tool_use_part.ts b/js/plugins/anthropic/src/parts/tool_use_part.ts index 9ff8cde59c..4522b25eda 100644 --- a/js/plugins/anthropic/src/parts/tool_use_part.ts +++ b/js/plugins/anthropic/src/parts/tool_use_part.ts @@ -14,26 +14,23 @@ * limitations under the License. */ +import { ToolUseBlock } from '@anthropic-ai/sdk/resources'; import { + createAbility, SupportedPart, SupportedPartWhat, SupportedPartWhen, - throwErrorWrongTypeForAbility, } from './part'; const ID = 'tool_use'; export const ToolUsePart: SupportedPart = { abilities: [ - { + createAbility({ id: [ID], when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], what: [SupportedPartWhat.ContentBlock], - func: (when, what, contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility(ID, when, what); - } - + func: (_when, _what, contentBlock) => { return { toolRequest: { ref: contentBlock.id, @@ -42,6 +39,6 @@ export const ToolUsePart: SupportedPart = { }, }; }, - }, + }), ], }; diff --git a/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts b/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts index cacddde9d0..4da69f8f44 100644 --- a/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts +++ b/js/plugins/anthropic/src/parts/web_search_tool_result_part.ts @@ -14,34 +14,31 @@ * limitations under the License. */ +import { WebSearchToolResultBlock } from '@anthropic-ai/sdk/resources'; import { Part } from 'genkit'; import { + createAbility, SupportedPart, SupportedPartWhat, SupportedPartWhen, - throwErrorWrongTypeForAbility, } from './part'; const ID = 'web_search_tool_result'; export const WebSearchToolResultPart: SupportedPart = { abilities: [ - { + createAbility({ id: [ID], when: [SupportedPartWhen.NonStream, SupportedPartWhen.StreamStart], what: [SupportedPartWhat.ContentBlock], - func: (when, what, contentBlock) => { - if (contentBlock.type !== ID) { - throwErrorWrongTypeForAbility(ID, when, what); - } - + func: (_when, _what, contentBlock) => { return toWebSearchToolResultPart({ type: contentBlock.type, toolUseId: contentBlock.tool_use_id, content: contentBlock.content, }); }, - }, + }), ], }; diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index cd326c21a9..37ec5e8715 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -51,7 +51,6 @@ import { type ClaudeRunnerParams, type ThinkingConfig, } from '../types.js'; -import { unsupportedServerToolError } from '../utils.js'; import { RunnerContentBlockParam, RunnerMessage, @@ -437,94 +436,70 @@ export abstract class BaseRunner { protected toGenkitPart( event: MessageStreamEvent | BetaRawMessageStreamEvent ): Part | undefined { - // Handle content_block_start events if (event.type === 'content_block_start') { - const contentBlock = event.content_block; - const contentBlockType = (event.content_block as { type?: string }).type; - - if ( - contentBlockType && - this.unsupportedServerToolBlockTypes.has(contentBlockType) - ) { - throw new Error( - unsupportedServerToolError(contentBlockType, this.isBeta ?? false) - ); - } - - const foundSupportedPartAbility = this.findSupportedPartAbility( - contentBlock.type, + return this.convertContentBlock( + event.content_block, SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock + true ); - if (foundSupportedPartAbility) { - return foundSupportedPartAbility.func( - SupportedPartWhen.StreamStart, - SupportedPartWhat.ContentBlock, - contentBlock - ); - } - - logger.warn( - `Unexpected Anthropic content block type in stream: ${contentBlockType}. Returning undefined. Content block: ${JSON.stringify(contentBlock)}` - ); - return undefined; } - // Handle content_block_delta events if (event.type === 'content_block_delta') { - const foundSupportedPartAbility = this.findSupportedPartAbility( - event.delta.type, + return this.convertContentBlock( + event.delta, SupportedPartWhen.StreamDelta, - SupportedPartWhat.ContentBlock + true ); - if (foundSupportedPartAbility) { - return foundSupportedPartAbility.func( - SupportedPartWhen.StreamDelta, - SupportedPartWhat.ContentBlock, - event.delta - ); - } - - // signature_delta - ignore - return undefined; } - // Other event types (message_start, message_delta, etc.) - ignore return undefined; } protected fromAnthropicContentBlock( contentBlock: ContentBlock | BetaContentBlock ): Part { - const contentBlockType = (contentBlock as { type?: string }).type; + return ( + this.convertContentBlock( + contentBlock, + SupportedPartWhen.NonStream, + false + ) ?? { text: '' } + ); + } - if ( - contentBlockType && - this.unsupportedServerToolBlockTypes.has(contentBlockType) - ) { + private convertContentBlock( + chunk: + | MessageStreamEvent + | BetaRawMessageStreamEvent + | ContentBlock + | BetaContentBlock + | { type: string }, + when: (typeof SupportedPartWhen)[keyof typeof SupportedPartWhen], + isStream: boolean + ): Part | undefined { + const chunkType = (chunk as { type: string }).type; + + if (this.unsupportedServerToolBlockTypes.has(chunkType)) { throw new Error( - unsupportedServerToolError(contentBlockType, this.isBeta ?? false) + `Anthropic ${this.isBeta ? 'beta' : 'stable'} runner does not yet support server-managed tool block '${chunkType}'.` ); } - const foundSupportedPartAbility = this.findSupportedPartAbility( - contentBlock.type, - SupportedPartWhen.NonStream, + const ability = this.findSupportedPartAbility( + chunkType, + when, SupportedPartWhat.ContentBlock ); - if (foundSupportedPartAbility) { - return foundSupportedPartAbility.func( - SupportedPartWhen.NonStream, - SupportedPartWhat.ContentBlock, - contentBlock - ); + + if (ability) { + return ability.func(when, SupportedPartWhat.ContentBlock, chunk as never); } - const unknownType = (contentBlock as { type: string }).type; logger.warn( - `Unexpected Anthropic content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify(contentBlock)}` + `Unexpected Anthropic content block type${isStream ? ' in stream' : ''}: ${chunkType}. ${isStream ? 'Returning undefined' : 'Returning empty text'}. Content block: ${JSON.stringify(chunk)}` ); - return { text: '' }; + + return isStream ? undefined : { text: '' }; } /** diff --git a/js/plugins/anthropic/src/utils.ts b/js/plugins/anthropic/src/utils.ts deleted file mode 100644 index 1bc7640fdd..0000000000 --- a/js/plugins/anthropic/src/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const unsupportedServerToolError = ( - blockType: string, - isBeta: boolean -): string => - `Anthropic ${isBeta ? 'beta' : 'stable'} runner does not yet support server-managed tool block '${blockType}'.`; From 5f3a3e22172d58c4e550b404978461fb5921a978 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 6 Jan 2026 11:30:42 +0000 Subject: [PATCH 8/9] refactor(anthropic): rename `toGenkitPart` to `fromAnthropicContentBlockChunk` --- js/plugins/anthropic/src/runner/base.ts | 4 ++-- js/plugins/anthropic/tests/beta_runner_test.ts | 12 +++++++----- js/plugins/anthropic/tests/stable_runner_test.ts | 12 ++++++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index 37ec5e8715..a978f7f9a7 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -433,7 +433,7 @@ export abstract class BaseRunner { return { system, messages: anthropicMsgs }; } - protected toGenkitPart( + protected fromAnthropicContentBlockChunk( event: MessageStreamEvent | BetaRawMessageStreamEvent ): Part | undefined { if (event.type === 'content_block_start') { @@ -592,7 +592,7 @@ export abstract class BaseRunner { ); const stream = this.streamMessages(body, abortSignal); for await (const event of stream) { - const part = this.toGenkitPart( + const part = this.fromAnthropicContentBlockChunk( event as MessageStreamEvent | BetaRawMessageStreamEvent ); if (part) { diff --git a/js/plugins/anthropic/tests/beta_runner_test.ts b/js/plugins/anthropic/tests/beta_runner_test.ts index e472fbafb5..74fa097f89 100644 --- a/js/plugins/anthropic/tests/beta_runner_test.ts +++ b/js/plugins/anthropic/tests/beta_runner_test.ts @@ -300,7 +300,7 @@ describe('BetaRunner', () => { }); const exposed = runner as any; - const textPart = exposed.toGenkitPart({ + const textPart = exposed.fromAnthropicContentBlockChunk({ type: 'content_block_start', index: 0, content_block: { type: 'text', text: 'hi' }, @@ -318,7 +318,7 @@ describe('BetaRunner', () => { server_name: 'srv', }, } as any; - const toolPart = exposed.toGenkitPart(serverToolEvent); + const toolPart = exposed.fromAnthropicContentBlockChunk(serverToolEvent); assert.deepStrictEqual(toolPart, { text: '[Anthropic server tool srv/myTool] input: {"foo":"bar"}', custom: { @@ -330,14 +330,16 @@ describe('BetaRunner', () => { }, }); - const deltaPart = exposed.toGenkitPart({ + const deltaPart = exposed.fromAnthropicContentBlockChunk({ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'hmm' }, } as any); assert.deepStrictEqual(deltaPart, { reasoning: 'hmm' }); - const ignored = exposed.toGenkitPart({ type: 'message_stop' } as any); + const ignored = exposed.fromAnthropicContentBlockChunk({ + type: 'message_stop', + } as any); assert.strictEqual(ignored, undefined); }); @@ -351,7 +353,7 @@ describe('BetaRunner', () => { const exposed = runner as any; assert.throws( () => - exposed.toGenkitPart({ + exposed.fromAnthropicContentBlockChunk({ type: 'content_block_start', index: 0, content_block: { diff --git a/js/plugins/anthropic/tests/stable_runner_test.ts b/js/plugins/anthropic/tests/stable_runner_test.ts index cd3cb9b55c..c530fce853 100644 --- a/js/plugins/anthropic/tests/stable_runner_test.ts +++ b/js/plugins/anthropic/tests/stable_runner_test.ts @@ -65,10 +65,12 @@ type RunnerProtectedMethods = { request: GenerateRequest, cacheSystemPrompt?: boolean ) => any; + fromAnthropicContentBlockChunk: ( + event: MessageStreamEvent + ) => Part | undefined; fromAnthropicStopReason: (reason: Message['stop_reason']) => any; fromAnthropicResponse: (message: Message) => GenerateResponseData; toGenkitResponse: (message: Message) => GenerateResponseData; - toGenkitPart: (event: MessageStreamEvent) => Part | undefined; }; const mockClient = createMockAnthropicClient(); @@ -650,7 +652,7 @@ describe('toAnthropicTool', () => { }); }); -describe('toGenkitPart', () => { +describe('fromAnthropicContentBlockChunk', () => { const testCases: { should: string; event: MessageStreamEvent; @@ -754,7 +756,9 @@ describe('toGenkitPart', () => { for (const test of testCases) { it(test.should, () => { - const actualOutput = testRunner.toGenkitPart(test.event); + const actualOutput = testRunner.fromAnthropicContentBlockChunk( + test.event + ); assert.deepStrictEqual(actualOutput, test.expectedOutput); }); } @@ -762,7 +766,7 @@ describe('toGenkitPart', () => { it('should throw for unsupported tool input streaming deltas', () => { assert.throws( () => - testRunner.toGenkitPart({ + testRunner.fromAnthropicContentBlockChunk({ index: 0, type: 'content_block_delta', delta: { From 84793d1b73a8a2e53f53b5a59b82847657f774a7 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 6 Jan 2026 11:33:56 +0000 Subject: [PATCH 9/9] refactor(anthropic): rename `toGenkitResponse` to `fromAnthropicResponse` --- js/plugins/anthropic/src/runner/base.ts | 6 +++--- js/plugins/anthropic/src/runner/beta.ts | 2 +- js/plugins/anthropic/src/runner/stable.ts | 2 +- js/plugins/anthropic/tests/stable_runner_test.ts | 5 ++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts index a978f7f9a7..e3080935c1 100644 --- a/js/plugins/anthropic/src/runner/base.ts +++ b/js/plugins/anthropic/src/runner/base.ts @@ -570,7 +570,7 @@ export abstract class BaseRunner { abortSignal: AbortSignal ): RunnerStream; - protected abstract toGenkitResponse( + protected abstract fromAnthropicResponse( message: RunnerMessage ): GenerateResponseData; @@ -603,7 +603,7 @@ export abstract class BaseRunner { } } const finalMessage = await stream.finalMessage(); - return this.toGenkitResponse(finalMessage); + return this.fromAnthropicResponse(finalMessage); } const body = this.toAnthropicRequestBody( @@ -612,6 +612,6 @@ export abstract class BaseRunner { this.cacheSystemPrompt ); const response = await this.createMessage(body, abortSignal); - return this.toGenkitResponse(response); + return this.fromAnthropicResponse(response); } } diff --git a/js/plugins/anthropic/src/runner/beta.ts b/js/plugins/anthropic/src/runner/beta.ts index 2dbdf437e2..6eeeea7006 100644 --- a/js/plugins/anthropic/src/runner/beta.ts +++ b/js/plugins/anthropic/src/runner/beta.ts @@ -415,7 +415,7 @@ export class BetaRunner extends BaseRunner { return body; } - protected toGenkitResponse(message: BetaMessage): GenerateResponseData { + protected fromAnthropicResponse(message: BetaMessage): GenerateResponseData { return { candidates: [ { diff --git a/js/plugins/anthropic/src/runner/stable.ts b/js/plugins/anthropic/src/runner/stable.ts index c5413c2e70..c17a340839 100644 --- a/js/plugins/anthropic/src/runner/stable.ts +++ b/js/plugins/anthropic/src/runner/stable.ts @@ -346,7 +346,7 @@ export class Runner extends BaseRunner { return this.client.messages.stream(body, { signal: abortSignal }); } - protected toGenkitResponse(message: Message): GenerateResponseData { + protected fromAnthropicResponse(message: Message): GenerateResponseData { return { candidates: [ { diff --git a/js/plugins/anthropic/tests/stable_runner_test.ts b/js/plugins/anthropic/tests/stable_runner_test.ts index c530fce853..72797251b9 100644 --- a/js/plugins/anthropic/tests/stable_runner_test.ts +++ b/js/plugins/anthropic/tests/stable_runner_test.ts @@ -70,7 +70,6 @@ type RunnerProtectedMethods = { ) => Part | undefined; fromAnthropicStopReason: (reason: Message['stop_reason']) => any; fromAnthropicResponse: (message: Message) => GenerateResponseData; - toGenkitResponse: (message: Message) => GenerateResponseData; }; const mockClient = createMockAnthropicClient(); @@ -820,7 +819,7 @@ describe('fromAnthropicStopReason', () => { } }); -describe('toGenkitResponse', () => { +describe('fromAnthropicResponse', () => { const testCases: { should: string; message: Message; @@ -919,7 +918,7 @@ describe('toGenkitResponse', () => { for (const test of testCases) { it(test.should, () => { - const actualOutput = testRunner.toGenkitResponse(test.message); + const actualOutput = testRunner.fromAnthropicResponse(test.message); // Check custom field exists and is the message assert.ok(actualOutput.custom); assert.strictEqual(actualOutput.custom, test.message);