Skip to content

Commit c16336a

Browse files
olddonkeyclaude
andcommitted
feat(kimi): forward prompt_cache_key on the Kimi Coding Plan presets
Kimi's Chat Completions API documents prompt_cache_key as required for Kimi Code Plan cache hits (a stable session/task id, unchanged across exit and resume). Opt the canonical `kimi` OAuth and `kimi-code` API-key presets into the existing openai-chat forwarding flag so the caller-supplied key reaches https://api.kimi.com/coding/v1. The adapter never invents a key: it forwards what the internal request already carries (Codex's session key on /v1/responses, or the session-scoped key the Claude /v1/messages inbound derives), an absent field stays absent, and an explicit provider-level `promptCacheKey: false` still opts out. All other OpenAI-compatible presets remain deny-by-default because strict backends reject the OpenAI-specific field. Also persist the flag through providerConfigSeed / enrichProviderFromRegistry like the sibling scalars (parallelToolCalls, modelSuffixBracketStrip): key-pool 429 rotation rebuilds the provider from the persisted config rather than the routed one, so without seeding the retried request would silently drop the key on exactly the quota-sensitive turns that need affinity most. Evidence: https://platform.kimi.com/docs/api/chat Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 903b62c commit c16336a

10 files changed

Lines changed: 108 additions & 0 deletions

File tree

docs-site/src/content/docs/guides/providers.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ ocx logout <provider>
9090
| `cursor` | `cursor` | `https://api2.cursor.sh` | Experimental PKCE login, live HTTP/2 transport, and account-filtered model discovery. |
9191
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. |
9292

93+
For the canonical Kimi Coding Plan presets (`kimi` account login and `kimi-code` API key),
94+
opencodex forwards the request's stable `prompt_cache_key` to the Chat Completions request. Kimi
95+
documents a stable session/task key as required for Kimi Code Plan cache hits; providers that may
96+
reject the field are left unchanged.
97+
9398
You can also start OAuth from the [web dashboard](/guides/web-dashboard/).
9499

95100
### Multiple OAuth accounts

docs-site/src/content/docs/ja/guides/providers.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ ocx logout <provider>
8484
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth を Cloud Code Assist wire で使用。 |
8585
| `cursor` | `cursor` | `https://api2.cursor.sh` | 実験的 PKCE ログイン、HTTP/2 トランスポート、アカウント別モデル探索をサポート。 |
8686

87+
正規の Kimi Coding Plan プリセット(`kimi` アカウントログインと `kimi-code` API key)では、
88+
opencodex はリクエストの安定した `prompt_cache_key` を Chat Completions リクエストへ転送します。
89+
Kimi のドキュメントでは、Kimi Code Plan のキャッシュヒットには安定したセッション/タスク key が
90+
必須とされています。このフィールドを拒否する可能性がある他のプロバイダーは変更されません。
91+
8792
[ウェブダッシュボード](/ja/guides/web-dashboard/)からも OAuth を開始できます。
8893

8994
### 複数の OAuth アカウント

docs-site/src/content/docs/ko/guides/providers.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ ocx logout <provider>
8484
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth를 Cloud Code Assist wire로 사용합니다. |
8585
| `cursor` | `cursor` | `https://api2.cursor.sh` | 실험적 PKCE 로그인, HTTP/2 전송, 계정별 모델 탐색을 지원합니다. |
8686

87+
정식 Kimi Coding Plan 프리셋(`kimi` 계정 로그인과 `kimi-code` API key)의 경우, opencodex는
88+
요청의 안정적인 `prompt_cache_key`를 Chat Completions 요청으로 전달합니다. Kimi 문서는 Kimi
89+
Code Plan 캐시 적중을 위해 안정적인 세션/작업 key를 필수로 명시합니다. 이 필드를 거부할 수
90+
있는 다른 프로바이더는 변경되지 않습니다.
91+
8792
[웹 대시보드](/ko/guides/web-dashboard/)에서도 OAuth를 시작할 수 있습니다.
8893

8994
### 여러 OAuth 계정

