Anthropic: wire explicit service tier params (#45453)

* Anthropic: add explicit service tier wrapper

* Runner: wire explicit Anthropic service tiers

* Tests: cover explicit Anthropic service tiers

* Changelog: note Anthropic service tier follow-up

* fix(agents): make Anthropic service tiers override fast mode

* fix(config): drop duplicate healed sourceConfig

* docs(anthropic): update fast mode service tier guidance

* fix(agents): remove dead Anthropic Bedrock exports

* fix(agents): avoid cross-provider Anthropic tier warnings

* fix(agents): avoid cross-provider OpenAI tier warnings
This commit is contained in:
Vincent Koc
2026-03-29 16:54:56 -07:00
committed by GitHub
parent feed2c42dd
commit 475defdf82
6 changed files with 299 additions and 10 deletions

View File

@@ -2122,6 +2122,179 @@ describe("applyExtraParamsToAgent", () => {
expect(payload.service_tier).toBe("standard_only");
});
it("injects configured Anthropic service_tier into direct Anthropic payloads", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
applyModelId: "claude-sonnet-4-5",
cfg: {
agents: {
defaults: {
models: {
"anthropic/claude-sonnet-4-5": {
params: {
serviceTier: "standard_only",
},
},
},
},
},
},
model: {
api: "anthropic-messages",
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://api.anthropic.com",
} as unknown as Model<"anthropic-messages">,
payload: {},
});
expect(payload.service_tier).toBe("standard_only");
});
it("injects configured Anthropic service_tier into OAuth-authenticated Anthropic payloads", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
applyModelId: "claude-sonnet-4-5",
cfg: {
agents: {
defaults: {
models: {
"anthropic/claude-sonnet-4-5": {
params: {
serviceTier: "standard_only",
},
},
},
},
},
},
model: {
api: "anthropic-messages",
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://api.anthropic.com",
} as unknown as Model<"anthropic-messages">,
options: {
apiKey: "sk-ant-oat-test-token",
},
payload: {},
});
expect(payload.service_tier).toBe("standard_only");
});
it("does not warn for valid Anthropic serviceTier values", () => {
const warnSpy = vi.spyOn(log, "warn").mockImplementation(() => undefined);
try {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
applyModelId: "claude-sonnet-4-5",
cfg: {
agents: {
defaults: {
models: {
"anthropic/claude-sonnet-4-5": {
params: {
serviceTier: "standard_only",
},
},
},
},
},
},
model: {
api: "anthropic-messages",
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://api.anthropic.com",
} as unknown as Model<"anthropic-messages">,
payload: {},
});
expect(payload.service_tier).toBe("standard_only");
expect(warnSpy).not.toHaveBeenCalled();
} finally {
warnSpy.mockRestore();
}
});
it("accepts snake_case Anthropic service_tier params", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
applyModelId: "claude-sonnet-4-5",
extraParamsOverride: {
service_tier: "standard_only",
},
model: {
api: "anthropic-messages",
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://api.anthropic.com",
} as unknown as Model<"anthropic-messages">,
payload: {},
});
expect(payload.service_tier).toBe("standard_only");
});
it("lets explicit Anthropic service_tier override fast mode defaults", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
applyModelId: "claude-sonnet-4-5",
cfg: {
agents: {
defaults: {
models: {
"anthropic/claude-sonnet-4-5": {
params: {
fastMode: true,
serviceTier: "standard_only",
},
},
},
},
},
},
model: {
api: "anthropic-messages",
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://api.anthropic.com",
} as unknown as Model<"anthropic-messages">,
payload: {},
});
expect(payload.service_tier).toBe("standard_only");
});
it("lets explicit Anthropic service_tier override OAuth fast mode defaults", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
applyModelId: "claude-sonnet-4-5",
cfg: {
agents: {
defaults: {
models: {
"anthropic/claude-sonnet-4-5": {
params: {
fastMode: true,
serviceTier: "standard_only",
},
},
},
},
},
},
model: {
api: "anthropic-messages",
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://api.anthropic.com",
} as unknown as Model<"anthropic-messages">,
options: {
apiKey: "sk-ant-oat-test-token",
},
payload: {},
});
expect(payload.service_tier).toBe("standard_only");
});
it("injects Anthropic fast mode service_tier for OAuth auth", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
@@ -2176,6 +2349,24 @@ describe("applyExtraParamsToAgent", () => {
expect(payload).not.toHaveProperty("service_tier");
});
it("does not inject explicit Anthropic service_tier for proxied base URLs", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "anthropic",
applyModelId: "claude-sonnet-4-5",
extraParamsOverride: {
serviceTier: "standard_only",
},
model: {
api: "anthropic-messages",
provider: "anthropic",
id: "claude-sonnet-4-5",
baseUrl: "https://proxy.example.com/anthropic",
} as unknown as Model<"anthropic-messages">,
payload: {},
});
expect(payload).not.toHaveProperty("service_tier");
});
it("maps fast mode to priority service_tier for openai-codex responses", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai-codex",
@@ -2305,12 +2496,47 @@ describe("applyExtraParamsToAgent", () => {
});
expect(payload).not.toHaveProperty("service_tier");
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith("ignoring invalid OpenAI service tier param: invalid");
} finally {
warnSpy.mockRestore();
}
});
it("does not warn for valid OpenAI serviceTier values", () => {
const warnSpy = vi.spyOn(log, "warn").mockImplementation(() => undefined);
try {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai",
applyModelId: "gpt-5.4",
cfg: {
agents: {
defaults: {
models: {
"openai/gpt-5.4": {
params: {
serviceTier: "priority",
},
},
},
},
},
},
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4",
baseUrl: "https://api.openai.com/v1",
} as unknown as Model<"openai-responses">,
});
expect(payload.service_tier).toBe("priority");
expect(warnSpy).not.toHaveBeenCalled();
} finally {
warnSpy.mockRestore();
}
});
it("does not force store for OpenAI Responses routed through non-OpenAI base URLs", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai",

View File

@@ -89,6 +89,17 @@ function hasOpenAiAnthropicToolPayloadCompatFlag(model: { compat?: unknown }): b
);
}
function normalizeAnthropicServiceTier(value: unknown): AnthropicServiceTier | undefined {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (normalized === "auto" || normalized === "standard_only") {
return normalized;
}
return undefined;
}
function requiresAnthropicToolPayloadCompatibilityForModel(
model: {
api?: unknown;
@@ -374,8 +385,42 @@ export function createAnthropicFastModeWrapper(
};
}
export function createAnthropicServiceTierWrapper(
baseStreamFn: StreamFn | undefined,
serviceTier: AnthropicServiceTier,
): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (
model.api !== "anthropic-messages" ||
model.provider !== "anthropic" ||
!isAnthropicPublicApiBaseUrl(model.baseUrl)
) {
return underlying(model, context, options);
}
return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
if (payloadObj.service_tier === undefined) {
payloadObj.service_tier = serviceTier;
}
});
};
}
export function resolveAnthropicFastMode(
extraParams: Record<string, unknown> | undefined,
): boolean | undefined {
return resolveFastModeParam(extraParams);
}
export function resolveAnthropicServiceTier(
extraParams: Record<string, unknown> | undefined,
): AnthropicServiceTier | undefined {
const raw = extraParams?.serviceTier ?? extraParams?.service_tier;
const normalized = normalizeAnthropicServiceTier(raw);
if (raw !== undefined && normalized === undefined) {
const rawSummary = typeof raw === "string" ? raw : typeof raw;
log.warn(`ignoring invalid Anthropic service tier param: ${rawSummary}`);
}
return normalized;
}

