@@ -13,6 +13,7 @@ interface FakeOpenAIClientOptions {
|
||||
interface CapturedCompletionsPayload {
|
||||
prompt_cache_key?: string;
|
||||
prompt_cache_retention?: "24h" | "in-memory" | null;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
@@ -170,6 +171,63 @@ describe("openai-completions prompt caching", () => {
|
||||
expect(headers["x-session-affinity"]).toBe("session-affinity");
|
||||
});
|
||||
|
||||
it("uses OpenAI no-session format when configured", async () => {
|
||||
const model = createModel({
|
||||
compat: { sendSessionAffinityHeaders: true, sessionAffinityFormat: "openai-nosession" },
|
||||
});
|
||||
const { payload, headers } = await captureRequest({ sessionId: "session-nosession" }, model);
|
||||
|
||||
expect(payload?.session_id).toBeUndefined();
|
||||
expect(payload?.prompt_cache_key).toBe("session-nosession");
|
||||
expect(headers.session_id).toBeUndefined();
|
||||
expect(headers["x-client-request-id"]).toBe("session-nosession");
|
||||
expect(headers["x-session-affinity"]).toBe("session-nosession");
|
||||
expect(headers["x-session-id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses OpenRouter session-affinity header when configured", async () => {
|
||||
const model = createModel({
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
compat: { sendSessionAffinityHeaders: true, sessionAffinityFormat: "openrouter" },
|
||||
});
|
||||
const { payload, headers } = await captureRequest({ sessionId: "session-proxy" }, model);
|
||||
|
||||
expect(payload?.session_id).toBeUndefined();
|
||||
expect(payload?.prompt_cache_key).toBeUndefined();
|
||||
expect(headers["x-session-id"]).toBe("session-proxy");
|
||||
expect(headers.session_id).toBeUndefined();
|
||||
expect(headers["x-client-request-id"]).toBeUndefined();
|
||||
expect(headers["x-session-affinity"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("auto-detects OpenRouter session-affinity header for OpenRouter endpoints", async () => {
|
||||
const model = createModel({
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: { sendSessionAffinityHeaders: true },
|
||||
});
|
||||
const { payload, headers } = await captureRequest({ sessionId: "session-openrouter" }, model);
|
||||
|
||||
expect(payload?.session_id).toBeUndefined();
|
||||
expect(payload?.prompt_cache_key).toBeUndefined();
|
||||
expect(headers["x-session-id"]).toBe("session-openrouter");
|
||||
expect(headers.session_id).toBeUndefined();
|
||||
expect(headers["x-client-request-id"]).toBeUndefined();
|
||||
expect(headers["x-session-affinity"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits OpenRouter session-affinity data when disabled", async () => {
|
||||
const model = createModel({
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
const { payload, headers } = await captureRequest({ sessionId: "session-openrouter" }, model);
|
||||
|
||||
expect(payload?.session_id).toBeUndefined();
|
||||
expect(payload?.prompt_cache_key).toBeUndefined();
|
||||
expect(headers["x-session-id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits session-affinity headers when cacheRetention is none", async () => {
|
||||
const model = createModel({
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
|
||||
@@ -39,6 +39,7 @@ const compat = {
|
||||
supportsStrictMode: true,
|
||||
cacheControlFormat: undefined,
|
||||
sendSessionAffinityHeaders: false,
|
||||
sessionAffinityFormat: "openai",
|
||||
supportsLongCacheRetention: true,
|
||||
} satisfies Required<Omit<OpenAICompletionsCompat, "cacheControlFormat">> & {
|
||||
cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"];
|
||||
|
||||
@@ -1247,6 +1247,7 @@ describe("openai-completions tool_choice", () => {
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: true,
|
||||
sendSessionAffinityHeaders: false,
|
||||
sessionAffinityFormat: "openai",
|
||||
supportsLongCacheRetention: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ const compat: Required<OpenAICompletionsCompat> = {
|
||||
supportsStrictMode: true,
|
||||
cacheControlFormat: "anthropic",
|
||||
sendSessionAffinityHeaders: false,
|
||||
sessionAffinityFormat: "openai",
|
||||
supportsLongCacheRetention: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ import type { Model } from "../src/types.ts";
|
||||
|
||||
type CapturedHeaders = Headers | string[][] | Record<string, string | readonly string[]> | undefined;
|
||||
|
||||
interface CapturedResponsesPayload {
|
||||
prompt_cache_key?: string;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
function getHeader(headers: CapturedHeaders, name: string): string | null {
|
||||
if (!headers) return null;
|
||||
if (headers instanceof Headers) return headers.get(name);
|
||||
@@ -25,11 +30,20 @@ function getHeader(headers: CapturedHeaders, name: string): string | null {
|
||||
async function captureOpenAIResponseHeaders(
|
||||
options: Parameters<typeof streamOpenAIResponses>[2],
|
||||
model: Model<"openai-responses"> = getModel("openai", "gpt-5.4"),
|
||||
): Promise<{ sessionId: string | null; clientRequestId: string | null }> {
|
||||
const captured = { sessionId: null as string | null, clientRequestId: null as string | null };
|
||||
): Promise<{
|
||||
sessionId: string | null;
|
||||
clientRequestId: string | null;
|
||||
xSessionId: string | null;
|
||||
}> {
|
||||
const captured = {
|
||||
sessionId: null as string | null,
|
||||
clientRequestId: null as string | null,
|
||||
xSessionId: null as string | null,
|
||||
};
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (_input, init) => {
|
||||
captured.sessionId = getHeader(init?.headers, "session_id");
|
||||
captured.clientRequestId = getHeader(init?.headers, "x-client-request-id");
|
||||
captured.xSessionId = getHeader(init?.headers, "x-session-id");
|
||||
return new Response("data: [DONE]\n\n", {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
@@ -224,12 +238,13 @@ describe("openai-responses provider defaults", () => {
|
||||
it("sets cache-affinity headers for official OpenAI Responses requests with a sessionId", async () => {
|
||||
const captured = await captureOpenAIResponseHeaders({ sessionId: "session-123" });
|
||||
|
||||
expect(captured).toEqual({ sessionId: "session-123", clientRequestId: "session-123" });
|
||||
expect(captured.sessionId).toBe("session-123");
|
||||
expect(captured.clientRequestId).toBe("session-123");
|
||||
});
|
||||
|
||||
it("clamps prompt_cache_key to OpenAI's 64-character limit", async () => {
|
||||
const sessionId = "x".repeat(67);
|
||||
let capturedPayload: { prompt_cache_key?: string } | undefined;
|
||||
let capturedPayload: Pick<CapturedResponsesPayload, "prompt_cache_key"> | undefined;
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response("data: [DONE]\n\n", {
|
||||
status: 200,
|
||||
@@ -247,7 +262,7 @@ describe("openai-responses provider defaults", () => {
|
||||
apiKey: "test-key",
|
||||
sessionId,
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as { prompt_cache_key?: string };
|
||||
capturedPayload = payload as Pick<CapturedResponsesPayload, "prompt_cache_key">;
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -267,19 +282,105 @@ describe("openai-responses provider defaults", () => {
|
||||
};
|
||||
const captured = await captureOpenAIResponseHeaders({ sessionId: "session-123" }, proxyModel);
|
||||
|
||||
expect(captured).toEqual({ sessionId: "session-123", clientRequestId: "session-123" });
|
||||
expect(captured.sessionId).toBe("session-123");
|
||||
expect(captured.clientRequestId).toBe("session-123");
|
||||
});
|
||||
|
||||
it("can omit the session_id header while preserving other cache-affinity headers", async () => {
|
||||
it("uses OpenRouter session-affinity header when configured", async () => {
|
||||
const proxyModel: Model<"openai-responses"> = {
|
||||
...getModel("openai", "gpt-5.4"),
|
||||
provider: "proxy",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
compat: { sessionAffinityFormat: "openrouter" },
|
||||
};
|
||||
let capturedPayload: CapturedResponsesPayload | undefined;
|
||||
const captured = await captureOpenAIResponseHeaders(
|
||||
{
|
||||
sessionId: "session-proxy",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as CapturedResponsesPayload;
|
||||
},
|
||||
},
|
||||
proxyModel,
|
||||
);
|
||||
|
||||
expect(captured.sessionId).toBeNull();
|
||||
expect(captured.clientRequestId).toBeNull();
|
||||
expect(captured.xSessionId).toBe("session-proxy");
|
||||
expect(capturedPayload?.session_id).toBeUndefined();
|
||||
expect(capturedPayload?.prompt_cache_key).toBe("session-proxy");
|
||||
});
|
||||
|
||||
it("auto-detects OpenRouter session-affinity header for OpenRouter Responses endpoints", async () => {
|
||||
const openRouterModel: Model<"openai-responses"> = {
|
||||
...getModel("openai", "gpt-5.4"),
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
};
|
||||
let capturedPayload: CapturedResponsesPayload | undefined;
|
||||
const captured = await captureOpenAIResponseHeaders(
|
||||
{
|
||||
sessionId: "session-openrouter",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as CapturedResponsesPayload;
|
||||
},
|
||||
},
|
||||
openRouterModel,
|
||||
);
|
||||
|
||||
expect(captured.sessionId).toBeNull();
|
||||
expect(captured.clientRequestId).toBeNull();
|
||||
expect(captured.xSessionId).toBe("session-openrouter");
|
||||
expect(capturedPayload?.session_id).toBeUndefined();
|
||||
expect(capturedPayload?.prompt_cache_key).toBe("session-openrouter");
|
||||
});
|
||||
|
||||
it("uses OpenAI no-session format when configured", async () => {
|
||||
const proxyModel: Model<"openai-responses"> = {
|
||||
...getModel("openai", "gpt-5.4"),
|
||||
provider: "proxy",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
compat: { sessionAffinityFormat: "openai-nosession" },
|
||||
};
|
||||
let capturedPayload: CapturedResponsesPayload | undefined;
|
||||
const captured = await captureOpenAIResponseHeaders(
|
||||
{
|
||||
sessionId: "session-proxy",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as CapturedResponsesPayload;
|
||||
},
|
||||
},
|
||||
proxyModel,
|
||||
);
|
||||
|
||||
expect(captured.sessionId).toBeNull();
|
||||
expect(captured.clientRequestId).toBe("session-proxy");
|
||||
expect(captured.xSessionId).toBeNull();
|
||||
expect(capturedPayload?.session_id).toBeUndefined();
|
||||
expect(capturedPayload?.prompt_cache_key).toBe("session-proxy");
|
||||
});
|
||||
|
||||
it("can omit OpenAI session_id header while preserving other affinity data", async () => {
|
||||
const proxyModel: Model<"openai-responses"> = {
|
||||
...getModel("openai", "gpt-5.4"),
|
||||
provider: "opencode",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
compat: { sendSessionIdHeader: false },
|
||||
compat: { sessionAffinityFormat: "openai-nosession" },
|
||||
};
|
||||
const captured = await captureOpenAIResponseHeaders({ sessionId: "session-123" }, proxyModel);
|
||||
let capturedPayload: CapturedResponsesPayload | undefined;
|
||||
const captured = await captureOpenAIResponseHeaders(
|
||||
{
|
||||
sessionId: "session-123",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as CapturedResponsesPayload;
|
||||
},
|
||||
},
|
||||
proxyModel,
|
||||
);
|
||||
|
||||
expect(captured).toEqual({ sessionId: null, clientRequestId: "session-123" });
|
||||
expect(captured.sessionId).toBeNull();
|
||||
expect(captured.clientRequestId).toBe("session-123");
|
||||
expect(capturedPayload?.prompt_cache_key).toBe("session-123");
|
||||
});
|
||||
|
||||
it("lets explicit headers override the default OpenAI cache-affinity headers", async () => {
|
||||
@@ -291,13 +392,15 @@ describe("openai-responses provider defaults", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(captured).toEqual({ sessionId: "override-session", clientRequestId: "override-request" });
|
||||
expect(captured.sessionId).toBe("override-session");
|
||||
expect(captured.clientRequestId).toBe("override-request");
|
||||
});
|
||||
|
||||
it("omits OpenAI cache-affinity headers when cacheRetention is none", async () => {
|
||||
const captured = await captureOpenAIResponseHeaders({ cacheRetention: "none", sessionId: "session-123" });
|
||||
|
||||
expect(captured).toEqual({ sessionId: null, clientRequestId: null });
|
||||
expect(captured.sessionId).toBeNull();
|
||||
expect(captured.clientRequestId).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
Reference in New Issue
Block a user