Repository navigation
fix(anthropic): accept server-side tools on /v1/messages #2702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,12 +88,52 @@ const anthropicMessageSchema = z.object({ | |
| .optional(), | ||
| }); | ||
|
|
||
| const anthropicToolSchema = z.object({ | ||
| // Standard Anthropic "custom" tools: a name plus a JSON schema describing the | ||
| // parameters the model should produce. | ||
| const anthropicCustomToolSchema = z.object({ | ||
| type: z.literal("custom").optional(), | ||
| name: z.string(), | ||
| description: z.string(), | ||
| description: z.string().optional(), | ||
| input_schema: z.record(z.unknown()), | ||
| cache_control: z | ||
| .object({ | ||
| type: z.enum(["ephemeral"]), | ||
| ttl: z.enum(["5m", "1h"]).optional(), | ||
| }) | ||
| .nullish(), | ||
| }); | ||
|
|
||
| // Anthropic server-side tools (e.g. web_search_20250305, code_execution_*). | ||
| // These are executed by Anthropic, carry a versioned `type` instead of an | ||
| // `input_schema`, and must not be validated as custom tools. | ||
| const anthropicServerToolSchema = z.object({ | ||
| type: z.string(), | ||
| name: z.string(), | ||
| max_uses: z.number().optional(), | ||
| allowed_domains: z.array(z.string()).optional(), | ||
| blocked_domains: z.array(z.string()).optional(), | ||
| user_location: z | ||
| .object({ | ||
| type: z.literal("approximate").optional(), | ||
| city: z.string().optional(), | ||
| region: z.string().optional(), | ||
| country: z.string().optional(), | ||
| timezone: z.string().optional(), | ||
| }) | ||
| .optional(), | ||
| cache_control: z | ||
| .object({ | ||
| type: z.enum(["ephemeral"]), | ||
| ttl: z.enum(["5m", "1h"]).optional(), | ||
| }) | ||
| .nullish(), | ||
| }); | ||
|
|
||
| const anthropicToolSchema = z.union([ | ||
| anthropicCustomToolSchema, | ||
| anthropicServerToolSchema, | ||
| ]); | ||
|
|
||
| const anthropicRequestSchema = z.object({ | ||
| model: z.string().openapi({ | ||
| description: "The model to use for completion", | ||
|
|
@@ -522,17 +562,49 @@ anthropic.openapi(messages, async (c) => { | |
| } | ||
| } | ||
|
|
||
| // Transform tools if provided | ||
| // Transform tools if provided. Custom tools map to OpenAI function tools; | ||
| // Anthropic server-side tools (e.g. web_search_20250305) carry a versioned | ||
| // `type` and no `input_schema`, so they're translated to the internal | ||
| // `web_search` tool the chat completions endpoint understands. Server tools | ||
| // we can't represent are dropped (with a warning) rather than rejected. | ||
| let openaiTools; | ||
| if (anthropicRequest.tools) { | ||
| openaiTools = anthropicRequest.tools.map((tool) => ({ | ||
| type: "function", | ||
| function: { | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: tool.input_schema, | ||
| }, | ||
| })); | ||
| openaiTools = anthropicRequest.tools | ||
| .map((tool) => { | ||
| if ("input_schema" in tool) { | ||
| return { | ||
| type: "function", | ||
| function: { | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: tool.input_schema, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| if (tool.type.startsWith("web_search")) { | ||
| return { | ||
| type: "web_search", | ||
| ...(tool.max_uses !== undefined ? { max_uses: tool.max_uses } : {}), | ||
|
Comment on lines
+585
to
+588
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a native Useful? React with 👍 / 👎. |
||
| ...(tool.user_location | ||
| ? { user_location: tool.user_location } | ||
| : {}), | ||
| ...(tool.allowed_domains | ||
| ? { allowed_domains: tool.allowed_domains } | ||
| : {}), | ||
| ...(tool.blocked_domains | ||
| ? { blocked_domains: tool.blocked_domains } | ||
| : {}), | ||
| }; | ||
|
Comment on lines
+585
to
+598
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a native Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| logger.warn("Dropping unsupported Anthropic server tool", { | ||
| type: tool.type, | ||
| name: tool.name, | ||
| }); | ||
| return null; | ||
| }) | ||
| .filter((tool): tool is NonNullable<typeof tool> => tool !== null); | ||
| } | ||
|
|
||
| // Build OpenAI request | ||
|
|
@@ -544,7 +616,7 @@ anthropic.openapi(messages, async (c) => { | |
| stream: anthropicRequest.stream, | ||
| }; | ||
|
|
||
| if (openaiTools) { | ||
| if (openaiTools && openaiTools.length > 0) { | ||
| openaiRequest.tools = openaiTools; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the server-tool schema accepts any string
type, a request liketools: [{ type: "custom", name: "get_weather", description: "..." }]no longer fails validation for the missinginput_schema: it misses the custom-tool union arm, matches this server-tool arm, and is then silently dropped as an unsupported server tool. This regresses malformed custom-tool validation for clients that include the explicittype: "custom"field that this same schema allows for valid custom tools.Useful? React with 👍 / 👎.