@arnilo/prism-provider-kimi 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,14 @@ All notable changes to @arnilo/prism-provider-kimi will be documented in this fi
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.0.6] - 2026-07-19
9
+
10
+ - Released with the exact 0.0.6 first-party package graph.
11
+
12
+ ## [0.0.5] - 2026-07-16
13
+
14
+ - Pinned the required `@arnilo/prism` peer and package metadata to 0.0.5; runtime behavior is unchanged.
15
+
9
16
 
10
17
  ## [0.0.4] - 2026-07-14
11
18
 
package/README.md CHANGED
@@ -1,28 +1,48 @@
1
1
  # @arnilo/prism-provider-kimi
2
2
 
3
- Kimi provider package for Prism.
3
+ Kimi For Coding (Anthropic `/messages`) + optional Moonshot Open Platform (Chat Completions) for Prism.
4
4
 
5
5
  ```ts
6
- import { createKimiProviderPackage } from "@arnilo/prism-provider-kimi";
6
+ import { createKimiProviderPackage, listKimiModels } from "@arnilo/prism-provider-kimi";
7
7
 
8
8
  api.registerProviderPackage(createKimiProviderPackage({ kimiApiKey: "fake-kimi-key" }));
9
+
10
+ // Opt-in callable Moonshot + featured Open Platform models
11
+ api.registerProviderPackage(createKimiProviderPackage({
12
+ kimiApiKey: "fake-kimi-key",
13
+ includeMoonshotModels: true,
14
+ moonshotApiKey: "fake-moonshot-key",
15
+ }));
16
+
17
+ // Caller-gated discovery (never during setup)
18
+ const models = await listKimiModels({ apiKey: "fake-moonshot-key" });
9
19
  ```
10
20
 
11
21
  Exports:
12
22
  - `createKimiProviderPackage()`
13
- - `createKimiCodingProvider()`
14
- - `kimiCodingModels`
15
- - `moonshotKimiModels`
16
- - `defineKimiModel()`
23
+ - `createKimiCodingProvider()` / `createMoonshotProvider()`
24
+ - `listKimiModels()` / `mapKimiModel()` / `defineKimiModel()`
25
+ - `kimiCodingModels` / `moonshotKimiModels`
26
+ - `kimiThinking` / `kimiReasoningEffort` / `kimiPreserveThinking`
17
27
 
18
28
  Security defaults:
19
29
  - No network calls during import, setup, build, or default tests.
20
30
  - No automatic environment, file, keychain, or shell credential lookup.
21
- - Kimi credentials are resolved per request from caller-supplied values or resolvers.
22
- - Moonshot/Open Platform model metadata is registered only with `includeMoonshotModels: true`.
31
+ - Credentials are resolved per request from caller-supplied values or resolvers.
32
+ - Moonshot Open Platform is registered only with `includeMoonshotModels: true`.
33
+
34
+ Official model ids:
35
+ - Coding featured: `kimi-for-coding`, `kimi-for-coding-highspeed`, `k3` (not Pi `k2p7`).
36
+ - Open Platform featured: `kimi-k2.7-code`, `kimi-k3` (+ `listKimiModels()`).
23
37
 
24
38
  Cache behavior:
25
- - Default catalog models use implicit caching (no `cache_control`); opt in via `ModelConfig.cache.kind: "cache_control"` on the Anthropic route.
26
- - When opted in, `cache_control` markers apply only to selected `cache.breakpoints` (`"long"` → `ttl: "1h"`); Moonshot OpenAI route sends none.
27
- - `cache_read_input_tokens`/`cache_creation_input_tokens` map to `Usage.cacheReadTokens`/`cacheWriteTokens`.
39
+ - Default Coding models use implicit caching (no `cache_control`); opt in via `ModelConfig.cache.kind: "cache_control"`.
40
+ - When opted in, markers apply only to selected `cache.breakpoints` (`"long"` → `ttl: "1h"`).
41
+ - Moonshot never sends Anthropic `cache_control`.
42
+ - Coding `cache_read_input_tokens`/`cache_creation_input_tokens` map to `Usage.cacheReadTokens`/`cacheWriteTokens`.
28
43
  - Provider-owned headers (`content-type`, `user-agent`, `authorization`) win over caller headers.
44
+
45
+ Thinking:
46
+ - K2.x: `compat.thinking` (`type` / optional `keep`); K2.7-code omits `thinking` by default (always on).
47
+ - K3 / Coding `k3`: `compat.reasoning_effort` (per-turn override wins).
48
+ - Moonshot replays `reasoning_content` when `preserveThinking`.
package/dist/index.d.ts CHANGED
@@ -1,14 +1,27 @@
1
1
  import { type CredentialValueSource, type ModelConfig, type ProviderPackage } from "@arnilo/prism";
2
2
  export interface KimiProviderPackageOptions {
3
3
  readonly kimiApiKey?: CredentialValueSource;
4
+ /** Moonshot Open Platform API key (not interchangeable with Kimi Coding keys). */
5
+ readonly moonshotApiKey?: CredentialValueSource;
4
6
  readonly fetch?: typeof fetch;
5
7
  readonly baseUrl?: string;
8
+ /** Moonshot Open Platform base URL (default `https://api.moonshot.ai/v1`). */
9
+ readonly moonshotBaseUrl?: string;
6
10
  readonly id?: string;
11
+ readonly moonshotId?: string;
7
12
  readonly userAgent?: string;
13
+ /** Overrides featured `kimiCodingModels` registered on the coding provider. */
8
14
  readonly models?: readonly ModelConfig[];
15
+ /**
16
+ * When true, registers a callable Moonshot Open Platform Chat Completions provider
17
+ * plus featured/override Moonshot models (`compat.route: "openai"`).
18
+ */
9
19
  readonly includeMoonshotModels?: boolean;
10
20
  readonly moonshotModels?: readonly ModelConfig[];
11
21
  }
