Skip to content

Commit 06708a5

Browse files
cabljachugoaguirre
authored andcommitted
refactor(js/plugins/anthropic): extract out some shared converters (#4046)
1 parent c844abb commit 06708a5

File tree

9 files changed

+312
-207
lines changed

9 files changed

+312
-207
lines changed

js/plugins/anthropic/src/runner/base.ts

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ import {
5050
RunnerTypes,
5151
} from './types.js';
5252

53-
const ANTHROPIC_THINKING_CUSTOM_KEY = 'anthropicThinking';
54-
5553
/**
5654
* Shared runner logic for Anthropic SDK integrations.
5755
*
@@ -298,35 +296,11 @@ export abstract class BaseRunner<ApiTypes extends RunnerTypes> {
298296
};
299297
}
300298

301-
protected createThinkingPart(thinking: string, signature?: string): Part {
302-
const custom =
303-
signature !== undefined
304-
? {
305-
[ANTHROPIC_THINKING_CUSTOM_KEY]: { signature },
306-
}
307-
: undefined;
308-
return custom
309-
? {
310-
reasoning: thinking,
311-
custom,
312-
}
313-
: {
314-
reasoning: thinking,
315-
};
316-
}
317-
318299
protected getThinkingSignature(part: Part): string | undefined {
319-
const custom = part.custom as Record<string, unknown> | undefined;
320-
const thinkingValue = custom?.[ANTHROPIC_THINKING_CUSTOM_KEY];
321-
if (
322-
typeof thinkingValue === 'object' &&
323-
thinkingValue !== null &&
324-
'signature' in thinkingValue &&
325-
typeof (thinkingValue as { signature: unknown }).signature === 'string'
326-
) {
327-
return (thinkingValue as { signature: string }).signature;
328-
}
329-
return undefined;
300+
const metadata = part.metadata as Record<string, unknown> | undefined;
301+
return typeof metadata?.thoughtSignature === 'string'
302+
? metadata.thoughtSignature
303+
: undefined;
330304
}
331305

332306
protected getRedactedThinkingData(part: Part): string | undefined {
@@ -363,24 +337,6 @@ export abstract class BaseRunner<ApiTypes extends RunnerTypes> {
363337
return undefined;
364338
}
365339

366-
protected toWebSearchToolResultPart(params: {
367-
toolUseId: string;
368-
content: unknown;
369-
type: string;
370-
}): Part {
371-
const { toolUseId, content, type } = params;
372-
return {
373-
text: `[Anthropic server tool result ${toolUseId}] ${JSON.stringify(content)}`,
374-
custom: {
375-
anthropicServerToolResult: {
376-
type,
377-
toolUseId,
378-
content,
379-
},
380-
},
381-
};
382-
}
383-
384340
/**
385341
* Converts a Genkit Part to the corresponding Anthropic content block.
386342
* Each runner implements this to return its specific API type.

js/plugins/anthropic/src/runner/beta.ts

Lines changed: 49 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,22 @@ import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js';
4646
import { AnthropicConfigSchema, type ClaudeRunnerParams } from '../types.js';
4747
import { removeUndefinedProperties } from '../utils.js';
4848
import { BaseRunner } from './base.js';
49+
import {
50+
betaServerToolUseBlockToPart,
51+
unsupportedServerToolError,
52+
} from './converters/beta.js';
53+
import {
54+
inputJsonDeltaError,
55+
redactedThinkingBlockToPart,
56+
textBlockToPart,
57+
textDeltaToPart,
58+
thinkingBlockToPart,
59+
thinkingDeltaToPart,
60+
toolUseBlockToPart,
61+
webSearchToolResultBlockToPart,
62+
} from './converters/shared.js';
4963
import { RunnerTypes } from './types.js';
5064

51-
/**
52-
* Server-managed tool blocks emitted by the beta API that Genkit cannot yet
53-
* interpret. We fail fast on these so callers do not accidentally treat them as
54-
* locally executable tool invocations.
55-
*/
56-
/**
57-
* Server tool types that exist in beta but are not yet supported.
58-
* Note: server_tool_use and web_search_tool_result ARE supported (same as stable API).
59-
*/
60-
const BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES = new Set<string>([
61-
'web_fetch_tool_result',
62-
'code_execution_tool_result',
63-
'bash_code_execution_tool_result',
64-
'text_editor_code_execution_tool_result',
65-
'mcp_tool_result',
66-
'mcp_tool_use',
67-
'container_upload',
68-
]);
69-
7065
const BETA_APIS = [
7166
// 'message-batches-2024-09-24',
7267
// 'prompt-caching-2024-07-31',
@@ -118,9 +113,6 @@ function toAnthropicSchema(
118113
return out;
119114
}
120115

121-
const unsupportedServerToolError = (blockType: string): string =>
122-
`Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`;
123-
124116
interface BetaRunnerTypes extends RunnerTypes {
125117
Message: BetaMessage;
126118
Stream: BetaMessageStream;
@@ -167,7 +159,7 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
167159
const signature = this.getThinkingSignature(part);
168160
if (!signature) {
169161
throw new Error(
170-
'Anthropic thinking parts require a signature when sending back to the API. Preserve the `custom.anthropicThinking.signature` value from the original response.'
162+
'Anthropic thinking parts require a signature when sending back to the API. Preserve the `metadata.thoughtSignature` value from the original response.'
171163
);
172164
}
173165
return {
@@ -462,89 +454,64 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
462454

463455
protected toGenkitPart(event: BetaRawMessageStreamEvent): Part | undefined {
464456
if (event.type === 'content_block_start') {
465-
const blockType = (event.content_block as { type?: string }).type;
466-
if (
467-
blockType &&
468-
BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(blockType)
469-
) {
470-
throw new Error(unsupportedServerToolError(blockType));
471-
}
472457
return this.fromBetaContentBlock(event.content_block);
473458
}
474459
if (event.type === 'content_block_delta') {
475460
if (event.delta.type === 'text_delta') {
476-
return { text: event.delta.text };
461+
return textDeltaToPart(event.delta);
477462
}
478463
if (event.delta.type === 'thinking_delta') {
479-
return { reasoning: event.delta.thinking };
464+
return thinkingDeltaToPart(event.delta);
480465
}
481-
// server/client tool input_json_delta not supported yet
466+
if (event.delta.type === 'input_json_delta') {
467+
throw inputJsonDeltaError();
468+
}
469+
// signature_delta - ignore
482470
return undefined;
483471
}
484472
return undefined;
485473
}
486474

487475
private fromBetaContentBlock(contentBlock: BetaContentBlock): Part {
488476
switch (contentBlock.type) {
489-
case 'tool_use': {
490-
return {
491-
toolRequest: {
492-
ref: contentBlock.id,
493-
name: contentBlock.name ?? 'unknown_tool',
494-
input: contentBlock.input,
495-
},
496-
};
497-
}
498-
499-
case 'mcp_tool_use':
500-
throw new Error(unsupportedServerToolError(contentBlock.type));
501-
502-
case 'server_tool_use': {
503-
const baseName = contentBlock.name ?? 'unknown_tool';
504-
const serverToolName =
505-
'server_name' in contentBlock && contentBlock.server_name
506-
? `${contentBlock.server_name}/${baseName}`
507-
: baseName;
508-
return {
509-
text: `[Anthropic server tool ${serverToolName}] input: ${JSON.stringify(contentBlock.input)}`,
510-
custom: {
511-
anthropicServerToolUse: {
512-
id: contentBlock.id,
513-
name: serverToolName,
514-
input: contentBlock.input,
515-
},
516-
},
517-
};
518-
}
477+
case 'text':
478+
return textBlockToPart(contentBlock);
519479

520-
case 'web_search_tool_result':
521-
return this.toWebSearchToolResultPart({
522-
type: contentBlock.type,
523-
toolUseId: contentBlock.tool_use_id,
524-
content: contentBlock.content,
480+
case 'tool_use':
481+
// Beta API may have undefined name, fallback to 'unknown_tool'
482+
return toolUseBlockToPart({
483+
id: contentBlock.id,
484+
name: contentBlock.name ?? 'unknown_tool',
485+
input: contentBlock.input,
525486
});
526487

527-
case 'text':
528-
return { text: contentBlock.text };
529-
530488
case 'thinking':
531-
return this.createThinkingPart(
532-
contentBlock.thinking,
533-
contentBlock.signature
534-
);
489+
return thinkingBlockToPart(contentBlock);
535490

536491
case 'redacted_thinking':
537-
return { custom: { redactedThinking: contentBlock.data } };
492+
return redactedThinkingBlockToPart(contentBlock);
493+
494+
case 'server_tool_use':
495+
return betaServerToolUseBlockToPart(contentBlock);
496+
497+
case 'web_search_tool_result':
498+
return webSearchToolResultBlockToPart(contentBlock);
499+
500+
// Unsupported beta server tool types
501+
case 'mcp_tool_use':
502+
case 'mcp_tool_result':
503+
case 'web_fetch_tool_result':
504+
case 'code_execution_tool_result':
505+
case 'bash_code_execution_tool_result':
506+
case 'text_editor_code_execution_tool_result':
507+
case 'container_upload':
508+
case 'tool_search_tool_result':
509+
throw new Error(unsupportedServerToolError(contentBlock.type));
538510

539511
default: {
540-
if (BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(contentBlock.type)) {
541-
throw new Error(unsupportedServerToolError(contentBlock.type));
542-
}
543512
const unknownType = (contentBlock as { type: string }).type;
544513
logger.warn(
545-
`Unexpected Anthropic beta content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify(
546-
contentBlock
547-
)}`
514+
`Unexpected Anthropic beta content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify(contentBlock)}`
548515
);
549516
return { text: '' };
550517
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Converters for beta API content blocks.
19+
*/
20+
21+
import type { Part } from 'genkit';
22+
23+
/**
24+
* Converts a server_tool_use block to a Genkit Part.
25+
* In the beta API, name may be undefined and server_name prefix is supported.
26+
*/
27+
export function betaServerToolUseBlockToPart(block: {
28+
id: string;
29+
name?: string;
30+
input: unknown;
31+
server_name?: string;
32+
}): Part {
33+
const baseName = block.name ?? 'unknown_tool';
34+
const serverToolName = block.server_name
35+
? `${block.server_name}/${baseName}`
36+
: baseName;
37+
return {
38+
text: `[Anthropic server tool ${serverToolName}] input: ${JSON.stringify(block.input)}`,
39+
metadata: {
40+
anthropicServerToolUse: {
41+
id: block.id,
42+
name: serverToolName,
43+
input: block.input,
44+
},
45+
},
46+
};
47+
}
48+
49+
/**
50+
* Error message for unsupported server tool block types in the beta API.
51+
*/
52+
export function unsupportedServerToolError(blockType: string): string {
53+
return `Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`;
54+
}

0 commit comments

Comments
 (0)