From 21a904f4b7a117b253dac05f27e39724ad9e2385 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 23:59:45 +0200 Subject: [PATCH] fix(ai): disable OpenCode long cache retention for rejecting routes closes #5702 --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/scripts/generate-models.ts | 16 +++++++ packages/ai/src/models.generated.ts | 56 +++++++++++++++++++----- packages/ai/test/cache-retention.test.ts | 40 +++++++++++++++++ packages/coding-agent/CHANGELOG.md | 1 + 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 03604c9b..71da18c3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenCode/OpenCode Go completion models that reject `prompt_cache_retention` to omit long-retention cache fields when `cacheRetention` is `long` ([#5702](https://github.com/earendil-works/pi/issues/5702)). + ## [0.79.3] - 2026-06-13 ### Fixed diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 1eaf9fe1..63c54a56 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -187,6 +187,15 @@ const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([ "gpt-5.5", ]); +const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([ + "opencode:deepseek-v4-flash", + "opencode:deepseek-v4-pro", + "opencode:kimi-k2.5", + "opencode:kimi-k2.6", + "opencode:minimax-m2.7", + "opencode-go:kimi-k2.6", +]); + function mergeThinkingLevelMap(model: Model, map: NonNullable["thinkingLevelMap"]>): void { model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map }; } @@ -1070,6 +1079,13 @@ async function loadModelsDevData(): Promise[]> { if (api === "openai-completions") { compat = { ...(compat ?? {}), maxTokensField: "max_tokens" }; + if ( + OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS.has( + `${variant.provider}:${modelId}`, + ) + ) { + compat = { ...compat, supportsLongCacheRetention: false }; + } } models.push({ diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 0b59d572..35295e5d 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7889,7 +7889,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -7927,7 +7927,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -8343,7 +8343,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -8361,7 +8361,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -8415,7 +8415,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -8579,7 +8579,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -10473,13 +10473,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, + input: 0.75, + output: 3.5, + cacheRead: 0.16, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 262144, } satisfies Model<"openai-completions">, "nex-agi/nex-n2-pro:free": { id: "nex-agi/nex-n2-pro:free", @@ -16823,6 +16823,24 @@ export const MODELS = { contextWindow: 200000, maxTokens: 131072, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "glm-5v-turbo": { id: "glm-5v-turbo", name: "GLM-5V-Turbo", @@ -16915,6 +16933,24 @@ export const MODELS = { contextWindow: 200000, maxTokens: 131072, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "glm-5v-turbo": { id: "glm-5v-turbo", name: "GLM-5V-Turbo", diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 8828ba86..6e2c1a5b 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { MODELS } from "../src/models.generated.ts"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; @@ -13,6 +14,11 @@ class PayloadCaptured extends Error { } } +interface OpenAICompletionsCachePayload { + prompt_cache_key?: string; + prompt_cache_retention?: string; +} + function stopAfterPayload(capture: (payload: TPayload) => void): (payload: unknown) => never { return (payload: unknown): never => { capture(payload as TPayload); @@ -455,5 +461,39 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { expect(capturedPayload.prompt_cache_key).toBeUndefined(); expect(capturedPayload.prompt_cache_retention).toBeUndefined(); }); + + it.each([ + MODELS.opencode["deepseek-v4-flash"], + MODELS.opencode["deepseek-v4-pro"], + MODELS.opencode["kimi-k2.5"], + MODELS.opencode["kimi-k2.6"], + MODELS.opencode["minimax-m2.7"], + MODELS["opencode-go"]["kimi-k2.6"], + ] as const)("should omit long cache retention for $provider/$id", async (metadata) => { + const model = metadata as Model<"openai-completions">; + let capturedPayload: OpenAICompletionsCachePayload | undefined; + + try { + const s = streamOpenAICompletions(model, context, { + apiKey: "fake-key", + cacheRetention: "long", + sessionId: "session-opencode-long-cache-unsupported", + onPayload: stopAfterPayload((payload) => { + capturedPayload = payload; + }), + }); + + for await (const event of s) { + if (event.type === "error") break; + } + } catch { + // Expected to fail + } + + expect(model.compat?.supportsLongCacheRetention).toBe(false); + expect(capturedPayload).toBeDefined(); + expect(capturedPayload?.prompt_cache_key).toBeUndefined(); + expect(capturedPayload?.prompt_cache_retention).toBeUndefined(); + }); }); }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6b17d85a..3cb2c685 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed inherited OpenCode/OpenCode Go completion model metadata to omit long-retention cache fields for routes that reject `prompt_cache_retention` ([#5702](https://github.com/earendil-works/pi/issues/5702)). - Fixed custom provider config so plain uppercase API key and header values remain literals instead of being treated as legacy environment references; use explicit `$ENV_VAR` syntax for environment variables ([#5661](https://github.com/earendil-works/pi/issues/5661)). ## [0.79.3] - 2026-06-13