12
22
  export declare function createKimiProviderPackage(options?: KimiProviderPackageOptions): ProviderPackage;
13
- export { defineKimiModel, kimiCodingModels, moonshotKimiModels, type KimiModelConfig } from "./models.js";
14
- export { createKimiCodingProvider, kimiAnthropicBody, kimiAnthropicEvents, type KimiCodingProviderOptions } from "./provider.js";
23
+ export { defineKimiModel, kimiCodingModels, listKimiModels, mapKimiModel, moonshotKimiModels, type KimiModelConfig, type KimiModelEntry, type ListKimiModelsOptions, } from "./models.js";
24
+ export { createKimiCodingProvider, kimiAnthropicBody, kimiAnthropicEvents, type KimiCodingProviderOptions, } from "./provider.js";
25
+ export { createMoonshotProvider, moonshotBody, moonshotEvents, serializeMoonshotMessage, type MoonshotProviderOptions, } from "./moonshot.js";
26
+ export { kimiPreserveThinking, kimiReasoningEffort, kimiThinking, stripKimiThinkingCompat, } from "./thinking.js";
27
+ export { applyKimiAnthropicCacheControl, kimiAnthropicCacheEnabled, } from "./cache.js";
package/dist/index.js CHANGED
@@ -1,23 +1,44 @@
1
1
  import { defineProviderPackage } from "@arnilo/prism";
2
2
  import { kimiCodingModels, moonshotKimiModels } from "./models.js";
3
+ import { createMoonshotProvider } from "./moonshot.js";
3
4
  import { createKimiCodingProvider } from "./provider.js";
4
5
  export function createKimiProviderPackage(options = {}) {
5
6
  const providerId = options.id ?? "kimi-coding";
7
+ const moonshotId = options.moonshotId ?? "moonshot";
6
8
  return defineProviderPackage({
7
9
  name: "@arnilo/prism-provider-kimi",
8
10
  description: "Kimi provider package for Prism.",
9
11
  docs: { links: ["docs/providers/kimi.md"] },
10
12
  setup(api) {
11
- api.registerProvider(createKimiCodingProvider({ ...options, id: providerId, apiKey: options.kimiApiKey }));
12
- for (const model of options.models ?? kimiCodingModels)
13
+ api.registerProvider(createKimiCodingProvider({
14
+ id: providerId,
15
+ apiKey: options.kimiApiKey,
16
+ fetch: options.fetch,
17
+ baseUrl: options.baseUrl,
18
+ userAgent: options.userAgent,
19
+ }));
20
+ for (const model of options.models ?? kimiCodingModels) {
13
21
  api.registerModel({ ...model, provider: providerId });
14
- if (options.includeMoonshotModels)
15
- for (const model of options.moonshotModels ?? moonshotKimiModels)
16
- api.registerModel(model);
22
+ }
17
23
  api.registerAuthMethod({ kind: "api_key", provider: providerId, credentialName: "apiKey" });
24
+ if (options.includeMoonshotModels) {
25
+ api.registerProvider(createMoonshotProvider({
26
+ id: moonshotId,
27
+ apiKey: options.moonshotApiKey ?? options.kimiApiKey,
28
+ fetch: options.fetch,
29
+ baseUrl: options.moonshotBaseUrl,
30
+ }));
31
+ for (const model of options.moonshotModels ?? moonshotKimiModels) {
32
+ api.registerModel({ ...model, provider: moonshotId });
33
+ }
34
+ api.registerAuthMethod({ kind: "api_key", provider: moonshotId, credentialName: "apiKey" });
35
+ }
18
36
  },
19
37
  });
20
38
  }
21
- export { defineKimiModel, kimiCodingModels, moonshotKimiModels } from "./models.js";
22
- export { createKimiCodingProvider, kimiAnthropicBody, kimiAnthropicEvents } from "./provider.js";
39
+ export { defineKimiModel, kimiCodingModels, listKimiModels, mapKimiModel, moonshotKimiModels, } from "./models.js";
40
+ export { createKimiCodingProvider, kimiAnthropicBody, kimiAnthropicEvents, } from "./provider.js";
41
+ export { createMoonshotProvider, moonshotBody, moonshotEvents, serializeMoonshotMessage, } from "./moonshot.js";
42
+ export { kimiPreserveThinking, kimiReasoningEffort, kimiThinking, stripKimiThinkingCompat, } from "./thinking.js";
43
+ export { applyKimiAnthropicCacheControl, kimiAnthropicCacheEnabled, } from "./cache.js";
23
44
  //# sourceMappingURL=index.js.map
package/dist/models.d.ts CHANGED
@@ -1,11 +1,65 @@
1
- import type { JsonObject, ModelConfig } from "@arnilo/prism";
1
+ import { type CredentialValueSource, type JsonObject, type ModelConfig } from "@arnilo/prism";
2
2
  export interface KimiModelConfig extends Omit<ModelConfig, "provider" | "compat"> {
3
3
  readonly provider?: "kimi-coding" | "moonshot";
4
4
  readonly compat?: JsonObject & {
5
5
  readonly route?: "anthropic" | "openai";
6
6
  readonly preserveThinking?: boolean;
7
+ /** Official K2.x `thinking` object (`type`, optional `keep`). */
8
+ readonly thinking?: boolean | JsonObject;
9
+ /** Official K3 `reasoning_effort` (`"max"` on Open Platform; Coding `k3` also `low`/`high`). */
10
+ readonly reasoning_effort?: string;
7
11
  };
8
12
  }
