From 0c0ae57e1e83dfbbf80c92f9e532613b7655613b Mon Sep 17 00:00:00 2001 From: mb Date: Wed, 9 Sep 2026 01:07:51 +0200 Subject: [PATCH 1/2] fix(agent-core-v2): only send prompt_cache_key to official OpenAI endpoints --- .../prompt-cache-key-official-openai-only.md | 6 ++++++ .../human/llm/requester/bases/openai-base-url.ts | 11 +++++++++++ .../requester/bases/openai-responses/format.ts | 5 ++++- .../human/llm/requester/bases/openai/format.ts | 5 ++++- .../src/human/test/llm/cache-key.test.ts | 16 +++++++++++++++- .../src/human/test/llm/response-format.test.ts | 16 ++++++++++++++++ 6 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 .changeset/prompt-cache-key-official-openai-only.md create mode 100644 packages/agent-core-v2/src/human/llm/requester/bases/openai-base-url.ts diff --git a/.changeset/prompt-cache-key-official-openai-only.md b/.changeset/prompt-cache-key-official-openai-only.md new file mode 100644 index 00000000000..d69e52119d9 --- /dev/null +++ b/.changeset/prompt-cache-key-official-openai-only.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core-v2": patch +"@moonshot-ai/kimi-code": patch +--- + +Only send the OpenAI prompt cache key to the official OpenAI API; custom OpenAI-compatible endpoints no longer receive the unknown parameter and stop rejecting requests with HTTP 400. \ No newline at end of file diff --git a/packages/agent-core-v2/src/human/llm/requester/bases/openai-base-url.ts b/packages/agent-core-v2/src/human/llm/requester/bases/openai-base-url.ts new file mode 100644 index 00000000000..ab87c966300 --- /dev/null +++ b/packages/agent-core-v2/src/human/llm/requester/bases/openai-base-url.ts @@ -0,0 +1,11 @@ +export function isOfficialOpenAIBaseUrl(baseUrl: string | undefined): boolean { + if (baseUrl === undefined) { + return true; + } + try { + const hostname = new URL(baseUrl).hostname; + return hostname === 'api.openai.com' || hostname.endsWith('.api.openai.com'); + } catch { + return false; + } +} \ No newline at end of file diff --git a/packages/agent-core-v2/src/human/llm/requester/bases/openai-responses/format.ts b/packages/agent-core-v2/src/human/llm/requester/bases/openai-responses/format.ts index 20118ba3073..71d848b7599 100644 --- a/packages/agent-core-v2/src/human/llm/requester/bases/openai-responses/format.ts +++ b/packages/agent-core-v2/src/human/llm/requester/bases/openai-responses/format.ts @@ -11,6 +11,7 @@ import { encodeReasoningEffortFallback } from '#/llm/thinking'; import type { TokenUsage } from '#/llm/usage'; import { isContextOverflowErrorCode, isOpenAIInsufficientQuotaCode } from '../openai/format'; +import { isOfficialOpenAIBaseUrl } from '../openai-base-url'; import { lowerMessage, type ResponsesInputItem } from './lower'; type RawObject = Record; @@ -365,7 +366,9 @@ function resolveRequestKwargs(input: FormatRequestInput): Record = {}; if (cacheKey !== undefined) { - kwargs = trait?.cacheKey?.(cacheKey, ctx) ?? { prompt_cache_key: cacheKey }; + kwargs = + trait?.cacheKey?.(cacheKey, ctx) ?? + (isOfficialOpenAIBaseUrl(ctx.model.baseUrl) ? { prompt_cache_key: cacheKey } : {}); } if (thinking !== undefined) { kwargs = applyThinking(kwargs, thinking, trait, ctx, (t) => diff --git a/packages/agent-core-v2/src/human/llm/requester/bases/openai/format.ts b/packages/agent-core-v2/src/human/llm/requester/bases/openai/format.ts index 963958b2531..eae5979918e 100644 --- a/packages/agent-core-v2/src/human/llm/requester/bases/openai/format.ts +++ b/packages/agent-core-v2/src/human/llm/requester/bases/openai/format.ts @@ -42,6 +42,7 @@ import { extractReasoning, extractReasoningDetails, } from './reasoning-key'; +import { isOfficialOpenAIBaseUrl } from '../openai-base-url'; function responseFormatToOpenAI(format: ResponseFormat): Record { if (format.type === 'json_object') { @@ -181,7 +182,9 @@ function resolveRequestKwargs(input: FormatRequestInput): ResolvedRequestKwargs } = input; let kwargs: Record = {}; if (cacheKey !== undefined) { - kwargs = trait?.cacheKey?.(cacheKey, ctx) ?? { prompt_cache_key: cacheKey }; + kwargs = + trait?.cacheKey?.(cacheKey, ctx) ?? + (isOfficialOpenAIBaseUrl(ctx.model.baseUrl) ? { prompt_cache_key: cacheKey } : {}); } let preserveThinking = false; if (thinking !== undefined) { diff --git a/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts b/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts index 777f70a79f9..91bc6e42c3c 100644 --- a/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts +++ b/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts @@ -11,7 +11,7 @@ const model: LlmModel = { provider: 'test', model: 'test-model', capability: UNKNOWN_CAPABILITY, - baseUrl: 'https://example.test/v1', + baseUrl: 'https://api.openai.com/v1', }; const messages: readonly Message[] = [createUserMessage('hi')]; @@ -121,6 +121,20 @@ describe('openai requester cacheKey', () => { expect(client.body()['extra_body']).toEqual({ trace_id: 't1' }); }); + it('omits prompt_cache_key for third-party OpenAI endpoints', async () => { + const client = stubOpenAIClient(chatCompletionChunks); + const requester = createOpenAIRequester(undefined, { clientFactory: client.clientFactory }); + await requester.generate( + { + model: { ...model, baseUrl: 'https://integrate.api.nvidia.com/v1' }, + cacheKey: 'session-1', + }, + { messages }, + { signal: new AbortController().signal }, + ); + expect(client.body()['prompt_cache_key']).toBeUndefined(); + }); + it('lets a trait override the cache key params', async () => { const client = stubOpenAIClient(chatCompletionChunks); const requester = createOpenAIRequester( diff --git a/packages/agent-core-v2/src/human/test/llm/response-format.test.ts b/packages/agent-core-v2/src/human/test/llm/response-format.test.ts index fbce3e823e7..670c52cb4bd 100644 --- a/packages/agent-core-v2/src/human/test/llm/response-format.test.ts +++ b/packages/agent-core-v2/src/human/test/llm/response-format.test.ts @@ -195,6 +195,22 @@ describe('openai requester responseFormat', () => { }); describe('openai-responses requester responseFormat', () => { + it('omits prompt_cache_key for third-party endpoints', async () => { + const client = stubResponsesClient(responsesStreamEvents); + const requester = createOpenAIResponsesRequester(undefined, { + clientFactory: client.clientFactory, + }); + await requester.generate( + { + model: { ...model, baseUrl: 'https://integrate.api.nvidia.com/v1' }, + cacheKey: 'session-1', + }, + { messages }, + { signal: new AbortController().signal }, + ); + expect(client.body()['prompt_cache_key']).toBeUndefined(); + }); + it('maps json_schema to text.format', async () => { const client = stubResponsesClient(responsesStreamEvents); const requester = createOpenAIResponsesRequester(undefined, { From 0620582c69eeb4210185c920f6e56acc45fe265f Mon Sep 17 00:00:00 2001 From: mb <5196346+creatiVision@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:11:35 +0200 Subject: [PATCH 2/2] fix(agent-core-v2): address review feedback on the prompt cache key changeset and tests --- .../prompt-cache-key-official-openai-only.md | 3 +- .../src/human/test/llm/cache-key.test.ts | 56 +++++++++++++++++++ .../human/test/llm/response-format.test.ts | 16 ------ 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/.changeset/prompt-cache-key-official-openai-only.md b/.changeset/prompt-cache-key-official-openai-only.md index d69e52119d9..e6737ae7e6b 100644 --- a/.changeset/prompt-cache-key-official-openai-only.md +++ b/.changeset/prompt-cache-key-official-openai-only.md @@ -1,6 +1,5 @@ --- -"@moonshot-ai/agent-core-v2": patch "@moonshot-ai/kimi-code": patch --- -Only send the OpenAI prompt cache key to the official OpenAI API; custom OpenAI-compatible endpoints no longer receive the unknown parameter and stop rejecting requests with HTTP 400. \ No newline at end of file +Only send the OpenAI prompt cache key to the official OpenAI API. \ No newline at end of file diff --git a/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts b/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts index 93f615c9565..9bcdc67c147 100644 --- a/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts +++ b/packages/agent-core-v2/src/human/test/llm/cache-key.test.ts @@ -5,6 +5,7 @@ import { createUserMessage, type Message } from '#/llm/message'; import type { LlmModel } from '#/llm/model'; import { createAnthropicRequester } from '#/llm/requester/bases/anthropic/requester'; import { createOpenAIRequester } from '#/llm/requester/bases/openai/requester'; +import { createOpenAIResponsesRequester } from '#/llm/requester/bases/openai-responses/requester'; import type { LlmClientContext } from '#/llm/requester/requester'; const model: LlmModel = { @@ -34,6 +35,17 @@ const anthropicStreamEvents: readonly Record[] = [ { type: 'message_stop' }, ]; +const responsesStreamEvents: readonly Record[] = [ + { + type: 'response.completed', + response: { + id: 'resp_1', + status: 'completed', + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }, + }, +]; + function createAsyncStream(chunks: readonly T[]): AsyncIterable { return { async *[Symbol.asyncIterator]() { @@ -74,6 +86,34 @@ function stubOpenAIClient(chunks: readonly Record[]): { }; } +function stubResponsesClient(events: readonly Record[]): { + clientFactory: (request: LlmClientContext) => never; + body: () => Record; +} { + const captured: Record[] = []; + return { + clientFactory: () => + ({ + responses: { + create: (params: Record) => { + captured.push(params); + return { + withResponse: async () => ({ + data: createAsyncStream(events), + response: new Response(null), + }), + }; + }, + }, + }) as never, + body: () => { + const last = captured.at(-1); + if (last === undefined) throw new Error('expected client to be called'); + return last; + }, + }; +} + function stubAnthropicClient(events: readonly Record[]): { clientFactory: (request: LlmClientContext) => never; body: () => Record; @@ -175,3 +215,19 @@ describe('anthropic requester cacheKey', () => { expect(client.body()['top_k']).toBe(5); }); }); + +describe('openai-responses requester cacheKey', () => { + it('omits prompt_cache_key for third-party endpoints', async () => { + const client = stubResponsesClient(responsesStreamEvents); + const requester = createOpenAIResponsesRequester({ clientFactory: client.clientFactory }); + await requester.generate( + { + model: { ...model, baseUrl: 'https://integrate.api.nvidia.com/v1' }, + cacheKey: 'session-1', + }, + { messages }, + { signal: new AbortController().signal }, + ); + expect(client.body()['prompt_cache_key']).toBeUndefined(); + }); +}); diff --git a/packages/agent-core-v2/src/human/test/llm/response-format.test.ts b/packages/agent-core-v2/src/human/test/llm/response-format.test.ts index 740dbb9a0ad..7fea582d590 100644 --- a/packages/agent-core-v2/src/human/test/llm/response-format.test.ts +++ b/packages/agent-core-v2/src/human/test/llm/response-format.test.ts @@ -195,22 +195,6 @@ describe('openai requester responseFormat', () => { }); describe('openai-responses requester responseFormat', () => { - it('omits prompt_cache_key for third-party endpoints', async () => { - const client = stubResponsesClient(responsesStreamEvents); - const requester = createOpenAIResponsesRequester({ - clientFactory: client.clientFactory, - }); - await requester.generate( - { - model: { ...model, baseUrl: 'https://integrate.api.nvidia.com/v1' }, - cacheKey: 'session-1', - }, - { messages }, - { signal: new AbortController().signal }, - ); - expect(client.body()['prompt_cache_key']).toBeUndefined(); - }); - it('maps json_schema to text.format', async () => { const client = stubResponsesClient(responsesStreamEvents); const requester = createOpenAIResponsesRequester({