From 26cb0cc2e133123c3e0fbb87633c32c2f946fd15 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 16 Jul 2026 19:46:25 -0700 Subject: [PATCH] FN-8180: add Kimi K3 catalog and pricing support Expose Kimi K3 throughout the native pi catalog and token-cost surfaces. - Add K3 pricing and normalized lookup coverage for Dashboard cost calculations. - Verify native K3 registry, API model route, and repeat-stop behavior end to end. - Add a minor release changeset for Kimi K3 model selection support. Files changed: .changeset/fn-8180-pi-sdk-kimi-k3.md | 7 +++ packages/core/src/__tests__/model-pricing.test.ts | 7 ++- packages/core/src/model-pricing.ts | 17 +++++- ...ister-model-routes-kimi-k3-supplemental.test.ts | 70 ++++++++++++++++++++++ .../pi-prompt-session-and-check-recursion.test.ts | 15 +++++ .../src/__tests__/provider-registration.test.ts | 17 ++++++ 6 files changed, 130 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-8180 Fusion-Task-Lineage: 07598d73-c866-4329-b5cf-3aebc1c0a180 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8180-pi-sdk-kimi-k3.md | 7 ++ .../core/src/__tests__/model-pricing.test.ts | 7 +- packages/core/src/model-pricing.ts | 17 ++++- ...-model-routes-kimi-k3-supplemental.test.ts | 70 +++++++++++++++++++ ...prompt-session-and-check-recursion.test.ts | 15 ++++ .../__tests__/provider-registration.test.ts | 17 +++++ 6 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-8180-pi-sdk-kimi-k3.md create mode 100644 packages/dashboard/src/__tests__/register-model-routes-kimi-k3-supplemental.test.ts diff --git a/.changeset/fn-8180-pi-sdk-kimi-k3.md b/.changeset/fn-8180-pi-sdk-kimi-k3.md new file mode 100644 index 0000000000..71caa2628a --- /dev/null +++ b/.changeset/fn-8180-pi-sdk-kimi-k3.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add Kimi K3 model selection and token-cost support. +category: feature +dev: Aligns pi SDK consumers to 0.80.10 and verifies native `kimi-coding:k3` catalog availability. diff --git a/packages/core/src/__tests__/model-pricing.test.ts b/packages/core/src/__tests__/model-pricing.test.ts index 0c936bea56..2a271afc8d 100644 --- a/packages/core/src/__tests__/model-pricing.test.ts +++ b/packages/core/src/__tests__/model-pricing.test.ts @@ -113,11 +113,13 @@ describe("model-pricing", () => { expect(result.usd).toBeCloseTo(4.875, 3); }); - it("prices GLM-5.2, MiniMax-M3, and Kimi K2.6 instead of reporting unavailable", () => { + it("prices GLM-5.2, MiniMax-M3, Kimi K2.6, and Kimi K3 instead of reporting unavailable", () => { const cases = [ { provider: "zai", model: "glm-5.2", expectedUsd: 2.28 }, { provider: "minimax", model: "MiniMax-M3", expectedUsd: 0.54 }, { provider: "kimi-coding", model: "kimi-k2.6-preview", expectedUsd: 1.75 }, + // K3: $3/M cache-miss input + $15/M output; 1M input + 200k output = $6. + { provider: "kimi-coding", model: "k3", expectedUsd: 6 }, ] as const; for (const { provider, model, expectedUsd } of cases) { @@ -271,6 +273,9 @@ describe("model-pricing", () => { expect(lookupPricing({ model: "kimi-k2.6-preview" })).toBe( MODEL_PRICING["kimi-coding:kimi-k2.6-preview"], ); + expect(lookupPricing({ provider: " KIMI-CODING ", model: " K3 " })).toBe( + MODEL_PRICING["kimi-coding:k3"], + ); }); it("returns undefined for empty / unknown input", () => { diff --git a/packages/core/src/model-pricing.ts b/packages/core/src/model-pricing.ts index d1056af860..d687cf48f4 100644 --- a/packages/core/src/model-pricing.ts +++ b/packages/core/src/model-pricing.ts @@ -28,7 +28,7 @@ * The date the rates in {@link MODEL_PRICING} were last verified, ISO-8601. * Bump this whenever you edit a rate. Surfaced in the UI as "prices as of". */ -export const pricingAsOf = "2026-07-11"; +export const pricingAsOf = "2026-07-16"; /** * Pricing entries older than this (relative to a caller-supplied `now`) are @@ -387,7 +387,12 @@ export const MODEL_PRICING: Readonly> = { /* * FNXC:ModelCatalog 2026-07-11-23:02: - * The LiteLLM pricing refresh only maps OpenAI, Anthropic, and Google providers, so Z.ai, MiniMax, and Kimi Coding runs need static rows to keep Dashboard token-cost surfaces from rendering `—`. Rates are verified from https://docs.z.ai/guides/overview/pricing.md, https://platform.minimax.io/docs/guides/pricing-paygo.md, and https://platform.kimi.ai/docs/pricing/chat-k26.md; MiniMax uses the Standard ≤512k pay-as-you-go tier because MODEL_PRICING has no request-tier dimension. + * The LiteLLM pricing refresh only maps OpenAI, Anthropic, and Google providers, so Z.ai, MiniMax, and Kimi Coding runs need static rows to keep Dashboard token-cost surfaces from rendering `—`. Rates are verified from https://docs.z.ai/guides/overview/pricing.md, https://platform.minimax.io/docs/guides/pricing-paygo.md, https://platform.kimi.ai/docs/pricing/chat-k26.md, and https://platform.kimi.ai/docs/pricing/chat-k3.md; MiniMax uses the Standard ≤512k pay-as-you-go tier because MODEL_PRICING has no request-tier dimension. + + * FNXC:ModelCatalog 2026-07-16-19:05: + * FN-8180 upgrades pi to 0.80.10, whose native kimi-coding catalog exposes `k3`. + * Keep the Dashboard's independent cost derivation aligned with Moonshot's published + * K3 rates rather than pi's intentionally zero-valued catalog cost placeholders. */ // ── Zhipu AI (Z.ai) ───────────────────────────────────────────────── // No distinct cache-write token charge → cacheWrite = input rate. @@ -418,6 +423,14 @@ export const MODEL_PRICING: Readonly> = { cacheWritePer1M: 0.95, source: "platform.kimi.ai/docs/pricing/chat-k26.md", }, + // Cache miss is the input rate; K3 has no separately published cache-write rate. + "kimi-coding:k3": { + inputPer1M: 3, + outputPer1M: 15, + cacheReadPer1M: 0.3, + cacheWritePer1M: 3, + source: "platform.kimi.ai/docs/pricing/chat-k3.md", + }, }; /** Reference to a model, by provider + id (either may be unset). */ diff --git a/packages/dashboard/src/__tests__/register-model-routes-kimi-k3-supplemental.test.ts b/packages/dashboard/src/__tests__/register-model-routes-kimi-k3-supplemental.test.ts new file mode 100644 index 0000000000..d0204ba67f --- /dev/null +++ b/packages/dashboard/src/__tests__/register-model-routes-kimi-k3-supplemental.test.ts @@ -0,0 +1,70 @@ +import type { Router } from "express"; +import { describe, expect, it, vi } from "vitest"; +import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent"; +import { registerModelRoutes } from "../routes/register-model-routes.js"; + +/* +FNXC:ModelCatalog 2026-07-16-19:05: +FN-8180 requires Kimi K3 to reach both model-catalog consumers. This route-level +coverage uses pi 0.80.10's actual built-in ModelRuntime catalog after refresh, so an +SDK catalog regression cannot leave the Dashboard dropdown missing K3 while the engine +registry test remains green. +*/ + +async function createNativeKimiRegistry(): Promise { + const runtime = await ModelRuntime.create({ + credentials: { + read: async (providerId) => providerId === "kimi-coding" + ? { type: "api_key", key: "test-kimi-key" } + : undefined, + list: async () => [{ providerId: "kimi-coding", type: "api_key" }], + modify: async (_providerId, fn) => fn(undefined), + delete: async () => undefined, + }, + modelsPath: null, + allowModelNetwork: false, + }); + return new ModelRegistry(runtime); +} + +function createModelsHandler(modelRegistry: ModelRegistry) { + const handlers = new Map void }) => Promise>(); + const router = { + get: vi.fn((path: string, handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise) => { + handlers.set(path, handler); + }), + } as unknown as Router; + const authStorage = { + reload: vi.fn(), + getOAuthProviders: vi.fn(() => []), + getApiKeyProviders: vi.fn(() => [{ id: "kimi-coding", name: "Kimi" }]), + get: vi.fn(() => ({ type: "api_key", key: "test-kimi-key" })), + hasApiKey: vi.fn((providerId: string) => providerId === "kimi-coding"), + hasAuth: vi.fn((providerId: string) => providerId === "kimi-coding"), + }; + + registerModelRoutes({ + router, + store: { + getGlobalSettingsStore: () => ({ getSettings: vi.fn().mockResolvedValue({}) }), + getSettingsFast: vi.fn().mockResolvedValue({}), + } as never, + runtimeLogger: { child: vi.fn(() => ({ warn: vi.fn() })) } as never, + options: { modelRegistry, authStorage } as never, + } as never); + + return handlers.get("/models")!; +} + +describe("FN-8180: Kimi K3 /api/models catalog", () => { + it("surfaces the native K3 model once for a configured Kimi provider", async () => { + const handler = createModelsHandler(await createNativeKimiRegistry()); + const json = vi.fn(); + + await handler({}, { json }); + + const response = json.mock.calls[0][0] as { models: Array<{ provider: string; id: string; name: string; reasoning: boolean; contextWindow: number }> }; + const k3Rows = response.models.filter((model) => model.provider === "kimi-coding" && model.id === "k3"); + expect(k3Rows).toEqual([{ provider: "kimi-coding", id: "k3", name: "Kimi K3", reasoning: true, contextWindow: 1_048_576 }]); + }); +}); diff --git a/packages/engine/src/__tests__/pi-prompt-session-and-check-recursion.test.ts b/packages/engine/src/__tests__/pi-prompt-session-and-check-recursion.test.ts index 1bb0de5005..e7f1eef929 100644 --- a/packages/engine/src/__tests__/pi-prompt-session-and-check-recursion.test.ts +++ b/packages/engine/src/__tests__/pi-prompt-session-and-check-recursion.test.ts @@ -57,6 +57,21 @@ describe("promptSessionAndCheck recursion guard (FN-4930)", () => { expect(warnMock).not.toHaveBeenCalledWith(expect.stringContaining("failed to inspect transcript")); }); + it("treats Kimi's finish_reason repeat as a soft stop", async () => { + const { promptSessionAndCheck } = await import("../pi.js"); + const state = { errorMessage: "", messages: [] }; + const session = { + model: { provider: "kimi-coding", id: "k3" }, + prompt: vi.fn(async () => { + state.errorMessage = "Provider finish_reason: repeat"; + }), + state, + } as any; + + await expect(promptSessionAndCheck(session, "hello")).resolves.toBeUndefined(); + expect(state.errorMessage).toBeUndefined(); + }); + it("annotates model-auth-tier incompatibility errors with model identity and actionable hint", async () => { const { promptSessionAndCheck } = await import("../pi.js"); diff --git a/packages/engine/src/__tests__/provider-registration.test.ts b/packages/engine/src/__tests__/provider-registration.test.ts index 9542da09f5..9beca33ef5 100644 --- a/packages/engine/src/__tests__/provider-registration.test.ts +++ b/packages/engine/src/__tests__/provider-registration.test.ts @@ -127,6 +127,23 @@ describe("seedDashboardProviders", () => { expect(providerIds).toEqual(expect.arrayContaining(["zai", "openrouter", "kimi-coding", "grok-cli"])); }); + it("keeps native Kimi K3 available through the installed pi model registry", async () => { + // FNXC:ModelCatalog 2026-07-16-19:05: FN-8180 requires catalog coverage to + // exercise pi's real 0.80.10 built-in registry, not a hand-written Kimi fixture. + const modelRegistry = await createInMemoryModelRegistry(); + await modelRegistry.refresh(); + + expect(modelRegistry.find("kimi-coding", "k3")).toMatchObject({ + provider: "kimi-coding", + id: "k3", + name: "Kimi K3", + api: "anthropic-messages", + baseUrl: "https://api.kimi.com/coding", + contextWindow: 1_048_576, + maxTokens: 131_072, + }); + }); + it("registers one custom provider alongside built-ins", async () => { const store = makeStore([customProvider()]); const authStorage = makeAuthStorage();