13
+ export interface ListKimiModelsOptions {
14
+ readonly apiKey?: CredentialValueSource;
15
+ readonly fetch?: typeof fetch;
16
+ /** Defaults to Open Platform `https://api.moonshot.ai/v1` (also supports `api.moonshot.cn/v1`). */
17
+ readonly baseUrl?: string;
18
+ readonly signal?: AbortSignal;
19
+ readonly headers?: Readonly<Record<string, string>>;
20
+ /** Defaults to `"moonshot"`. */
21
+ readonly provider?: "moonshot" | string;
22
+ }
23
+ /**
24
+ * Official Moonshot / Kimi Open Platform `GET /v1/models` entry.
25
+ * @see https://platform.kimi.ai/docs/api/list-models
26
+ */
27
+ export interface KimiModelEntry {
28
+ readonly id: string;
29
+ readonly object?: string;
30
+ readonly created?: number;
31
+ readonly owned_by?: string;
32
+ readonly context_length?: number;
33
+ readonly supports_image_in?: boolean;
34
+ readonly supports_video_in?: boolean;
35
+ readonly supports_reasoning?: boolean;
36
+ }
9
37
  export declare function defineKimiModel(config: KimiModelConfig): ModelConfig;
10
- export declare const kimiCodingModels: readonly [ModelConfig];
11
- export declare const moonshotKimiModels: readonly [ModelConfig];
38
+ /**
39
+ * Caller-gated Moonshot Open Platform model discovery via official `GET /v1/models`.
40
+ * Never invoked by `createKimiProviderPackage` — hosts call this and pass results via
41
+ * `moonshotModels:` / `models:` (or register themselves).
42
+ * Kimi For Coding (`api.kimi.com/coding`) has no public list API; use featured
43
+ * `kimiCodingModels` (official Coding ids) as offline bootstrap.
44
+ * @see https://platform.kimi.ai/docs/api/list-models
45
+ */
46
+ export declare function listKimiModels(options?: ListKimiModelsOptions): Promise<ModelConfig[]>;
47
+ /**
48
+ * Map an official Moonshot `/v1/models` entry to Prism `ModelConfig`.
49
+ * Open Platform models use Chat Completions (`compat.route: "openai"`).
50
+ */
51
+ export declare function mapKimiModel(entry: KimiModelEntry, options?: {
52
+ readonly provider?: string;
53
+ }): ModelConfig;
54
+ /**
55
+ * Featured Kimi For Coding offline bootstrap aliases — official Coding model ids
56
+ * from https://www.kimi.com/code/docs/en/kimi-code/models (not Pi's `k2p7` alias).
57
+ * Refresh Open Platform catalogs via `listKimiModels()`.
58
+ */
59
+ export declare const kimiCodingModels: readonly [ModelConfig, ModelConfig, ModelConfig];
60
+ /**
61
+ * Featured Moonshot Open Platform offline bootstrap aliases — official Open Platform
62
+ * ids from https://platform.kimi.ai/docs/models. Callable via `createMoonshotProvider`
63
+ * when `includeMoonshotModels: true`. Refresh via `listKimiModels()`.
64
+ */
65
+ export declare const moonshotKimiModels: readonly [ModelConfig, ModelConfig];
package/dist/models.js CHANGED
@@ -1,23 +1,189 @@
1
+ import { redactSecrets, resolveCredentialValue, } from "@arnilo/prism";
2
+ import { readBoundedResponseText } from "@arnilo/prism/providers/transport";
1
3
  export function defineKimiModel(config) {
2
- return { ...config, provider: config.provider ?? "kimi-coding", capabilities: { input: ["text"], output: ["text"], reasoning: true, tools: true, streaming: true, ...config.capabilities } };
4
+ return {
5
+ ...config,
6
+ provider: config.provider ?? "kimi-coding",
7
+ capabilities: {
8
+ input: ["text"],
9
+ output: ["text"],
10
+ reasoning: true,
11
+ tools: true,
12
+ streaming: true,
13
+ ...config.capabilities,
14
+ },
15
+ };
3
16
  }
