diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index e2d179883..d7b39d968 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -371,7 +371,7 @@ Claude Code's `/effort` setting is preserved across the adapter: | --- | --- | | `thinking.type: "adaptive"` + `output_config.effort` | Effort passed directly (`minimal`\|`low`\|`medium`\|`high`\|`xhigh`\|`max`\|`ultra`) | | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`, ≤16384→`medium`, above→`high` | -| `thinking.type: "disabled"` | Reasoning parameters omitted entirely | +| `thinking.type: "disabled"` | `reasoning: { effort: "none" }`; summary omitted | The resolved value appears in the request log's **Reasoning effort** column. @@ -389,7 +389,7 @@ The proxy translates every Anthropic Messages API request into the Codex Respons | User `tool_result` | `function_call_output` (`is_error` → `[tool error]` prefix) | | `thinking` / `redacted_thinking` replay | Dropped | | Function tools | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) | -| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, named→`{type:"function",name}` | +| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, named function→`{type:"function",name}`, hosted WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index 546204648..7b58804e1 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -250,7 +250,7 @@ Claude Code の `/effort` 設定はアダプターでも維持されます。 --- | --- | | `thinking.type: "adaptive"` + `output_config.effort` | Effort をそのまま渡します(`minimal`\|`low`\|`medium`\|`high`\|`xhigh`\|`max`\|`ultra`) | | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`、≤16384→`medium`、それより大→`high` | -| `thinking.type: "disabled"` | 推論パラメータをすべて省略します | +| `thinking.type: "disabled"` | `reasoning: { effort: "none" }` を明示し、`summary` は省略します | 解釈された値はリクエストログの **Reasoning effort** 列に表示されます。 @@ -268,7 +268,7 @@ Claude Code の `/effort` 設定はアダプターでも維持されます。 | ユーザー `tool_result` | `function_call_output`(`is_error` → `[tool error]` 接頭辞) | | `thinking` / `redacted_thinking` 再生 | 破棄 | | Function ツール | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) | -| `tool_choice` | `auto`→`auto`、`none`→`none`、`any`→`required`、名前指定→`{type:"function",name}` | +| `tool_choice` | `auto`→`auto`、`none`→`none`、`any`→`required`、名前指定関数→`{type:"function",name}`、ホスト型 WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md index 665c350b0..fd225263e 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -287,7 +287,7 @@ Claude Code의 `/effort` 설정은 어댑터에서도 유지돼요. | --- | --- | | `thinking.type: "adaptive"` + `output_config.effort` | Effort를 그대로 전달해요(`minimal`\|`low`\|`medium`\|`high`\|`xhigh`\|`max`\|`ultra`) | | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`, ≤16384→`medium`, 그보다 크면→`high` | -| `thinking.type: "disabled"` | 추론 매개변수를 모두 생략해요 | +| `thinking.type: "disabled"` | `reasoning: { effort: "none" }`을 명시하고 `summary`는 생략해요 | 해석된 값은 요청 로그의 **Reasoning effort** 열에 표시돼요. @@ -305,7 +305,7 @@ Claude Code의 `/effort` 설정은 어댑터에서도 유지돼요. | 사용자 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 접두사) | | `thinking` / `redacted_thinking` 재생 | 버려요 | | Function 도구 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) | -| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, 이름 지정→`{type:"function",name}` | +| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, 이름 지정 함수→`{type:"function",name}`, 호스팅 WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index 7d3a65ce9..c8ad0ba44 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -266,7 +266,7 @@ Claude Code — это лишь учётные данные для доступ | --- | --- | | `thinking.type: "adaptive"` + `output_config.effort` | Уровень передаётся напрямую (`minimal`\|`low`\|`medium`\|`high`\|`xhigh`\|`max`\|`ultra`) | | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`, ≤16384→`medium`, выше→`high` | -| `thinking.type: "disabled"` | Параметры рассуждений полностью опускаются | +| `thinking.type: "disabled"` | Явно передаётся `reasoning: { effort: "none" }`, а `summary` опускается | Итоговое значение отображается в столбце **Reasoning effort** журнала запросов. @@ -284,7 +284,7 @@ Claude Code — это лишь учётные данные для доступ | `tool_result` пользователя | `function_call_output` (`is_error` → префикс `[tool error]`) | | Повтор `thinking` / `redacted_thinking` | Отбрасывается | | Function-инструменты | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) | -| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, именованный→`{type:"function",name}` | +| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, именованная функция→`{type:"function",name}`, размещённый WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 31410c921..82599d70a 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -245,7 +245,7 @@ Claude Code 的 `/effort` 设置会完整保留并传递给适配器: | --- | --- | | `thinking.type: "adaptive"` + `output_config.effort` | 直接传递强度(`minimal`\|`low`\|`medium`\|`high`\|`xhigh`\|`max`\|`ultra`) | | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`,≤16384→`medium`,更高→`high` | -| `thinking.type: "disabled"` | 完全省略推理参数 | +| `thinking.type: "disabled"` | 显式发送 `reasoning: { effort: "none" }`,并省略 `summary` | 解析后的值会显示在请求日志的 **Reasoning effort** 列中。 @@ -263,7 +263,7 @@ Claude Code 的 `/effort` 设置会完整保留并传递给适配器: | 用户 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 前缀) | | 重放 `thinking` / `redacted_thinking` | 丢弃 | | Function 工具 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) | -| `tool_choice` | `auto`→`auto`,`none`→`none`,`any`→`required`,指定名称→`{type:"function",name}` | +| `tool_choice` | `auto`→`auto`,`none`→`none`,`any`→`required`,指定函数→`{type:"function",name}`,托管 WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | diff --git a/src/claude/inbound.ts b/src/claude/inbound.ts index cda3799d7..a4830f6d7 100644 --- a/src/claude/inbound.ts +++ b/src/claude/inbound.ts @@ -13,6 +13,7 @@ import type { OcxClaudeCodeConfig } from "../types"; import { resolveAlias } from "./alias"; import { stripOneMillionMarker } from "./context-windows"; import { resolveDesktop3pAlias } from "./desktop-3p"; +import { isClaudeWebSearchToolName } from "./outbound"; import { createHash } from "node:crypto"; export class AnthropicRequestError extends Error {} @@ -375,7 +376,12 @@ function toolChoiceToResponses(choice: unknown, body: Rec): void { if (typeof choice.name !== "string" || choice.name.length === 0) { throw new AnthropicRequestError("tool_choice.tool requires a name"); } - body.tool_choice = { type: "function", name: choice.name }; + // Anthropic represents hosted WebSearch as a named tool choice, while + // Responses requires the choice type to match the hosted declaration. + // Preserve forced-tool intent rather than weakening it to `auto`. + body.tool_choice = isClaudeWebSearchToolName(choice.name) + ? { type: "web_search" } + : { type: "function", name: choice.name }; break; default: break; } @@ -502,8 +508,9 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode // An explicit "disabled" is an instruction, not an absence. Dropping it made this // indistinguishable from a request that never mentioned thinking — and for models that // think by default, omission means thinking is ON, sharing the caller's max_tokens (#545). - // "none" is the parser's disable sentinel (parser.ts REASONING_EFFORTS). - body.reasoning = { effort: "none", summary: "none" }; + // `none` is the effort disable sentinel. It is not a valid OpenAI summary + // value, so do not attach the similarly named internal catalog sentinel. + body.reasoning = { effort: "none" }; } else if (isRec(thinking) || outputConfigEffort !== undefined) { const reasoning: Rec = { summary: "auto" }; if (outputConfigEffort !== undefined) { diff --git a/src/responses/schema.ts b/src/responses/schema.ts index 1d497464e..551d9d761 100644 --- a/src/responses/schema.ts +++ b/src/responses/schema.ts @@ -107,7 +107,7 @@ export const toolSchema = z.object({ const builtinToolSchema = z.object({ type: z.string() }).loose(); const hostedToolType = z.enum([ - "web_search_preview", "file_search", "computer_use_preview", + "web_search", "web_search_preview", "file_search", "computer_use_preview", "code_interpreter", "image_generation", "mcp", ]); diff --git a/tests/claude-inbound.test.ts b/tests/claude-inbound.test.ts index 5049609d6..f9f608529 100644 --- a/tests/claude-inbound.test.ts +++ b/tests/claude-inbound.test.ts @@ -96,7 +96,7 @@ describe("claude inbound translation", () => { expect((anthropicToResponsesBody({ ...base, thinking: { type: "adaptive" } }) as any).reasoning).toEqual({ summary: "auto" }); // "disabled" and omitted must NOT collapse to the same state: for a model that thinks by // default, omission means thinking is ON and shares the caller's max_tokens (#545). - expect((anthropicToResponsesBody({ ...base, thinking: { type: "disabled" } }) as any).reasoning).toEqual({ effort: "none", summary: "none" }); // justified: sibling assertions in this test use the same cast + expect((anthropicToResponsesBody({ ...base, thinking: { type: "disabled" } }) as any).reasoning).toEqual({ effort: "none" }); // justified: sibling assertions in this test use the same cast expect((anthropicToResponsesBody(base) as any).reasoning).toBeUndefined(); expect(effortForThinkingBudget(1024)).toBe("low"); expect(effortForThinkingBudget(8192)).toBe("medium"); @@ -134,7 +134,7 @@ describe("claude inbound translation", () => { // rather than left to think anyway (#545). expect(reasoningOf(anthropicToResponsesBody({ ...base, thinking: { type: "disabled" }, output_config: { effort: "high" }, - }))).toEqual({ effort: "none", summary: "none" }); + }))).toEqual({ effort: "none" }); // unknown effort strings are dropped so downstream defaults win expect(reasoningOf(anthropicToResponsesBody({ ...base, thinking: { type: "adaptive" }, output_config: { effort: "turbo" }, @@ -149,6 +149,23 @@ describe("claude inbound translation", () => { .toEqual({ type: "function", name: "Read" }); }); + test("forced Claude WebSearch stays a hosted Responses tool choice", () => { + const body = anthropicToResponsesBody({ + model: "gpt-5.6-luna", + max_tokens: 10, + messages: [{ role: "user", content: "search" }], + tools: [{ type: "web_search_20250305", name: "web_search" }], + tool_choice: { type: "tool", name: "web_search" }, + thinking: { type: "disabled" }, + }) as Record; + + expect(body.tools).toEqual([{ type: "web_search" }]); + expect(body.tool_choice).toEqual({ type: "web_search" }); + expect(body.reasoning).toEqual({ effort: "none" }); + expect(() => responsesRequestSchema.parse(body)).not.toThrow(); + expect(() => parseRequest(body)).not.toThrow(); + }); + test("system role messages fold into instructions (real Claude Code sends them; native backend rejects system items)", () => { const body = anthropicToResponsesBody({ model: "m", max_tokens: 10,