fix(ai): support OpenRouter session affinity (#6496)

Fixes #6366
This commit is contained in:
Petr Houška
2026-07-13 09:57:53 +02:00
committed by GitHub
parent f7b78e2a42
commit 298665cfb9
14 changed files with 229 additions and 25 deletions
+5
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Breaking Changes
- Removed the `OpenAIResponsesCompat.sendSessionIdHeader` flag. Session-affinity behavior is now controlled by `compat.sessionAffinityFormat` (`"openai"`, `"openai-nosession"`, or `"openrouter"`). Replace `sendSessionIdHeader: false` with `sessionAffinityFormat: "openai-nosession"` ([#6366](https://github.com/earendil-works/pi/issues/6366)).
### Added
- Added cache-friendly dynamic tool loading. `ToolResultMessage.addedToolNames` marks where tools from `Context.tools` became available; Anthropic and OpenAI Responses use native deferred loading so late tools stay out of the cached prefix, while other providers continue using `Context.tools` normally ([#6474](https://github.com/earendil-works/pi-mono/pull/6474)).
@@ -14,6 +18,7 @@
- Fixed OpenRouter model context windows to use the top provider's actual context length ([#6481](https://github.com/earendil-works/pi-mono/pull/6481) by [@davidbrai](https://github.com/davidbrai)).
- Fixed the GitHub Copilot `mai-code-1-flash-picker` model to route through the `/responses` endpoint.
- Fixed Amazon Bedrock requests to use the generic `apiKey` stream option as a Bedrock bearer token.
- Fixed OpenRouter OpenAI-compatible session IDs to use the `x-session-id` header instead of OpenAI-specific session-affinity fields ([#6366](https://github.com/earendil-works/pi/issues/6366)).
## [0.80.6] - 2026-07-09
+3 -2
View File
@@ -1070,7 +1070,8 @@ interface OpenAICompletionsCompat {
supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)
supportsUsageInStreaming?: boolean; // Whether provider supports `stream_options: { include_usage: true }` (default: true)
supportsStrictMode?: boolean; // Whether provider supports `strict` in tool definitions (default: true)
sendSessionAffinityHeaders?: boolean; // Whether to send `session_id`, `x-client-request-id`, and `x-session-affinity` from `sessionId` when caching is enabled (default: false)
sendSessionAffinityHeaders?: boolean; // Send session-affinity data from `sessionId` (default: false)
sessionAffinityFormat?: 'openai' | 'openai-nosession' | 'openrouter'; // Format for session affinity: 'openai' uses `prompt_cache_key`, `session_id`, `x-client-request-id`, and `x-session-affinity`; 'openai-nosession' uses `prompt_cache_key`, `x-client-request-id`, and `x-session-affinity`; 'openrouter' uses `x-session-id` (default: auto-detected)
maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
@@ -1085,7 +1086,7 @@ interface OpenAICompletionsCompat {
interface OpenAIResponsesCompat {
supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)
sendSessionIdHeader?: boolean; // Whether to send `session_id` from `sessionId` when caching is enabled (default: true)
sessionAffinityFormat?: 'openai' | 'openai-nosession' | 'openrouter'; // Session-affinity header format: 'openai' sends `session_id` and `x-client-request-id`; 'openai-nosession' sends `x-client-request-id`; 'openrouter' sends `x-session-id`. Does not affect the `prompt_cache_key` body param (default: auto-detected)
supportsLongCacheRetention?: boolean; // Whether provider supports `prompt_cache_retention: "24h"` (default: true)
}
```
+11 -3
View File
@@ -519,9 +519,15 @@ function createClient(
}
if (sessionId && compat.sendSessionAffinityHeaders) {
headers.session_id = sessionId;
headers["x-client-request-id"] = sessionId;
headers["x-session-affinity"] = sessionId;
if (compat.sessionAffinityFormat === "openrouter") {
headers["x-session-id"] = sessionId;
} else {
if (compat.sessionAffinityFormat === "openai") {
headers.session_id = sessionId;
}
headers["x-client-request-id"] = sessionId;
headers["x-session-affinity"] = sessionId;
}
}
// Merge options headers last so they can override defaults
@@ -1250,6 +1256,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
cacheControlFormat,
sendSessionAffinityHeaders: false,
sessionAffinityFormat: isOpenRouter ? "openrouter" : "openai",
supportsLongCacheRetention: !(
isTogether ||
isCloudflareWorkersAI ||
@@ -1289,6 +1296,7 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders,
sessionAffinityFormat: model.compat.sessionAffinityFormat ?? detected.sessionAffinityFormat,
supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention,
};
}
+12 -4
View File
@@ -44,6 +44,10 @@ function getClientApiKey(provider: string, apiKey: string | undefined, headers:
throw new Error(`No API key for provider: ${provider}`);
}
function detectSessionAffinityFormat(model: Pick<Model<"openai-responses">, "provider" | "baseUrl">) {
return model.provider === "openrouter" || model.baseUrl.includes("openrouter.ai") ? "openrouter" : "openai";
}
/**
* Resolve cache retention preference.
* Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
@@ -61,7 +65,7 @@ function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEn
function getCompat(model: Model<"openai-responses">): Required<OpenAIResponsesCompat> {
return {
supportsDeveloperRole: model.compat?.supportsDeveloperRole ?? true,
sendSessionIdHeader: model.compat?.sendSessionIdHeader ?? true,
sessionAffinityFormat: model.compat?.sessionAffinityFormat ?? detectSessionAffinityFormat(model),
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
supportsToolSearch: model.compat?.supportsToolSearch ?? false,
};
@@ -203,10 +207,14 @@ function createClient(
}
if (sessionId) {
if (compat.sendSessionIdHeader) {
headers.session_id = sessionId;
if (compat.sessionAffinityFormat === "openrouter") {
headers["x-session-id"] = sessionId;
} else {
if (compat.sessionAffinityFormat === "openai") {
headers.session_id = sessionId;
}
headers["x-client-request-id"] = sessionId;
}
headers["x-client-request-id"] = sessionId;
}
// Merge options headers last so they can override defaults
+6 -3
View File
@@ -100,6 +100,7 @@ export type Transport = "sse" | "websocket" | "websocket-cached" | "auto";
/** Provider-scoped environment overrides. Values take precedence over process.env. */
export type ProviderEnv = Record<string, string>;
export type ProviderHeaders = Record<string, string | null>;
export type SessionAffinityFormat = "openai" | "openai-nosession" | "openrouter";
export interface ProviderResponse {
status: number;
@@ -517,8 +518,10 @@ export interface OpenAICompletionsCompat {
supportsStrictMode?: boolean;
/** Cache control convention for prompt caching. "anthropic" applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. */
cacheControlFormat?: "anthropic";
/** Whether to send known session-affinity headers (`session_id`, `x-client-request-id`, `x-session-affinity`) from `options.sessionId` when caching is enabled. Default: false. */
/** Whether to send session-affinity data from `options.sessionId`. Default: false. */
sendSessionAffinityHeaders?: boolean;
/** Session-affinity header format: `openai` sends `session_id`, `x-client-request-id`, and `x-session-affinity`; `openai-nosession` sends `x-client-request-id` and `x-session-affinity`; `openrouter` sends `x-session-id`. Does not affect the `prompt_cache_key` body param, which is governed by cache retention. Default: auto-detected. */
sessionAffinityFormat?: SessionAffinityFormat;
/** Whether the provider supports long prompt cache retention (`prompt_cache_retention: "24h"` or Anthropic-style `cache_control.ttl: "1h"`, depending on format). Default: true. */
supportsLongCacheRetention?: boolean;
}
@@ -527,8 +530,8 @@ export interface OpenAICompletionsCompat {
export interface OpenAIResponsesCompat {
/** Whether the provider supports the `developer` role (vs `system`). Default: true. */
supportsDeveloperRole?: boolean;
/** Whether to send the OpenAI `session_id` cache-affinity header from `options.sessionId` when caching is enabled. Default: true. */
sendSessionIdHeader?: boolean;
/** Session-affinity header format: `openai` sends `session_id` and `x-client-request-id`; `openai-nosession` sends `x-client-request-id`; `openrouter` sends `x-session-id`. Does not affect the `prompt_cache_key` body param, which is governed by cache retention. Default: auto-detected. */
sessionAffinityFormat?: SessionAffinityFormat;
/** Whether the provider supports `prompt_cache_retention: "24h"`. Default: true. */
supportsLongCacheRetention?: boolean;
/** Whether the model supports client-executed tool search for deferred tools. Default: false. */
@@ -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([
+5
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Breaking Changes
- Removed the `openai-responses` `compat.sendSessionIdHeader` flag from `models.json`. Session-affinity behavior is now controlled by `compat.sessionAffinityFormat` (`"openai"`, `"openai-nosession"`, or `"openrouter"`). Replace `sendSessionIdHeader: false` with `sessionAffinityFormat: "openai-nosession"` ([#6366](https://github.com/earendil-works/pi/issues/6366)).
### New Features
- **Cache-friendly dynamic tool loading** - Extensions can add tools during execution while supported Anthropic and OpenAI Responses models preserve prompt-cache prefixes. See [Dynamic Tool Loading](docs/extensions.md#dynamic-tool-loading).
@@ -17,6 +21,7 @@
### Fixed
- Fixed inherited OpenRouter model context windows to use the top provider's actual context length ([#6481](https://github.com/earendil-works/pi-mono/pull/6481) by [@davidbrai](https://github.com/davidbrai)).
- Fixed inherited OpenRouter OpenAI-compatible session IDs to use the `x-session-id` header instead of OpenAI-specific session-affinity fields ([#6366](https://github.com/earendil-works/pi/issues/6366)).
- Fixed `Ctrl+V` to paste clipboard text when the pasteboard does not contain an image.
- Fixed `/login amazon-bedrock` to prompt for and save a Bedrock API key instead of only displaying ambient AWS credential setup instructions.
@@ -722,6 +722,8 @@ interface ProviderModelConfig {
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling";
chatTemplateKwargs?: Record<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort"; omitWhenOff?: boolean }>;
cacheControlFormat?: "anthropic";
sessionAffinityFormat?: "openai" | "openai-nosession" | "openrouter";
sendSessionAffinityHeaders?: boolean;
// anthropic-messages
supportsEagerToolInputStreaming?: boolean;
+2
View File
@@ -445,6 +445,8 @@ For providers with partial OpenAI compatibility, use the `compat` field.
| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters |
| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values |
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. |
| `sendSessionAffinityHeaders` | For `openai-completions`, send session-affinity headers from the session id when caching is enabled. Default: `false`. |
| `sessionAffinityFormat` | For `openai-completions` and `openai-responses`, the session-affinity header format: `openai` sends `session_id`/`x-client-request-id` (completions also `x-session-affinity`), `openai-nosession` omits the underscore-containing `session_id` header, `openrouter` sends `x-session-id`. Does not affect the `prompt_cache_key` body param. Default: auto-detected. |
| `supportsStrictMode` | Include the `strict` field in tool definitions |
| `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. |
| `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |
@@ -133,12 +133,18 @@ const OpenAICompletionsCompatSchema = Type.Object({
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
supportsStrictMode: Type.Optional(Type.Boolean()),
sendSessionAffinityHeaders: Type.Optional(Type.Boolean()),
sessionAffinityFormat: Type.Optional(
Type.Union([Type.Literal("openai"), Type.Literal("openai-nosession"), Type.Literal("openrouter")]),
),
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
});
const OpenAIResponsesCompatSchema = Type.Object({
supportsDeveloperRole: Type.Optional(Type.Boolean()),
sendSessionIdHeader: Type.Optional(Type.Boolean()),
sessionAffinityFormat: Type.Optional(
Type.Union([Type.Literal("openai"), Type.Literal("openai-nosession"), Type.Literal("openrouter")]),
),
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
supportsToolSearch: Type.Optional(Type.Boolean()),
});