17
+ /**
18
+ * Caller-gated Moonshot Open Platform model discovery via official `GET /v1/models`.
19
+ * Never invoked by `createKimiProviderPackage` — hosts call this and pass results via
20
+ * `moonshotModels:` / `models:` (or register themselves).
21
+ * Kimi For Coding (`api.kimi.com/coding`) has no public list API; use featured
22
+ * `kimiCodingModels` (official Coding ids) as offline bootstrap.
23
+ * @see https://platform.kimi.ai/docs/api/list-models
24
+ */
25
+ export async function listKimiModels(options = {}) {
26
+ const provider = options.provider ?? "moonshot";
27
+ const baseUrl = (options.baseUrl ?? "https://api.moonshot.ai/v1").replace(/\/$/, "");
28
+ const token = await resolveCredentialValue(options.apiKey, { provider, name: "apiKey" });
29
+ const response = await (options.fetch ?? fetch)(`${baseUrl}/models`, {
30
+ method: "GET",
31
+ headers: { ...options.headers, ...(token ? { authorization: `Bearer ${token}` } : {}) },
32
+ signal: options.signal,
33
+ });
34
+ if (!response.ok) {
35
+ const body = await readBoundedResponseText(response, { secrets: [token] });
36
+ throw new Error(`Kimi model discovery failed: ${response.status} ${redactSecrets(body, [token])}`);
37
+ }
38
+ const payload = (await response.json());
39
+ if (!Array.isArray(payload.data))
40
+ throw new Error("Kimi model discovery response missing data array");
41
+ return payload.data.map((entry) => mapKimiModel(entry, { provider }));
42
+ }
43
+ /**
44
+ * Map an official Moonshot `/v1/models` entry to Prism `ModelConfig`.
45
+ * Open Platform models use Chat Completions (`compat.route: "openai"`).
46
+ */
47
+ export function mapKimiModel(entry, options = {}) {
48
+ if (!entry || typeof entry.id !== "string" || entry.id.length === 0) {
49
+ throw new Error("Kimi model entry missing id");
50
+ }
51
+ const id = entry.id;
52
+ const reasoning = entry.supports_reasoning === true || looksLikeReasoningModel(id);
53
+ const input = entry.supports_image_in ? ["text", "image"] : ["text"];
54
+ return defineKimiModel({
55
+ provider: options.provider ?? "moonshot",
56
+ model: id,
57
+ displayName: id,
58
+ capabilities: {
59
+ input,
60
+ output: ["text"],
61
+ reasoning,
62
+ tools: true,
63
+ streaming: true,
64
+ },
65
+ limits: cleanLimits({
66
+ contextWindow: typeof entry.context_length === "number" ? entry.context_length : undefined,
67
+ }),
68
+ compat: cleanJson({
69
+ route: "openai",
70
+ preserveThinking: reasoning && shouldPreserveThinkingByDefault(id),
71
+ ...thinkingDefaultsForModel(id),
72
+ moonshot: cleanJson({
73
+ owned_by: entry.owned_by,
74
+ created: entry.created,
75
+ supports_video_in: entry.supports_video_in,
76
+ supports_reasoning: entry.supports_reasoning,
77
+ }),
78
+ }),
79
+ });
80
+ }
81
+ /**
82
+ * Featured Kimi For Coding offline bootstrap aliases — official Coding model ids
83
+ * from https://www.kimi.com/code/docs/en/kimi-code/models (not Pi's `k2p7` alias).
84
+ * Refresh Open Platform catalogs via `listKimiModels()`.
85
+ */
4
86
  export const kimiCodingModels = [
5
87
  defineKimiModel({
6
88
  provider: "kimi-coding",
7
- model: "kimi-k2.7-code",
8
- displayName: "Kimi K2.7 Code",
89
+ model: "kimi-for-coding",
90
+ displayName: "Kimi For Coding (K2.7 Code)",
9
91
  capabilities: { input: ["text", "document", "file"] },
10
92
  limits: { contextWindow: 256_000, maxOutputTokens: 64_000 },
11
- compat: { route: "anthropic", preserveThinking: true },
93
+ compat: {
94
+ route: "anthropic",
95
+ // Official: thinking always on — omit `thinking` on the wire unless the host sets it.
96
+ preserveThinking: true,
97
+ },
98
+ }),
99
+ defineKimiModel({
100
+ provider: "kimi-coding",
101
+ model: "kimi-for-coding-highspeed",
102
+ displayName: "Kimi For Coding Highspeed",
103
+ capabilities: { input: ["text", "document", "file"] },
104
+ limits: { contextWindow: 256_000, maxOutputTokens: 64_000 },
105
+ compat: {
106
+ route: "anthropic",
107
+ preserveThinking: true,
108
+ },
109
+ }),
110
+ defineKimiModel({
111
+ provider: "kimi-coding",
112
+ model: "k3",
113
+ displayName: "Kimi K3 (Coding)",
114
+ capabilities: { input: ["text", "image", "document", "file"] },
115
+ limits: { contextWindow: 1_048_576, maxOutputTokens: 64_000 },
116
+ compat: {
117
+ route: "anthropic",
118
+ preserveThinking: true,
119
+ // Coding docs: low / high / max (default max).
120
+ reasoning_effort: "max",
121
+ },
12
122
  }),
13
123
  ];
124
+ /**
125
+ * Featured Moonshot Open Platform offline bootstrap aliases — official Open Platform
126
+ * ids from https://platform.kimi.ai/docs/models. Callable via `createMoonshotProvider`
127
+ * when `includeMoonshotModels: true`. Refresh via `listKimiModels()`.
128
+ */
14
129
  export const moonshotKimiModels = [
15
130
  defineKimiModel({
16
131
  provider: "moonshot",
17
- model: "kimi-k2.7-code-preview",
18
- displayName: "Kimi K2.7 Code Preview",
132
+ model: "kimi-k2.7-code",
133
+ displayName: "Kimi K2.7 Code",
134
+ capabilities: { input: ["text"] },
19
135
  limits: { contextWindow: 256_000, maxOutputTokens: 64_000 },
20
- compat: { route: "openai", preserveThinking: true },
136
+ compat: {
137
+ route: "openai",
138
+ // Official: omit `thinking` for K2.7-code; Preserved Thinking still required on replay.
139
+ preserveThinking: true,
140
+ },
141
+ }),
142
+ defineKimiModel({
143
+ provider: "moonshot",
144
+ model: "kimi-k3",
145
+ displayName: "Kimi K3",
146
+ capabilities: { input: ["text", "image"] },
147
+ limits: { contextWindow: 1_048_576, maxOutputTokens: 64_000 },
148
+ compat: {
149
+ route: "openai",
150
+ preserveThinking: true,
151
+ // Open Platform currently documents only `"max"`.
152
+ reasoning_effort: "max",
153
+ },
21
154
  }),
22
155
  ];