View File

@@ -12,8 +12,10 @@ import type { ProviderRuntimeModel } from "../../plugins/types.js";
import {
createAnthropicBetaHeadersWrapper,
createAnthropicFastModeWrapper,
createAnthropicServiceTierWrapper,
createAnthropicToolPayloadCompatibilityWrapper,
resolveAnthropicFastMode,
resolveAnthropicServiceTier,
resolveAnthropicBetas,
resolveCacheRetention,
} from "./anthropic-stream-wrappers.js";
@@ -364,6 +366,19 @@ function applyPostPluginStreamWrappers(
// upstream model-ID heuristics for Gemini 3.1 variants.
ctx.agent.streamFn = createGoogleThinkingPayloadWrapper(ctx.agent.streamFn, ctx.thinkingLevel);
if (ctx.provider === "anthropic") {
const anthropicServiceTier = resolveAnthropicServiceTier(ctx.effectiveExtraParams);
if (anthropicServiceTier) {
log.debug(
`applying Anthropic service_tier=${anthropicServiceTier} for ${ctx.provider}/${ctx.modelId}`,
);
ctx.agent.streamFn = createAnthropicServiceTierWrapper(
ctx.agent.streamFn,
anthropicServiceTier,
);
}
}
const anthropicFastMode = resolveAnthropicFastMode(ctx.effectiveExtraParams);
if (anthropicFastMode !== undefined) {
log.debug(
@@ -388,12 +403,14 @@ function applyPostPluginStreamWrappers(
ctx.agent.streamFn = createOpenAIFastModeWrapper(ctx.agent.streamFn);
}
const openAIServiceTier = resolveOpenAIServiceTier(ctx.effectiveExtraParams);
if (openAIServiceTier) {
log.debug(
`applying OpenAI service_tier=${openAIServiceTier} for ${ctx.provider}/${ctx.modelId}`,
);
ctx.agent.streamFn = createOpenAIServiceTierWrapper(ctx.agent.streamFn, openAIServiceTier);
if (ctx.provider === "openai" || ctx.provider === "openai-codex") {
const openAIServiceTier = resolveOpenAIServiceTier(ctx.effectiveExtraParams);
if (openAIServiceTier) {
log.debug(
`applying OpenAI service_tier=${openAIServiceTier} for ${ctx.provider}/${ctx.modelId}`,
);
ctx.agent.streamFn = createOpenAIServiceTierWrapper(ctx.agent.streamFn, openAIServiceTier);
}
}
// Work around upstream pi-ai hardcoding `store: false` for Responses API.