@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,416 @@
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, type MoonshotResponseEnvelope } from './moonshot-twin.ts';
23
+ import type { MessagesSseEvent, SseEvent } from './moonshot-types.ts';
24
+
25
+ export type ConformanceViolation = { check: string; detail: string };
26
+ export type MoonshotConformanceReport = {
27
+ ok: boolean;
28
+ checksRun: number;
29
+ probes: number;
30
+ violations: ConformanceViolation[];
31
+ };
32
+
33
+ type Body = Record<string, any>;
34
+ const isObj = (v: unknown): v is Body => !!v && typeof v === 'object' && !Array.isArray(v);
35
+
36
+ /**
37
+ * THE CLAIMED SURFACE. Every entry is a real request plus the OUTCOME a live handler produces.
38
+ * The expected values are LITERALS here, never imported from the handler — an assertion against
39
+ * the module's own constant is a tautology that cannot catch the value drifting.
40
+ */
41
+ type Probe = {
42
+ /** `"<METHOD> <path>"` — the claim key, and the bijection key. */
43
+ key: string;
44
+ method: string;
45
+ path: string;
46
+ body?: unknown;
47
+ /** Which statuses a live handler may answer with. */
48
+ statuses: number[];
49
+ /** What a live handler's body must look like. */
50
+ ok: (body: unknown, status: number) => boolean;
51
+ /** Seed the root before probing (returns nothing; failures surface as probe failures). */
52
+ seed?: (h: (m: string, p: string, b?: unknown) => Promise<MoonshotResponseEnvelope>) => Promise<void>;
53
+ };
54
+
55
+ const CHAT = { model: 'kimi-k3', messages: [{ role: 'user', content: 'hi' }] };
56
+
57
+ const PROBES: Probe[] = [
58
+ {
59
+ key: 'POST /v1/chat/completions',
60
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: CHAT, statuses: [200],
61
+ ok: (b) => isObj(b) && b.object === 'chat.completion' && Array.isArray(b.choices)
62
+ && (b.choices as Body[])[0]?.message?.role === 'assistant'
63
+ && typeof (b.choices as Body[])[0]?.message?.content === 'string'
64
+ && isObj(b.usage) && typeof b.usage.total_tokens === 'number' && typeof b.usage.cached_tokens === 'number',
65
+ },
66
+ {
67
+ key: 'POST /v1/responses',
68
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k3', input: 'hi' }, statuses: [200],
69
+ ok: (b) => isObj(b) && b.object === 'response' && b.status === 'completed' && Array.isArray(b.output)
70
+ && (b.output as Body[]).some((o) => o.type === 'message')
71
+ && isObj(b.usage) && typeof b.usage.total_tokens === 'number',
72
+ },
73
+ {
74
+ key: 'POST /anthropic/v1/messages',
75
+ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hi' }], max_tokens: 100 }, statuses: [200],
76
+ ok: (b) => isObj(b) && b.type === 'message' && b.role === 'assistant' && Array.isArray(b.content)
77
+ && (b.content as Body[])[0]?.type === 'thinking'
78
+ && typeof b.stop_reason === 'string' && isObj(b.usage) && typeof b.usage.input_tokens === 'number',
79
+ },
80
+ {
81
+ key: 'GET /v1/models',
82
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/models`, statuses: [200],
83
+ ok: (b) => isObj(b) && b.object === 'list' && Array.isArray(b.data) && (b.data as Body[]).some((m) => m.id === 'kimi-k3' && m.object === 'model' && m.owned_by === 'moonshot'),
84
+ },
85
+ {
86
+ key: 'POST /v1/files',
87
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/files`, body: { purpose: 'batch', filename: 'in.jsonl', content: '{}' }, statuses: [200],
88
+ ok: (b) => isObj(b) && b.object === 'file' && b.purpose === 'batch' && b.status === 'ready' && String(b.id).startsWith('file_twin_'),
89
+ },
90
+ {
91
+ key: 'GET /v1/files',
92
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/files`, statuses: [200],
93
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'seed.jsonl', content: '{}' }); },
94
+ ok: (b) => isObj(b) && b.object === 'list' && (b.data as Body[]).some((f) => f.filename === 'seed.jsonl'),
95
+ },
96
+ {
97
+ key: 'GET /v1/files/{file_id}',
98
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1`, statuses: [200],
99
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: 'x' }); },
100
+ ok: (b) => isObj(b) && b.id === 'file_twin_1' && b.filename === 'one.jsonl',
101
+ },
102
+ {
103
+ key: 'GET /v1/files/{file_id}/content',
104
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1/content`, statuses: [200],
105
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: '{"custom_id":"a"}' }); },
106
+ ok: (b) => b === '{"custom_id":"a"}',
107
+ },
108
+ {
109
+ key: 'DELETE /v1/files/{file_id}',
110
+ method: 'DELETE', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1`, statuses: [200],
111
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: 'x' }); },
112
+ ok: (b) => isObj(b) && b.deleted === true && b.id === 'file_twin_1' && b.object === 'file',
113
+ },
114
+ {
115
+ key: 'POST /v1/batches',
116
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches`, body: { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' }, statuses: [200],
117
+ seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' }); },
118
+ ok: (b) => isObj(b) && b.object === 'batch' && b.status === 'validating' && b.endpoint === '/v1/chat/completions' && typeof b.expires_at === 'number',
119
+ },
120
+ {
121
+ key: 'GET /v1/batches',
122
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches`, statuses: [200],
123
+ seed: async (h) => {
124
+ await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
125
+ await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
126
+ },
127
+ ok: (b) => isObj(b) && b.object === 'list' && (b.data as Body[]).some((x) => x.id === 'batch_twin_1'),
128
+ },
129
+ {
130
+ key: 'GET /v1/batches/{batch_id}',
131
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1`, statuses: [200],
132
+ seed: async (h) => {
133
+ await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
134
+ await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
135
+ },
136
+ ok: (b) => isObj(b) && b.id === 'batch_twin_1' && b.input_file_id === 'file_twin_1',
137
+ },
138
+ {
139
+ key: 'POST /v1/batches/{batch_id}/cancel',
140
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1/cancel`, statuses: [200],
141
+ seed: async (h) => {
142
+ await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
143
+ await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
144
+ },
145
+ ok: (b) => isObj(b) && b.status === 'cancelling' && typeof b.cancelling_at === 'number',
146
+ },
147
+ {
148
+ key: 'GET /v1/users/me/balance',
149
+ method: 'GET', path: `${MOONSHOT_API_PREFIX}/users/me/balance`, statuses: [200],
150
+ ok: (b) => isObj(b) && b.code === 0 && b.scode === '0x0' && b.status === true
151
+ && isObj(b.data) && typeof b.data.available_balance === 'number' && typeof b.data.voucher_balance === 'number' && typeof b.data.cash_balance === 'number',
152
+ },
153
+ {
154
+ key: 'POST /v1/tokenizers/estimate-token-count',
155
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tokenizers/estimate-token-count`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hello world, estimate me' }] }, statuses: [200],
156
+ ok: (b) => isObj(b) && isObj(b.data) && typeof b.data.total_tokens === 'number' && b.data.total_tokens > 0,
157
+ },
158
+ {
159
+ key: 'POST /v1/signatures/verify',
160
+ 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],
161
+ ok: (b) => isObj(b) && b.valid === false,
162
+ },
163
+ {
164
+ key: 'POST /v1/tools/search',
165
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search`, body: { text_query: 'kimi api limits', limit: 3 }, statuses: [200],
166
+ ok: (b) => isObj(b) && Array.isArray(b.search_results) && (b.search_results as Body[]).length === 3
167
+ && typeof (b.search_results as Body[])[0]?.url === 'string' && typeof (b.search_results as Body[])[0]?.snippet === 'string',
168
+ },
169
+ {
170
+ key: 'POST /v1/tools/search_pro',
171
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search_pro`, body: { text_query: 'kimi api limits', sites: ['platform.kimi.ai'] }, statuses: [200],
172
+ ok: (b) => isObj(b) && Array.isArray(b.search_results) && (b.search_results as Body[]).length === 5
173
+ && Array.isArray((b.search_results as Body[])[0]?.chunks),
174
+ },
175
+ {
176
+ key: 'POST /v1/tools/fetch',
177
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/fetch`, body: { url: 'https://platform.kimi.ai/docs' }, statuses: [200],
178
+ ok: (b) => isObj(b) && b.url === 'https://platform.kimi.ai/docs' && typeof b.markdown === 'string' && b.markdown.includes('[twin-stub]'),
179
+ },
180
+ ];
181
+
182
+ /**
183
+ * THE ROUTER CENSUS — hand-authored from `routeMoonshot`'s dispatch branches, paired with a
184
+ * request that REACHES that branch. Each entry is checked live: the router must BRANCH on it
185
+ * (i.e. not fall through to its own `Unknown request URL` not-found) — so deleting a handler
186
+ * branch reddens the census by name, independently of that endpoint's own probe.
187
+ */
188
+ const ROUTER_SURFACE: Array<{ key: string; method: string; path: string; body?: unknown }> = [
189
+ { key: 'GET /v1/models', method: 'GET', path: `${MOONSHOT_API_PREFIX}/models` },
190
+ { key: 'POST /v1/chat/completions', method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: CHAT },
191
+ { key: 'POST /v1/responses', method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k3', input: 'x' } },
192
+ { 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' }] } },
193
+ { key: 'POST /v1/signatures/verify', method: 'POST', path: `${MOONSHOT_API_PREFIX}/signatures/verify`, body: { nonce: 'n', timestamp: 1, model: 'kimi-k3', signature: 's' } },
194
+ { key: 'POST /v1/tools/search', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search`, body: { text_query: 'x' } },
195
+ { key: 'POST /v1/tools/search_pro', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search_pro`, body: { text_query: 'x' } },
196
+ { key: 'POST /v1/tools/fetch', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/fetch`, body: { url: 'https://x.example' } },
197
+ { key: 'GET /v1/users/me/balance', method: 'GET', path: `${MOONSHOT_API_PREFIX}/users/me/balance` },
198
+ { key: 'POST /v1/files', method: 'POST', path: `${MOONSHOT_API_PREFIX}/files`, body: { purpose: 'batch', filename: 'a.jsonl', content: 'x' } },
199
+ { key: 'GET /v1/files', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files` },
200
+ { key: 'GET /v1/files/{file_id}', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1` },
201
+ { key: 'GET /v1/files/{file_id}/content', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1/content` },
202
+ { key: 'DELETE /v1/files/{file_id}', method: 'DELETE', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1` },
203
+ { 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' } },
204
+ { key: 'GET /v1/batches', method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches` },
205
+ { key: 'GET /v1/batches/{batch_id}', method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1` },
206
+ { key: 'POST /v1/batches/{batch_id}/cancel', method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1/cancel` },
207
+ { key: 'POST /anthropic/v1/messages', method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 } },
208
+ ];
209
+
210
+ /** The literal the router falls through to when NO branch matched. A census entry answering this
211
+ * means its branch is gone. Kept as a LITERAL — importing the handler's string would be a
212
+ * tautology that could not catch the message drifting. */
213
+ const ROUTER_MISS = 'Unknown request URL';
214
+
215
+ const ROUTER_SURFACE_KEYS: string[] = ROUTER_SURFACE.map((e) => e.key);
216
+
217
+ /**
218
+ * Surface the twin must NOT serve: the bare `/openai/v1` prefix (that is GROQ's shape, not
219
+ * Moonshot's), paths outside both prefixes, and the kimi-k2.x models on the kimi-k3-only
220
+ * endpoints (the OpenAPI's per-request model enums are the closed sets). Each must answer the
221
+ * vendor-shaped not-found/error envelope.
222
+ */
223
+ const MUST_NOT_SERVE: Array<{ method: string; path: string; body?: unknown; why: string }> = [
224
+ { method: 'POST', path: '/openai/v1/chat/completions', body: CHAT, why: 'the /openai/v1 prefix is GroqCloud\'s shape; Moonshot serves /v1' },
225
+ { method: 'GET', path: '/v1/fine_tuning/jobs', why: 'OpenAI has this endpoint; Moonshot\'s OpenAPI does not' },
226
+ { method: 'GET', path: '/v1/embeddings', why: 'Moonshot publishes no embeddings endpoint' },
227
+ { 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' },
228
+ { method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k2.6', input: 'x' }, why: 'the Responses API is kimi-k3 only' },
229
+ { 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' },
230
+ ];
231
+
232
+ /** Run the offline conformance checks against a fresh temp root. */
233
+ export async function checkMoonshotConformance(opts: { root?: string } = {}): Promise<MoonshotConformanceReport> {
234
+ const violations: ConformanceViolation[] = [];
235
+ let checksRun = 0;
236
+ const fail = (check: string, detail: string) => violations.push({ check, detail });
237
+
238
+ // ── 1. PROBES: each in its OWN throwaway root, so a probe's seed can't leak into another. ──
239
+ for (const probe of PROBES) {
240
+ checksRun++;
241
+ // A FRESH root per probe, always. `opts.root` is the PARENT directory, never a shared root:
242
+ // sharing it let `nextId` ratchet across probes, so the `files/file_twin_1` probe read the id
243
+ // an earlier probe had minted and `world-moonshot conformance --root DIR` reported false
244
+ // violations (the groq pack's §9 round-two finding, back-applied here from the start).
245
+ const root = mkdtempSync(join(opts.root ?? tmpdir(), 'moonshot-conf-'));
246
+ const h = (method: string, path: string, body?: unknown) =>
247
+ handleMoonshotTwinRequest({ method, path, ...(body === undefined ? {} : { body: JSON.stringify(body) }), root });
248
+ try {
249
+ if (probe.seed) await probe.seed(h);
250
+ const res = await h(probe.method, probe.path, probe.body);
251
+ if (!probe.statuses.includes(res.status)) {
252
+ fail(`probe:${probe.key}`, `status ${res.status} (expected one of ${probe.statuses.join('/')}) body=${JSON.stringify(res.body).slice(0, 200)}`);
253
+ } else if (!probe.ok(res.body, res.status)) {
254
+ fail(`probe:${probe.key}`, `body did not satisfy the live-handler predicate: ${JSON.stringify(res.body).slice(0, 300)}`);
255
+ }
256
+ } finally {
257
+ rmSync(root, { recursive: true, force: true });
258
+ }
259
+ }
260
+
261
+ // ── 2. BIJECTION: probe table ⇄ router census, both directions. ──
262
+ checksRun++;
263
+ const probeKeys = new Set(PROBES.map((p) => p.key));
264
+ for (const key of ROUTER_SURFACE_KEYS) if (!probeKeys.has(key)) fail('bijection', `router branch "${key}" has no probe`);
265
+ for (const key of probeKeys) if (!ROUTER_SURFACE_KEYS.includes(key)) fail('bijection', `probe "${key}" is not in the hand-authored router census`);
266
+ if (PROBES.length !== new Set(PROBES.map((p) => p.key)).size) fail('bijection', 'duplicate probe key');
267
+ if (ROUTER_SURFACE.length !== ROUTER_SURFACE_KEYS.length) fail('bijection', 'the router census and its key list disagree');
268
+
269
+ // ── 2b. THE CENSUS HAS TEETH: every declared branch must actually be REACHED. A branch whose
270
+ // handler was deleted falls through to the router's own not-found, which is what this
271
+ // catches — independently of that endpoint's own probe.
272
+ for (const entry of ROUTER_SURFACE) {
273
+ checksRun++;
274
+ const root = mkdtempSync(join(opts.root ?? tmpdir(), 'moonshot-conf-'));
275
+ try {
276
+ const res = await handleMoonshotTwinRequest({ method: entry.method, path: entry.path, ...(entry.body === undefined ? {} : { body: JSON.stringify(entry.body) }), root });
277
+ const message = String(((res.body as Body)?.error as Body)?.message ?? ((res.body as Body)?.error?.message as string) ?? '');
278
+ if (message.includes(ROUTER_MISS)) {
279
+ fail('router_surface', `${entry.key} fell through to the router's not-found — the branch is gone (answered ${res.status}: ${message})`);
280
+ }
281
+ if (!ROUTER_SURFACE_KEYS.includes(entry.key)) fail('router_surface', `${entry.key} is exercised but not declared`);
282
+ } finally {
283
+ rmSync(root, { recursive: true, force: true });
284
+ }
285
+ }
286
+
287
+ // ── 3. The twin must not serve surface it does not model (or the vendor does not have). ──
288
+ {
289
+ const root = opts.root ?? mkdtempSync(join(tmpdir(), 'moonshot-conf-'));
290
+ try {
291
+ for (const entry of MUST_NOT_SERVE) {
292
+ checksRun++;
293
+ const res = await handleMoonshotTwinRequest({ method: entry.method, path: entry.path, ...(entry.body === undefined ? {} : { body: JSON.stringify(entry.body) }), root });
294
+ if (res.status < 400) fail('must_not_serve', `${entry.method} ${entry.path} answered ${res.status} — ${entry.why}`);
295
+ }
296
+
297
+ // ── 4. The error envelope on /v1 carries `error.message` (REQUIRED) plus only keys
298
+ // Moonshot's ErrorResponse schema declares: message, type, code. The key list is a
299
+ // LITERAL here — asserting against the handler's own constant would be a tautology.
300
+ checksRun++;
301
+ const ERROR_OBJECT_KEYS = new Set(['message', 'type', 'code']);
302
+ const bad = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ messages: [] }), root });
303
+ const eb = bad.body as Body;
304
+ if (bad.status !== 400 || !isObj(eb?.error)) {
305
+ fail('error.envelope', `missing model did not yield a 400 with an error envelope (status ${bad.status})`);
306
+ } else {
307
+ const undeclared = Object.keys(eb.error).filter((k) => !ERROR_OBJECT_KEYS.has(k));
308
+ if (undeclared.length) fail('error.envelope', `error object carries key(s) Moonshot's ErrorResponse schema does not declare: ${undeclared.join(', ')}`);
309
+ if (typeof eb.error.message !== 'string' || !eb.error.message) fail('error.envelope', 'error.message is not a non-empty string');
310
+ if (eb.error.type !== 'invalid_request_error') fail('error.envelope', `error.type is ${String(eb.error.type)}`);
311
+ }
312
+
313
+ // ── 4b. The Messages surface answers its OWN envelope ({type:'error',error:{type,message}})
314
+ // on EVERY failure — the validation 400 AND the cross-cutting 401/429/503/405 that
315
+ // fire before routing (each of which used to leak the /v1 envelope regardless of
316
+ // prefix, the round-two finding). Anthropic's own error types apply here.
317
+ checksRun++;
318
+ const anthropicShape = (b: unknown): boolean => {
319
+ const mb = b as Body;
320
+ return mb?.type === 'error' && isObj(mb.error) && typeof mb.error.type === 'string' && typeof mb.error.message === 'string' && !('code' in mb.error);
321
+ };
322
+ const mErr = await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }] }), root });
323
+ if (mErr.status !== 400) fail('messages.error_envelope', `missing max_tokens answered ${mErr.status}, not 400`);
324
+ else {
325
+ if (!anthropicShape(mErr.body)) fail('messages.error_envelope', 'Messages error body is not the MessagesErrorResponse shape { type, error:{type,message} }');
326
+ if ('error' in (mErr.body as Body) && isObj((mErr.body as Body).error) && 'code' in (mErr.body as Body).error) fail('messages.error_envelope', 'Messages error envelope carries a `code` key its schema does not declare');
327
+ }
328
+ const cross: Array<[string, MoonshotResponseEnvelope, string]> = [
329
+ ['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'],
330
+ ['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'],
331
+ ['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'],
332
+ ['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'],
333
+ ];
334
+ for (const [what, res, wantType] of cross) {
335
+ if (res.status !== Number(what)) fail('messages.error_envelope', `the ${what} cross-cutting failure answered ${res.status}`);
336
+ else if (!anthropicShape(res.body)) fail('messages.error_envelope', `the ${what} cross-cutting failure leaked a non-Anthropic envelope: ${JSON.stringify(res.body).slice(0, 120)}`);
337
+ else if ((res.body as Body).error.type !== wantType) fail('messages.error_envelope', `the ${what} cross-cutting failure answered error.type ${(res.body as Body).error.type}, not ${wantType}`);
338
+ }
339
+
340
+ // ── 5. Per-model parameter gating, straight from the OpenAPI's per-request schemas: ──
341
+ // kimi-k2.7-code REJECTS thinking.type 'disabled'; kimi-k2.6 accepts it; kimi-k3 has
342
+ // no thinking parameter at all; reasoning_effort is kimi-k3-only.
343
+ checksRun++;
344
+ 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 });
345
+ 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 });
346
+ 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 });
347
+ 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 });
348
+ 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 });
349
+ if (k27Disabled.status !== 400) fail('chat.per_model_gating', `kimi-k2.7-code thinking.type 'disabled' answered ${k27Disabled.status}, not 400`);
350
+ if (k26Disabled.status !== 200) fail('chat.per_model_gating', `kimi-k2.6 thinking.type 'disabled' answered ${k26Disabled.status}, not 200`);
351
+ if (k3Thinking.status !== 400) fail('chat.per_model_gating', `kimi-k3 'thinking' answered ${k3Thinking.status}, not 400 (kimi-k3 uses reasoning_effort)`);
352
+ if (k3Effort.status !== 200) fail('chat.per_model_gating', `kimi-k3 reasoning_effort answered ${k3Effort.status}, not 200`);
353
+ if (k26Effort.status !== 400) fail('chat.per_model_gating', `kimi-k2.6 reasoning_effort answered ${k26Effort.status}, not 400`);
354
+
355
+ // ── 6. The streaming chunk sequence, including Moonshot's usage-bearing tail chunk. ──
356
+ checksRun++;
357
+ const events: SseEvent[] = [];
358
+ await handleMoonshotTwinRequest({
359
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, root,
360
+ body: JSON.stringify({ ...CHAT, stream: true }),
361
+ sseSink: (e) => events.push(e),
362
+ });
363
+ const hasRole = events.some((e) => !e.done && ((e.data!.choices as Body[])?.[0]?.delta as Body)?.role === 'assistant');
364
+ const done = events.length > 0 && events[events.length - 1]!.done === true;
365
+ const firstObj = events[0]?.data?.object;
366
+ const tail = events[events.length - 2]?.data as Body | undefined;
367
+ if (!hasRole) fail('chat.stream', 'no role delta chunk');
368
+ if (!done) fail('chat.stream', 'stream did not end with [DONE]');
369
+ if (firstObj !== 'chat.completion.chunk') fail('chat.stream', `first chunk object is ${String(firstObj)}`);
370
+ if (!tail || !Array.isArray(tail.choices) || tail.choices.length !== 0 || !isObj(tail.usage)) {
371
+ fail('chat.stream', 'final chunk is not Moonshot\'s empty-choices usage tail');
372
+ }
373
+
374
+ // ── 7. The Anthropic-compatible streaming grammar: named events, message_start →
375
+ // thinking/text deltas → message_delta → message_stop, NO [DONE] sentinel. ──
376
+ checksRun++;
377
+ const mEvents: MessagesSseEvent[] = [];
378
+ await handleMoonshotTwinRequest({
379
+ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, root,
380
+ body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 200, stream: true }),
381
+ messagesSseSink: (e) => mEvents.push(e),
382
+ });
383
+ const names = mEvents.filter((e) => !e.done).map((e) => e.event ?? '');
384
+ if (names[0] !== 'message_start') fail('messages.stream', `first event is ${String(names[0])}, not message_start`);
385
+ if (!names.includes('content_block_delta') || !names.includes('content_block_stop')) fail('messages.stream', 'no content_block deltas');
386
+ if (names[names.length - 1] !== 'message_stop') fail('messages.stream', `last event is ${String(names[names.length - 1])}, not message_stop`);
387
+ if (mEvents.some((e) => (e as SseEvent).done === false && (e as SseEvent).data === undefined && e.event === undefined)) fail('messages.stream', 'unnamed event in the Anthropic grammar');
388
+
389
+ // ── 8. tool_calls envelope when tools are provided. ──
390
+ checksRun++;
391
+ const tool = await handleMoonshotTwinRequest({
392
+ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, root,
393
+ body: JSON.stringify({ ...CHAT, tools: [{ type: 'function', function: { name: 'get_weather', parameters: { type: 'object', properties: { city: { type: 'string' } } } } }] }),
394
+ });
395
+ const choice = ((tool.body as Body).choices as Body[])?.[0];
396
+ if (choice?.finish_reason !== 'tool_calls') fail('chat.tool_calls', 'finish_reason not tool_calls');
397
+ const calls = choice?.message?.tool_calls as Body[];
398
+ if (!Array.isArray(calls) || calls[0]?.type !== 'function' || calls[0]?.function?.name !== 'get_weather') fail('chat.tool_calls', 'no get_weather function tool_call');
399
+
400
+ // ── 9. Thinking mode surfaces reasoning_content; disabled k2.6 does not. ──
401
+ checksRun++;
402
+ const k3 = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify(CHAT), root });
403
+ const k3Msg = ((k3.body as Body).choices as Body[])?.[0]?.message as Body;
404
+ if (typeof k3Msg?.reasoning_content !== 'string' || !k3Msg.reasoning_content.includes('[twin-stub:kimi-k3]')) {
405
+ fail('chat.reasoning_content', 'kimi-k3 (always thinking) did not surface reasoning_content');
406
+ }
407
+ 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 });
408
+ const k26Msg = ((k26off.body as Body).choices as Body[])?.[0]?.message as Body;
409
+ if ('reasoning_content' in (k26Msg ?? {})) fail('chat.reasoning_content', "kimi-k2.6 with thinking.type 'disabled' surfaced reasoning_content");
410
+ } finally {
411
+ if (opts.root === undefined) rmSync(root, { recursive: true, force: true });
412
+ }
413
+ }
414
+
415
+ return { ok: violations.length === 0, checksRun, probes: PROBES.length, violations };
416
+ }