156
+ function looksLikeReasoningModel(modelId) {
157
+ const id = modelId.toLowerCase();
158
+ return (id.includes("kimi-k3")
159
+ || id === "k3"
160
+ || id.includes("k2.7")
161
+ || id.includes("k2.6")
162
+ || id.includes("k2.5")
163
+ || id.includes("thinking")
164
+ || id.includes("for-coding"));
165
+ }
166
+ function shouldPreserveThinkingByDefault(modelId) {
167
+ const id = modelId.toLowerCase();
168
+ // Official: K2.7-code / Coding always preserve; K3 requires historical reasoning_content.
169
+ return id.includes("k2.7") || id.includes("for-coding") || id.includes("kimi-k3") || id === "k3";
170
+ }
171
+ function thinkingDefaultsForModel(modelId) {
172
+ const id = modelId.toLowerCase();
173
+ if (id.includes("kimi-k3") || id === "k3") {
174
+ return { reasoning_effort: "max" };
175
+ }
176
+ // Official: K2.7-code thinking is always on — omit the parameter unless the host sets it.
177
+ if (id.includes("k2.6") || id.includes("k2.5")) {
178
+ return { thinking: { type: "enabled" } };
179
+ }
180
+ return {};
181
+ }
182
+ function cleanLimits(value) {
183
+ const entries = Object.entries(value).filter(([, item]) => item !== undefined);
184
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
185
+ }
186
+ function cleanJson(value) {
187
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
188
+ }
23
189
  //# sourceMappingURL=models.js.map
