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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8180-pi-sdk-kimi-k3.md
Normal file
7
.changeset/fn-8180-pi-sdk-kimi-k3.md
Normal file
@@ -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.
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<Record<string, ModelPricing>> = {
|
||||
|
||||
/*
|
||||
* 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<Record<string, ModelPricing>> = {
|
||||
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). */
|
||||
|
||||
@@ -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<ModelRegistry> {
|
||||
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<string, (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>>();
|
||||
const router = {
|
||||
get: vi.fn((path: string, handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>) => {
|
||||
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 }]);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user