docs-site/src/content/docs/ru/guides/providers.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ ocx logout <provider>
9393
| `cursor` | `cursor` | `https://api2.cursor.sh` | Экспериментальный PKCE-вход, живой транспорт HTTP/2 и обнаружение моделей с фильтрацией по аккаунту. |
9494
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. |
9595

96+
Для канонических пресетов Kimi Coding Plan (вход через аккаунт `kimi` и API-ключ `kimi-code`)
97+
opencodex передаёт стабильный `prompt_cache_key` запроса в запрос Chat Completions. Документация
98+
Kimi требует стабильный ключ сессии/задачи для попаданий в кэш Kimi Code Plan; провайдеры,
99+
которые могут отклонить это поле, остаются без изменений.
100+
96101
OAuth можно запустить и из [веб-дашборда](/ru/guides/web-dashboard/).
97102

98103
### Несколько OAuth-аккаунтов

docs-site/src/content/docs/zh-cn/guides/providers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ ocx logout <provider>
7878
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | 通过 Cloud Code Assist 协议使用 Google OAuth。 |
7979
| `cursor` | `cursor` | `https://api2.cursor.sh` | 实验性 PKCE 登录、HTTP/2 传输和按账号筛选的模型发现。 |
8080

81+
对于规范的 Kimi Coding Plan 预设(`kimi` 账号登录和 `kimi-code` API key),opencodex
82+
会把请求中稳定的 `prompt_cache_key` 转发到 Chat Completions 请求。Kimi 文档要求 Kimi Code
83+
Plan 使用稳定的会话/任务 key 才能获得可靠的缓存命中;可能拒绝该字段的其他 provider 保持不变。
84+
8185
你也可以从 [web 仪表盘](/zh-cn/guides/web-dashboard/) 启动 OAuth。
8286

8387
### 多个 OAuth 账号

src/providers/derive.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
129129
...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
130130
...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
131131
...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
132+
...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}),
132133
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
133134
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
134135
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
@@ -242,6 +243,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
242243
if (!prov.noTopPModels && seed.noTopPModels) prov.noTopPModels = [...seed.noTopPModels];
243244
if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels];
244245
if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
246+
if (prov.promptCacheKey === undefined && seed.promptCacheKey !== undefined) prov.promptCacheKey = seed.promptCacheKey;
245247
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
246248
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
247249
if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];

src/providers/registry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
480480
baseUrl: "https://api.kimi.com/coding/v1",
481481
authKind: "oauth",
482482
modelSuffixBracketStrip: true,
483+
// Kimi Code Plan requires a stable session/task prompt_cache_key for reliable cache affinity.
484+
// The chat adapter only forwards a key already on the internal request (Codex's session key,
485+
// or the one the Claude /v1/messages inbound derives); the adapter itself never invents one.
486+
// Evidence: https://platform.kimi.com/docs/api/chat
487+
promptCacheKey: true,
483488
featured: true,
484489
oauthId: "kimi",
485490
jawcodeBundle: "moonshot",
@@ -1018,6 +1023,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
10181023
id: "kimi-code", label: "Kimi (coding)", baseUrl: "https://api.kimi.com/coding/v1", adapter: "openai-chat", authKind: "key",
10191024
dashboardUrl: "https://platform.moonshot.cn/console/api-keys", defaultModel: "kimi-k2.7-code",
10201025
modelSuffixBracketStrip: true,
1026+
// API-key form of the same Kimi Code Plan transport; keep cache affinity identical to OAuth.
1027+
promptCacheKey: true,
10211028
models: KIMI_CODING_MODELS,
10221029
modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
10231030
modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,

structure/04_transports-and-sidecars.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,19 @@ lookalike hosts, and custom proxy paths fail validation. A model override replac
271271
merges the provider-wide default, keeping precedence deterministic. With no preference configured,
272272
the request body is byte-for-byte unchanged in this area and OpenRouter retains its default routing.
273273

