diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 36515d3fb..da2e4fb9e 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Moonshot/Kimi native OpenAI-compatible request metadata so Kimi K2 uses `max_tokens` and omits OpenAI-only `store`, restoring first-turn output with `MOONSHOT_API_KEY` ([#2289](https://github.com/can1357/oh-my-pi/issues/2289)). + ## [15.11.0] - 2026-06-10 ### Fixed diff --git a/packages/catalog/src/compat/openai.ts b/packages/catalog/src/compat/openai.ts index b190cbcf7..a7b678c24 100644 --- a/packages/catalog/src/compat/openai.ts +++ b/packages/catalog/src/compat/openai.ts @@ -102,7 +102,8 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv const isZhipu = modelMatchesHost(hostModel, "zhipu"); const isKilo = modelMatchesHost(hostModel, "kilo"); const isKimiModel = isKimiModelId(spec.id); - const isMoonshotKimi = isKimiModel && modelMatchesHost(hostModel, "moonshotNative"); + const isMoonshotNative = modelMatchesHost(hostModel, "moonshotNative"); + const isMoonshotKimi = isKimiModel && isMoonshotNative; const usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id); const isAnthropicModel = modelMatchesHost(hostModel, "anthropic") || isClaudeModelId(spec.id) || isAnthropicNamespacedModelId(spec.id); @@ -145,11 +146,16 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv isKilo || isQwen || isXiaomiHost || + isMoonshotNative || isOpenCodeHost; const isOpenCodeProvider = provider === "opencode-go" || provider === "opencode-zen"; const useMaxTokens = - isMistral || hostMatchesUrl(baseUrl, "chutes") || hostMatchesUrl(baseUrl, "fireworks") || isDirectDeepseekApi; + isMistral || + isMoonshotNative || + hostMatchesUrl(baseUrl, "chutes") || + hostMatchesUrl(baseUrl, "fireworks") || + isDirectDeepseekApi; // Hosts whose chat-completions endpoints are known to accept multiple // leading `system`/`developer` messages (preferred for KV-cache reuse). diff --git a/packages/catalog/test/issue-2113-repro.test.ts b/packages/catalog/test/issue-2113-repro.test.ts index 38386e347..e26b30221 100644 --- a/packages/catalog/test/issue-2113-repro.test.ts +++ b/packages/catalog/test/issue-2113-repro.test.ts @@ -17,7 +17,7 @@ * moonshot discovery mapper and stamps default thinking metadata. */ import { describe, expect, it } from "bun:test"; -import { streamOpenAICompletions } from "@oh-my-pi/pi-ai/providers/openai-completions"; +import { type OpenAICompletionsOptions, streamOpenAICompletions } from "@oh-my-pi/pi-ai/providers/openai-completions"; import type { AssistantMessage, Context } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { Effort } from "@oh-my-pi/pi-catalog/effort"; @@ -82,6 +82,7 @@ interface CapturedRequest { async function runHiTurn( model: Model<"openai-completions">, + options?: Pick, ): Promise<{ captured: CapturedRequest; assistant: AssistantMessage }> { const captured: CapturedRequest = { url: "", body: {} }; const fetchMock = (async (input: string | URL | Request, init?: RequestInit): Promise => { @@ -92,7 +93,7 @@ async function runHiTurn( return buildMockMoonshotResponse(); }) as typeof fetch; - const stream = streamOpenAICompletions(model, basicContext(), { apiKey: "test-key", fetch: fetchMock }); + const stream = streamOpenAICompletions(model, basicContext(), { apiKey: "test-key", fetch: fetchMock, ...options }); for await (const _ of stream) { // drain until terminal event } @@ -155,6 +156,14 @@ describe("issue #2113 — moonshot kimi-k2.6 discovery and wire format", () => { expect(textBlock).toBeDefined(); }); + it("uses Moonshot-native max_tokens and omits OpenAI store control", async () => { + const model = moonshotKimiModel("kimi-k2.5", true); + const { captured } = await runHiTurn(model, { reasoning: "high" }); + + expect(captured.body.max_tokens).toBeDefined(); + expect(captured.body.max_completion_tokens).toBeUndefined(); + expect(captured.body.store).toBeUndefined(); + }); it("wire body includes thinking.keep='all' when reasoning is explicitly requested", async () => { const model = moonshotKimiModel("kimi-k2.6", true); const captured: CapturedRequest = { url: "", body: {} };