@@ -0,0 +1,23 @@
1
+ import type { AIProvider, CredentialValueSource, JsonObject, Message, ModelCapabilities, ProviderEvent, ProviderRequest } from "@arnilo/prism";
2
+ export interface MoonshotProviderOptions {
3
+ readonly id?: string;
4
+ /** Defaults to Open Platform `https://api.moonshot.ai/v1`. */
5
+ readonly baseUrl?: string;
6
+ readonly apiKey?: CredentialValueSource;
7
+ readonly fetch?: typeof fetch;
8
+ }
9
+ /**
10
+ * Moonshot / Kimi Open Platform Chat Completions provider (`POST /chat/completions`).
11
+ * Official base: `https://api.moonshot.ai/v1` (or `api.moonshot.cn/v1`).
12
+ * Distinct from Kimi For Coding Anthropic `/messages` (`createKimiCodingProvider`).
13
+ * @see https://platform.kimi.ai/docs/api/overview
14
+ */
15
+ export declare function createMoonshotProvider(options?: MoonshotProviderOptions): AIProvider;
16
+ export declare function moonshotBody(request: ProviderRequest): JsonObject;
17
+ export declare function moonshotEvents(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncIterable<ProviderEvent>;
18
+ /**
19
+ * Open Platform message serialization. When `preserveThinking`, historical thinking
20
+ * blocks become top-level `reasoning_content` (official Preserved Thinking contract).
21
+ * Anthropic `cache_control` is never emitted on this route.
22
+ */
23
+ export declare function serializeMoonshotMessage(message: Message, capabilities?: ModelCapabilities, preserveThinking?: boolean): JsonObject;
@@ -0,0 +1,175 @@
1
+ import { assertStructuredOutputRequestSupported, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, resolveCredentialValue, toolCallContent, } from "@arnilo/prism";
2
+ import { applyOpenAIChatStructuredOutput, mapOpenAIChatUsage, serializeOpenAITool, } from "@arnilo/prism/providers/openai";
3
+ import { parseJsonObjectArguments, readBoundedResponseText, readSseData } from "@arnilo/prism/providers/transport";
4
+ import { kimiPreserveThinking, kimiReasoningEffort, kimiThinking, stripKimiThinkingCompat, } from "./thinking.js";
5
+ /**
6
+ * Moonshot / Kimi Open Platform Chat Completions provider (`POST /chat/completions`).
7
+ * Official base: `https://api.moonshot.ai/v1` (or `api.moonshot.cn/v1`).
8
+ * Distinct from Kimi For Coding Anthropic `/messages` (`createKimiCodingProvider`).
9
+ * @see https://platform.kimi.ai/docs/api/overview
10
+ */
11
+ export function createMoonshotProvider(options = {}) {
12
+ const id = options.id ?? "moonshot";
13
+ const baseUrl = (options.baseUrl ?? "https://api.moonshot.ai/v1").replace(/\/$/, "");
14
+ return {
15
+ id,
16
+ async *generate(request) {
17
+ if (request.signal?.aborted)
18
+ throw request.signal.reason ?? new Error("aborted");
19
+ let token;
20
+ const secrets = [];
21
+ try {
22
+ const body = moonshotBody(request);
23
+ token = await resolveCredentialValue(options.apiKey, { provider: id, name: "apiKey" });
24
+ secrets.push(token);
25
+ const response = await (options.fetch ?? fetch)(`${baseUrl}/chat/completions`, {
26
+ method: "POST",
27
+ headers: {
28
+ ...request.options?.headers,
29
+ "content-type": "application/json",
30
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
31
+ },
32
+ body: JSON.stringify(body),
33
+ signal: request.signal,
34
+ });
35
+ if (!response.ok) {
36
+ return yield providerError(new Error(`Moonshot request failed: ${response.status} ${await readBoundedResponseText(response, { secrets })}`), secrets);
37
+ }
38
+ if (!response.body)
39
+ return yield providerError(new Error("Moonshot response had no body"), secrets);
40
+ yield* moonshotEvents(response.body, request.signal);
41
+ }
42
+ catch (error) {
43
+ yield providerError(error, secrets);
44
+ }
45
+ },
46
+ };
47
+ }
48
+ export function moonshotBody(request) {
49
+ assertStructuredOutputRequestSupported(request.model, request.options);
50
+ const preserveThinking = kimiPreserveThinking(request);
51
+ const { maxTokens, ...parameters } = request.model.parameters ?? {};
52
+ const body = {
53
+ model: request.model.model,
54
+ messages: request.messages.map((message) => serializeMoonshotMessage(message, request.model.capabilities ?? {}, preserveThinking)),
55
+ tools: request.tools?.map(serializeOpenAITool),
56
+ stream: true,
57
+ stream_options: { include_usage: true },
58
+ thinking: kimiThinking(request),
59
+ reasoning_effort: kimiReasoningEffort(request),
60
+ ...parameters,
61
+ max_tokens: maxTokens ?? request.model.limits?.maxOutputTokens,
62
+ ...stripKimiThinkingCompat(request.options?.compat),
63
+ ...request.options?.extra,
64
+ };
65
+ applyOpenAIChatStructuredOutput(body, request.options?.structuredOutput);
66
+ return clean(body);
67
+ }
68
+ export async function* moonshotEvents(body, signal) {
69
+ const tools = new Map();
70
+ let usage;
71
+ for await (const data of readSseData(body, { signal })) {
72
+ if (data === "[DONE]")
73
+ break;
74
+ const chunk = JSON.parse(data);
75
+ usage = mapOpenAIChatUsage(chunk.usage) ?? usage;
76
+ if (chunk.usage) {
77
+ const mapped = mapOpenAIChatUsage(chunk.usage);
78
+ if (mapped)
79
+ yield providerUsage(mapped);
80
+ }
81
+ for (const choice of chunk.choices ?? []) {
82
+ const delta = choice.delta ?? {};
83
+ if (delta.content)
84
+ yield providerTextDelta(delta.content);
85
+ if (delta.reasoning_content)
86
+ yield providerThinkingDelta(delta.reasoning_content);
87
+ for (const tool of delta.tool_calls ?? []) {
88
+ const index = tool.index ?? 0;
89
+ const current = tools.get(index) ?? { argumentsText: "" };
90
+ current.id = tool.id ?? current.id;
91
+ current.name = tool.function?.name ?? current.name;
92
+ current.argumentsText += tool.function?.arguments ?? "";
93
+ tools.set(index, current);
94
+ yield providerToolCallDelta({
95
+ index,
96
+ id: tool.id,
97
+ name: tool.function?.name,
98
+ argumentsText: tool.function?.arguments,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ for (const call of tools.values()) {
104
+ if (call.id && call.name) {
105
+ yield providerToolCall(toolCallContent(call.id, call.name, parseJsonObjectArguments(call.argumentsText, { toolName: call.name })));
106
+ }
107
+ }
108
+ yield providerDone(usage);
109
+ }
110
+ /**
111
+ * Open Platform message serialization. When `preserveThinking`, historical thinking
112
+ * blocks become top-level `reasoning_content` (official Preserved Thinking contract).
113
+ * Anthropic `cache_control` is never emitted on this route.
114
+ */
115
+ export function serializeMoonshotMessage(message, capabilities = {}, preserveThinking = false) {
116
+ if (message.role === "tool") {
117
+ const result = message.content.find((part) => part.type === "tool_result");
118
+ return {
119
+ role: "tool",
120
+ tool_call_id: result?.toolCallId ?? "",
121
+ content: result ? JSON.stringify(result.result ?? result.error ?? null) : "",
122
+ };
123
+ }
124
+ if (message.role === "assistant") {
125
+ const toolCalls = message.content.filter((part) => part.type === "tool_call");
126
+ const textParts = message.content.filter((part) => part.type === "text");
127
+ const thinkingParts = message.content.filter((part) => part.type === "thinking");
128
+ const text = textParts.map((part) => part.text).join("\n");
129
+ const reasoning = thinkingParts.map((part) => part.text).join("\n");
130
+ const base = {
131
+ role: "assistant",
132
+ content: text || (toolCalls.length > 0 ? null : ""),
133
+ };
134
+ if (preserveThinking && reasoning)
135
+ base.reasoning_content = reasoning;
136
+ if (toolCalls.length > 0) {
137
+ base.tool_calls = toolCalls.map((call) => ({
138
+ id: call.id,
139
+ type: "function",
140
+ function: { name: call.name, arguments: JSON.stringify(call.arguments) },
141
+ }));
142
+ }
143
+ return base;
144
+ }
145
+ // user / system — fold thinking into text (should not normally appear)
146
+ const content = [];
147
+ for (const part of message.content) {
148
+ if (part.type === "text" || part.type === "thinking") {
149
+ content.push({ type: "text", text: part.text });
150
+ }
151
+ else if (part.type === "image") {
152
+ if (!capabilities.input?.includes("image")) {
153
+ throw new Error(`Moonshot ${message.role} message includes image but model does not declare image input capability`);
154
+ }
155
+ const url = part.url ?? (part.data ? `data:${part.mimeType ?? "image/png"};base64,${part.data}` : undefined);
156
+ if (!url)
157
+ throw new Error("Moonshot image block missing url or data");
158
+ content.push({ type: "image_url", image_url: { url } });
159
+ }
160
+ else if (part.type === "audio" || part.type === "file" || part.type === "document") {
161
+ throw new Error(`Moonshot Chat Completions does not support ${part.type} content blocks`);
162
+ }
163
+ else if (part.type === "tool_call" || part.type === "tool_result") {
164
+ throw new Error(`Moonshot ${part.type} blocks must use assistant/tool roles`);
165
+ }
166
+ }
167
+ if (content.length === 1 && content[0].type === "text") {
168
+ return { role: message.role, content: content[0].text };
169
+ }
170
+ return { role: message.role, content };
171
+ }
172
+ function clean(value) {
173
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && !(Array.isArray(item) && item.length === 0)));
174
+ }
175
+ //# sourceMappingURL=moonshot.js.map
package/dist/provider.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { assertStructuredOutputRequestSupported, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, resolveCredentialValue, toolCallContent } from "@arnilo/prism";
2
- import { assertProviderMediaCapability, bytesToBase64, isPdfMediaType, rejectProviderMediaBlock, resolveProviderMediaBlock, serializePdfDocumentWireBlock, } from "@arnilo/prism/providers/media";
2
+ import { bytesToBase64, isPdfMediaType, rejectProviderMediaBlock, resolveProviderMediaMessages, serializePdfDocumentWireBlock, } from "@arnilo/prism/providers/media";
3
3
  import { parseJsonObjectArguments, readBoundedResponseText, readSseData } from "@arnilo/prism/providers/transport";
4
4
  import { applyKimiAnthropicCacheControl } from "./cache.js";
5
+ import { kimiPreserveThinking, kimiReasoningEffort, kimiThinking, stripKimiThinkingCompat, } from "./thinking.js";
5
6
  export function createKimiCodingProvider(options = {}) {
6
7
  const id = options.id ?? "kimi-coding";
7
8
  const baseUrl = (options.baseUrl ?? "https://api.kimi.com/coding").replace(/\/$/, "");
@@ -10,9 +11,12 @@ export function createKimiCodingProvider(options = {}) {
10
11
  async *generate(request) {
11
12
  if (request.signal?.aborted)
12
13
  throw request.signal.reason ?? new Error("aborted");
13
- const token = await resolveCredentialValue(options.apiKey, { provider: id, name: "apiKey" });
14
- const secrets = [token];
14
+ let token;
15
+ const secrets = [];
15
16
  try {
17
+ const body = await kimiAnthropicBody(request);
18
+ token = await resolveCredentialValue(options.apiKey, { provider: id, name: "apiKey" });
19
+ secrets.push(token);
16
20
  const response = await (options.fetch ?? fetch)(`${baseUrl}/messages`, {
17
21
  method: "POST",
18
22
  headers: {
@@ -21,7 +25,7 @@ export function createKimiCodingProvider(options = {}) {
21
25
  "user-agent": options.userAgent ?? "KimiCLI/1.5",
22
26
  ...(token ? { authorization: `Bearer ${token}` } : {}),
23
27
  },
24
- body: JSON.stringify(await kimiAnthropicBody(request)),
28
+ body: JSON.stringify(body),
25
29
  signal: request.signal,
26
30
  });
27
31
  if (!response.ok) {
@@ -39,18 +43,23 @@ export function createKimiCodingProvider(options = {}) {
39
43
  }
40
44
  export async function kimiAnthropicBody(request) {
41
45
  assertStructuredOutputRequestSupported(request.model, request.options);
42
- const preserveThinking = request.model.compat?.preserveThinking === true;
46
+ const preserveThinking = kimiPreserveThinking(request);
43
47
  const { maxTokens, ...parameters } = request.model.parameters ?? {};
44
48
  const messages = applyKimiAnthropicCacheControl(request);
49
+ const resolvedMedia = await resolveProviderMediaMessages(messages, request.model, { signal: request.signal });
50
+ // Official thinking/reasoning_effort fields (K2.x / K3) when documented for the model;
51
+ // Anthropic `/messages` contract remains under-documented — fields are best-effort passthrough.
45
52
  return clean({
46
53
  model: request.model.model,
47
- messages: await Promise.all(messages.filter((m) => m.role !== "system").map((message) => toMessage(message, request.model, preserveThinking, request.signal))),
54
+ messages: await Promise.all(messages.filter((m) => m.role !== "system").map((message) => toMessage(message, request.model, preserveThinking, resolvedMedia))),
48
55
  system: messages.filter((m) => m.role === "system").map((m) => text(m, preserveThinking)).join("\n\n") || undefined,
49
56
  tools: request.tools?.map(toTool),
50
57
  stream: true,
58
+ thinking: kimiThinking(request),
59
+ reasoning_effort: kimiReasoningEffort(request),
51
60
  ...parameters,
52
61
  max_tokens: maxTokens ?? request.model.limits?.maxOutputTokens ?? 4096,
53
- ...request.options?.compat,
62
+ ...stripKimiThinkingCompat(request.options?.compat),
54
63
  ...request.options?.extra,
55
64
  });
56
65
  }
@@ -88,7 +97,7 @@ export async function* kimiAnthropicEvents(body, signal) {
88
97
  }
89
98
  yield providerDone(usage);
90
99
  }
91
- async function toMessage(message, model, preserveThinking = false, signal) {
100
+ async function toMessage(message, model, preserveThinking, resolvedMedia) {
92
101
  const capabilities = model.capabilities ?? {};
93
102
  if (message.role === "tool") {
94
103
  const result = message.content.find((part) => part.type === "tool_result");
@@ -118,17 +127,15 @@ async function toMessage(message, model, preserveThinking = false, signal) {
118
127
  }
119
128
  }
120
129
  else if (part.type === "image") {
121
- assertProviderMediaCapability("image", capabilities, model);
122
- const source = part.url
123
- ? { type: "url", url: part.url }
124
- : { type: "base64", media_type: part.mimeType ?? "image/png", data: part.data ?? "" };
130
+ const resolved = resolvedMedia.get(part);
131
+ const source = { type: "base64", media_type: resolved.mediaType, data: bytesToBase64(resolved.bytes) };
125
132
  content.push(withMarker({ type: "image", source }, marker));
126
133
  }
127
134
  else if (part.type === "document") {
128
- content.push(withMarker(await toAnthropicDocument(part, model, signal), marker));
135
+ content.push(withMarker(toAnthropicDocument(part, resolvedMedia), marker));
129
136
  }
130
137
  else if (part.type === "file") {
131
- content.push(withMarker(await toAnthropicFile(part, model, signal), marker));
138
+ content.push(withMarker(toAnthropicFile(part, resolvedMedia), marker));
132
139
  }
133
140
  else if (part.type === "audio") {
134
141
  rejectProviderMediaBlock(part, capabilities, model);
@@ -142,18 +149,16 @@ async function toMessage(message, model, preserveThinking = false, signal) {
142
149
  }
143
150
  return { role: message.role === "assistant" ? "assistant" : "user", content: content.length > 0 ? content : [{ type: "text", text: "" }] };
144
151
  }
145
- async function toAnthropicDocument(part, model, signal) {
146
- assertProviderMediaCapability("document", model.capabilities ?? {}, model);
147
- const resolved = await resolveProviderMediaBlock(part, { signal });
152
+ function toAnthropicDocument(part, resolvedMedia) {
153
+ const resolved = resolvedMedia.get(part);
148
154
  return serializePdfDocumentWireBlock({
149
155
  mediaType: resolved.mediaType,
150
156
  data: bytesToBase64(resolved.bytes),
151
157
  title: resolved.name,
152
158
  });
153
159
  }
154
- async function toAnthropicFile(part, model, signal) {
155
- assertProviderMediaCapability("file", model.capabilities ?? {}, model);
156
- const resolved = await resolveProviderMediaBlock(part, { signal });
160
+ function toAnthropicFile(part, resolvedMedia) {
161
+ const resolved = resolvedMedia.get(part);
157
162
  if (!isPdfMediaType(resolved.mediaType)) {
158
163
  throw new Error(`Kimi Anthropic route only maps PDF file blocks; got ${resolved.mediaType}`);
159
164
  }
@@ -0,0 +1,22 @@
1
+ import type { JsonObject, ProviderRequest } from "@arnilo/prism";
2
+ /**
3
+ * Official K2.x Chat Completions / Anthropic-compat `thinking` object.
4
+ * Request `options.compat.thinking` wins over `model.compat.thinking`.
5
+ * @see https://platform.kimi.ai/docs/guide/use-kimi-k2-thinking-model
6
+ */
7
+ export declare function kimiThinking(request: ProviderRequest): JsonObject | undefined;
8
+ /**
9
+ * Official K3 top-level `reasoning_effort` (Open Platform currently documents `"max"`;
10
+ * Kimi Code additionally maps `low` / `high` / `max` for model id `k3`).
11
+ * Request wins over model default.
12
+ * @see https://platform.kimi.ai/docs/guide/use-thinking-effort
13
+ */
14
+ export declare function kimiReasoningEffort(request: ProviderRequest): string | undefined;
15
+ /**
16
+ * Whether to replay historical thinking blocks (Anthropic `thinking` content or
17
+ * Open Platform `reasoning_content`). K2.7-code / Coding models always preserve.
18
+ * Request `compat.preserveThinking` wins over model default.
19
+ */
20
+ export declare function kimiPreserveThinking(request: ProviderRequest): boolean;
21
+ /** Strip thinking-owned keys so opaque compat spread cannot invert explicit resolvers. */
22
+ export declare function stripKimiThinkingCompat(compat: JsonObject | undefined): JsonObject;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Official K2.x Chat Completions / Anthropic-compat `thinking` object.
3
+ * Request `options.compat.thinking` wins over `model.compat.thinking`.
4
+ * @see https://platform.kimi.ai/docs/guide/use-kimi-k2-thinking-model
5
+ */
6
+ export function kimiThinking(request) {
7
+ const value = request.options?.compat?.thinking ?? request.model.compat?.thinking;
8
+ if (value === false)
9
+ return { type: "disabled" };
10
+ if (value && typeof value === "object")
11
+ return value;
12
+ return value === true ? { type: "enabled" } : undefined;
13
+ }
14
+ /**
15
+ * Official K3 top-level `reasoning_effort` (Open Platform currently documents `"max"`;
16
+ * Kimi Code additionally maps `low` / `high` / `max` for model id `k3`).
17
+ * Request wins over model default.
18
+ * @see https://platform.kimi.ai/docs/guide/use-thinking-effort
19
+ */
20
+ export function kimiReasoningEffort(request) {
21
+ const effort = request.options?.compat?.reasoning_effort
22
+ ?? request.options?.compat?.reasoningEffort
23
+ ?? request.model.compat?.reasoning_effort;
24
+ return typeof effort === "string" ? effort : undefined;
25
+ }
26
+ /**
27
+ * Whether to replay historical thinking blocks (Anthropic `thinking` content or
28
+ * Open Platform `reasoning_content`). K2.7-code / Coding models always preserve.
29
+ * Request `compat.preserveThinking` wins over model default.
30
+ */
31
+ export function kimiPreserveThinking(request) {
32
+ const value = request.options?.compat?.preserveThinking ?? request.model.compat?.preserveThinking;
33
+ return value === true;
34
+ }
35
+ /** Strip thinking-owned keys so opaque compat spread cannot invert explicit resolvers. */
36
+ export function stripKimiThinkingCompat(compat) {
37
+ if (!compat)
38
+ return {};
39
+ const { thinking: _thinking, reasoning_effort: _effort, reasoningEffort: _effortCamel, preserveThinking: _preserve, ...rest } = compat;
40
+ return rest;
41
+ }
42
+ //# sourceMappingURL=thinking.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-provider-kimi",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Kimi provider package for Prism.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "pack:dry-run": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {
28
- "@arnilo/prism": "0.0.4"
28
+ "@arnilo/prism": "0.0.6"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../.."