fix(ai): support long cache retention compat

closes #3543
This commit is contained in:
Mario Zechner
2026-04-23 23:43:34 +02:00
parent 1312346199
commit 4cd4cfd98e
11 changed files with 331 additions and 29 deletions

View File

@@ -24,6 +24,7 @@
### Fixed
- Fixed `models.json` provider compatibility to accept `compat.supportsLongCacheRetention`, allowing proxies to opt out of long-retention cache fields when needed while long retention is enabled by default when requested ([#3543](https://github.com/badlogic/pi-mono/issues/3543))
- Fixed `--thinking xhigh` for `openai-codex` `gpt-5.5` so it is no longer downgraded to `high`.
- Fixed git package installs with custom `npmCommand` values such as `pnpm` by avoiding npm-specific production flags in that compatibility path ([#3604](https://github.com/badlogic/pi-mono/issues/3604))
- Fixed first user messages rendering without spacing after existing notices such as compaction summaries or status messages ([#3613](https://github.com/badlogic/pi-mono/issues/3613))

View File

@@ -284,7 +284,8 @@ By default pi sends per-tool `eager_input_streaming: true`. If a proxy or Anthro
"api": "anthropic-messages",
"apiKey": "ANTHROPIC_PROXY_KEY",
"compat": {
"supportsEagerToolInputStreaming": false
"supportsEagerToolInputStreaming": false,
"supportsLongCacheRetention": true
},
"models": [
{
@@ -301,6 +302,7 @@ By default pi sends per-tool `eager_input_streaming: true`. If a proxy or Anthro
| Field | Description |
|-------|-------------|
| `supportsEagerToolInputStreaming` | Whether the provider accepts per-tool `eager_input_streaming`. Default: `true`. Set to `false` to omit that field and use the legacy fine-grained tool streaming beta header on tool-enabled requests. |
| `supportsLongCacheRetention` | Whether the provider accepts Anthropic long cache retention (`cache_control.ttl: "1h"`) when cache retention is `long`. Default: `true`. |
## OpenAI Compatibility
@@ -339,6 +341,7 @@ For providers with partial OpenAI compatibility, use the `compat` field.
| `thinkingFormat` | Use `reasoning_effort`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters |
| `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. |
| `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). |
| `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) |

View File

@@ -111,14 +111,17 @@ const OpenAICompletionsCompatSchema = Type.Object({
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
supportsStrictMode: Type.Optional(Type.Boolean()),
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
});
const OpenAIResponsesCompatSchema = Type.Object({
// Reserved for future use
sendSessionIdHeader: Type.Optional(Type.Boolean()),
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
});
const AnthropicMessagesCompatSchema = Type.Object({
supportsEagerToolInputStreaming: Type.Optional(Type.Boolean()),
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
});
const ProviderCompatSchema = Type.Union([

View File

@@ -461,6 +461,35 @@ describe("ModelRegistry", () => {
expect(compat?.supportsEagerToolInputStreaming).toBe(false);
});
test("compat schema accepts long cache retention flag", () => {
writeRawModelsJson({
demo: {
baseUrl: "https://example.com",
apiKey: "DEMO_KEY",
api: "anthropic-messages",
compat: {
supportsLongCacheRetention: false,
},
models: [
{
id: "demo-model",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000,
maxTokens: 100,
},
],
},
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const compat = registry.find("demo", "demo-model")?.compat as AnthropicMessagesCompat | undefined;
expect(registry.getError()).toBeUndefined();
expect(compat?.supportsLongCacheRetention).toBe(false);
});
test("model-level baseUrl overrides provider-level baseUrl for custom models", () => {
writeRawModelsJson({
"opencode-go": {