diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 3b109a6a5..dc08672d4 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -3,7 +3,7 @@ import * as fs from "node:fs"; import { scheduler } from "node:timers/promises"; import * as tls from "node:tls"; import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic"; -import { isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts"; +import { hostMatchesUrl, isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts"; import { mapEffortToAnthropicAdaptiveEffort } from "@oh-my-pi/pi-catalog/model-thinking"; import { calculateCost, getBundledModel } from "@oh-my-pi/pi-catalog/models"; import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils"; @@ -2856,15 +2856,20 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A // official API, so only known non-official signing routes need the beta // (#6717). Classify the effective URL: Foundry and provider overrides can // reroute a model without rebuilding its materialized compat. - // Vertex rawPredict is excluded like every other HTTP-beta path here: it can - // only accept betas in the JSON body (`anthropic_beta`), never as this - // `anthropic-beta` HTTP header, so advertising it there earns a 400 (#5614). + // Two signing routes still can't take the beta as this `anthropic-beta` HTTP + // header, so they're excluded: Vertex rawPredict accepts betas only in the + // JSON body (`anthropic_beta`) and 400s on the header (#5614), and GitHub + // Copilot rejects Anthropic betas outright — the `github-copilot` provider + // branch below strips them, but a custom provider id or a canonical model + // rerouted to `api.githubcopilot.com` / `copilot-api.*` reaches the generic + // header builder instead, so exclude those effective URLs here too. const needsInterleavedBeta = interleavedThinking && (!model.thinking?.supportsDisplay || (!isOfficialAnthropicApiUrl(baseUrl) && isAnthropicSigningProxyUrl(baseUrl) && - !isVertexRawPredictUrl(baseUrl ?? ""))); + !isVertexRawPredictUrl(baseUrl ?? "") && + !hostMatchesUrl(baseUrl, "githubCopilot"))); const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey); const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl); const needsFineGrainedToolStreamingBeta = diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index 7028654a6..cf9eb9797 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -1852,11 +1852,23 @@ describe("Anthropic request fingerprint alignment", () => { apiKey: "vertex-adc", interleavedThinking: true, }); + // A custom provider on a Copilot host is signing, but the proxy rejects + // Anthropic betas outright and this path bypasses the provider branch. + const copilotUrlProxy = buildAnthropicClientOptions({ + model: buildModel({ + ...adaptiveProxySpec, + provider: "custom-copilot", + baseUrl: "https://api.githubcopilot.com", + }), + apiKey: "ghu_test", + interleavedThinking: true, + }); expect(signingProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); expect(reroutedSigningProxy.defaultHeaders["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); expect(nonSigningProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); expect(vertexRawPredict.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); + expect(copilotUrlProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14"); }); it("adds legacy fine-grained tool-streaming beta only for tool requests on incompatible models", () => {