@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user