@volter/twin-moonshot 0.1.0

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.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +164 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +25 -0
  5. package/dist/src/index.d.ts +14 -0
  6. package/dist/src/index.js +86 -0
  7. package/dist/src/moonshot-budget.d.ts +57 -0
  8. package/dist/src/moonshot-budget.js +142 -0
  9. package/dist/src/moonshot-capabilities.d.ts +4 -0
  10. package/dist/src/moonshot-capabilities.js +1200 -0
  11. package/dist/src/moonshot-conformance.d.ts +14 -0
  12. package/dist/src/moonshot-conformance.js +405 -0
  13. package/dist/src/moonshot-connector.d.ts +168 -0
  14. package/dist/src/moonshot-connector.js +416 -0
  15. package/dist/src/moonshot-models.d.ts +36 -0
  16. package/dist/src/moonshot-models.js +37 -0
  17. package/dist/src/moonshot-scenario.d.ts +54 -0
  18. package/dist/src/moonshot-scenario.js +175 -0
  19. package/dist/src/moonshot-server.d.ts +13 -0
  20. package/dist/src/moonshot-server.js +202 -0
  21. package/dist/src/moonshot-stub.d.ts +70 -0
  22. package/dist/src/moonshot-stub.js +222 -0
  23. package/dist/src/moonshot-twin.d.ts +144 -0
  24. package/dist/src/moonshot-twin.js +1647 -0
  25. package/dist/src/moonshot-types.d.ts +251 -0
  26. package/dist/src/moonshot-types.js +19 -0
  27. package/package.json +53 -0
  28. package/src/cli.ts +25 -0
  29. package/src/index.ts +129 -0
  30. package/src/moonshot-budget.ts +163 -0
  31. package/src/moonshot-capabilities.ts +1220 -0
  32. package/src/moonshot-conformance.ts +416 -0
  33. package/src/moonshot-connector.ts +465 -0
  34. package/src/moonshot-models.ts +89 -0
  35. package/src/moonshot-scenario.ts +194 -0
  36. package/src/moonshot-server.ts +220 -0
  37. package/src/moonshot-stub.ts +230 -0
  38. package/src/moonshot-twin.ts +1670 -0
  39. package/src/moonshot-types.ts +225 -0
