@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,14 @@
1
+ export type ConformanceViolation = {
2
+ check: string;
3
+ detail: string;
4
+ };
5
+ export type MoonshotConformanceReport = {
6
+ ok: boolean;
7
+ checksRun: number;
8
+ probes: number;
9
+ violations: ConformanceViolation[];
10
+ };
11
+ /** Run the offline conformance checks against a fresh temp root. */
12
+ export declare function checkMoonshotConformance(opts?: {
13
+ root?: string;
14
+ }): Promise<MoonshotConformanceReport>;
@@ -0,0 +1,405 @@
1
+ // Moonshot twin conformance — an offline check with REAL TEETH: delete a handler branch and it
2
+ // goes RED. Moonshot publishes its own OpenAPI document, so the claimed surface has a first-party
3
+ // denominator; the harness still does not compare two constants (the false-green ADDING_A_TWIN.md
4
+ // §6 documents). Instead it runs THREE passes:
5
+ //
6
+ // 1. PROBES — one real request per CLAIMED endpoint, graded on the OUTCOME a live handler
7
+ // produces: an expected status set PLUS a predicate over the body. A handler that returned
8
+ // `{}`, or whose branch was deleted (so the router falls through to its not-found), fails.
9
+ // 2. BIJECTION — the probe table and the claimed-endpoint snapshot must match two ways, so a
10
+ // claim with no probe and a probe with no claim are both RED.
11
+ // 3. ROUTER_SURFACE — a HAND-AUTHORED census of every method/path pair `routeMoonshot` branches
12
+ // on, each carrying a live request that must actually REACH that branch (not fall through to
13
+ // the router's not-found). Plus MUST_NOT_SERVE: paths the vendor does not serve, which must
14
+ // answer the vendor-shaped not-found envelope.
15
+ //
16
+ // Fully offline + deterministic (drives the local handler against a temp root) so it runs without
17
+ // an API key. Honest scope: it checks the protocol envelope, NOT model output (a deterministic
18
+ // labeled stub by design).
19
+ import { mkdtempSync, rmSync } from 'node:fs';
20
+ import { tmpdir } from 'node:os';
21
+ import { join } from 'node:path';
22
+ import { handleMoonshotTwinRequest, MESSAGES_PREFIX, MOONSHOT_API_PREFIX } from "./moonshot-twin.js";
23
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
24
+ const CHAT = { model: 'kimi-k3', messages: [{ role: 'user', content: 'hi' }] };
25
+ const PROBES = [
26
+ {
27
+ key: 'POST /v1/chat/completions',
28
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: CHAT, statuses: [200],
29
+ ok: (b) => isObj(b) && b.object === 'chat.completion' && Array.isArray(b.choices)
30
+ && b.choices[0]?.message?.role === 'assistant'
31
+ && typeof b.choices[0]?.message?.content === 'string'
32
+ && isObj(b.usage) && typeof b.usage.total_tokens === 'number' && typeof b.usage.cached_tokens === 'number',
33
+ },
34
+ {
35
+ key: 'POST /v1/responses',
36
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k3', input: 'hi' }, statuses: [200],
37
+ ok: (b) => isObj(b) && b.object === 'response' && b.status === 'completed' && Array.isArray(b.output)
38
+ && b.output.some((o) => o.type === 'message')
39
+ && isObj(b.usage) && typeof b.usage.total_tokens === 'number',
40
+ },
41
+ {
42
+ key: 'POST /anthropic/v1/messages',
43
+ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hi' }], max_tokens: 100 }, statuses: [200],
44
+ ok: (b) => isObj(b) && b.type === 'message' && b.role === 'assistant' && Array.isArray(b.content)
45
+ && b.content[0]?.type === 'thinking'
46
+ && typeof b.stop_reason === 'string' && isObj(b.usage) && typeof b.usage.input_tokens === 'number',
47
+ },
48
+ {
49
+ key: 'GET /v1/models',
50
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/models`, statuses: [200],
51
+ ok: (b) => isObj(b) && b.object === 'list' && Array.isArray(b.data) && b.data.some((m) => m.id === 'kimi-k3' && m.object === 'model' && m.owned_by === 'moonshot'),
52
+ },
53
+ {
54
+ key: 'POST /v1/files',
55
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/files`, body: { purpose: 'batch', filename: 'in.jsonl', content: '{}' }, statuses: [200],
56
+ ok: (b) => isObj(b) && b.object === 'file' && b.purpose === 'batch' && b.status === 'ready' && String(b.id).startsWith('file_twin_'),
57
+ },
58
+ {
59
+ key: 'GET /v1/files',
60
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/files`, statuses: [200],
61
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'seed.jsonl', content: '{}' }); },
62
+ ok: (b) => isObj(b) && b.object === 'list' && b.data.some((f) => f.filename === 'seed.jsonl'),
63
+ },
64
+ {
65
+ key: 'GET /v1/files/{file_id}',
66
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1`, statuses: [200],
67
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: 'x' }); },
68
+ ok: (b) => isObj(b) && b.id === 'file_twin_1' && b.filename === 'one.jsonl',
69
+ },
70
+ {
71
+ key: 'GET /v1/files/{file_id}/content',
72
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1/content`, statuses: [200],
73
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: '{"custom_id":"a"}' }); },
74
+ ok: (b) => b === '{"custom_id":"a"}',
75
+ },
76
+ {
77
+ key: 'DELETE /v1/files/{file_id}',
78
+ method: 'DELETE', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1`, statuses: [200],
79
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: 'x' }); },
80
+ ok: (b) => isObj(b) && b.deleted === true && b.id === 'file_twin_1' && b.object === 'file',
81
+ },
82
+ {
83
+ key: 'POST /v1/batches',
84
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches`, body: { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' }, statuses: [200],
85
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' }); },
86
+ ok: (b) => isObj(b) && b.object === 'batch' && b.status === 'validating' && b.endpoint === '/v1/chat/completions' && typeof b.expires_at === 'number',
87
+ },
88
+ {
89
+ key: 'GET /v1/batches',
90
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches`, statuses: [200],
91
+ seed: async (h) => {
92
+ await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
93
+ await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
94
+ },
95
+ ok: (b) => isObj(b) && b.object === 'list' && b.data.some((x) => x.id === 'batch_twin_1'),
96
+ },
97
+ {
98
+ key: 'GET /v1/batches/{batch_id}',
99
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1`, statuses: [200],
100
+ seed: async (h) => {
101
+ await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
102
+ await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
103
+ },
104
+ ok: (b) => isObj(b) && b.id === 'batch_twin_1' && b.input_file_id === 'file_twin_1',
105
+ },
106
+ {
107
+ key: 'POST /v1/batches/{batch_id}/cancel',
108
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1/cancel`, statuses: [200],
109
+ seed: async (h) => {
110
+ await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
111
+ await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
112
+ },
113
+ ok: (b) => isObj(b) && b.status === 'cancelling' && typeof b.cancelling_at === 'number',
114
+ },
115
+ {
116
+ key: 'GET /v1/users/me/balance',
117
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/users/me/balance`, statuses: [200],
118
+ ok: (b) => isObj(b) && b.code === 0 && b.scode === '0x0' && b.status === true
119
+ && isObj(b.data) && typeof b.data.available_balance === 'number' && typeof b.data.voucher_balance === 'number' && typeof b.data.cash_balance === 'number',
120
+ },
121
+ {
122
+ key: 'POST /v1/tokenizers/estimate-token-count',
123
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tokenizers/estimate-token-count`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hello world, estimate me' }] }, statuses: [200],
124
+ ok: (b) => isObj(b) && isObj(b.data) && typeof b.data.total_tokens === 'number' && b.data.total_tokens > 0,
125
+ },
126
+ {
127
+ key: 'POST /v1/signatures/verify',
128
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/signatures/verify`, body: { nonce: 'n-1', timestamp: 1786338000123, model: 'kimi-k2.7-code', signature: 'reqsigv1_not-a-real-token' }, statuses: [200],
129
+ ok: (b) => isObj(b) && b.valid === false,
130
+ },
131
+ {
132
+ key: 'POST /v1/tools/search',
133
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search`, body: { text_query: 'kimi api limits', limit: 3 }, statuses: [200],
134
+ ok: (b) => isObj(b) && Array.isArray(b.search_results) && b.search_results.length === 3
135
+ && typeof b.search_results[0]?.url === 'string' && typeof b.search_results[0]?.snippet === 'string',
136
+ },
137
+ {
138
+ key: 'POST /v1/tools/search_pro',
139
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search_pro`, body: { text_query: 'kimi api limits', sites: ['platform.kimi.ai'] }, statuses: [200],
140
+ ok: (b) => isObj(b) && Array.isArray(b.search_results) && b.search_results.length === 5
141
+ && Array.isArray(b.search_results[0]?.chunks),
142
+ },
143
+ {
144
+ key: 'POST /v1/tools/fetch',
145
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/fetch`, body: { url: 'https://platform.kimi.ai/docs' }, statuses: [200],
146
+ ok: (b) => isObj(b) && b.url === 'https://platform.kimi.ai/docs' && typeof b.markdown === 'string' && b.markdown.includes('[twin-stub]'),
147
+ },
148
+ ];
149
+ /**
150
+ * THE ROUTER CENSUS — hand-authored from `routeMoonshot`'s dispatch branches, paired with a
151
+ * request that REACHES that branch. Each entry is checked live: the router must BRANCH on it
152
+ * (i.e. not fall through to its own `Unknown request URL` not-found) — so deleting a handler
153
+ * branch reddens the census by name, independently of that endpoint's own probe.
154
+ */
155
+ const ROUTER_SURFACE = [
156
+ { key: 'GET /v1/models', method: 'GET', path: `${MOONSHOT_API_PREFIX}/models` },
157
+ { key: 'POST /v1/chat/completions', method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: CHAT },
158
+ { key: 'POST /v1/responses', method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k3', input: 'x' } },
159
+ { key: 'POST /v1/tokenizers/estimate-token-count', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tokenizers/estimate-token-count`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }] } },
160
+ { key: 'POST /v1/signatures/verify', method: 'POST', path: `${MOONSHOT_API_PREFIX}/signatures/verify`, body: { nonce: 'n', timestamp: 1, model: 'kimi-k3', signature: 's' } },
161
+ { key: 'POST /v1/tools/search', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search`, body: { text_query: 'x' } },
162
+ { key: 'POST /v1/tools/search_pro', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search_pro`, body: { text_query: 'x' } },
163
+ { key: 'POST /v1/tools/fetch', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/fetch`, body: { url: 'https://x.example' } },
164
+ { key: 'GET /v1/users/me/balance', method: 'GET', path: `${MOONSHOT_API_PREFIX}/users/me/balance` },
165
+ { key: 'POST /v1/files', method: 'POST', path: `${MOONSHOT_API_PREFIX}/files`, body: { purpose: 'batch', filename: 'a.jsonl', content: 'x' } },
166
+ { key: 'GET /v1/files', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files` },
167
+ { key: 'GET /v1/files/{file_id}', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1` },
168
+ { key: 'GET /v1/files/{file_id}/content', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1/content` },
169
+ { key: 'DELETE /v1/files/{file_id}', method: 'DELETE', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1` },
170
+ { key: 'POST /v1/batches', method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches`, body: { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' } },
171
+ { key: 'GET /v1/batches', method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches` },
172
+ { key: 'GET /v1/batches/{batch_id}', method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1` },
173
+ { key: 'POST /v1/batches/{batch_id}/cancel', method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1/cancel` },
174
+ { key: 'POST /anthropic/v1/messages', method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 } },
175
+ ];
176
+ /** The literal the router falls through to when NO branch matched. A census entry answering this
177
+ * means its branch is gone. Kept as a LITERAL — importing the handler's string would be a
178
+ * tautology that could not catch the message drifting. */
179
+ const ROUTER_MISS = 'Unknown request URL';
180
+ const ROUTER_SURFACE_KEYS = ROUTER_SURFACE.map((e) => e.key);
181
+ /**
182
+ * Surface the twin must NOT serve: the bare `/openai/v1` prefix (that is GROQ's shape, not
183
+ * Moonshot's), paths outside both prefixes, and the kimi-k2.x models on the kimi-k3-only
184
+ * endpoints (the OpenAPI's per-request model enums are the closed sets). Each must answer the
185
+ * vendor-shaped not-found/error envelope.
186
+ */
187
+ const MUST_NOT_SERVE = [
188
+ { method: 'POST', path: '/openai/v1/chat/completions', body: CHAT, why: 'the /openai/v1 prefix is GroqCloud\'s shape; Moonshot serves /v1' },
189
+ { method: 'GET', path: '/v1/fine_tuning/jobs', why: 'OpenAI has this endpoint; Moonshot\'s OpenAPI does not' },
190
+ { method: 'GET', path: '/v1/embeddings', why: 'Moonshot publishes no embeddings endpoint' },
191
+ { method: 'GET', path: `${MOONSHOT_API_PREFIX}/models/kimi-k3`, why: 'Moonshot\'s OpenAPI declares NO /v1/models/{model} retrieve operation — serving a 200 here was a 200 for an unmodeled route' },
192
+ { method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k2.6', input: 'x' }, why: 'the Responses API is kimi-k3 only' },
193
+ { method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k2.7-code', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }, why: 'the Messages API is kimi-k3 only' },
194
+ ];
195
+ /** Run the offline conformance checks against a fresh temp root. */
196
+ export async function checkMoonshotConformance(opts = {}) {
197
+ const violations = [];
198
+ let checksRun = 0;
199
+ const fail = (check, detail) => violations.push({ check, detail });
200
+ // ── 1. PROBES: each in its OWN throwaway root, so a probe's seed can't leak into another. ──
201
+ for (const probe of PROBES) {
202
+ checksRun++;
203
+ // A FRESH root per probe, always. `opts.root` is the PARENT directory, never a shared root:
204
+ // sharing it let `nextId` ratchet across probes, so the `files/file_twin_1` probe read the id
205
+ // an earlier probe had minted and `world-moonshot conformance --root DIR` reported false
206
+ // violations (the groq pack's §9 round-two finding, back-applied here from the start).
207
+ const root = mkdtempSync(join(opts.root ?? tmpdir(), 'moonshot-conf-'));
208
+ const h = (method, path, body) => handleMoonshotTwinRequest({ method, path, ...(body === undefined ? {} : { body: JSON.stringify(body) }), root });
209
+ try {
210
+ if (probe.seed)
211
+ await probe.seed(h);
212
+ const res = await h(probe.method, probe.path, probe.body);
213
+ if (!probe.statuses.includes(res.status)) {
214
+ fail(`probe:${probe.key}`, `status ${res.status} (expected one of ${probe.statuses.join('/')}) body=${JSON.stringify(res.body).slice(0, 200)}`);
215
+ }
216
+ else if (!probe.ok(res.body, res.status)) {
217
+ fail(`probe:${probe.key}`, `body did not satisfy the live-handler predicate: ${JSON.stringify(res.body).slice(0, 300)}`);
218
+ }
219
+ }
220
+ finally {
221
+ rmSync(root, { recursive: true, force: true });
222
+ }
223
+ }
224
+ // ── 2. BIJECTION: probe table ⇄ router census, both directions. ──
225
+ checksRun++;
226
+ const probeKeys = new Set(PROBES.map((p) => p.key));
227
+ for (const key of ROUTER_SURFACE_KEYS)
228
+ if (!probeKeys.has(key))
229
+ fail('bijection', `router branch "${key}" has no probe`);
230
+ for (const key of probeKeys)
231
+ if (!ROUTER_SURFACE_KEYS.includes(key))
232
+ fail('bijection', `probe "${key}" is not in the hand-authored router census`);
233
+ if (PROBES.length !== new Set(PROBES.map((p) => p.key)).size)
234
+ fail('bijection', 'duplicate probe key');
235
+ if (ROUTER_SURFACE.length !== ROUTER_SURFACE_KEYS.length)
236
+ fail('bijection', 'the router census and its key list disagree');
237
+ // ── 2b. THE CENSUS HAS TEETH: every declared branch must actually be REACHED. A branch whose
238
+ // handler was deleted falls through to the router's own not-found, which is what this
239
+ // catches — independently of that endpoint's own probe.
240
+ for (const entry of ROUTER_SURFACE) {
241
+ checksRun++;
242
+ const root = mkdtempSync(join(opts.root ?? tmpdir(), 'moonshot-conf-'));
243
+ try {
244
+ const res = await handleMoonshotTwinRequest({ method: entry.method, path: entry.path, ...(entry.body === undefined ? {} : { body: JSON.stringify(entry.body) }), root });
245
+ const message = String(res.body?.error?.message ?? res.body?.error?.message ?? '');
246
+ if (message.includes(ROUTER_MISS)) {
247
+ fail('router_surface', `${entry.key} fell through to the router's not-found — the branch is gone (answered ${res.status}: ${message})`);
248
+ }
249
+ if (!ROUTER_SURFACE_KEYS.includes(entry.key))
250
+ fail('router_surface', `${entry.key} is exercised but not declared`);
251
+ }
252
+ finally {
253
+ rmSync(root, { recursive: true, force: true });
254
+ }
255
+ }
256
+ // ── 3. The twin must not serve surface it does not model (or the vendor does not have). ──
257
+ {
258
+ const root = opts.root ?? mkdtempSync(join(tmpdir(), 'moonshot-conf-'));
259
+ try {
260
+ for (const entry of MUST_NOT_SERVE) {
261
+ checksRun++;
262
+ const res = await handleMoonshotTwinRequest({ method: entry.method, path: entry.path, ...(entry.body === undefined ? {} : { body: JSON.stringify(entry.body) }), root });
263
+ if (res.status < 400)
264
+ fail('must_not_serve', `${entry.method} ${entry.path} answered ${res.status} — ${entry.why}`);
265
+ }
266
+ // ── 4. The error envelope on /v1 carries `error.message` (REQUIRED) plus only keys
267
+ // Moonshot's ErrorResponse schema declares: message, type, code. The key list is a
268
+ // LITERAL here — asserting against the handler's own constant would be a tautology.
269
+ checksRun++;
270
+ const ERROR_OBJECT_KEYS = new Set(['message', 'type', 'code']);
271
+ const bad = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ messages: [] }), root });
272
+ const eb = bad.body;
273
+ if (bad.status !== 400 || !isObj(eb?.error)) {
274
+ fail('error.envelope', `missing model did not yield a 400 with an error envelope (status ${bad.status})`);
275
+ }
276
+ else {
277
+ const undeclared = Object.keys(eb.error).filter((k) => !ERROR_OBJECT_KEYS.has(k));
278
+ if (undeclared.length)
279
+ fail('error.envelope', `error object carries key(s) Moonshot's ErrorResponse schema does not declare: ${undeclared.join(', ')}`);
280
+ if (typeof eb.error.message !== 'string' || !eb.error.message)
281
+ fail('error.envelope', 'error.message is not a non-empty string');
282
+ if (eb.error.type !== 'invalid_request_error')
283
+ fail('error.envelope', `error.type is ${String(eb.error.type)}`);
284
+ }
285
+ // ── 4b. The Messages surface answers its OWN envelope ({type:'error',error:{type,message}})
286
+ // on EVERY failure — the validation 400 AND the cross-cutting 401/429/503/405 that
287
+ // fire before routing (each of which used to leak the /v1 envelope regardless of
288
+ // prefix, the round-two finding). Anthropic's own error types apply here.
289
+ checksRun++;
290
+ const anthropicShape = (b) => {
291
+ const mb = b;
292
+ return mb?.type === 'error' && isObj(mb.error) && typeof mb.error.type === 'string' && typeof mb.error.message === 'string' && !('code' in mb.error);
293
+ };
294
+ const mErr = await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }] }), root });
295
+ if (mErr.status !== 400)
296
+ fail('messages.error_envelope', `missing max_tokens answered ${mErr.status}, not 400`);
297
+ else {
298
+ if (!anthropicShape(mErr.body))
299
+ fail('messages.error_envelope', 'Messages error body is not the MessagesErrorResponse shape { type, error:{type,message} }');
300
+ if ('error' in mErr.body && isObj(mErr.body.error) && 'code' in mErr.body.error)
301
+ fail('messages.error_envelope', 'Messages error envelope carries a `code` key its schema does not declare');
302
+ }
303
+ const cross = [
304
+ ['401', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, headers: { 'x-api-key': '' } }), 'authentication_error'],
305
+ ['429', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, headers: { authorization: 'Bearer sk-twin', 'x-twin-force-rate-limit': '1' } }), 'rate_limit_error'],
306
+ ['503', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, headers: { authorization: 'Bearer sk-twin', 'x-twin-force-server-unavailable': '1' } }), 'overloaded_error'],
307
+ ['405', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, readOnly: true }), 'invalid_request_error'],
308
+ ];
309
+ for (const [what, res, wantType] of cross) {
310
+ if (res.status !== Number(what))
311
+ fail('messages.error_envelope', `the ${what} cross-cutting failure answered ${res.status}`);
312
+ else if (!anthropicShape(res.body))
313
+ fail('messages.error_envelope', `the ${what} cross-cutting failure leaked a non-Anthropic envelope: ${JSON.stringify(res.body).slice(0, 120)}`);
314
+ else if (res.body.error.type !== wantType)
315
+ fail('messages.error_envelope', `the ${what} cross-cutting failure answered error.type ${res.body.error.type}, not ${wantType}`);
316
+ }
317
+ // ── 5. Per-model parameter gating, straight from the OpenAPI's per-request schemas: ──
318
+ // kimi-k2.7-code REJECTS thinking.type 'disabled'; kimi-k2.6 accepts it; kimi-k3 has
319
+ // no thinking parameter at all; reasoning_effort is kimi-k3-only.
320
+ checksRun++;
321
+ const k27Disabled = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.7-code', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'disabled' } }), root });
322
+ const k26Disabled = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.6', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'disabled' } }), root });
323
+ const k3Thinking = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'enabled' } }), root });
324
+ const k3Effort = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], reasoning_effort: 'low' }), root });
325
+ const k26Effort = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.6', messages: [{ role: 'user', content: 'x' }], reasoning_effort: 'low' }), root });
326
+ if (k27Disabled.status !== 400)
327
+ fail('chat.per_model_gating', `kimi-k2.7-code thinking.type 'disabled' answered ${k27Disabled.status}, not 400`);
328
+ if (k26Disabled.status !== 200)
329
+ fail('chat.per_model_gating', `kimi-k2.6 thinking.type 'disabled' answered ${k26Disabled.status}, not 200`);
330
+ if (k3Thinking.status !== 400)
331
+ fail('chat.per_model_gating', `kimi-k3 'thinking' answered ${k3Thinking.status}, not 400 (kimi-k3 uses reasoning_effort)`);
332
+ if (k3Effort.status !== 200)
333
+ fail('chat.per_model_gating', `kimi-k3 reasoning_effort answered ${k3Effort.status}, not 200`);
334
+ if (k26Effort.status !== 400)
335
+ fail('chat.per_model_gating', `kimi-k2.6 reasoning_effort answered ${k26Effort.status}, not 400`);
336
+ // ── 6. The streaming chunk sequence, including Moonshot's usage-bearing tail chunk. ──
337
+ checksRun++;
338
+ const events = [];
339
+ await handleMoonshotTwinRequest({
340
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, root,
341
+ body: JSON.stringify({ ...CHAT, stream: true }),
342
+ sseSink: (e) => events.push(e),
343
+ });
344
+ const hasRole = events.some((e) => !e.done && e.data.choices?.[0]?.delta?.role === 'assistant');
345
+ const done = events.length > 0 && events[events.length - 1].done === true;
346
+ const firstObj = events[0]?.data?.object;
347
+ const tail = events[events.length - 2]?.data;
348
+ if (!hasRole)
349
+ fail('chat.stream', 'no role delta chunk');
350
+ if (!done)
351
+ fail('chat.stream', 'stream did not end with [DONE]');
352
+ if (firstObj !== 'chat.completion.chunk')
353
+ fail('chat.stream', `first chunk object is ${String(firstObj)}`);
354
+ if (!tail || !Array.isArray(tail.choices) || tail.choices.length !== 0 || !isObj(tail.usage)) {
355
+ fail('chat.stream', 'final chunk is not Moonshot\'s empty-choices usage tail');
356
+ }
357
+ // ── 7. The Anthropic-compatible streaming grammar: named events, message_start →
358
+ // thinking/text deltas → message_delta → message_stop, NO [DONE] sentinel. ──
359
+ checksRun++;
360
+ const mEvents = [];
361
+ await handleMoonshotTwinRequest({
362
+ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, root,
363
+ body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 200, stream: true }),
364
+ messagesSseSink: (e) => mEvents.push(e),
365
+ });
366
+ const names = mEvents.filter((e) => !e.done).map((e) => e.event ?? '');
367
+ if (names[0] !== 'message_start')
368
+ fail('messages.stream', `first event is ${String(names[0])}, not message_start`);
369
+ if (!names.includes('content_block_delta') || !names.includes('content_block_stop'))
370
+ fail('messages.stream', 'no content_block deltas');
371
+ if (names[names.length - 1] !== 'message_stop')
372
+ fail('messages.stream', `last event is ${String(names[names.length - 1])}, not message_stop`);
373
+ if (mEvents.some((e) => e.done === false && e.data === undefined && e.event === undefined))
374
+ fail('messages.stream', 'unnamed event in the Anthropic grammar');
375
+ // ── 8. tool_calls envelope when tools are provided. ──
376
+ checksRun++;
377
+ const tool = await handleMoonshotTwinRequest({
378
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, root,
379
+ body: JSON.stringify({ ...CHAT, tools: [{ type: 'function', function: { name: 'get_weather', parameters: { type: 'object', properties: { city: { type: 'string' } } } } }] }),
380
+ });
381
+ const choice = tool.body.choices?.[0];
382
+ if (choice?.finish_reason !== 'tool_calls')
383
+ fail('chat.tool_calls', 'finish_reason not tool_calls');
384
+ const calls = choice?.message?.tool_calls;
385
+ if (!Array.isArray(calls) || calls[0]?.type !== 'function' || calls[0]?.function?.name !== 'get_weather')
386
+ fail('chat.tool_calls', 'no get_weather function tool_call');
387
+ // ── 9. Thinking mode surfaces reasoning_content; disabled k2.6 does not. ──
388
+ checksRun++;
389
+ const k3 = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify(CHAT), root });
390
+ const k3Msg = k3.body.choices?.[0]?.message;
391
+ if (typeof k3Msg?.reasoning_content !== 'string' || !k3Msg.reasoning_content.includes('[twin-stub:kimi-k3]')) {
392
+ fail('chat.reasoning_content', 'kimi-k3 (always thinking) did not surface reasoning_content');
393
+ }
394
+ const k26off = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.6', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'disabled' } }), root });
395
+ const k26Msg = k26off.body.choices?.[0]?.message;
396
+ if ('reasoning_content' in (k26Msg ?? {}))
397
+ fail('chat.reasoning_content', "kimi-k2.6 with thinking.type 'disabled' surfaced reasoning_content");
398
+ }
399
+ finally {
400
+ if (opts.root === undefined)
401
+ rmSync(root, { recursive: true, force: true });
402
+ }
403
+ }
404
+ return { ok: violations.length === 0, checksRun, probes: PROBES.length, violations };
405
+ }
@@ -0,0 +1,168 @@
1
+ import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
2
+ import { MoonshotBudget, type MoonshotBudgetOptions } from './moonshot-budget.js';
3
+ /**
4
+ * The injected real-Moonshot boundary. `request` issues ONE Moonshot REST call:
5
+ * method — 'GET' | 'POST' | 'DELETE'
6
+ * path — e.g. '/v1/files' or '/v1/batches/batch_123/cancel'
7
+ * body — JSON body for POST (omitted otherwise)
8
+ * Returns the parsed JSON (an object, a `{ data }` list, or an `{ error }` envelope).
9
+ */
10
+ export type MoonshotExecute = (method: 'GET' | 'POST' | 'DELETE', path: string, body?: Record<string, unknown>) => Promise<{
11
+ data?: any;
12
+ error?: {
13
+ message?: string;
14
+ type?: string;
15
+ };
16
+ [k: string]: unknown;
17
+ }>;
18
+ /** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
19
+ export type LiveMoonshotOptions = {
20
+ /** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
21
+ fetchImpl?: typeof fetch;
22
+ /** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
23
+ budget?: MoonshotBudget;
24
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
25
+ budgetOptions?: MoonshotBudgetOptions;
26
+ };
27
+ /**
28
+ * A live executor against the real Moonshot REST API (the user's own API key). Sends the required
29
+ * `Authorization: Bearer` header. Never imported by the pack's own serve path — only constructed
30
+ * by a caller that opts into real I/O.
31
+ *
32
+ * THIS IS THE ONE PLACE this pack issues a live `api.moonshot.ai` request, and therefore the one
33
+ * place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
34
+ * the request goes out (`checkBudget`, which THROWS `MoonshotBudgetError` instead of returning
35
+ * when the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
36
+ * `retry-after` / 429 / `x-ratelimit-remaining: 0` signal becomes a persisted cooldown that makes
37
+ * every later call fail fast WITHOUT touching Moonshot. There is deliberately no OPTION to
38
+ * disable the guard, and no value a caller can pass for `budget` that yields an unguarded client.
39
+ * What that does NOT claim is immunity from a caller who WANTS one: a fresh `budgetOptions.path`
40
+ * per construction, or an injected clock, restores the allowance, because the same seam tests
41
+ * need cannot be denied to a determined caller in the same process. See `moonshot-budget.ts` and
42
+ * the kernel header for the limits of the guarantee.
43
+ */
44
+ export declare function liveMoonshotExecute(apiKey: string, base?: string, opts?: LiveMoonshotOptions): MoonshotExecute;
45
+ /** Map a real-Moonshot model object → a twin sync resource. */
46
+ export declare function mapModel(m: Record<string, unknown>): SyncResource;
47
+ /** Map a real-Moonshot File object → a twin sync resource. */
48
+ export declare function mapFile(f: Record<string, unknown>): SyncResource;
49
+ /** Map a real-Moonshot Batch object → a twin sync resource. */
50
+ export declare function mapBatch(b: Record<string, unknown>): SyncResource;
51
+ /** Map the real-Moonshot balance → the twin's 'balance' resource (id 'me'). */
52
+ export declare function mapBalance(b: Record<string, unknown>): SyncResource;
53
+ /** Pull all modeled real collections via the executor and map them to twin sync resources. */
54
+ export declare function pullMoonshotState(execute: MoonshotExecute): Promise<SyncResource[]>;
55
+ /**
56
+ * Pull from real Moonshot and fold into the twin (mirror seeding). Observation entries are
57
+ * content-addressed, so a re-pull of identical state appends nothing.
58
+ *
59
+ * `occurredAt` has NO pinned default on purpose: the kernel hashes an observed event over
60
+ * (occurredAt + post-state), so under a fixed poll time a vendor value that REVERTS across polls
61
+ * collides with its own earlier observation and the fold reports a phantom delta while the
62
+ * projection keeps the stale value (ADDING_A_TWIN.md §6). Callers pass a moving timestamp.
63
+ */
64
+ export declare function syncMoonshotFromReal(execute: MoonshotExecute, opts: {
65
+ root?: string;
66
+ occurredAt: string;
67
+ }): Promise<{
68
+ observed: number;
69
+ deltasAppended: number;
70
+ }>;
71
+ /** Why this action cannot be pushed, or null if it can. Pure — no vendor call on its path. */
72
+ export declare function unpushableReason(op: string): string | null;
73
+ /**
74
+ * Resolve the id the VENDOR knows this subject by.
75
+ *
76
+ * The kernel records the vendor's minted id at landing time (`vendorSubjectId` → the landed copy
77
+ * carries the vendor's id, `aliasOf` the local one), and `resolveSubjectId` answers it — so a
78
+ * later delete/cancel addresses the vendor by THAT. A locally-minted subject with no recorded
79
+ * alias is refused rather than guessed at — addressing the real account by the twin's own mint
80
+ * would DELETE or CANCEL a resource the vendor never had.
81
+ */
82
+ export declare function externalIdFor(subjectType: string, subjectId: string, root?: string): string | null;
83
+ /**
84
+ * Resolve the REST (method, path) for ONE pending action — faithful to the real Moonshot REST
85
+ * surface:
86
+ * - <type>.create → POST <collection>
87
+ * - <type>.cancel → POST <collection>/:id/cancel
88
+ * - <type>.delete → DELETE <collection>/:id
89
+ */
90
+ export declare function moonshotRequestForAction(action: Pick<TwinAction, 'operation' | 'subject'>,
91
+ /** The id the VENDOR knows this subject by. Required for anything but a create — see
92
+ * `externalIdFor`; passing the twin's own mint would address a resource the vendor never had. */
93
+ externalId?: string): {
94
+ method: 'GET' | 'POST' | 'DELETE';
95
+ path: string;
96
+ };
97
+ /**
98
+ * Push ONE pending action to REAL Moonshot via the injected executor. Returns the real external
99
+ * id (the object id from the response; for a create that's a freshly minted id, otherwise it
100
+ * echoes the subject). WRITES TO THE REAL ACCOUNT.
101
+ */
102
+ export declare function pushMoonshotAction(execute: MoonshotExecute, action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>, opts?: {
103
+ externalId?: string;
104
+ }): Promise<{
105
+ externalId: string;
106
+ }>;
107
+ /** The pack's executor over the kernel's: the same Moonshot call, carried by the head. The
108
+ * credential never enters this file — the kernel's executor applies it to the authorization
109
+ * header before this sees the request. */
110
+ export declare function moonshotExecuteOver(execute: RemoteExecute): MoonshotExecute;
111
+ /** The refresh adapter: pull the account's models / files / batches / balance through the executor. */
112
+ export declare function syncMoonshotFromRemote(execute: RemoteExecute, opts?: {
113
+ root?: string;
114
+ origin?: string;
115
+ occurredAt?: string;
116
+ }): Promise<ReturnType<typeof syncMoonshotFromReal>>;
117
+ /**
118
+ * The PERFORM adapter: one deployable entry crosses to Moonshot, or settles with the reason it
119
+ * never could. `file.create` is unpushable-BY-DESIGN (the real endpoint is multipart with the
120
+ * file body; this executor sends JSON) — it settles without crossing rather than throwing, so it
121
+ * cannot wedge every later batch action behind it. A locally-minted subject with no vendor alias
122
+ * THROWS (the head records the failure): addressing the real account by the twin's own mint would
123
+ * delete or cancel a resource the vendor never had.
124
+ */
125
+ export declare function performMoonshotAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome>;
126
+ /**
127
+ * Push the twin's PENDING local actions to real Moonshot and CONFIRM each. Idempotency: a
128
+ * confirmed action is no longer deployable, so a re-push enacts NOTHING.
129
+ *
130
+ * Every action this pack records is SINGLE-RESOURCE (one file, one batch), so confirming with
131
+ * `fields` alone is correct — see ADDING_A_TWIN.md §5 on compound actions, which this pack has
132
+ * none of. If a compound write is ever added here, it must ride its extra resources in
133
+ * `additionalObservations` or the push will silently delete them.
134
+ */
135
+ export declare function pushPendingMoonshotActions(execute: MoonshotExecute, opts: {
136
+ root?: string;
137
+ occurredAt: string;
138
+ }): Promise<{
139
+ pushed: number;
140
+ confirmed: string[];
141
+ externalIds: Record<string, string>;
142
+ refused: Array<{
143
+ actionId: string;
144
+ operation: string;
145
+ reason: string;
146
+ }>;
147
+ }>;
148
+ /**
149
+ * FULL bi-directional sync over the injected client: (1) PUSH every deployable local entry to
150
+ * real Moonshot and confirm it, then (2) PULL all modeled collections back and fold them onto
151
+ * the root's log. Pushing first means the pull observes the twin's own writes as confirmed
152
+ * external state (no double-count). Re-running with nothing deployable and identical real state
153
+ * is a no-op.
154
+ */
155
+ export declare function fullSyncMoonshot(execute: MoonshotExecute, opts: {
156
+ root?: string;
157
+ occurredAt: string;
158
+ }): Promise<{
159
+ pushed: number;
160
+ observed: number;
161
+ deltasAppended: number;
162
+ collections: number;
163
+ refused: Array<{
164
+ actionId: string;
165
+ operation: string;
166
+ reason: string;
167
+ }>;
168
+ }>;