From efe3c648a8b8f11ccf446505ed04e5401aa0309a Mon Sep 17 00:00:00 2001 From: Ling-Sen Peng Date: Fri, 14 Aug 2026 17:45:29 -0700 Subject: [PATCH] fix(agents): support Claude Sonnet 5 extended thinking; update examples to claude-sonnet-5 Claude Sonnet 5 rejects the legacy thinking.type=enabled + budget_tokens shape (HTTP 400) and the server's Anthropic provider still renders thinkingConfig that way for Sonnet-line models. Translate thinkingBudgetTokens into reasoningEffort client-side for Sonnet 5+ (same tiers the server uses for Opus); Sonnet 5 runs adaptive thinking by default. Also replace the retired claude-sonnet-4-20250514 with claude-sonnet-5 across examples. --- examples/agents/56-rag-agent.ts | 2 +- examples/agents/58-scatter-gather.ts | 2 +- examples/agents/59-coding-agent.ts | 4 +- examples/agents/60-github-coding-agent.ts | 8 +-- .../agents/60a-github-coding-agent-simple.ts | 8 +-- examples/agents/README.md | 2 +- examples/agents/adk/35-rag-agent.ts | 2 +- src/agents/__tests__/serializer.test.ts | 58 ++++++++++++++++++- src/agents/serializer.ts | 54 +++++++++++++++-- 9 files changed, 120 insertions(+), 20 deletions(-) diff --git a/examples/agents/56-rag-agent.ts b/examples/agents/56-rag-agent.ts index 411449ff..431371e3 100644 --- a/examples/agents/56-rag-agent.ts +++ b/examples/agents/56-rag-agent.ts @@ -55,7 +55,7 @@ const DOCUMENTS = [ text: "Agent Configuration. Agents are defined with a name, model, instructions, " + "and tools. The model field uses the format 'provider/model_name', e.g. " + - "'openai/gpt-4o' or 'anthropic/claude-sonnet-4-20250514'. Instructions can be " + + "'openai/gpt-4o' or 'anthropic/claude-sonnet-5'. Instructions can be " + 'a string or a PromptTemplate referencing a stored prompt. Tools can be ' + '@tool-decorated Python functions, http_tool for REST APIs, mcp_tool for ' + 'MCP servers, or agent_tool to wrap another agent as a callable tool. ' + diff --git a/examples/agents/58-scatter-gather.ts b/examples/agents/58-scatter-gather.ts index 443a89d7..63ff778e 100644 --- a/examples/agents/58-scatter-gather.ts +++ b/examples/agents/58-scatter-gather.ts @@ -45,7 +45,7 @@ const searchKnowledgeBase = tool( export const researcher = new Agent({ name: 'researcher', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a country analyst. You will be given the name of a country. ' + 'Use the search_knowledge_base tool ONCE to research that country, then ' + diff --git a/examples/agents/59-coding-agent.ts b/examples/agents/59-coding-agent.ts index 43ad5463..9dc60f8f 100644 --- a/examples/agents/59-coding-agent.ts +++ b/examples/agents/59-coding-agent.ts @@ -18,7 +18,7 @@ import { Agent, AgentRuntime } from '@io-orkes/conductor-javascript/agents'; export const qaTester = new Agent({ name: 'qa_tester', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a meticulous QA engineer. Review the code written by the ' + 'coder for correctness, edge cases, and bugs. Write and execute test ' + @@ -36,7 +36,7 @@ export const qaTester = new Agent({ export const coder = new Agent({ name: 'coder', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are an expert Python developer. Write clean, well-structured ' + 'Python code to solve the given problem. Always execute your code to ' + diff --git a/examples/agents/60-github-coding-agent.ts b/examples/agents/60-github-coding-agent.ts index c8065626..9ac6f8d8 100644 --- a/examples/agents/60-github-coding-agent.ts +++ b/examples/agents/60-github-coding-agent.ts @@ -258,7 +258,7 @@ const qaTools = [readFile, listFiles]; export const githubAgent = new Agent({ name: 'github_agent', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a GitHub operations specialist. You handle all git and GitHub CLI interactions.\n\n' + 'IMPORTANT: Read the conversation history carefully. If the conversation already contains ' + @@ -283,7 +283,7 @@ export const githubAgent = new Agent({ export const coderAgent = new Agent({ name: 'coder', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are an expert developer. Write clean, well-structured code.\n\n' + 'WHEN YOU RECEIVE A TASK:\n' + @@ -303,7 +303,7 @@ export const coderAgent = new Agent({ export const qaTester = new Agent({ name: 'qa_tester', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a meticulous QA engineer. Review the code written by the coder.\n\n' + '1. Use read_file to read the code that was written\n' + @@ -323,7 +323,7 @@ export const qaTester = new Agent({ export const codingTeam = new Agent({ name: 'coding_team', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a coding team coordinator. Delegate the incoming request ' + 'to github_agent to get started. Call transfer_to_github_agent now.', diff --git a/examples/agents/60a-github-coding-agent-simple.ts b/examples/agents/60a-github-coding-agent-simple.ts index b4dd21cc..76aecfbf 100644 --- a/examples/agents/60a-github-coding-agent-simple.ts +++ b/examples/agents/60a-github-coding-agent-simple.ts @@ -21,7 +21,7 @@ const WORK_DIR = `/tmp/codingexamples-${randomBytes(4).toString('hex')}`; export const githubAgent = new Agent({ name: 'github_agent', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a GitHub operations specialist.\n\n' + `Repo: ${REPO}\nWork dir: ${WORK_DIR}\n\n` + @@ -45,7 +45,7 @@ export const githubAgent = new Agent({ export const coder = new Agent({ name: 'coder', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are an expert developer.\n\n' + `The repo is cloned at ${WORK_DIR}.\n\n` + @@ -65,7 +65,7 @@ export const coder = new Agent({ export const qaTester = new Agent({ name: 'qa_tester', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a meticulous QA engineer.\n\n' + `The repo is at ${WORK_DIR}. You can read files with:\n cat ${WORK_DIR}/src/main.py\n\n` + @@ -83,7 +83,7 @@ export const qaTester = new Agent({ export const codingTeam = new Agent({ name: 'coding_team', - model: 'anthropic/claude-sonnet-4-20250514', + model: 'anthropic/claude-sonnet-5', instructions: 'You are a coding team coordinator. Delegate to github_agent. ' + 'Call transfer_to_github_agent now.', diff --git a/examples/agents/README.md b/examples/agents/README.md index f2cbf011..562c3b65 100644 --- a/examples/agents/README.md +++ b/examples/agents/README.md @@ -127,7 +127,7 @@ The `CONDUCTOR_AGENT_LLM_MODEL` variable uses the `provider/model-name` format. | Provider | Model string | API key env var | |----------|-------------|-----------------| | OpenAI | `anthropic/claude-sonnet-4-6` (default) | `OPENAI_API_KEY` | -| Anthropic | `anthropic/claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` | +| Anthropic | `anthropic/claude-sonnet-5` | `ANTHROPIC_API_KEY` | | Google Gemini | `google_gemini/gemini-2.0-flash` | `GOOGLE_GEMINI_API_KEY` | | AWS Bedrock | `aws_bedrock/...` | AWS credentials | | Azure OpenAI | `azure_openai/...` | Azure credentials | diff --git a/examples/agents/adk/35-rag-agent.ts b/examples/agents/adk/35-rag-agent.ts index 5c006dfd..3c7bf19b 100644 --- a/examples/agents/adk/35-rag-agent.ts +++ b/examples/agents/adk/35-rag-agent.ts @@ -71,7 +71,7 @@ const DOCUMENTS: Document[] = [ text: "Agent Configuration. Agents are defined with a name, model, instructions, " + "and tools. The model field uses the format 'provider/model_name', e.g. " + - "'openai/gpt-4o' or 'anthropic/claude-sonnet-4-20250514'. Instructions can be " + + "'openai/gpt-4o' or 'anthropic/claude-sonnet-5'. Instructions can be " + 'a string or a PromptTemplate referencing a stored prompt. Tools can be ' + '@tool-decorated Python functions, http_tool for REST APIs, mcp_tool for ' + 'MCP servers, or agent_tool to wrap another agent as a callable tool. ' + diff --git a/src/agents/__tests__/serializer.test.ts b/src/agents/__tests__/serializer.test.ts index 405ba843..7c1d817f 100644 --- a/src/agents/__tests__/serializer.test.ts +++ b/src/agents/__tests__/serializer.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "@jest/globals"; import { z } from "zod"; -import { AgentConfigSerializer } from "../serializer.js"; +import { AgentConfigSerializer, budgetToEffort } from "../serializer.js"; import { Agent, PromptTemplate, scatterGather } from "../agent.js"; import type { CallbackHandler, TerminationCondition, HandoffCondition } from "../agent.js"; import { @@ -592,6 +592,62 @@ describe("serializeAgent() — thinkingConfig", () => { }); expect(config).not.toHaveProperty("thinkingBudgetTokens"); }); + + it("keeps legacy thinkingConfig for Sonnet 4.x models", () => { + const a = new Agent({ + name: "test", + model: "anthropic/claude-sonnet-4-6", + thinkingBudgetTokens: 2048, + }); + const config = serializer.serializeAgent(a); + + expect(config.thinkingConfig).toEqual({ enabled: true, budgetTokens: 2048 }); + expect(config).not.toHaveProperty("reasoningEffort"); + }); + + it("translates budget to reasoningEffort for Claude Sonnet 5 (rejects legacy enabled shape)", () => { + const a = new Agent({ + name: "test", + model: "anthropic/claude-sonnet-5", + thinkingBudgetTokens: 4096, + }); + const config = serializer.serializeAgent(a); + + expect(config).not.toHaveProperty("thinkingConfig"); + expect(config.reasoningEffort).toBe("medium"); + }); + + it("matches future Sonnet 5.x minors without a code change", () => { + const a = new Agent({ + name: "test", + model: "anthropic/claude-sonnet-5-1", + thinkingBudgetTokens: 40_000, + }); + const config = serializer.serializeAgent(a); + + expect(config).not.toHaveProperty("thinkingConfig"); + expect(config.reasoningEffort).toBe("xhigh"); + }); + + it("does not override an explicit reasoningEffort on Sonnet 5", () => { + const a = new Agent({ + name: "test", + model: "anthropic/claude-sonnet-5", + thinkingBudgetTokens: 4096, + reasoningEffort: "high", + }); + const config = serializer.serializeAgent(a); + + expect(config).not.toHaveProperty("thinkingConfig"); + expect(config.reasoningEffort).toBe("high"); + }); + + it("maps budget tiers to effort like the server does for Opus", () => { + expect(budgetToEffort(1024)).toBe("low"); + expect(budgetToEffort(4096)).toBe("medium"); + expect(budgetToEffort(16_384)).toBe("high"); + expect(budgetToEffort(64_000)).toBe("xhigh"); + }); }); // ── reasoningEffort / contextWindowBudget / maskedFields ── diff --git a/src/agents/serializer.ts b/src/agents/serializer.ts index 801f8962..8c155e64 100644 --- a/src/agents/serializer.ts +++ b/src/agents/serializer.ts @@ -244,12 +244,27 @@ export class AgentConfigSerializer { // includeContents if (agent.includeContents) config.includeContents = agent.includeContents; - // thinkingBudgetTokens → thinkingConfig + // thinkingBudgetTokens → thinkingConfig. + // + // Exception: Claude Sonnet 5+ rejects the legacy Anthropic + // ``thinking.type: "enabled" + budget_tokens`` shape with HTTP 400 + // (adaptive thinking replaced it), and the server's Anthropic provider + // (≤ 3.32.x) still renders thinkingConfig as that legacy shape for + // Sonnet-line models. Sonnet 5 runs adaptive thinking by default when no + // thinking param is sent, so for those models the budget is translated + // client-side into ``reasoningEffort`` (server → output_config.effort) + // instead of thinkingConfig — same tiers as the server uses for Opus. if (agent.thinkingBudgetTokens !== undefined) { - config.thinkingConfig = { - enabled: true, - budgetTokens: agent.thinkingBudgetTokens, - }; + if (requiresAdaptiveThinking(agent.model)) { + if (config.reasoningEffort === undefined) { + config.reasoningEffort = budgetToEffort(agent.thinkingBudgetTokens); + } + } else { + config.thinkingConfig = { + enabled: true, + budgetTokens: agent.thinkingBudgetTokens, + }; + } } // requiredTools @@ -528,3 +543,32 @@ export class AgentConfigSerializer { return { taskName: `${agentName}_gate` }; } } + +/** + * Anthropic model lines that reject the legacy `thinking.type: "enabled"` + + * `budget_tokens` shape (HTTP 400) AND are mistranslated by the server's + * Anthropic provider (≤ 3.32.x), whose adaptive-model matrix covers the + * Opus / Fable / Mythos lines but not Sonnet 5. Matched as a substring so + * future Sonnet 5.x minors work without a code change; `sonnet-5` (not the + * bare line name) because Sonnet 4.6-and-earlier still accept enabled. + */ +const CLIENT_ADAPTIVE_LINES = ["sonnet-5"]; + +/** True if `model` needs the client-side adaptive-thinking translation. */ +export function requiresAdaptiveThinking(model: string | undefined): boolean { + if (!model) return false; + const m = model.toLowerCase(); + return CLIENT_ADAPTIVE_LINES.some((line) => m.includes(line)); +} + +/** + * Map a legacy thinking-token budget onto an adaptive `effort` tier. Same + * thresholds as the server's Anthropic provider uses for Opus + * (AnthropicChatModel.budgetToEffort): bigger budgets → more thorough thinking. + */ +export function budgetToEffort(budget: number): string { + if (budget < 4_000) return "low"; + if (budget < 16_000) return "medium"; + if (budget < 32_000) return "high"; + return "xhigh"; +}