fix(ai): classified interleaved beta by effective endpoint
- Classified known signing proxies from the resolved request URL instead of materialized model compat. - Excluded Vertex rawPredict by URL regardless of provider id. - Covered canonical-model reroutes and custom-provider Vertex routes. Fixes #6717
This commit is contained in:
@@ -2,7 +2,8 @@ import * as nodeCrypto from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import { scheduler } from "node:timers/promises";
|
||||
import * as tls from "node:tls";
|
||||
import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
||||
import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
||||
import { 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";
|
||||
@@ -2850,18 +2851,21 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
||||
} = args;
|
||||
const compat = model.compat;
|
||||
const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools;
|
||||
const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
|
||||
// Adaptive models (`supportsDisplay`) get native interleaved thinking on the
|
||||
// official API, so the beta is only needed on non-official signing proxies
|
||||
// that still validate replayed multi-thinking-block turns against it (#6717).
|
||||
// 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).
|
||||
const needsInterleavedBeta =
|
||||
interleavedThinking &&
|
||||
(!model.thinking?.supportsDisplay ||
|
||||
(compat.signingEndpoint && !compat.officialEndpoint && model.provider !== "google-vertex"));
|
||||
(!isOfficialAnthropicApiUrl(baseUrl) &&
|
||||
isAnthropicSigningProxyUrl(baseUrl) &&
|
||||
!isVertexRawPredictUrl(baseUrl ?? "")));
|
||||
const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
|
||||
const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
|
||||
const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
|
||||
const needsFineGrainedToolStreamingBeta =
|
||||
hasTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming;
|
||||
|
||||
@@ -1804,7 +1804,7 @@ describe("Anthropic request fingerprint alignment", () => {
|
||||
expect(modern.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14");
|
||||
});
|
||||
|
||||
it("adds the interleaved-thinking beta for adaptive models only on signing proxies", () => {
|
||||
it("uses the effective route for adaptive interleaved-thinking beta headers", () => {
|
||||
const adaptiveProxySpec: ModelSpec<"anthropic-messages"> = {
|
||||
...ANTHROPIC_MODEL_SPEC,
|
||||
id: "claude-opus-4-8",
|
||||
@@ -1817,12 +1817,21 @@ describe("Anthropic request fingerprint alignment", () => {
|
||||
supportsDisplay: true,
|
||||
},
|
||||
};
|
||||
const signingProxyUrl = "https://gateway.ai.cloudflare.com/v1/account/gateway/anthropic";
|
||||
const signingProxy = buildAnthropicClientOptions({
|
||||
model: buildModel({
|
||||
...adaptiveProxySpec,
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/account/gateway/anthropic",
|
||||
}),
|
||||
model: buildModel({ ...adaptiveProxySpec, baseUrl: signingProxyUrl }),
|
||||
apiKey: "sk-proxy-test",
|
||||
interleavedThinking: true,
|
||||
});
|
||||
const canonicalModel = buildModel({
|
||||
...adaptiveProxySpec,
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
});
|
||||
const reroutedSigningProxy = buildAnthropicClientOptions({
|
||||
// Runtime provider overrides replace baseUrl without rebuilding the
|
||||
// canonical model's official-endpoint compat.
|
||||
model: { ...canonicalModel, baseUrl: signingProxyUrl },
|
||||
apiKey: "sk-proxy-test",
|
||||
interleavedThinking: true,
|
||||
});
|
||||
@@ -1831,12 +1840,12 @@ describe("Anthropic request fingerprint alignment", () => {
|
||||
apiKey: "sk-proxy-test",
|
||||
interleavedThinking: true,
|
||||
});
|
||||
// Vertex rawPredict is a signing endpoint but only accepts betas in the
|
||||
// JSON body (`anthropic_beta`); the HTTP header would 400 (#5614).
|
||||
const vertexProxy = buildAnthropicClientOptions({
|
||||
// Vertex rawPredict is signing regardless of provider id, but only
|
||||
// accepts betas in the JSON body (`anthropic_beta`) (#5614).
|
||||
const vertexRawPredict = buildAnthropicClientOptions({
|
||||
model: buildModel({
|
||||
...adaptiveProxySpec,
|
||||
provider: "google-vertex",
|
||||
provider: "custom-vertex",
|
||||
baseUrl:
|
||||
"https://us-east5-aiplatform.googleapis.com/v1/projects/p/locations/us-east5/publishers/anthropic/models/claude-opus-4-8:rawPredict",
|
||||
}),
|
||||
@@ -1845,8 +1854,9 @@ describe("Anthropic request fingerprint alignment", () => {
|
||||
});
|
||||
|
||||
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(vertexProxy.defaultHeaders["anthropic-beta"] ?? "").not.toContain("interleaved-thinking-2025-05-14");
|
||||
expect(vertexRawPredict.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", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* defaults come from provider ids, strict host checks, and model-id
|
||||
* classification, with explicit spec overrides assigned on top.
|
||||
*/
|
||||
import { modelMatchesHost } from "../hosts";
|
||||
import { hostMatchesUrl, modelMatchesHost } from "../hosts";
|
||||
import {
|
||||
hasOpus47ApiRestrictions,
|
||||
isAnthropicFableOrMythosModel,
|
||||
@@ -96,6 +96,23 @@ function isAzureAnthropicRoute(baseUrl?: string): boolean {
|
||||
return baseUrl !== undefined && AZURE_ANTHROPIC_URL_MARKER.test(baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Known non-official URLs that enforce Anthropic thinking signatures on replay.
|
||||
*
|
||||
* Runtime routing calls this with the effective URL because a model's resolved
|
||||
* compat can be stale after Foundry or a provider base-URL override reroutes it.
|
||||
*/
|
||||
export function isAnthropicSigningProxyUrl(baseUrl?: string): boolean {
|
||||
return (
|
||||
hostMatchesUrl(baseUrl, "githubCopilot") ||
|
||||
hostMatchesUrl(baseUrl, "zenmux") ||
|
||||
isCloudflareAnthropicGateway(baseUrl) ||
|
||||
isVertexAnthropicRoute(baseUrl) ||
|
||||
isBedrockAnthropicRoute(baseUrl) ||
|
||||
isAzureAnthropicRoute(baseUrl)
|
||||
);
|
||||
}
|
||||
|
||||
/** Build the resolved anthropic-messages compat record for a model spec. */
|
||||
export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): ResolvedAnthropicCompat {
|
||||
const baseUrl = spec.baseUrl;
|
||||
@@ -113,11 +130,8 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res
|
||||
// (issue #4192).
|
||||
const isZenmux = modelMatchesHost(spec, "zenmux");
|
||||
const requiresThinkingEnabled = modelMatchesHost(spec, "moonshotNative") && matchesKimiMandatoryThinkingModel(spec);
|
||||
const isVertex = isVertexAnthropicRoute(baseUrl);
|
||||
const isBedrock = isBedrockAnthropicRoute(baseUrl);
|
||||
const isAzure = isAzureAnthropicRoute(baseUrl);
|
||||
const signingEndpoint =
|
||||
official || isCopilot || isZenmux || isCloudflareAnthropicGateway(baseUrl) || isVertex || isBedrock || isAzure;
|
||||
const signingEndpoint = official || isCopilot || isZenmux || isAnthropicSigningProxyUrl(baseUrl);
|
||||
const compat: ResolvedAnthropicCompat = {
|
||||
officialEndpoint: official,
|
||||
signingEndpoint,
|
||||
|
||||
Reference in New Issue
Block a user