@@ -0,0 +1,251 @@
1
+ /** A chat message param as the caller sends it (Message schema). Content is a string OR a
2
+ * content-part array (text / image_url / video_url — Moonshot's multimodal form). */
3
+ export type MoonshotMessageParam = {
4
+ role: 'system' | 'user' | 'assistant' | 'tool';
5
+ content?: string | Array<Record<string, unknown>> | null;
6
+ name?: string;
7
+ /** Partial Mode: set `partial: true` on the last ASSISTANT message to continue it. */
8
+ partial?: boolean;
9
+ tool_calls?: MoonshotToolCall[];
10
+ tool_call_id?: string;
11
+ /** kimi-k3's dynamic tool loading message: role 'system' with `tools` and NO content. */
12
+ tools?: unknown[];
13
+ };
14
+ /** A function tool_call (faithful shape). */
15
+ export type MoonshotToolCall = {
16
+ id: string;
17
+ type: 'function';
18
+ function: {
19
+ name: string;
20
+ arguments: string;
21
+ };
22
+ };
23
+ /** The chat `usage` object: token counts plus Moonshot's automatic context-cache split. */
24
+ export type MoonshotUsage = {
25
+ prompt_tokens: number;
26
+ completion_tokens: number;
27
+ total_tokens: number;
28
+ /** Tokens served from the automatic context cache (>256 prompt tokens to hit). */
29
+ cached_tokens?: number;
30
+ };
31
+ /** The assistant message Moonshot returns. `reasoning_content` is present when thinking mode
32
+ * is enabled (kimi-k3 always; kimi-k2.6/k2.7-code when `thinking.type` is 'enabled'). */
33
+ export type MoonshotAssistantMessage = {
34
+ role: 'assistant';
35
+ content: string | null;
36
+ tool_calls?: MoonshotToolCall[];
37
+ reasoning_content?: string | null;
38
+ };
39
+ export type MoonshotChoice = {
40
+ index: number;
41
+ message: MoonshotAssistantMessage;
42
+ logprobs?: unknown;
43
+ finish_reason: 'stop' | 'length' | 'tool_calls';
44
+ };
45
+ /** The unary chat.completion response envelope (faithful shape). */
46
+ export type MoonshotChatCompletion = {
47
+ id: string;
48
+ object: 'chat.completion';
49
+ created: number;
50
+ model: string;
51
+ choices: MoonshotChoice[];
52
+ usage: MoonshotUsage;
53
+ };
54
+ /** A streaming chat chunk (ChatCompletionChunk schema). */
55
+ export type MoonshotChatChunk = {
56
+ id: string;
57
+ object: 'chat.completion.chunk';
58
+ created: number;
59
+ model: string;
60
+ choices: Array<{
61
+ index: number;
62
+ delta: {
63
+ role?: string;
64
+ content?: string;
65
+ reasoning_content?: string;
66
+ tool_calls?: Array<{
67
+ index?: number;
68
+ id?: string;
69
+ type?: 'function';
70
+ function?: {
71
+ name?: string;
72
+ arguments?: string;
73
+ };
74
+ }>;
75
+ };
76
+ finish_reason: 'stop' | 'length' | 'tool_calls' | null;
77
+ usage: MoonshotUsage | null;
78
+ }>;
79
+ /** The FINAL chunk carries the whole usage object (stream_options.include_usage / Moonshot's
80
+ * documented final-chunk usage); ordinary chunks carry null. */
81
+ usage?: MoonshotUsage | null;
82
+ };
83
+ /** The served model object (OpenAI-shaped; Moonshot's /v1/models rows). */
84
+ export type MoonshotModelObject = {
85
+ id: string;
86
+ object: 'model';
87
+ created: number;
88
+ owned_by: string;
89
+ };
90
+ /** File purposes — a CLOSED documented set (FileObject schema). */
91
+ export declare const FILE_PURPOSES: readonly ["file-extract", "image", "video", "batch"];
92
+ export type MoonshotFilePurpose = (typeof FILE_PURPOSES)[number];
93
+ export type MoonshotFile = {
94
+ id: string;
95
+ object: 'file';
96
+ bytes: number;
97
+ created_at: number;
98
+ filename: string;
99
+ purpose: MoonshotFilePurpose;
100
+ status: string;
101
+ status_details?: string;
102
+ };
103
+ export type MoonshotBatchCounts = {
104
+ completed: number;
105
+ failed: number;
106
+ total: number;
107
+ };
108
+ export type MoonshotBatch = {
109
+ id: string;
110
+ object: 'batch';
111
+ endpoint: string;
112
+ input_file_id: string;
113
+ completion_window: string;
114
+ status: 'validating' | 'failed' | 'in_progress' | 'finalizing' | 'completed' | 'expired' | 'cancelling' | 'cancelled';
115
+ output_file_id: string | null;
116
+ error_file_id: string | null;
117
+ created_at: number;
118
+ in_progress_at: number | null;
119
+ expires_at: number | null;
120
+ finalizing_at: number | null;
121
+ completed_at: number | null;
122
+ failed_at: number | null;
123
+ cancelling_at: number | null;
124
+ cancelled_at: number | null;
125
+ request_counts: MoonshotBatchCounts;
126
+ metadata: Record<string, string> | null;
127
+ };
128
+ /**
129
+ * Moonshot's OpenAI-surface error envelope (ErrorResponse schema): `error.message` REQUIRED,
130
+ * `type` and `code` optional. The `type` values are Moonshot's documented error-code page
131
+ * (platform.kimi.ai/docs/api/errors, read 2026-09-16) — see moonshot-twin.ts for the mapping.
132
+ */
133
+ export type MoonshotError = {
134
+ error: {
135
+ message: string;
136
+ type?: string;
137
+ code?: string;
138
+ };
139
+ };
140
+ /** A Messages content block the RESPONSE emits, ordered thinking → text → tool_use. */
141
+ export type MoonshotMessagesBlock = {
142
+ type: 'thinking';
143
+ thinking: string;
144
+ signature?: string;
145
+ } | {
146
+ type: 'text';
147
+ text: string;
148
+ } | {
149
+ type: 'tool_use';
150
+ id: string;
151
+ name: string;
152
+ input: Record<string, unknown>;
153
+ };
154
+ export type MoonshotMessagesResponse = {
155
+ id: string;
156
+ type: 'message';
157
+ role: 'assistant';
158
+ model: string;
159
+ content: MoonshotMessagesBlock[];
160
+ stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | 'refusal' | null;
161
+ stop_sequence: string | null;
162
+ usage: {
163
+ input_tokens: number;
164
+ output_tokens: number;
165
+ cache_read_input_tokens?: number;
166
+ cache_creation_input_tokens?: number;
167
+ };
168
+ };
169
+ /** A Messages SSE event. `data` is the JSON payload; there is no [DONE] sentinel — the stream
170
+ * ends after `message_stop` (Anthropic's grammar, which this surface follows). */
171
+ export type MessagesSseEvent = {
172
+ event?: string;
173
+ data?: Record<string, unknown>;
174
+ done?: boolean;
175
+ };
176
+ /** An output item, ordered web_search_call → reasoning → message → tool calls. */
177
+ export type MoonshotResponsesOutputItem = {
178
+ type: 'reasoning';
179
+ id: string;
180
+ summary: Array<{
181
+ type: 'summary_text';
182
+ text: string;
183
+ }>;
184
+ status?: 'completed';
185
+ } | {
186
+ type: 'message';
187
+ id: string;
188
+ role: 'assistant';
189
+ status: 'completed';
190
+ content: Array<{
191
+ type: 'output_text';
192
+ text: string;
193
+ annotations: unknown[];
194
+ }>;
195
+ } | {
196
+ type: 'function_call';
197
+ id: string;
198
+ call_id: string;
199
+ name: string;
200
+ arguments: string;
201
+ status?: 'completed';
202
+ } | {
203
+ type: 'custom_tool_call';
204
+ id: string;
205
+ call_id: string;
206
+ name: string;
207
+ input: string;
208
+ } | {
209
+ type: 'web_search_call';
210
+ id: string;
211
+ status: 'completed';
212
+ };
213
+ export type MoonshotResponsesUsage = {
214
+ input_tokens: number;
215
+ input_tokens_details?: {
216
+ cached_tokens: number;
217
+ cache_write_tokens: number;
218
+ };
219
+ output_tokens: number;
220
+ output_tokens_details?: {
221
+ reasoning_tokens: number;
222
+ };
223
+ total_tokens: number;
224
+ };
225
+ export type MoonshotResponsesResponse = {
226
+ id: string;
227
+ object: 'response';
228
+ created_at: number;
229
+ completed_at: number | null;
230
+ status: 'in_progress' | 'completed' | 'incomplete' | 'failed';
231
+ model: string;
232
+ output: MoonshotResponsesOutputItem[];
233
+ usage: MoonshotResponsesUsage | null;
234
+ incomplete_details: {
235
+ reason: 'max_output_tokens' | 'content_filter';
236
+ } | null;
237
+ error: {
238
+ code: string;
239
+ message: string;
240
+ } | null;
241
+ store: false;
242
+ };
243
+ /** A single Server-Sent Event the OpenAI-compatible streaming paths emit. `data` is the JSON
244
+ * payload; `[DONE]` is signalled with `done: true` (no data object). */
245
+ export type SseEvent = {
246
+ data?: Record<string, unknown>;
247
+ done?: boolean;
248
+ };
249
+ /** A sink the streaming path writes events into (an injected collector in tests / a real
250
+ * HTTP SSE writer in the server). NO real sockets or setTimeout in the handler. */
251
+ export type SseSink = (event: SseEvent) => void;
@@ -0,0 +1,19 @@
1
+ // Shared wire-shape types for the Moonshot (Kimi) API surface, transcribed from Moonshot's own
2
+ // OpenAPI 3.1.0 document (https://platform.kimi.ai/docs/openapi.json, read 2026-09-16) — the
3
+ // first-party artifact, not an SDK's internal types (no official Moonshot SDK exists; clients
4
+ // are the standard `openai` / `anthropic` SDKs pointed at Moonshot's base URLs, so THIS document
5
+ // is the only generated contract).
6
+ //
7
+ // MOONSHOT IS NOT OPENAI, and the differences below are load-bearing:
8
+ // • THREE inference protocols share one host: OpenAI-compatible `/v1/chat/completions` +
9
+ // `/v1/responses`, and an Anthropic-compatible `/anthropic/v1/messages` whose envelope,
10
+ // content blocks and SSE event grammar are Anthropic's, not OpenAI's;
11
+ // • chat responses carry `reasoning_content` (thinking mode) — not Groq's `reasoning`, not
12
+ // OpenAI's `refusal`;
13
+ // • `usage` carries `cached_tokens` (Moonshot's automatic context caching);
14
+ // • the error envelope is `{ error: { message, type, code? } }` (ErrorResponse schema);
15
+ // • the Messages surface answers `{ type:'error', error:{type,message}, request_id? }`
16
+ // (MessagesErrorResponse schema) — a DIFFERENT envelope on the same host.
17
+ // ── Files ───────────────────────────────────────────────────────────────────────────────
18
+ /** File purposes — a CLOSED documented set (FileObject schema). */
19
+ export const FILE_PURPOSES = ['file-extract', 'image', 'video', 'batch'];
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@volter/twin-moonshot",
3
+ "version": "0.1.0",
4
+ "description": "Local Moonshot (Kimi) twin — a faithful, stateful local Moonshot Platform API your real `openai` / `anthropic` SDKs (swapped base URL) talk to unmodified. The model is stubbed (deterministic), but the protocol envelope across three inference protocols (OpenAI chat completions + Responses, Anthropic-compatible Messages, with streaming grammars, tool calls and cache-split usage), models, files, batches, balance, token counting and signature verify are vendor-faithful. Built on @volter/world-core.",
5
+ "author": "Volter (https://github.com/volter-ai)",
6
+ "license": "Apache-2.0",
7
+ "files": [
8
+ "src",
9
+ "README.md",
10
+ "LICENSE",
11
+ "!**/*.test.ts",
12
+ "!**/*.test.tsx",
13
+ "dist"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/volter-ai/twin.git",
18
+ "directory": "packages/twin/moonshot"
19
+ },
20
+ "homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/moonshot#readme",
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/src/index.d.ts",
25
+ "default": "./dist/src/index.js"
26
+ }
27
+ },
28
+ "bin": {
29
+ "world-moonshot": "dist/src/cli.js"
30
+ },
31
+ "scripts": {
32
+ "test": "bun test src/*.test.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "build": "node ../../../scripts/publish/build.mjs",
35
+ "prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
36
+ "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
37
+ },
38
+ "peerDependencies": {
39
+ "@volter/world-core": "2.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@anthropic-ai/sdk": "^0.69.0",
43
+ "@types/bun": "^1.2.20",
44
+ "@types/node": "^24.0.0",
45
+ "@volter/world-core": "2.0.0",
46
+ "@volter/world-tooling": "0.1.0",
47
+ "openai": "^6.16.0",
48
+ "typescript": "^5.9.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=22.3"
52
+ }
53
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { hasFlag, optionValue } from '@volter/world-core/args';
3
+ import { createMoonshotTwinServer } from './moonshot-server.ts';
4
+
5
+ const [cmd, ...rest] = process.argv.slice(2);
6
+
7
+ if (cmd === 'serve') {
8
+ const root = optionValue(rest, '--root') || process.env.VOLTER_STATE_DIR;
9
+ const port = Number(optionValue(rest, '--port') || 0) || undefined;
10
+ const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
11
+ const scenario = optionValue(rest, '--scenario') || undefined; // scripted completions (JSON file)
12
+ const server = await createMoonshotTwinServer({
13
+ ...(root ? { root } : {}), ...(port ? { port } : {}), readOnly, ...(scenario ? { scenarioPath: scenario } : {}),
14
+ });
15
+ process.stdout.write(`moonshot twin (Moonshot Platform API at /v1 and /anthropic/v1; model output is a deterministic stub)${readOnly ? ' [read-only]' : ''}${scenario ? ` [scenario: ${scenario}]` : ''} at http://127.0.0.1:${server.port}\n`);
16
+ } else if (cmd === 'conformance') {
17
+ // Lazy — the conformance module is DEV-ONLY and must not be reachable from the runtime
18
+ // entrypoints (recipe §3/§4, enforced by scripts/architecture.test.ts).
19
+ const { checkMoonshotConformance } = await import('./moonshot-conformance.ts');
20
+ const report = await checkMoonshotConformance({ root: process.env.VOLTER_STATE_DIR });
21
+ console.log(JSON.stringify(report, null, 2));
22
+ process.exit(report.ok ? 0 : 1);
23
+ } else {
24
+ console.log('usage: world-moonshot serve [--port N] [--root DIR] [--read-only] [--scenario FILE] | conformance');
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,129 @@
1
+ // @volter/twin-moonshot — the Moonshot (Kimi) twin (one vendor, one package), built on the shared
2
+ // @volter/world-core kernel. The Moonshot Platform API: a vendor-faithful protocol envelope on
3
+ // THREE inference protocols (OpenAI-compatible /v1/chat/completions + /v1/responses, and the
4
+ // Anthropic-compatible /anthropic/v1/messages), a static models catalog, STATEFUL files +
5
+ // batches + account balance, and the deterministic stateless helpers (token counting, signature
6
+ // verify, web-search tools) — over an event/action log.
7
+ //
8
+ // THE DETERMINISTIC STUB IS THE ANSWER: the twin runs no model, so the three inference endpoints
9
+ // return a DETERMINISTIC STUB completion (clearly labeled), never pretending to be real
10
+ // inference. Everything around them — the wire protocol, the rejections, the stateful objects —
11
+ // is faithful.
12
+ //
13
+ // MOONSHOT SHIPS NO SDK OF ITS OWN: its documented integration path is the standard `openai` /
14
+ // `anthropic` SDKs with a swapped base URL (platform.kimi.ai/docs/overview, read 2026-09-16), so
15
+ // the pack claims no vendor SDK in `adoption.sdks` and the fidelity test drives the real
16
+ // `openai` SDK at the twin. (Conformance + capability tooling live in @volter/world-tooling, a
17
+ // dev dependency — NOT re-exported here, per E2.)
18
+ export { handleMoonshotTwinRequest, streamChat, streamMessages, streamResponses, buildChatCompletion, buildMessagesResponse, buildResponsesResponse, MOONSHOT_API_PREFIX, MESSAGES_PREFIX, messagesSignature } from './moonshot-twin.ts';
19
+ export type { MoonshotRequest, MoonshotResponseEnvelope, MessagesSseSink } from './moonshot-twin.ts';
20
+ export { createMoonshotTwinFetch, createMoonshotTwinServer, type MoonshotTwinFetchOptions } from './moonshot-server.ts';
21
+ export { MOONSHOT_MODELS, findModel, BATCH_MODELS, REASONING_EFFORTS, K26_THINKING_TYPES, K27_THINKING_TYPES } from './moonshot-models.ts';
22
+ export {
23
+ buildChatUsage, contentToText, countPromptTokens, estimateTokens, fnv1a, lastUserText,
24
+ stubAssistantText, stubCachedTokens, stubFetchedMarkdown, stubJsonObject, stubReasoningContent,
25
+ stubSearchResults, stubSignature, stubToolArguments, stubToolCall,
26
+ } from './moonshot-stub.ts';
27
+ export type {
28
+ MoonshotAssistantMessage, MoonshotBatch, MoonshotChatChunk, MoonshotChatCompletion, MoonshotChoice,
29
+ MoonshotError, MoonshotFile, MoonshotMessageParam, MoonshotMessagesBlock, MoonshotMessagesResponse,
30
+ MoonshotModelObject, MoonshotResponsesOutputItem, MoonshotResponsesResponse, MoonshotResponsesUsage,
31
+ MoonshotToolCall, MoonshotUsage, SseEvent, SseSink, MessagesSseEvent,
32
+ } from './moonshot-types.ts';
33
+ export { createMoonshotScenarioEngine, moonshotScenarioAdapter, loadMoonshotScenarioDocument, realizeMoonshotRespond } from './moonshot-scenario.ts';
34
+ export type { MoonshotScenarioEngine, MoonshotScenarioRequest, MoonshotScenarioRespond, ScenarioToolCall, ScriptedResult } from './moonshot-scenario.ts';
35
+ export {
36
+ fullSyncMoonshot,
37
+ externalIdFor,
38
+ liveMoonshotExecute,
39
+ mapBalance,
40
+ mapBatch,
41
+ mapFile,
42
+ mapModel,
43
+ moonshotExecuteOver,
44
+ moonshotRequestForAction,
45
+ performMoonshotAction,
46
+ pullMoonshotState,
47
+ pushMoonshotAction,
48
+ pushPendingMoonshotActions,
49
+ syncMoonshotFromReal,
50
+ syncMoonshotFromRemote,
51
+ unpushableReason,
52
+ } from './moonshot-connector.ts';
53
+ export type { MoonshotExecute, LiveMoonshotOptions } from './moonshot-connector.ts';
54
+ // The client-side rate budget — the fail-closed backstop `liveMoonshotExecute` routes every live
55
+ // request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
56
+ // here is Moonshot's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
57
+ // bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
58
+ // `MoonshotBudgetError` by type; there is deliberately no export that disables the guard.
59
+ export {
60
+ MOONSHOT_BUDGET_CEILING,
61
+ MOONSHOT_BUDGET_MAX_RETRY_AFTER_S,
62
+ MOONSHOT_BUDGET_WINDOW_MS,
63
+ MOONSHOT_CALL_WEIGHTS,
64
+ MOONSHOT_RATE_BUDGET,
65
+ MoonshotBudget,
66
+ MoonshotBudgetError,
67
+ moonshotBudgetPath,
68
+ moonshotCallWeight,
69
+ } from './moonshot-budget.ts';
70
+ export type { MoonshotBudgetErrorKind, MoonshotBudgetOptions, MoonshotBudgetReservation, MoonshotBudgetSnapshot } from './moonshot-budget.ts';
71
+
72
+ // Registry descriptor: the pack self-describes so tooling can discover it.
73
+ import { registerPack, type TwinPack } from '@volter/world-core';
74
+ import { MOONSHOT_RATE_BUDGET as RATE_BUDGET } from './moonshot-budget.ts';
75
+ import { performMoonshotAction, syncMoonshotFromRemote } from './moonshot-connector.ts';
76
+ export const pack: TwinPack = {
77
+ vendor: 'moonshot',
78
+ // PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
79
+ // the real state system: perform one entry against Moonshot, refresh the root from it.
80
+ protocol: '2',
81
+ refresh: { every: '5m', onDemand: { atMost: '30s' } },
82
+ stateSystem: { perform: performMoonshotAction, refresh: syncMoonshotFromRemote },
83
+ // the round trip: a file — the one thing this API mints an id for and keeps, and a fresh id each
84
+ // time, so it repeats cleanly on a branch. (A batch needs a seeded input file; a completion is
85
+ // answered, not kept.)
86
+ roundTrip: { method: 'POST', path: '/v1/files', body: { purpose: 'batch', filename: 'round-trip.jsonl', content: 'x' }, headers: { authorization: 'Bearer sk-round-trip' } },
87
+ // The SAME object moonshot-budget.ts declares at module load — one source of truth, so
88
+ // registering the pack and importing the connector can never arm two different ceilings.
89
+ rateBudget: RATE_BUDGET,
90
+ transport: 'rest',
91
+ archetype: 'generative',
92
+ bin: 'world-moonshot',
93
+ // R2 adopted-as-debt, legibly: declared types no replay can create, each with its reason.
94
+ resourcesUnreachable: {
95
+ 'model': 'the vendor catalog',
96
+ 'balance': 'the account ledger the vendor keeps; read-only',
97
+ },
98
+ resources: ['file', 'batch', 'balance', 'model'],
99
+ specSource: 'Moonshot Platform API — enumerated from Moonshot\'s own OpenAPI 3.1.0 document (https://platform.kimi.ai/docs/openapi.json, read 2026-09-16; 19 operations) + platform.kimi.ai/docs/{overview,api-reference,errors,models,pricing/limits,guides/context-caching} (read 2026-09-16). Envelope-faithful across the OpenAI-compatible /v1 and Anthropic-compatible /anthropic/v1 surfaces; model output is a labeled deterministic stub.',
100
+ description: 'Moonshot (Kimi) API twin — faithful protocol envelope across three inference protocols (OpenAI chat completions, Responses, Anthropic-compatible Messages) with streaming grammars, tool calls and cache-split usage; models catalog; stateful files/batches/balance; deterministic token counting, signature verify and web-search tool stubs; Moonshot\'s own documented rejections.',
101
+ // Moonshot serves BOTH surfaces on one host. browserRouting points the browser lane at the
102
+ // OpenAI-compatible prefix, which is the dominant client form.
103
+ browserRouting: { apiPathPrefix: '/v1/', loaderHost: 'https://api.moonshot.ai' },
104
+ // ADOPTION (adding-a-twin.md §3): how an app repo betrays that it talks to Moonshot. Declared
105
+ // HERE, not in world-runtime's central SDK_TWINS/ENV_STEM_VENDORS maps — declaring in both
106
+ // throws. Moonshot ships NO SDK of its own: its documented clients are the standard `openai`
107
+ // and `anthropic` SDKs with a swapped base URL, and BOTH of those npm names belong to their
108
+ // own vendors' packs (openai → @volter/twin-openai, anthropic → @volter/twin-anthropic) — a
109
+ // repo depending on `openai` is an OpenAI repo unless its base URL says otherwise. What
110
+ // betrays Moonshot specifically is the ENV STEM (`MOONSHOT_API_KEY` / `MOONSHOT_BASE_URL`)
111
+ // and the deepseek precedent: no sdk claim, envStems only.
112
+ adoption: {
113
+ // No official Moonshot PyPI client is documented (the docs name the OpenAI Python SDK).
114
+ pypi: [],
115
+ sdks: [], envStems: ['MOONSHOT'],
116
+ },
117
+ // INTERCEPTION: the ONE host both documented clients address (the openai SDK pointed at
118
+ // https://api.moonshot.ai/v1 and the anthropic SDK pointed at https://api.moonshot.ai/anthropic).
119
+ hosts: [{ host: 'api.moonshot.ai' }],
120
+ // No app-read endpoint env, deliberately. Moonshot documents no MOONSHOT_BASE_URL-style env
121
+ // var: the standard SDKs take their base URL as a CONSTRUCTOR option (`new OpenAI({
122
+ // baseURL })`), not from the environment, so an app-read var would cover only env-configured
123
+ // callers while disabling the interception that covers all of them (the deepseek/groq
124
+ // precedent). Emitting both would quietly forgo interception (`init.test.ts`, "the app-read
125
+ // endpoint table cannot go stale behind the injector").
126
+ endpointEnvNone: 'Moonshot ships no SDK of its own; its documented clients (the standard openai/anthropic SDKs) take their base URL as a constructor option and read no MOONSHOT_BASE_URL env, so an app-read var would cover only env-configured callers while disabling the api.moonshot.ai interception that covers all traffic.',
127
+ };
128
+
129
+ registerPack(pack);
@@ -0,0 +1,163 @@
1
+ // The client-side rate budget — the fail-closed backstop `liveMoonshotExecute` routes every live
2
+ // request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
3
+ // here is Moonshot's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
4
+ // bindings, exactly as every other pack does.
5
+ //
6
+ // THE GROUNDING (platform.kimi.ai/docs/pricing/limits, read 2026-09-16): Moonshot publishes a
7
+ // per-tier table whose LOWEST published row (Tier 0, deposit ≥ $1) is concurrency 1, RPM 3,
8
+ // TPM 500,000, TPD 1,500,000. RPM 3 is an order of magnitude UNDER the kernel's undeclared
9
+ // fallback (30 calls/min), so the declaration buys resolution DOWNWARD from the fallback, never
10
+ // headroom: the ceiling is pinned at the kernel fallback in units, and the INFERENCE rules price
11
+ // the model-facing endpoints at a weight that keeps the real RPM-3 worst case inside the window.
12
+ // The tier table scales up to Tier 5 (RPM 300), but no caller's tier is knowable here — the
13
+ // budget protects the LOWEST tier by construction, and a higher tier is only ever under-spent.
14
+ // Web-search endpoints (POST /v1/tools/search, /v1/tools/search_pro, /v1/tools/fetch) are
15
+ // metered on their OWN QPS (QPS 1 at Tier 0), independent of the inference RPM — they get their
16
+ // own rule so a search sweep cannot be priced as a cheap read.
17
+ //
18
+ // THE 429 SHAPE (same limits page): a real 429 carries `X-RateLimit-Limit` /
19
+ // `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers. `recordCall` reads the standard
20
+ // `retry-after` backstop plus these.
21
+ import {
22
+ declareRateBudget, rateBudgetPath, rateBudgetWeight,
23
+ RateBudget, RateBudgetError, type RateBudgetDeclaration, type RateBudgetOptions,
24
+ type RateBudgetReservation, type RateBudgetSnapshot,
25
+ } from '@volter/world-core';
26
+
27
+ const VENDOR = 'moonshot';
28
+
29
+ /** Rolling window, in ms. Spend older than this is pruned. */
30
+ export const MOONSHOT_BUDGET_WINDOW_MS = 60_000;
31
+
32
+ /**
33
+ * Weighted units allowed inside one window. 60/60s at `defaultWeight` 2 = 30 calls a minute —
34
+ * EXACTLY the kernel's undeclared fallback. Moonshot DOES publish an RPM scalar (3 at Tier 0),
35
+ * but it is a CALL ceiling, not a weight-unit ceiling; pricing every call 2 keeps the fallback
36
+ * shape and lets the inference rules spend the allowance where the vendor actually meters it.
37
+ */
38
+ export const MOONSHOT_BUDGET_CEILING = 60;
39
+
40
+ /** Seconds. A `retry-after` above this means the key is throttled hard — fail loudly, don't sleep. */
41
+ export const MOONSHOT_BUDGET_MAX_RETRY_AFTER_S = 300;
42
+
43
+ /** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is documented vs. judged. */
44
+ export const MOONSHOT_CALL_WEIGHTS = {
45
+ /** Model-facing inference: /v1/chat/completions, /v1/responses, /anthropic/v1/messages.
46
+ * At 60/6 that is at most 10 inference calls a window — inside Tier 0's RPM 3×window shape
47
+ * for a single burst while leaving room for the interleaved reads a real client issues. */
48
+ inference: 6,
49
+ /** The web-search tool endpoints — Moonshot meters them on their OWN QPS (1 at Tier 0),
50
+ * independent of inference RPM. Priced like inference so a sweep cannot ride the read weight. */
51
+ webSearch: 6,
52
+ /** Everything else: models, files, batches, balance, token counting, signature verify. */
53
+ other: 2,
54
+ } as const;
55
+
56
+ /** THE PACK'S DECLARATION — pure data, the only Moonshot-specific thing in the whole budget. */
57
+ export const MOONSHOT_RATE_BUDGET: RateBudgetDeclaration = {
58
+ windowMs: MOONSHOT_BUDGET_WINDOW_MS,
59
+ ceiling: MOONSHOT_BUDGET_CEILING,
60
+ defaultWeight: MOONSHOT_CALL_WEIGHTS.other,
61
+ maxRetryAfterSeconds: MOONSHOT_BUDGET_MAX_RETRY_AFTER_S,
62
+ rules: [
63
+ // Anchored on Moonshot's REAL paths. Inference is token-metered (TPM 500,000 at Tier 0
64
+ // dwarfs RPM 3 as the binding constraint on real traffic), so model-facing POSTs cost 6.
65
+ { match: '^POST /v1/chat/completions$', weight: MOONSHOT_CALL_WEIGHTS.inference },
66
+ { match: '^POST /v1/responses$', weight: MOONSHOT_CALL_WEIGHTS.inference },
67
+ { match: '^POST /anthropic/v1/messages$', weight: MOONSHOT_CALL_WEIGHTS.inference },
68
+ // Web-search tools: own QPS pool at the vendor, priced at inference weight here.
69
+ { match: '^POST /v1/tools/search$', weight: MOONSHOT_CALL_WEIGHTS.webSearch },
70
+ { match: '^POST /v1/tools/search_pro$', weight: MOONSHOT_CALL_WEIGHTS.webSearch },
71
+ { match: '^POST /v1/tools/fetch$', weight: MOONSHOT_CALL_WEIGHTS.webSearch },
72
+ ],
73
+ reason:
74
+ 'Moonshot publishes a per-tier limit table (platform.kimi.ai/docs/pricing/limits, read ' +
75
+ '2026-09-16): Tier 0 (deposit ≥ $1) is concurrency 1, RPM 3, TPM 500,000, TPD 1,500,000, ' +
76
+ 'Web Search QPS 1; Tier 5 (≥ $3000) is 100/300/5M/Unlimited/50. A 429 carries ' +
77
+ 'X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset. Because the LOWEST published ' +
78
+ 'RPM (3) is far under the kernel fallback of 30 calls/min, the ceiling is pinned AT the ' +
79
+ 'fallback (60 units / 60s, defaultWeight 2) and the declaration spends it downward: the three ' +
80
+ 'model-facing inference endpoints (POST /v1/chat/completions, POST /v1/responses, ' +
81
+ 'POST /anthropic/v1/messages) cost 6 each — at most 10 land in a window — because those calls ' +
82
+ 'are token-metered (TPM) at the vendor and one call spends far more of a real account\'s ' +
83
+ 'allowance than a list poll. The web-search tool endpoints get their own 6-weight rule ' +
84
+ 'because Moonshot meters them on a SEPARATE QPS pool (1 at Tier 0), so a search sweep must ' +
85
+ 'not be priceable as a cheap read. The per-tier judgment call is the strict direction: the ' +
86
+ 'budget protects Tier 0 by construction and only ever under-spends a higher tier. The window ' +
87
+ 'bounds the 60s AVERAGE and does not pace; a `retry-after` read off a real 429 is the ' +
88
+ 'persisted cooldown backstop.',
89
+ };
90
+
91
+ // Declared at module load, so merely importing this module (which `moonshot-connector.ts` does)
92
+ // is enough to arm the real ceiling.
93
+ declareRateBudget(VENDOR, MOONSHOT_RATE_BUDGET);
94
+
95
+ /**
96
+ * Price one call. The key is `"<METHOD> <path>"` with the query string split off, so a rule can
97
+ * price by method (a write is not a read) without the kernel knowing anything about Moonshot. An
98
+ * unclassified endpoint still costs `defaultWeight` — nothing is ever free.
99
+ */
100
+ export function moonshotCallWeight(method: string, path: string): number {
101
+ const { bare, query } = moonshotSplitQuery(path);
102
+ // UPPER-CASE the method: `fetch` normalizes a known lowercase method before sending, so
103
+ // `execute('post', …)` really does issue a POST and must be priced as one.
104
+ return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
105
+ }
106
+
107
+ /**
108
+ * `/v1/chat/completions?a=1` -> `{ bare: '/v1/chat/completions', query: { a: '1' } }`. Rules
109
+ * match the path; NORMALIZED, because the anchored rules are otherwise trivially evaded: `fetch`
110
+ * upper-cases a known method before sending, so `execute('post', …)` issues a real WRITE that a
111
+ * `^POST ` rule would price as a read; and a trailing slash makes a path miss a `$` anchor while
112
+ * most routers treat it as the same endpoint.
113
+ */
114
+ function moonshotSplitQuery(path: string): { bare: string; query: Record<string, string> } {
115
+ const at = path.indexOf('?');
116
+ const query: Record<string, string> = {};
117
+ if (at !== -1) for (const [k, v] of new URLSearchParams(path.slice(at + 1))) query[k] = v;
118
+ // Collapse REPEATED slashes as well as a trailing one: `//v1/chat/completions` reaches the
119
+ // same endpoint on most routers but misses a `^POST /v1/...$` rule, which would price an
120
+ // inference call as a 2-unit read.
121
+ const raw = (at === -1 ? path : path.slice(0, at)).replace(/\/{2,}/g, '/');
122
+ const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
123
+ return { bare, query };
124
+ }
125
+
126
+ /** Where Moonshot's ledger lives. Token-keyed and cwd-independent by default (Moonshot's limits
127
+ * are per ACCOUNT, so a cwd-scoped ledger would hand the same key a fresh allowance in every
128
+ * checkout, worktree and CI matrix leg); pass `root` for world-scoped accounting. */
129
+ export function moonshotBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
130
+ const o = typeof opts === 'string' ? { root: opts } : opts;
131
+ // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
132
+ // excess-property check only catches object literals) must not redirect this pack's ledger.
133
+ return rateBudgetPath({ ...o, vendor: VENDOR });
134
+ }
135
+
136
+ /** Construction options for Moonshot's budget. The vendor is fixed; everything else may only TIGHTEN. */
137
+ export type MoonshotBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
138
+
139
+ /**
140
+ * Moonshot's budget — the shared kernel guard bound to this vendor's declaration. A real
141
+ * subclass, not an alias, so `budget instanceof MoonshotBudget` in `liveMoonshotExecute` means
142
+ * "a budget that accounts against MOONSHOT's ledger under MOONSHOT's ceiling".
143
+ */
144
+ export class MoonshotBudget extends RateBudget {
145
+ constructor(opts: MoonshotBudgetOptions = {}) {
146
+ super({
147
+ ...opts,
148
+ // Field-per-line so the mutation gate can neuter the vendor binding: a budget whose
149
+ // `vendor` is not 'moonshot' accounts against another vendor's ledger, and the ceiling
150
+ // verify reads `err.vendor` off the refusal — the neuter must redden THAT assertion.
151
+ vendor: VENDOR,
152
+ });
153
+ }
154
+ }
155
+
156
+ /** The typed refusal. One error class shared with every other vendor's budget; `err.vendor` says
157
+ * which one refused, and `err.kind` says why. */
158
+ export { RateBudgetError as MoonshotBudgetError } from '@volter/world-core';
159
+ export type { RateBudgetErrorKind as MoonshotBudgetErrorKind } from '@volter/world-core';
160
+ /** Re-exported so a caller can age a window out against the DECLARED window, not a hard-coded 60_000. */
161
+ export { MOONSHOT_BUDGET_WINDOW_MS as MOONSHOT_RATE_BUDGET_WINDOW_MS };
162
+ export type MoonshotBudgetReservation = RateBudgetReservation;
163
+ export type MoonshotBudgetSnapshot = RateBudgetSnapshot;