Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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` |

Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/content/docs/ja/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** 列に表示されます。

Expand All @@ -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` |

Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/content/docs/ko/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** 열에 표시돼요.

Expand All @@ -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` |

Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/content/docs/ru/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** журнала запросов.

Expand All @@ -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` |

Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/content/docs/zh-cn/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** 列中。

Expand All @@ -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` |

Expand Down
13 changes: 10 additions & 3 deletions src/claude/inbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/responses/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]);

Expand Down
21 changes: 19 additions & 2 deletions tests/claude-inbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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" },
Expand All @@ -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<string, unknown>;

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,
Expand Down
Loading