diff --git a/docs/tools/web_search.md b/docs/tools/web_search.md index f14e74e6e..76e3aa922 100644 --- a/docs/tools/web_search.md +++ b/docs/tools/web_search.md @@ -50,7 +50,7 @@ | `recency` | `"day" \| "week" \| "month" \| "year"` | No | Time filter. Only providers that implement it use it; code maps it for Brave, Perplexity, Tavily, SearXNG, Kagi, TinyFish, Firecrawl, and xAI. | | `limit` | `number` | No | Max results to return. Usually becomes the provider request's result-count parameter when `num_search_results` is absent. TinyFish uses it for paginated fetches before slicing; xAI sends it as `search_parameters.max_search_results` when `num_search_results` is absent and also caps parsed sources/citations locally, defaulting to `10` and max `30`. | | `max_tokens` | `number` | No | Passed through as provider token caps (`maxOutputTokens`, `max_tokens`, or xAI `max_output_tokens`) only by Anthropic, Gemini, xAI, and Perplexity API-key mode. Ignored by the other providers. | -| `temperature` | `number` | No | Passed through only by Anthropic, Gemini, xAI, and Perplexity API-key mode. Ignored by the other providers. | +| `temperature` | `number` | No | Passed through only by Anthropic models that support sampling parameters, Gemini, xAI, and Perplexity API-key mode. Ignored or omitted by the other provider/model paths. | | `num_search_results` | `number` | No | Requested search breadth or local result cap. Most providers send it upstream. TinyFish clamps to `1..20` with default `10`, sends it as `num_results` per page, and uses paginated fetches before slicing. xAI sends it as `search_parameters.max_search_results` and caps parsed sources/citations locally with default `10` and max `30`. | ## Outputs @@ -126,7 +126,7 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec - `ANTHROPIC_SEARCH_BASE_URL` — search-only base URL for either `ANTHROPIC_SEARCH_API_KEY` or fallback Anthropic credentials; overrides `ANTHROPIC_BASE_URL` (and `FOUNDRY_BASE_URL` in Foundry mode); defaults to `https://api.anthropic.com`. - `ANTHROPIC_SEARCH_MODEL` — search model; defaults to `claude-haiku-4-5`. - Querying: Claude Messages API with web-search tool enabled. - - `max_tokens` and `temperature` pass through. + - `max_tokens` passes through. `temperature` passes through only for models that support sampling parameters; it is omitted for Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+ because those APIs reject sampling parameters. - `limit` and `num_search_results` are collapsed together before dispatch: `num_results = params.numSearchResults ?? params.limit`. - Output may include `answer`, `sources`, `citations`, `searchQueries`, `usage.searchRequests`, `model`, `requestId`. - **Codex** — `packages/coding-agent/src/web/search/providers/codex.ts` diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0506fe563..e5d57bd45 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -25,6 +25,7 @@ ### Added - Added `requestIdFormat` (`"string"` | `"number"`, default `"string"`) to MCP server config, honored by the stdio, HTTP, and SSE transports. JSON-RPC 2.0 permits both id shapes, but Apple's `xcrun mcpbridge` decodes `id` as an integer only and silently drops OMP's snowflake strings (`mcpbridge.DecodeError Code=1`), hanging every request until it times out. Opting a server into `"number"` allocates per-connection sequential integer ids instead. The option is OMP-specific, so set it in an OMP-owned config (`.omp/mcp.json`, `~/.omp/agent/mcp.json`, a project `mcp.json`/`.mcp.json`, or an OMP plugin); servers imported from another tool's config ignore it ([#7053](https://github.com/can1357/oh-my-pi/issues/7053)). +- Fixed Anthropic web search sending unsupported temperature parameters to sampling-restricted Claude models ([#7195](https://github.com/can1357/oh-my-pi/pull/7195) by [@will-bogusz](https://github.com/will-bogusz)). ## [17.2.2] - 2026-07-31 diff --git a/packages/coding-agent/src/web/search/providers/anthropic.ts b/packages/coding-agent/src/web/search/providers/anthropic.ts index d34661d85..9ecb205df 100644 --- a/packages/coding-agent/src/web/search/providers/anthropic.ts +++ b/packages/coding-agent/src/web/search/providers/anthropic.ts @@ -19,6 +19,7 @@ import { withAuth, wrapFetchForCch, } from "@oh-my-pi/pi-ai"; +import { hasOpus47ApiRestrictions } from "@oh-my-pi/pi-catalog/identity/family"; import { $env } from "@oh-my-pi/pi-utils"; import type { AnthropicApiResponse, @@ -176,7 +177,8 @@ async function callSearch( body.metadata = { user_id: metadataUserId }; } - if (temperature !== undefined) { + // Opus 4.7+, Sonnet 5+, and Fable/Mythos 5 reject sampling parameters with a 400. + if (temperature !== undefined && !hasOpus47ApiRestrictions(model)) { body.temperature = temperature; } diff --git a/packages/coding-agent/test/web/search/anthropic.test.ts b/packages/coding-agent/test/web/search/anthropic.test.ts index eeaa96476..6495632c2 100644 --- a/packages/coding-agent/test/web/search/anthropic.test.ts +++ b/packages/coding-agent/test/web/search/anthropic.test.ts @@ -25,6 +25,20 @@ function makeCaptureFetch(): { fetch: FetchImpl; body: () => Record captured }; } +function withSearchModel(model: string) { + const original = Bun.env.ANTHROPIC_SEARCH_MODEL; + Bun.env.ANTHROPIC_SEARCH_MODEL = model; + return { + [Symbol.dispose]() { + if (original === undefined) { + delete Bun.env.ANTHROPIC_SEARCH_MODEL; + } else { + Bun.env.ANTHROPIC_SEARCH_MODEL = original; + } + }, + }; +} + describe("Anthropic search request body", () => { it("forwards the raw session id as metadata.user_id for API-key auth", async () => { using tempDir = TempDir.createSync("@pi-anthropic-search-apikey-"); @@ -127,4 +141,50 @@ describe("Anthropic search request body", () => { authStorage.close(); } }); + + it("omits temperature for sampling-restricted models", async () => { + using _model = withSearchModel("claude-opus-5"); + using tempDir = TempDir.createSync("@pi-anthropic-search-opus-"); + const authStorage = await CodingAuthStorage.create(path.join(tempDir.path(), "auth.db")); + try { + authStorage.setRuntimeApiKey("anthropic", "test-key"); + + const cap = makeCaptureFetch(); + await searchAnthropic({ + query: "sampling compatibility", + systemPrompt: "Use web search.", + temperature: 0.1, + authStorage, + fetch: cap.fetch, + }); + + expect(cap.body()?.model).toBe("claude-opus-5"); + expect(cap.body()).not.toHaveProperty("temperature"); + } finally { + authStorage.close(); + } + }); + + it("preserves temperature for compatible models", async () => { + using _model = withSearchModel("claude-haiku-4-5"); + using tempDir = TempDir.createSync("@pi-anthropic-search-haiku-"); + const authStorage = await CodingAuthStorage.create(path.join(tempDir.path(), "auth.db")); + try { + authStorage.setRuntimeApiKey("anthropic", "test-key"); + + const cap = makeCaptureFetch(); + await searchAnthropic({ + query: "sampling compatibility", + systemPrompt: "Use web search.", + temperature: 0.1, + authStorage, + fetch: cap.fetch, + }); + + expect(cap.body()?.model).toBe("claude-haiku-4-5"); + expect(cap.body()?.temperature).toBe(0.1); + } finally { + authStorage.close(); + } + }); });