274+
## Kimi Coding Plan prompt-cache affinity
275+
276+
The canonical `kimi` OAuth and `kimi-code` API-key presets opt into forwarding the internal
277+
request's `prompt_cache_key` to Kimi's Chat Completions body. Kimi Code Plan documents a stable
278+
session/task key as required for reliable prompt-cache affinity. The chat adapter never invents a
279+
key of its own: it forwards what the request already carries — Codex's session key on
280+
`/v1/responses`, or the session-scoped key the Claude `/v1/messages` inbound derives
281+
(metadata.user_id hash, else the system+tools cohort hash) — and a request with no key stays
282+
keyless. An explicit provider-level `promptCacheKey: false` continues to opt out, and the flag is
283+
persisted through `providerConfigSeed`/`enrichProviderFromRegistry` so key-pool 429 rotation keeps
284+
it. Other OpenAI-compatible providers remain deny-by-default because strict backends may reject
285+
the OpenAI-specific field.
286+
274287
## xAI Grok hardening (official Grok Build contract parity)
275288

276289
Grounded in the open-sourced official client (xai-org/grok-build); unit + evidence:

tests/openai-chat-hardening.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,61 @@ describe("openai-chat credential hardening", () => {
161161

162162
expect(body).not.toHaveProperty("prompt_cache_key");
163163
});
164+
165+
test("canonical Kimi Coding Plan routes forward Codex prompt_cache_key", () => {
166+
for (const [providerName, authMode] of [
167+
["kimi", "oauth"],
168+
["kimi-code", "key"],
169+
] as const) {
170+
const config: OcxConfig = {
171+
port: 10100,
172+
defaultProvider: providerName,
173+
providers: {
174+
[providerName]: {
175+
adapter: "openai-chat",
176+
baseUrl: "https://api.kimi.com/coding/v1",
177+
apiKey: "test-kimi-credential",
178+
authMode,
179+
},
180+
},
181+
};
182+
const route = routeModel(config, `${providerName}/k3`);
183+
const req = parsed();
184+
req.modelId = route.modelId;
185+
req.options.promptCacheKey = "codex-kimi-session-v1";
186+
187+
expect(route.provider.promptCacheKey).toBe(true);
188+
const body = JSON.parse(createOpenAIChatAdapter(route.provider).buildRequest(req).body);
189+
expect(body).toMatchObject({
190+
model: "k3",
191+
prompt_cache_key: "codex-kimi-session-v1",
192+
});
193+
}
194+
});
195+
196+
test("an explicit Kimi promptCacheKey false remains an opt-out", () => {
197+
const config: OcxConfig = {
198+
port: 10100,
199+
defaultProvider: "kimi",
200+
providers: {
201+
kimi: {
202+
adapter: "openai-chat",
203+
baseUrl: "https://api.kimi.com/coding/v1",
204+
apiKey: "test-kimi-credential",
205+
authMode: "oauth",
206+
promptCacheKey: false,
207+
},
208+
},
209+
};
210+
const route = routeModel(config, "kimi/k3");
211+
const req = parsed();
212+
req.modelId = route.modelId;
213+
req.options.promptCacheKey = "codex-kimi-session-v1";
214+
215+
expect(route.provider.promptCacheKey).toBe(false);
216+
const body = JSON.parse(createOpenAIChatAdapter(route.provider).buildRequest(req).body);
217+
expect(body).not.toHaveProperty("prompt_cache_key");
218+
});
164219
});
165220

166221
describe("openai-chat max output defaults", () => {

tests/provider-registry-parity.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,13 @@ describe("provider registry parity", () => {
374374
expect(entry?.[field]).toContain("kimi-for-coding");
375375
}
376376
expect(entry?.modelSuffixBracketStrip).toBe(true);
377+
expect(entry?.promptCacheKey).toBe(true);
378+
// Key-pool 429 rotation rebuilds the provider from the persisted config (not the routed
379+
// one), so the flag must survive seeding/enrichment, not just the router's registry backfill.
380+
expect(providerConfigSeed(entry!).promptCacheKey).toBe(true);
381+
const enriched: OcxProviderConfig = { adapter: "openai-chat", baseUrl: entry!.baseUrl };
382+
enrichProviderFromCatalog(providerId, enriched);
383+
expect(enriched.promptCacheKey).toBe(true);
377384
expect(entry?.noReasoningModels).not.toContain("k3");
378385
expect(entry?.noReasoningModels).not.toContain("k3[1m]");
379386
expect(entry?.modelReasoningEfforts?.k3).toEqual(["low", "high", "max"]);

0 commit comments

Comments
 (0)