diff --git a/src/chat/inbound.ts b/src/chat/inbound.ts index 1012354b2d..46268f13b4 100644 --- a/src/chat/inbound.ts +++ b/src/chat/inbound.ts @@ -299,6 +299,7 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec { if (raw.stop !== undefined) body.stop = raw.stop; if (typeof raw.user === "string") body.user = raw.user; if (typeof raw.parallel_tool_calls === "boolean") body.parallel_tool_calls = raw.parallel_tool_calls; + if (typeof raw.service_tier === "string") body.service_tier = raw.service_tier; if (typeof raw.prompt_cache_key === "string") body.prompt_cache_key = raw.prompt_cache_key; if (raw.metadata !== undefined) body.metadata = raw.metadata; diff --git a/tests/chat-completions-endpoint.test.ts b/tests/chat-completions-endpoint.test.ts index c536b905a0..cd72053827 100644 --- a/tests/chat-completions-endpoint.test.ts +++ b/tests/chat-completions-endpoint.test.ts @@ -237,6 +237,90 @@ test("chatCompletionsToResponsesBody maps messages/tools/system", () => { expect(input.some(i => i.type === "function_call_output" && i.call_id === "call_1")).toBe(true); }); +describe("chatCompletionsToResponsesBody service_tier", () => { + test("preserves a caller-supplied service_tier", () => { + const body = chatCompletionsToResponsesBody({ + model: "mock/test-model", + messages: [{ role: "user", content: "hi" }], + service_tier: "flex", + }); + expect(body.service_tier).toBe("flex"); + }); + + test("does not inject service_tier when the caller omitted it", () => { + const body = chatCompletionsToResponsesBody({ + model: "mock/test-model", + messages: [{ role: "user", content: "hi" }], + }); + expect(body).not.toHaveProperty("service_tier"); + }); +}); + +async function driveChatFallbackServiceTier( + providerOverrides: Partial, +): Promise> { + const { handleChatCompletions } = await import("../src/server/chat-completions"); + const captured: Record[] = []; + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + captured.push(JSON.parse(String(init?.body ?? "{}")) as Record); + return new Response([ + 'data: {"choices":[{"index":0,"delta":{"role":"assistant","content":"ok"}}]}\n\n', + 'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', + "data: [DONE]\n\n", + ].join(""), { headers: { "content-type": "text/event-stream" } }); + }) as typeof fetch; + + const providerName = "chat-tier-fixture"; + const config = { + port: 0, + defaultProvider: providerName, + providers: { + [providerName]: { + adapter: "openai-chat", + baseUrl: "https://chat-tier.example.test/v1", + authMode: "key", + apiKey: "sk-test", + chatServiceTier: true, + supportsServiceTier: true, + ...providerOverrides, + }, + }, + } as OcxConfig; + const response = await handleChatCompletions( + new Request("http://localhost/v1/chat/completions", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: `${providerName}/model`, + messages: [{ role: "user", content: "ping" }], + stream: true, + // Force the Chat -> Responses fallback so this exercises the converter. + store: true, + service_tier: "flex", + }), + }), + config, + { model: "", provider: "" }, + ); + + expect(response.status).toBe(200); + await response.text(); + expect(captured).toHaveLength(1); + return captured[0]!; +} + +describe("POST /v1/chat/completions service_tier fallback", () => { + test("forwards the caller tier through a service-tier-capable openai-chat route", async () => { + const outboundBody = await driveChatFallbackServiceTier({}); + expect(outboundBody.service_tier).toBe("flex"); + }); + + test("strips the caller tier when the provider explicitly disables service tiers", async () => { + const outboundBody = await driveChatFallbackServiceTier({ supportsServiceTier: false }); + expect(outboundBody).not.toHaveProperty("service_tier"); + }); +}); + describe("chatCompletionsToResponsesBody reasoning summary", () => { test("defaults summary to auto when the client only sent reasoning_effort", () => { const body = chatCompletionsToResponsesBody({ diff --git a/tests/fastwire-characterization-wire.test.ts b/tests/fastwire-characterization-wire.test.ts index f8291ab47a..406eb8fa5c 100644 --- a/tests/fastwire-characterization-wire.test.ts +++ b/tests/fastwire-characterization-wire.test.ts @@ -228,12 +228,13 @@ describe("FastWire characterization: known bugs", () => { expect(body.service_tier).toBe("flex"); }); - test("characterization (known bug): chat-to-responses conversion drops service_tier", () => { + test("characterization: chat-to-responses conversion preserves service_tier", () => { + // FastWire #1886 chat-tier-copy bug-fix unit flips the A0 known-bug characterization. const body = chatCompletionsToResponsesBody({ model: "model", messages: [{ role: "user", content: "ping" }], service_tier: "priority", }); - expect(body).not.toHaveProperty("service_tier"); + expect(body.service_tier).toBe("priority"); }); });