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 0000000000..d69e52119d --- /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 0000000000..ab87c96630 --- /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 20118ba307..71d848b759 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 963958b253..eae5979918 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 777f70a79f..91bc6e42c3 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 fbce3e823e..670c52cb4b 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, {