@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,175 @@
1
+ // The moonshot pack's scenario system on the kernel's ONE engine (@volter/world-core
2
+ // scenario.ts): Moonshot chat-completions vocabulary + the scripted-turn respond shape. THE
3
+ // KERNEL ENGINE IS NOT RE-IMPLEMENTED HERE — this file is only the pack's adapter (features,
4
+ // matchers, load validation) plus the realizer that turns a validated `respond` into Moonshot's
5
+ // own envelope shapes.
6
+ //
7
+ // The handler FILE (handlers/moonshot.json in a world dir) is the only write surface; the doors
8
+ // (`GET /twin`, `GET /twin/scenario`) are read-only. Scenario support is twin-only scaffolding
9
+ // for eval worlds, NOT vendor surface: the manifest's `moonshot.scenario.*` entries verify THIS
10
+ // scaffolding (filed under their own `scenario` area, section-commented "NOT vendor surface"),
11
+ // and the behavior is gated by moonshot-scenario.test.ts.
12
+ import { getActiveWorldStore, parseScenarioDocument, ScenarioError, ScenarioEngine } from '@volter/world-core';
13
+ import { contentToText, fnv1a, lastUserText } from "./moonshot-stub.js";
14
+ const RESPOND_KEYS = new Set(['text', 'reasoning', 'toolCalls', 'finishReason', 'error']);
15
+ const FINISH_REASONS = new Set(['stop', 'length', 'tool_calls']);
16
+ const ERROR_TYPES = new Set(['rate_limit_reached_error', 'server_unavailable', 'server_error']);
17
+ const THINKING_TYPES = new Set(['enabled', 'disabled']);
18
+ const nonEmptyString = (cond) => typeof cond === 'string' && cond.length > 0;
19
+ function lastToolResultNames(messages) {
20
+ const names = new Set();
21
+ const last = messages[messages.length - 1];
22
+ if (!last || last.role !== 'tool' || typeof last.tool_call_id !== 'string')
23
+ return names;
24
+ for (const m of messages) {
25
+ const am = m;
26
+ if (am.role !== 'assistant' || !Array.isArray(am.tool_calls))
27
+ continue;
28
+ for (const tc of am.tool_calls) {
29
+ if (tc?.id === last.tool_call_id && typeof tc?.function?.name === 'string')
30
+ names.add(tc.function.name);
31
+ }
32
+ }
33
+ return names;
34
+ }
35
+ function toolNames(tools) {
36
+ if (!Array.isArray(tools))
37
+ return [];
38
+ return tools.map((t) => typeof t?.function?.name === 'string' ? t.function.name : typeof t?.name === 'string' ? t.name : null).filter((n) => n !== null);
39
+ }
40
+ export const moonshotScenarioAdapter = {
41
+ vendor: 'moonshot',
42
+ // R15 — a status fault in THIS vendor's envelope: the same {error:{message,type}} bodies
43
+ // moonshot-twin.ts serves for its own refusals (its 429 is `type: 'rate_limit_reached_error'`,
44
+ // its outage is `server_unavailable`). The Messages surface re-wears the body in the Anthropic
45
+ // envelope at its own route; the fault result carries the Moonshot shape.
46
+ renderFault: (f) => ({
47
+ body: f.status === 429
48
+ ? { error: { message: f.message ?? 'Request rate limit reached: 3 requests per minute (Tier 0). Please retry after 20 seconds.', type: 'rate_limit_reached_error' } }
49
+ : f.status >= 500
50
+ ? { error: { message: f.message ?? 'The server is overloaded or not ready to handle the request. Please try again later.', type: f.status === 503 ? 'server_unavailable' : 'server_error' } }
51
+ : { error: { message: f.message ?? 'The request was refused by a scripted fault.', type: 'invalid_request_error' } },
52
+ }),
53
+ features: (req) => ({
54
+ model: req.model,
55
+ lastUserText: lastUserText(req.messages).slice(0, 300),
56
+ tools: toolNames(req.tools),
57
+ lastMessageIsToolResult: req.messages[req.messages.length - 1]?.role === 'tool',
58
+ toolResultFor: [...lastToolResultNames(req.messages)],
59
+ thinkingType: req.thinking?.type ?? 'enabled',
60
+ }),
61
+ matchers: {
62
+ modelEquals: (req, cond) => nonEmptyString(cond) && req.model === cond,
63
+ userTextIncludes: (req, cond) => nonEmptyString(cond) && lastUserText(req.messages).toLowerCase().includes(cond.toLowerCase()),
64
+ anyTextIncludes: (req, cond) => nonEmptyString(cond) && req.messages.map((m) => contentToText(m.content)).join('\n').toLowerCase().includes(cond.toLowerCase()),
65
+ lastMessageIsToolResult: (req, cond) => typeof cond === 'boolean' && (req.messages[req.messages.length - 1]?.role === 'tool') === cond,
66
+ toolResultFor: (req, cond) => nonEmptyString(cond) && lastToolResultNames(req.messages).has(cond),
67
+ hasTool: (req, cond) => nonEmptyString(cond) && toolNames(req.tools).includes(cond),
68
+ // Moonshot-specific: script behaviour per thinking mode (kimi-k2.6 accepts 'disabled';
69
+ // kimi-k2.7-code does not).
70
+ thinkingTypeEquals: (req, cond) => nonEmptyString(cond) && (req.thinking?.type ?? 'enabled') === cond,
71
+ },
72
+ text: (req) => req.messages.map((m) => contentToText(m.content)).join('\n'),
73
+ validateOn: (on) => {
74
+ for (const k of ['modelEquals', 'userTextIncludes', 'anyTextIncludes', 'toolResultFor', 'hasTool']) {
75
+ if (on[k] !== undefined && (typeof on[k] !== 'string' || !on[k]))
76
+ return `on.${k} is a non-empty string`;
77
+ }
78
+ if (on.lastMessageIsToolResult !== undefined && typeof on.lastMessageIsToolResult !== 'boolean')
79
+ return 'on.lastMessageIsToolResult is a boolean';
80
+ if (on.thinkingTypeEquals !== undefined) {
81
+ if (typeof on.thinkingTypeEquals !== 'string' || !THINKING_TYPES.has(on.thinkingTypeEquals)) {
82
+ return `on.thinkingTypeEquals is one of ${[...THINKING_TYPES].join(', ')}`;
83
+ }
84
+ }
85
+ return null;
86
+ },
87
+ validateRespond: (respond) => {
88
+ if (typeof respond !== 'object' || respond === null || Array.isArray(respond))
89
+ return 'respond is an object { text?, reasoning?, toolCalls?, finishReason?, error? }';
90
+ const r = respond;
91
+ for (const k of Object.keys(r))
92
+ if (!RESPOND_KEYS.has(k))
93
+ return `respond: unknown key "${k}" (valid: ${[...RESPOND_KEYS].join(', ')})`;
94
+ if (r.error !== undefined) {
95
+ const e = r.error;
96
+ if (!e || typeof e !== 'object' || Array.isArray(e))
97
+ return 'respond.error is an object { type, message? }';
98
+ if (typeof e.type !== 'string' || !ERROR_TYPES.has(e.type))
99
+ return `respond.error.type is one of ${[...ERROR_TYPES].join(', ')}`;
100
+ if (e.message !== undefined && typeof e.message !== 'string')
101
+ return 'respond.error.message is a string';
102
+ // An error handler scripts a FAILURE — mixing it with success content is a mis-typed rule.
103
+ for (const k of ['text', 'reasoning', 'toolCalls', 'finishReason'])
104
+ if (r[k] !== undefined)
105
+ return `respond.error cannot be combined with respond.${k}`;
106
+ return null;
107
+ }
108
+ if (r.text !== undefined && typeof r.text !== 'string')
109
+ return 'respond.text is a string';
110
+ if (r.reasoning !== undefined && typeof r.reasoning !== 'string')
111
+ return 'respond.reasoning is a string';
112
+ if (r.finishReason !== undefined && (typeof r.finishReason !== 'string' || !FINISH_REASONS.has(r.finishReason)))
113
+ return `respond.finishReason is one of ${[...FINISH_REASONS].join(', ')}`;
114
+ if (r.toolCalls !== undefined) {
115
+ for (const tc of Array.isArray(r.toolCalls) ? r.toolCalls : [r.toolCalls]) {
116
+ const t = tc;
117
+ if (!t || typeof t !== 'object' || Array.isArray(t))
118
+ return 'respond.toolCalls entries are objects';
119
+ if (typeof t.name !== 'string' || !t.name)
120
+ return 'respond.toolCalls[].name is a non-empty string';
121
+ if (!t.arguments || typeof t.arguments !== 'object' || Array.isArray(t.arguments))
122
+ return 'respond.toolCalls[].arguments is an object';
123
+ if (t.id !== undefined && typeof t.id !== 'string')
124
+ return 'respond.toolCalls[].id is a string';
125
+ }
126
+ }
127
+ if (r.text === undefined && r.toolCalls === undefined)
128
+ return 'respond needs text or toolCalls (or an error)';
129
+ return null;
130
+ },
131
+ };
132
+ /** Load + STRICTLY validate a scenario document. A malformed file throws at load with what is
133
+ * wrong — never a silent ignore-and-stub (a mis-typed rule falling back is a fake success). */
134
+ export function loadMoonshotScenarioDocument(path) {
135
+ let parsed;
136
+ try {
137
+ // Read through the ACTIVE WorldStore, never the filesystem directly (runtime contract
138
+ // R12b): the handlers document is WORLD STATE, so a MemoryWorldStore / DO-backed world
139
+ // serves ITS OWN scenario instead of whatever happens to sit on the host disk — and the
140
+ // serve path stays workerd-clean. A missing document keeps the historical ENOENT wording,
141
+ // so the loud load-time failure reads byte-identically to the read it replaces.
142
+ const raw = getActiveWorldStore().read(path);
143
+ if (raw === null)
144
+ throw new Error(`ENOENT: no such file or directory, open '${path}'`);
145
+ parsed = JSON.parse(raw);
146
+ }
147
+ catch (e) {
148
+ throw new ScenarioError(`moonshot scenario: cannot read/parse ${path}: ${e instanceof Error ? e.message : String(e)}`);
149
+ }
150
+ return parseScenarioDocument(parsed, moonshotScenarioAdapter);
151
+ }
152
+ export function createMoonshotScenarioEngine(document) {
153
+ return new ScenarioEngine(moonshotScenarioAdapter, document);
154
+ }
155
+ /**
156
+ * Turn a validated `respond` into the pack's own faithful assistant turn.
157
+ *
158
+ * Determinism (CLAUDE.md): the tool-call id is derived from the scripted call's own content and
159
+ * its position in the handler — never a module-level counter, which would make two identical
160
+ * scripted requests answer differently after a restart.
161
+ */
162
+ export function realizeMoonshotRespond(respond) {
163
+ const toolCalls = [];
164
+ const scripted = respond.toolCalls ? (Array.isArray(respond.toolCalls) ? respond.toolCalls : [respond.toolCalls]) : [];
165
+ scripted.forEach((tc, index) => {
166
+ const seed = `${index}:${tc.name}:${JSON.stringify(tc.arguments)}`;
167
+ toolCalls.push({ id: tc.id ?? `call_scripted_${fnv1a(seed).toString(36)}`, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments) } });
168
+ });
169
+ return {
170
+ text: respond.text ?? (toolCalls.length ? null : ''),
171
+ reasoning: respond.reasoning ?? null,
172
+ toolCalls,
173
+ finishReason: respond.finishReason ?? (toolCalls.length ? 'tool_calls' : 'stop'),
174
+ };
175
+ }
@@ -0,0 +1,13 @@
1
+ /** Options every Moonshot-twin HTTP surface needs, independent of who owns the socket. */
2
+ export interface MoonshotTwinFetchOptions {
3
+ root?: string;
4
+ readOnly?: boolean;
5
+ scenarioPath?: string;
6
+ }
7
+ export declare function createMoonshotTwinFetch(options: MoonshotTwinFetchOptions): (request: Request) => Promise<Response>;
8
+ export declare function createMoonshotTwinServer(options?: MoonshotTwinFetchOptions & {
9
+ port?: number;
10
+ }): Promise<{
11
+ port: number;
12
+ stop: () => void;
13
+ }>;
@@ -0,0 +1,202 @@
1
+ // Moonshot twin HTTP server — serve the full Moonshot twin handler over HTTP so the REAL clients
2
+ // work UNMODIFIED: the standard `openai` SDK with `baseURL: 'http://127.0.0.1:<port>/v1'`, the
3
+ // `anthropic` SDK with `baseURL: 'http://127.0.0.1:<port>/anthropic'`, and plain HTTP callers.
4
+ // There is no vendor SDK to configure — Moonshot's documented integration path IS these clients
5
+ // with a swapped base URL (platform.kimi.ai/docs/overview, read 2026-09-16).
6
+ //
7
+ // Streaming: when a chat/responses/messages request body has `"stream": true`, the server
8
+ // constructs a REAL SSE response by feeding the handler an injected sink — OpenAI grammar writes
9
+ // `data: <json>\n\n` frames ending with `data: [DONE]\n\n`; the Anthropic-compatible Messages
10
+ // surface and the Responses surface write `event: <name>\ndata: <json>\n\n` frames (Anthropic's
11
+ // grammar has no [DONE] sentinel; the stream ends after `message_stop` / `response.completed`).
12
+ //
13
+ // Multipart: POST /v1/files takes `multipart/form-data`. The server parses the form into the
14
+ // handler's JSON contract so the handler stays a pure JSON function.
15
+ //
16
+ // FETCH-FIRST (runtime contract R12b): the surface is the plain `createMoonshotTwinFetch` and the
17
+ // SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
18
+ // (`createTwinFetchFromHandler`): three SSE grammars plus the multipart adaptation genuinely
19
+ // exceed the common shape — openai-server.ts is the reference for that lane.
20
+ import { serveHttp, twinManifest, worldNow } from '@volter/world-core';
21
+ import { handleMoonshotTwinRequest, MESSAGES_PREFIX, MOONSHOT_API_PREFIX, } from "./moonshot-twin.js";
22
+ import { createMoonshotScenarioEngine, loadMoonshotScenarioDocument } from "./moonshot-scenario.js";
23
+ function wantsStream(body) {
24
+ if (!body)
25
+ return false;
26
+ try {
27
+ return JSON.parse(body)?.stream === true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ function encodeSse(event) {
34
+ if (event.done)
35
+ return 'data: [DONE]\n\n';
36
+ return `data: ${JSON.stringify(event.data)}\n\n`;
37
+ }
38
+ /** Named-event framing (Anthropic Messages / Responses grammar): `event: <name>\ndata: <json>`. */
39
+ function encodeNamedSse(event) {
40
+ if (event.done)
41
+ return '\n';
42
+ const name = event.event ?? 'message';
43
+ return `event: ${name}\ndata: ${JSON.stringify(event.data)}\n\n`;
44
+ }
45
+ // The three streaming POST endpoints, each with its own wire grammar.
46
+ const OPENAI_STREAMABLE = new Set([`${MOONSHOT_API_PREFIX}/chat/completions`]);
47
+ const NAMED_STREAMABLE = new Set([`${MESSAGES_PREFIX}/messages`, `${MOONSHOT_API_PREFIX}/responses`]);
48
+ async function multipartToJson(request) {
49
+ try {
50
+ const form = await request.formData();
51
+ const out = {};
52
+ // Forward every scalar field generically: `purpose` and `filename` on Moonshot's Files API.
53
+ for (const [key, value] of form.entries()) {
54
+ if (typeof value === 'string') {
55
+ if (key.endsWith('[]')) {
56
+ const k = key.slice(0, -2);
57
+ const prior = out[k];
58
+ out[k] = Array.isArray(prior) ? [...prior, value] : [value];
59
+ }
60
+ else {
61
+ out[key] = value;
62
+ }
63
+ }
64
+ }
65
+ const file = form.get('file');
66
+ if (!(file instanceof File)) {
67
+ // The vendor's Upload File REQUIRES the file body (§9 round two, F3): a form without one
68
+ // reaches createFile marked, and the handler refuses it rather than minting an empty
69
+ // 'ready' file. (form.get returns the FIRST 'file' part when several are sent; the
70
+ // vendor's form has one.)
71
+ out._multipart_missing_file = true;
72
+ }
73
+ if (file instanceof File) {
74
+ const bytes = new Uint8Array(await file.arrayBuffer());
75
+ out.file = file.name || 'upload';
76
+ out.filename = file.name || 'upload';
77
+ // The handler's JSON contract carries content as a STRING, so binary uploads travel
78
+ // base64-encoded WITH the `binary_content: true` marker; the file row stores the marker
79
+ // and the /content route decodes before serving (the twin imports nothing from the
80
+ // server — the marker is the contract). Encoding was a §9-round-one finding: the adapter
81
+ // used to store the base64 TEXT itself, so a real multipart client got base64 back from
82
+ // GET /content while `bytes` counted the raw length.
83
+ out.content = Buffer.from(bytes).toString('base64');
84
+ out.binary_content = true;
85
+ out.media_type = file.type || '';
86
+ out.bytes = bytes.length;
87
+ }
88
+ return JSON.stringify(out);
89
+ }
90
+ catch {
91
+ return '{}';
92
+ }
93
+ }
94
+ export function createMoonshotTwinFetch(options) {
95
+ const readOnly = options.readOnly ?? false;
96
+ // Scenario scripting (moonshot-scenario.ts): a JSON scenario file — via the scenarioPath option
97
+ // or the TWIN_MOONSHOT_SCENARIO env var — scripts the three inference endpoints. Loaded ONCE at
98
+ // startup (a malformed file fails loudly here, never silently).
99
+ const scenarioPath = options.scenarioPath ?? process.env.TWIN_MOONSHOT_SCENARIO;
100
+ const scenarioEngine = scenarioPath ? createMoonshotScenarioEngine(loadMoonshotScenarioDocument(scenarioPath)) : undefined;
101
+ return async function moonshotTwinFetch(request) {
102
+ const url = new URL(request.url);
103
+ // THE READ DOORS (TWIN-PROGRAMMING-MODEL): discovery + inspection, read-only.
104
+ if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin') {
105
+ return Response.json(twinManifest({
106
+ vendor: 'moonshot',
107
+ twinOf: 'Moonshot (Kimi) Platform API — OpenAI-compatible under /v1, Anthropic-compatible under /anthropic/v1; Moonshot ships no SDK of its own, its documented clients are the standard openai/anthropic SDKs with a swapped base URL',
108
+ stateSentence: 'Seed files through the ordinary API (POST /v1/files, multipart) with any key; files, batches and the account balance are stateful, everything else is deterministic.',
109
+ behaviorSentence: "The three inference endpoints are scripted by MSW-shaped handlers in the world dir (handlers/moonshot.json): {on:{userTextIncludes|anyTextIncludes|modelEquals|hasTool|toolResultFor|lastMessageIsToolResult|thinkingTypeEquals}, respond:{text|reasoning|toolCalls, finishReason?} | {error:{type:rate_limit_reached_error|server_unavailable|server_error}}, once?, phase?}. Unmatched requests answer a labeled stub naming this door.",
110
+ exampleHandler: { on: { userTextIncludes: 'summarize', hasTool: 'search_docs' }, respond: { text: 'Scripted summary.' }, once: true },
111
+ engine: scenarioEngine,
112
+ }));
113
+ }
114
+ if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin/scenario') {
115
+ return Response.json(scenarioEngine ? scenarioEngine.status() : { vendor: 'moonshot', handlers: [], misses: 0, recentMisses: [] });
116
+ }
117
+ const path = url.pathname + (url.search || '');
118
+ // Collapse REPEATED slashes as well as the trailing one, exactly as `routeMoonshot` does when
119
+ // it builds `seg` (and as the budget's split does for its anchored rules) — the two
120
+ // normalizations must agree or `POST //v1/chat/completions` with `stream: true` reaches the
121
+ // streaming ROUTE but misses the STREAMABLE set and is answered with a unary JSON body to a
122
+ // client reading text/event-stream (the groq pack's §9 round-two finding).
123
+ const cleanPath = url.pathname.replace(/\/{2,}/g, '/').replace(/\/+$/, '');
124
+ const contentType = request.headers.get('content-type') ?? '';
125
+ const passHeaders = {};
126
+ // `x-api-key` + `anthropic-version`: the /anthropic surface's documented client is the
127
+ // UNMODIFIED `@anthropic-ai/sdk`, which authenticates with x-api-key (never a bearer) and
128
+ // sends anthropic-version on every call — not forwarding them made the whole surface
129
+ // 401-dead for exactly the client Moonshot documents.
130
+ for (const k of ['authorization', 'x-api-key', 'anthropic-version', 'x-msh-request-nonce', 'x-twin-force-rate-limit', 'x-twin-force-server-unavailable']) {
131
+ const v = request.headers.get(k);
132
+ if (v !== null)
133
+ passHeaders[k] = v;
134
+ }
135
+ let body = '';
136
+ if (request.method !== 'GET') {
137
+ body = contentType.includes('multipart/form-data') ? await multipartToJson(request) : await request.text();
138
+ }
139
+ // Streaming POST → a real text/event-stream response built from the sink.
140
+ //
141
+ // Events are COLLECTED first, then framed. The handler is synchronous-fast, so buffering
142
+ // costs nothing — and it is what lets a PRE-STREAM failure (a 400 for a bad body, an auth
143
+ // 401, a rate-limit 429) answer with its REAL status and the vendor's JSON error envelope.
144
+ // Emitting the refusal as a lone data frame inside a 200 text/event-stream was a FAKE
145
+ // SUCCESS: Moonshot rejects a bad request BEFORE opening the event stream.
146
+ const isStreamable = !readOnly && request.method.toUpperCase() === 'POST'
147
+ && (OPENAI_STREAMABLE.has(cleanPath) || NAMED_STREAMABLE.has(cleanPath))
148
+ && wantsStream(body);
149
+ if (isStreamable) {
150
+ const named = NAMED_STREAMABLE.has(cleanPath);
151
+ const events = [];
152
+ const sink = (e) => events.push(e);
153
+ const namedSink = (e) => events.push(e);
154
+ const { status, body: out, headers: errHeaders } = await handleMoonshotTwinRequest({
155
+ ...(scenarioEngine ? { scenarioEngine } : {}),
156
+ method: request.method, path, body, readOnly, occurredAt: worldNow(), headers: passHeaders,
157
+ ...(options.root !== undefined ? { root: options.root } : {}),
158
+ sseSink: named ? undefined : sink,
159
+ messagesSseSink: named ? namedSink : undefined,
160
+ });
161
+ const resHeaders = { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache', connection: 'keep-alive', ...(errHeaders ?? {}) };
162
+ if (status !== 200) {
163
+ // A pre-stream failure answers its REAL status + the vendor's JSON envelope.
164
+ return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', ...(errHeaders ?? {}) } });
165
+ }
166
+ const frames = events.map((e) => ('event' in e && e.event !== undefined ? encodeNamedSse(e) : encodeSse(e))).join('');
167
+ // The Msh-Request-* signature headers are minted IN THE HANDLER (one signer for every
168
+ // surface — see handleMoonshotTwinRequest) and arrive here through `errHeaders`, which
169
+ // resHeaders already spreads.
170
+ return new Response(frames, { status: 200, headers: resHeaders });
171
+ }
172
+ // Unary path.
173
+ const { status, body: out, headers: errHeaders } = await handleMoonshotTwinRequest({
174
+ ...(scenarioEngine ? { scenarioEngine } : {}),
175
+ method: request.method, path, body, readOnly, occurredAt: worldNow(), headers: passHeaders,
176
+ ...(options.root !== undefined ? { root: options.root } : {}),
177
+ });
178
+ // The file-content route returns raw bytes; everything else is JSON. The handler's
179
+ // `x-twin-content-binary` response header (§9 round two, F1) says how to turn the body
180
+ // STRING into bytes: binary content is latin1-carried (every byte value intact, converted
181
+ // back here — a plain `new Response(string)` would re-encode as UTF-8 and mangle every
182
+ // byte ≥ 0x80); text content is the string itself, written UTF-8 so a non-ASCII text file
183
+ // round-trips byte for byte.
184
+ const isRawContent = request.method === 'GET' && /\/v1\/files\/[^/]+\/content$/.test(cleanPath);
185
+ if (isRawContent && status === 200) {
186
+ const binary = errHeaders?.['x-twin-content-binary'] === '1';
187
+ const bytes = binary ? Buffer.from(String(out), 'latin1') : Buffer.from(String(out), 'utf8');
188
+ const headers = { ...errHeaders };
189
+ delete headers['x-twin-content-binary'];
190
+ return new Response(bytes, { status, headers: { 'content-type': 'application/octet-stream', ...headers } });
191
+ }
192
+ return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', ...(errHeaders ?? {}) } });
193
+ };
194
+ }
195
+ export async function createMoonshotTwinServer(options = {}) {
196
+ const server = await serveHttp({
197
+ port: options.port ?? 0,
198
+ idleTimeout: 60,
199
+ fetch: createMoonshotTwinFetch(options),
200
+ });
201
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
202
+ }
@@ -0,0 +1,70 @@
1
+ import type { MoonshotMessageParam, MoonshotToolCall } from './moonshot-types.js';
2
+ /** Deterministic token estimate for a string: ~1 token per 4 chars (faithful order of
3
+ * magnitude; deterministic so usage counts are assertable). Never zero for non-empty text. */
4
+ export declare function estimateTokens(text: string): number;
5
+ /** Flatten a chat message's content (string OR content-part array) to its text for token
6
+ * counting / echo. Non-text parts contribute their JSON length so the count is deterministic
7
+ * and reflects payload size. */
8
+ export declare function contentToText(content: unknown): string;
9
+ /** Deterministic prompt-token count for a set of chat messages. */
10
+ export declare function countPromptTokens(messages: MoonshotMessageParam[]): number;
11
+ /** The last user turn's text — the thing the stub echoes (deterministic, clearly labeled). */
12
+ export declare function lastUserText(messages: MoonshotMessageParam[]): string;
13
+ /**
14
+ * Build the deterministic stub ASSISTANT text. It is unmistakably a twin stub: it carries the
15
+ * `[twin-stub:<model>]` marker and echoes the prompt, so no caller can mistake it for real
16
+ * model output. Deterministic for a given prompt → assertable in tests.
17
+ */
18
+ export declare function stubAssistantText(messages: MoonshotMessageParam[], model: string): string;
19
+ /**
20
+ * The `reasoning_content` string a thinking-mode model returns (kimi-k3 always; k2.6/k2.7-code
21
+ * when thinking is enabled). A labeled stub, like the content — never a real chain of thought.
22
+ */
23
+ export declare function stubReasoningContent(messages: MoonshotMessageParam[], model: string): string;
24
+ /** A deterministic stub signature for a Messages `thinking` block (the vendor asks callers to
25
+ * pass it back unchanged; the twin's is a deterministic label). */
26
+ export declare function stubSignature(messages: MoonshotMessageParam[], model: string): string;
27
+ /**
28
+ * Moonshot's automatic context caching: a hit requires MORE THAN 256 prompt tokens
29
+ * (platform.kimi.ai/docs/guides/context-caching — automatic caching, read 2026-09-16). The twin
30
+ * derives the cached share deterministically from the token count: at or below the threshold the
31
+ * cache is cold (0); above it a fixed 25% of the prompt is served from cache. That fraction is a
32
+ * judgement call, not a vendor figure — what IS vendor-shaped is the FIELD (cached_tokens in
33
+ * chat usage; cached_tokens / cache_write_tokens in Responses usage) and the threshold rule.
34
+ */
35
+ export declare const CACHE_HIT_THRESHOLD = 256;
36
+ export declare function stubCachedTokens(promptTokens: number): number;
37
+ export declare function buildChatUsage(promptTokens: number, completionTokens: number, promptCacheKey?: string): {
38
+ prompt_tokens: number;
39
+ completion_tokens: number;
40
+ total_tokens: number;
41
+ cached_tokens: number;
42
+ };
43
+ /** Build a deterministic stub argument string for a tool (schema-typed placeholders). Reads the
44
+ * OpenAI shape (`function.parameters`), the bare `parameters`, AND the Anthropic Messages shape
45
+ * (`input_schema`) — the Messages surface stubs its tool_use input through the same helper. */
46
+ export declare function stubToolArguments(tool: unknown): string;
47
+ /**
48
+ * When tools are provided, the stub deterministically "calls" the tool selected by `forcedName`
49
+ * (a named tool_choice) or the FIRST provided tool. Returns the tool_call, or null when no
50
+ * tools were provided.
51
+ */
52
+ export declare function stubToolCall(tools: unknown, seq: number, forcedName?: string): MoonshotToolCall | null;
53
+ /** Build a deterministic JSON-object stub for `response_format` json_object / json_schema. */
54
+ export declare function stubJsonObject(messages: MoonshotMessageParam[], model: string, jsonSchema?: unknown): string;
55
+ /** A small deterministic 32-bit hash (FNV-1a) of a string. */
56
+ export declare function fnv1a(text: string): number;
57
+ /** A deterministic id suffix from a request (so ids are stable + assertable). */
58
+ export declare function stableSuffix(...parts: unknown[]): string;
59
+ /**
60
+ * A deterministic stub web-search result set for POST /v1/tools/search{,_pro} and the Responses
61
+ * web_search tool. The twin cannot reach the web (D4 — no real network), so the results are a
62
+ * labeled, deterministic echo of the query: same query → same results.
63
+ */
64
+ export declare function stubSearchResults(textQuery: string, limit: number, pro: boolean, includeContent?: boolean): Array<Record<string, unknown>>;
65
+ /** A deterministic stub markdown page for POST /v1/tools/fetch. */
66
+ export declare function stubFetchedMarkdown(url: string): {
67
+ url: string;
68
+ markdown: string;
69
+ title: string;
70
+ };