@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.
- package/LICENSE +202 -0
- package/README.md +164 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +25 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +86 -0
- package/dist/src/moonshot-budget.d.ts +57 -0
- package/dist/src/moonshot-budget.js +142 -0
- package/dist/src/moonshot-capabilities.d.ts +4 -0
- package/dist/src/moonshot-capabilities.js +1200 -0
- package/dist/src/moonshot-conformance.d.ts +14 -0
- package/dist/src/moonshot-conformance.js +405 -0
- package/dist/src/moonshot-connector.d.ts +168 -0
- package/dist/src/moonshot-connector.js +416 -0
- package/dist/src/moonshot-models.d.ts +36 -0
- package/dist/src/moonshot-models.js +37 -0
- package/dist/src/moonshot-scenario.d.ts +54 -0
- package/dist/src/moonshot-scenario.js +175 -0
- package/dist/src/moonshot-server.d.ts +13 -0
- package/dist/src/moonshot-server.js +202 -0
- package/dist/src/moonshot-stub.d.ts +70 -0
- package/dist/src/moonshot-stub.js +222 -0
- package/dist/src/moonshot-twin.d.ts +144 -0
- package/dist/src/moonshot-twin.js +1647 -0
- package/dist/src/moonshot-types.d.ts +251 -0
- package/dist/src/moonshot-types.js +19 -0
- package/package.json +53 -0
- package/src/cli.ts +25 -0
- package/src/index.ts +129 -0
- package/src/moonshot-budget.ts +163 -0
- package/src/moonshot-capabilities.ts +1220 -0
- package/src/moonshot-conformance.ts +416 -0
- package/src/moonshot-connector.ts +465 -0
- package/src/moonshot-models.ts +89 -0
- package/src/moonshot-scenario.ts +194 -0
- package/src/moonshot-server.ts +220 -0
- package/src/moonshot-stub.ts +230 -0
- package/src/moonshot-twin.ts +1670 -0
- package/src/moonshot-types.ts +225 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
// Moonshot CONNECTOR — the live-vendor pull/push path that gives the Moonshot twin the full
|
|
2
|
+
// "git for SaaS" lifecycle (pull real state → mirror; push local writes → real).
|
|
3
|
+
//
|
|
4
|
+
// PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
|
|
5
|
+
// the real state system.
|
|
6
|
+
// REFRESH (real → twin): fetch the real Models / Files / Batches / Balance, map them to
|
|
7
|
+
// observed resources, and fold them onto the root's log through the
|
|
8
|
+
// kernel's `observeResources` (content-addressed entries, so re-pulling
|
|
9
|
+
// identical state appends nothing).
|
|
10
|
+
// PERFORM (twin → real): the HEAD calls `performMoonshotAction` per deployable entry; the
|
|
11
|
+
// vendor's minted id comes back as the `externalId` the kernel adopts.
|
|
12
|
+
//
|
|
13
|
+
// The vendor I/O is an INJECTED executor (B3 auth boundary): the kernel + this pack hold NO
|
|
14
|
+
// Moonshot key and import NO SDK at runtime. Tests pass a fake executor; live runs pass
|
|
15
|
+
// `liveMoonshotExecute(apiKey)`. Same code path either way — fully exercisable offline.
|
|
16
|
+
import { assertBudgetGuardIntact, confirmAction, deployableEntries, observeResources, resolveSubjectId } from '@volter/world-core';
|
|
17
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
18
|
+
import { MoonshotBudget, MoonshotBudgetError, moonshotCallWeight, type MoonshotBudgetOptions } from './moonshot-budget.ts';
|
|
19
|
+
|
|
20
|
+
const SERVICE = 'moonshot';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The injected real-Moonshot boundary. `request` issues ONE Moonshot REST call:
|
|
24
|
+
* method — 'GET' | 'POST' | 'DELETE'
|
|
25
|
+
* path — e.g. '/v1/files' or '/v1/batches/batch_123/cancel'
|
|
26
|
+
* body — JSON body for POST (omitted otherwise)
|
|
27
|
+
* Returns the parsed JSON (an object, a `{ data }` list, or an `{ error }` envelope).
|
|
28
|
+
*/
|
|
29
|
+
export type MoonshotExecute = (
|
|
30
|
+
method: 'GET' | 'POST' | 'DELETE',
|
|
31
|
+
path: string,
|
|
32
|
+
body?: Record<string, unknown>,
|
|
33
|
+
) => Promise<{ data?: any; error?: { message?: string; type?: string }; [k: string]: unknown }>;
|
|
34
|
+
|
|
35
|
+
/** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
|
|
36
|
+
export type LiveMoonshotOptions = {
|
|
37
|
+
/** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
/** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
|
|
40
|
+
budget?: MoonshotBudget;
|
|
41
|
+
/** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
|
|
42
|
+
budgetOptions?: MoonshotBudgetOptions;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A live executor against the real Moonshot REST API (the user's own API key). Sends the required
|
|
47
|
+
* `Authorization: Bearer` header. Never imported by the pack's own serve path — only constructed
|
|
48
|
+
* by a caller that opts into real I/O.
|
|
49
|
+
*
|
|
50
|
+
* THIS IS THE ONE PLACE this pack issues a live `api.moonshot.ai` request, and therefore the one
|
|
51
|
+
* place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
|
|
52
|
+
* the request goes out (`checkBudget`, which THROWS `MoonshotBudgetError` instead of returning
|
|
53
|
+
* when the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
|
|
54
|
+
* `retry-after` / 429 / `x-ratelimit-remaining: 0` signal becomes a persisted cooldown that makes
|
|
55
|
+
* every later call fail fast WITHOUT touching Moonshot. There is deliberately no OPTION to
|
|
56
|
+
* disable the guard, and no value a caller can pass for `budget` that yields an unguarded client.
|
|
57
|
+
* What that does NOT claim is immunity from a caller who WANTS one: a fresh `budgetOptions.path`
|
|
58
|
+
* per construction, or an injected clock, restores the allowance, because the same seam tests
|
|
59
|
+
* need cannot be denied to a determined caller in the same process. See `moonshot-budget.ts` and
|
|
60
|
+
* the kernel header for the limits of the guarantee.
|
|
61
|
+
*/
|
|
62
|
+
export function liveMoonshotExecute(
|
|
63
|
+
apiKey: string,
|
|
64
|
+
base = 'https://api.moonshot.ai',
|
|
65
|
+
opts: LiveMoonshotOptions = {},
|
|
66
|
+
): MoonshotExecute {
|
|
67
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
68
|
+
// ONE expression decides which budget is used, so there is no second, weaker test that could
|
|
69
|
+
// disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
|
|
70
|
+
// must be an UNMODIFIED MoonshotBudget — a duck-typed stand-in, a SUBCLASS overriding
|
|
71
|
+
// `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that
|
|
72
|
+
// would otherwise hand back a client with no ceiling. The default ledger is keyed by a hash of
|
|
73
|
+
// THIS key: Moonshot limits per account, so a cwd-scoped ledger would hand the same key a
|
|
74
|
+
// fresh allowance per checkout/worktree/CI leg.
|
|
75
|
+
const budget = opts.budget !== undefined && opts.budget !== null
|
|
76
|
+
? assertBudgetGuardIntact(opts.budget, MoonshotBudget, 'liveMoonshotExecute')
|
|
77
|
+
: new MoonshotBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
|
|
78
|
+
return async (method, path, body) => {
|
|
79
|
+
// A path this pack never modelled is refused BEFORE the budget is even charged: an executor
|
|
80
|
+
// that will happily issue any URL is how a careless script reaches an unpriced endpoint.
|
|
81
|
+
if (!path.startsWith('/v1/') && !path.startsWith('/anthropic/v1/')) {
|
|
82
|
+
throw new Error(`moonshot: refusing to call an unmodeled path "${path}" — Moonshot serves /v1 and /anthropic/v1`);
|
|
83
|
+
}
|
|
84
|
+
const headers: Record<string, string> = { authorization: `Bearer ${apiKey}` };
|
|
85
|
+
const init: { method: string; headers: Record<string, string>; body?: string } = { method, headers };
|
|
86
|
+
if (method === 'POST') {
|
|
87
|
+
headers['content-type'] = 'application/json';
|
|
88
|
+
init.body = JSON.stringify(body ?? {});
|
|
89
|
+
}
|
|
90
|
+
const weight = moonshotCallWeight(method, path);
|
|
91
|
+
// THROWS instead of calling. Nothing below this line runs when the budget refuses.
|
|
92
|
+
const reservation = budget.checkBudget(weight);
|
|
93
|
+
const res = await doFetch(`${base}${path}`, init);
|
|
94
|
+
const resHeaders: Record<string, string> = {};
|
|
95
|
+
res.headers.forEach((v: string, k: string) => { resHeaders[k.toLowerCase()] = v; });
|
|
96
|
+
const parsed = (await res.json()) as { data?: any; error?: { message?: string; type?: string } };
|
|
97
|
+
// Settles the reservation and, on a back-off signal, arms the cooldown.
|
|
98
|
+
// recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
|
|
99
|
+
// call that louder refusal wins; an answer Moonshot ACCEPTED is kept, so a write that landed is
|
|
100
|
+
// never recorded as failed and performed again on retry.
|
|
101
|
+
try {
|
|
102
|
+
budget.recordCall(weight, resHeaders, { status: res.status, reservation });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!(error instanceof MoonshotBudgetError) || !res.ok) throw error;
|
|
105
|
+
}
|
|
106
|
+
return parsed;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function listOf(res: { data?: unknown }): any[] {
|
|
111
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A REFUSED pull is NOT an empty account. Moonshot answers failures with its documented
|
|
115
|
+
* `{ error: { message, type } }` envelope, so a status check alone is not enough — throw on the
|
|
116
|
+
* envelope rather than folding an empty list over real observed state.
|
|
117
|
+
*/
|
|
118
|
+
function throwIfError(res: { error?: { message?: string; type?: string } }, ctx: string): void {
|
|
119
|
+
if (res.error) throw new Error(`moonshot ${ctx} failed: ${res.error.message ?? res.error.type ?? 'unknown error'}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── PULL ────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/** Map a real-Moonshot model object → a twin sync resource. */
|
|
125
|
+
export function mapModel(m: Record<string, unknown>): SyncResource {
|
|
126
|
+
return {
|
|
127
|
+
type: 'model',
|
|
128
|
+
id: String(m.id),
|
|
129
|
+
fields: {
|
|
130
|
+
object: 'model',
|
|
131
|
+
created: (m.created as number) ?? null,
|
|
132
|
+
owned_by: (m.owned_by as string) ?? null,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Map a real-Moonshot File object → a twin sync resource. */
|
|
138
|
+
export function mapFile(f: Record<string, unknown>): SyncResource {
|
|
139
|
+
return {
|
|
140
|
+
type: 'file',
|
|
141
|
+
id: String(f.id),
|
|
142
|
+
fields: {
|
|
143
|
+
object: 'file',
|
|
144
|
+
bytes: (f.bytes as number) ?? 0,
|
|
145
|
+
created_at: (f.created_at as number) ?? null,
|
|
146
|
+
filename: (f.filename as string) ?? null,
|
|
147
|
+
purpose: (f.purpose as string) ?? null,
|
|
148
|
+
status: (f.status as string) ?? null,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Map a real-Moonshot Batch object → a twin sync resource. */
|
|
154
|
+
export function mapBatch(b: Record<string, unknown>): SyncResource {
|
|
155
|
+
const counts = (b.request_counts && typeof b.request_counts === 'object') ? (b.request_counts as Record<string, unknown>) : {};
|
|
156
|
+
return {
|
|
157
|
+
type: 'batch',
|
|
158
|
+
id: String(b.id),
|
|
159
|
+
fields: {
|
|
160
|
+
object: 'batch',
|
|
161
|
+
endpoint: (b.endpoint as string) ?? null,
|
|
162
|
+
input_file_id: (b.input_file_id as string) ?? null,
|
|
163
|
+
completion_window: (b.completion_window as string) ?? null,
|
|
164
|
+
status: (b.status as string) ?? null,
|
|
165
|
+
output_file_id: (b.output_file_id as string) ?? null,
|
|
166
|
+
created_at: (b.created_at as number) ?? null,
|
|
167
|
+
completed_at: (b.completed_at as number) ?? null,
|
|
168
|
+
request_counts: counts,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Map the real-Moonshot balance → the twin's 'balance' resource (id 'me'). */
|
|
174
|
+
export function mapBalance(b: Record<string, unknown>): SyncResource {
|
|
175
|
+
const d = (b.data && typeof b.data === 'object') ? (b.data as Record<string, unknown>) : {};
|
|
176
|
+
return {
|
|
177
|
+
type: 'balance',
|
|
178
|
+
id: 'me',
|
|
179
|
+
fields: {
|
|
180
|
+
available_balance: (d.available_balance as number) ?? null,
|
|
181
|
+
voucher_balance: (d.voucher_balance as number) ?? null,
|
|
182
|
+
cash_balance: (d.cash_balance as number) ?? null,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const COLLECTIONS: Array<{ path: string; map: (r: Record<string, unknown>) => SyncResource }> = [
|
|
188
|
+
{ path: '/v1/models', map: mapModel },
|
|
189
|
+
{ path: '/v1/files', map: mapFile },
|
|
190
|
+
{ path: '/v1/batches', map: mapBatch },
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
/** Pull all modeled real collections via the executor and map them to twin sync resources. */
|
|
194
|
+
export async function pullMoonshotState(execute: MoonshotExecute): Promise<SyncResource[]> {
|
|
195
|
+
const out: SyncResource[] = [];
|
|
196
|
+
for (const c of COLLECTIONS) {
|
|
197
|
+
const res = await execute('GET', c.path);
|
|
198
|
+
throwIfError(res, `pull ${c.path}`);
|
|
199
|
+
for (const item of listOf(res)) out.push(c.map(item));
|
|
200
|
+
}
|
|
201
|
+
// The balance is its own envelope ({code, data:{…}, scode, status}) — no `data` list.
|
|
202
|
+
const bal = await execute('GET', '/v1/users/me/balance');
|
|
203
|
+
throwIfError(bal, 'pull /v1/users/me/balance');
|
|
204
|
+
out.push(mapBalance(bal as Record<string, unknown>));
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Pull from real Moonshot and fold into the twin (mirror seeding). Observation entries are
|
|
210
|
+
* content-addressed, so a re-pull of identical state appends nothing.
|
|
211
|
+
*
|
|
212
|
+
* `occurredAt` has NO pinned default on purpose: the kernel hashes an observed event over
|
|
213
|
+
* (occurredAt + post-state), so under a fixed poll time a vendor value that REVERTS across polls
|
|
214
|
+
* collides with its own earlier observation and the fold reports a phantom delta while the
|
|
215
|
+
* projection keeps the stale value (ADDING_A_TWIN.md §6). Callers pass a moving timestamp.
|
|
216
|
+
*/
|
|
217
|
+
export async function syncMoonshotFromReal(
|
|
218
|
+
execute: MoonshotExecute,
|
|
219
|
+
opts: { root?: string; occurredAt: string },
|
|
220
|
+
): Promise<{ observed: number; deltasAppended: number }> {
|
|
221
|
+
const resources = await pullMoonshotState(execute);
|
|
222
|
+
const result = observeResources(SERVICE, resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields })), {
|
|
223
|
+
...(opts.root !== undefined ? { root: opts.root } : {}), at: opts.occurredAt, batch: `obs:${SERVICE}:${opts.occurredAt}`,
|
|
224
|
+
});
|
|
225
|
+
return { observed: result.observed, deltasAppended: result.appended };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── PUSH ────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
// The twin operations this connector knows how to push to real Moonshot. Anything not here must
|
|
231
|
+
// FAIL LOUDLY rather than be silently dropped — pushing an unrecognized op risks hitting the
|
|
232
|
+
// wrong endpoint or no-op'ing a real change.
|
|
233
|
+
const PUSHABLE_VERBS = new Set(['create', 'cancel', 'delete']);
|
|
234
|
+
|
|
235
|
+
/** Throw if `op` is not a write operation this connector can faithfully push. */
|
|
236
|
+
function assertPushable(op: string): void {
|
|
237
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
238
|
+
if (!PUSHABLE_VERBS.has(verb)) {
|
|
239
|
+
throw new Error(`moonshot push: unsupported operation '${op}' — refusing to silently drop a local write`);
|
|
240
|
+
}
|
|
241
|
+
const why = UNPUSHABLE[op];
|
|
242
|
+
if (why) throw new Error(`moonshot push: cannot push '${op}' — ${why}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Why this action cannot be pushed, or null if it can. Pure — no vendor call on its path. */
|
|
246
|
+
export function unpushableReason(op: string): string | null {
|
|
247
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
248
|
+
if (!PUSHABLE_VERBS.has(verb)) return `unsupported operation '${op}'`;
|
|
249
|
+
return UNPUSHABLE[op] ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Map a subject type → its REST collection path.
|
|
253
|
+
const COLLECTION_PATH: Record<string, string> = {
|
|
254
|
+
file: '/v1/files',
|
|
255
|
+
batch: '/v1/batches',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Pushes this connector must REFUSE rather than fake, keyed `"<type>.<verb>"`.
|
|
260
|
+
*
|
|
261
|
+
* `file.create` is the case: real Moonshot's `POST /v1/files` is multipart/form-data carrying
|
|
262
|
+
* the actual file body, while this executor sends JSON. Pushing `{purpose, filename}` as JSON
|
|
263
|
+
* would 400 at the vendor, and the twin stores no binary content to upload anyway. Refusing
|
|
264
|
+
* loudly is the honest answer; the gap is filed as `moonshot.connector.push_file_create`.
|
|
265
|
+
*/
|
|
266
|
+
const UNPUSHABLE: Record<string, string> = {
|
|
267
|
+
'file.create': "real Moonshot's POST /v1/files is multipart/form-data with the file body; this JSON executor cannot express it, and the twin stores no real bytes to send",
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The twin's LOCALLY-MINTED id namespace (`file_twin_1`, `batch_twin_3` — see `nextId` in
|
|
272
|
+
* moonshot-twin.ts). A subject still bearing one has no counterpart in the real account.
|
|
273
|
+
*/
|
|
274
|
+
const LOCAL_ID = /_twin_\d+$/;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Resolve the id the VENDOR knows this subject by.
|
|
278
|
+
*
|
|
279
|
+
* The kernel records the vendor's minted id at landing time (`vendorSubjectId` → the landed copy
|
|
280
|
+
* carries the vendor's id, `aliasOf` the local one), and `resolveSubjectId` answers it — so a
|
|
281
|
+
* later delete/cancel addresses the vendor by THAT. A locally-minted subject with no recorded
|
|
282
|
+
* alias is refused rather than guessed at — addressing the real account by the twin's own mint
|
|
283
|
+
* would DELETE or CANCEL a resource the vendor never had.
|
|
284
|
+
*/
|
|
285
|
+
export function externalIdFor(subjectType: string, subjectId: string, root?: string): string | null {
|
|
286
|
+
if (!LOCAL_ID.test(subjectId)) return subjectId; // already a vendor id (e.g. observed by a pull)
|
|
287
|
+
const resolved = resolveSubjectId(SERVICE, subjectType, subjectId, root);
|
|
288
|
+
return resolved !== subjectId ? resolved : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolve the REST (method, path) for ONE pending action — faithful to the real Moonshot REST
|
|
293
|
+
* surface:
|
|
294
|
+
* - <type>.create → POST <collection>
|
|
295
|
+
* - <type>.cancel → POST <collection>/:id/cancel
|
|
296
|
+
* - <type>.delete → DELETE <collection>/:id
|
|
297
|
+
*/
|
|
298
|
+
export function moonshotRequestForAction(
|
|
299
|
+
action: Pick<TwinAction, 'operation' | 'subject'>,
|
|
300
|
+
/** The id the VENDOR knows this subject by. Required for anything but a create — see
|
|
301
|
+
* `externalIdFor`; passing the twin's own mint would address a resource the vendor never had. */
|
|
302
|
+
externalId?: string,
|
|
303
|
+
): { method: 'GET' | 'POST' | 'DELETE'; path: string } {
|
|
304
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
305
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
306
|
+
const collection = COLLECTION_PATH[action.subject.type];
|
|
307
|
+
if (!collection) throw new Error(`moonshot push: no REST collection for subject type '${action.subject.type}'`);
|
|
308
|
+
if (verb === 'create') return { method: 'POST', path: collection };
|
|
309
|
+
const vendorId = externalId ?? action.subject.id;
|
|
310
|
+
if (LOCAL_ID.test(vendorId)) {
|
|
311
|
+
throw new Error(`moonshot push: refusing to address the real account by the twin's own id '${vendorId}' — no vendor id was ever recorded for this subject`);
|
|
312
|
+
}
|
|
313
|
+
if (verb === 'cancel') return { method: 'POST', path: `${collection}/${vendorId}/cancel` };
|
|
314
|
+
if (verb === 'delete') return { method: 'DELETE', path: `${collection}/${vendorId}` };
|
|
315
|
+
// assertPushable rejects anything else, so this is only reached for the pushable verbs above.
|
|
316
|
+
return { method: 'POST', path: collection };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Push ONE pending action to REAL Moonshot via the injected executor. Returns the real external
|
|
321
|
+
* id (the object id from the response; for a create that's a freshly minted id, otherwise it
|
|
322
|
+
* echoes the subject). WRITES TO THE REAL ACCOUNT.
|
|
323
|
+
*/
|
|
324
|
+
export async function pushMoonshotAction(
|
|
325
|
+
execute: MoonshotExecute,
|
|
326
|
+
action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>,
|
|
327
|
+
opts: { externalId?: string } = {},
|
|
328
|
+
): Promise<{ externalId: string }> {
|
|
329
|
+
assertPushable(action.operation ?? `${action.subject.type}.update`);
|
|
330
|
+
const { method, path } = moonshotRequestForAction(action, opts.externalId);
|
|
331
|
+
const verb = (action.operation ?? '').includes('.') ? action.operation!.slice(action.operation!.indexOf('.') + 1) : '';
|
|
332
|
+
const payload = verb === 'create' ? createPayload(action) : undefined;
|
|
333
|
+
const res = await execute(method, path, payload);
|
|
334
|
+
throwIfError(res, `push ${action.subject.type}`);
|
|
335
|
+
const id = (res as { id?: unknown }).id;
|
|
336
|
+
return { externalId: typeof id === 'string' && id ? id : action.subject.id };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function createPayload(action: Pick<TwinAction, 'subject' | 'fields'>): Record<string, unknown> {
|
|
340
|
+
const f = (action.fields ?? {}) as Record<string, unknown>;
|
|
341
|
+
switch (action.subject.type) {
|
|
342
|
+
case 'batch':
|
|
343
|
+
return { input_file_id: f.input_file_id, endpoint: f.endpoint, completion_window: f.completion_window, ...(f.metadata ? { metadata: f.metadata } : {}) };
|
|
344
|
+
default:
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── PROTOCOL 2: the pack's half of the real state system ────────────────────────────────────
|
|
350
|
+
/** The pack's executor over the kernel's: the same Moonshot call, carried by the head. The
|
|
351
|
+
* credential never enters this file — the kernel's executor applies it to the authorization
|
|
352
|
+
* header before this sees the request. */
|
|
353
|
+
export function moonshotExecuteOver(execute: RemoteExecute): MoonshotExecute {
|
|
354
|
+
return async (method, path, body) => {
|
|
355
|
+
const res = await execute({
|
|
356
|
+
method, path,
|
|
357
|
+
headers: { accept: 'application/json', authorization: 'Bearer twin', ...(method === 'POST' ? { 'content-type': 'application/json' } : {}) },
|
|
358
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
359
|
+
});
|
|
360
|
+
if (res.body === '') return {};
|
|
361
|
+
try { return JSON.parse(res.body) as Record<string, unknown>; } catch { return { error: { type: 'server_error', message: res.body.slice(0, 200) } }; }
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The refresh adapter: pull the account's models / files / batches / balance through the executor. */
|
|
366
|
+
export async function syncMoonshotFromRemote(execute: RemoteExecute, opts: { root?: string; origin?: string; occurredAt?: string } = {}): Promise<ReturnType<typeof syncMoonshotFromReal>> {
|
|
367
|
+
return syncMoonshotFromReal(moonshotExecuteOver(execute), {
|
|
368
|
+
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
369
|
+
occurredAt: opts.occurredAt ?? new Date().toISOString(),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* The PERFORM adapter: one deployable entry crosses to Moonshot, or settles with the reason it
|
|
375
|
+
* never could. `file.create` is unpushable-BY-DESIGN (the real endpoint is multipart with the
|
|
376
|
+
* file body; this executor sends JSON) — it settles without crossing rather than throwing, so it
|
|
377
|
+
* cannot wedge every later batch action behind it. A locally-minted subject with no vendor alias
|
|
378
|
+
* THROWS (the head records the failure): addressing the real account by the twin's own mint would
|
|
379
|
+
* delete or cancel a resource the vendor never had.
|
|
380
|
+
*/
|
|
381
|
+
export async function performMoonshotAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome> {
|
|
382
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
383
|
+
const why = unpushableReason(op);
|
|
384
|
+
if (why) return { externalId: action.subject.id, data: { performed: false, reason: why } };
|
|
385
|
+
const vendorId = ctx.resolve(action.subject.type, action.subject.id);
|
|
386
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
387
|
+
if (verb !== 'create' && LOCAL_ID.test(vendorId)) {
|
|
388
|
+
throw new Error(`moonshot push: refusing to address the real account by the twin's own id '${vendorId}' — no vendor id was ever recorded for this subject`);
|
|
389
|
+
}
|
|
390
|
+
const { externalId } = await pushMoonshotAction(moonshotExecuteOver(execute), { operation: op, subject: action.subject, fields: action.fields ?? {} }, ...(verb !== 'create' ? [{ externalId: vendorId }] as const : [] as const));
|
|
391
|
+
return { externalId };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Push the twin's PENDING local actions to real Moonshot and CONFIRM each. Idempotency: a
|
|
396
|
+
* confirmed action is no longer deployable, so a re-push enacts NOTHING.
|
|
397
|
+
*
|
|
398
|
+
* Every action this pack records is SINGLE-RESOURCE (one file, one batch), so confirming with
|
|
399
|
+
* `fields` alone is correct — see ADDING_A_TWIN.md §5 on compound actions, which this pack has
|
|
400
|
+
* none of. If a compound write is ever added here, it must ride its extra resources in
|
|
401
|
+
* `additionalObservations` or the push will silently delete them.
|
|
402
|
+
*/
|
|
403
|
+
export async function pushPendingMoonshotActions(
|
|
404
|
+
execute: MoonshotExecute,
|
|
405
|
+
opts: { root?: string; occurredAt: string },
|
|
406
|
+
): Promise<{ pushed: number; confirmed: string[]; externalIds: Record<string, string>; refused: Array<{ actionId: string; operation: string; reason: string }> }> {
|
|
407
|
+
const confirmed: string[] = [];
|
|
408
|
+
const externalIds: Record<string, string> = {};
|
|
409
|
+
const refused: Array<{ actionId: string; operation: string; reason: string }> = [];
|
|
410
|
+
for (const action of deployableEntries(SERVICE, opts.root)) {
|
|
411
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
412
|
+
// An action this connector cannot faithfully push is SKIPPED AND REPORTED, never confirmed
|
|
413
|
+
// and never silently dropped: it stays deployable, and it is named in `refused` so a caller
|
|
414
|
+
// can see it. Throwing instead meant ONE unpushable action — e.g. the local file create,
|
|
415
|
+
// whose real endpoint is multipart — aborted the whole sweep and took every unrelated
|
|
416
|
+
// pending write down with it.
|
|
417
|
+
const why = unpushableReason(op);
|
|
418
|
+
if (why) { refused.push({ actionId: action.id, operation: op, reason: why }); continue; }
|
|
419
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
420
|
+
// Anything but a create must address the vendor by the id the vendor issued. A locally-minted
|
|
421
|
+
// subject with none recorded is REFUSED, never guessed.
|
|
422
|
+
let external: string | undefined;
|
|
423
|
+
if (verb !== 'create') {
|
|
424
|
+
const resolved = externalIdFor(action.subject.type, action.subject.id, opts.root);
|
|
425
|
+
if (resolved === null) {
|
|
426
|
+
refused.push({ actionId: action.id, operation: op, reason: `no vendor id recorded for ${action.subject.type} '${action.subject.id}' — it was never pushed to the real account, so there is nothing there to ${verb}` });
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
external = resolved;
|
|
430
|
+
}
|
|
431
|
+
const { externalId } = await pushMoonshotAction(execute, action, ...(external !== undefined ? [{ externalId: external }] as const : [] as const));
|
|
432
|
+
// The kernel records the vendor's id as the subject ALIAS (the landed copy carries the
|
|
433
|
+
// vendor's id, aliasOf the local one), so a later delete/cancel resolves through
|
|
434
|
+
// `resolveSubjectId` — no `_external_id` field to read back.
|
|
435
|
+
confirmAction({
|
|
436
|
+
service: SERVICE, actionId: action.id, subject: action.subject,
|
|
437
|
+
fields: action.fields ?? {},
|
|
438
|
+
occurredAt: opts.occurredAt, vendorSubjectId: externalId, receipt: { status: 'deployed' },
|
|
439
|
+
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
440
|
+
});
|
|
441
|
+
confirmed.push(action.id);
|
|
442
|
+
externalIds[action.id] = externalId;
|
|
443
|
+
}
|
|
444
|
+
return { pushed: confirmed.length, confirmed, externalIds, refused };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── FULL bi-directional sync ─────────────────────────────────────────────────
|
|
448
|
+
/**
|
|
449
|
+
* FULL bi-directional sync over the injected client: (1) PUSH every deployable local entry to
|
|
450
|
+
* real Moonshot and confirm it, then (2) PULL all modeled collections back and fold them onto
|
|
451
|
+
* the root's log. Pushing first means the pull observes the twin's own writes as confirmed
|
|
452
|
+
* external state (no double-count). Re-running with nothing deployable and identical real state
|
|
453
|
+
* is a no-op.
|
|
454
|
+
*/
|
|
455
|
+
export async function fullSyncMoonshot(
|
|
456
|
+
execute: MoonshotExecute,
|
|
457
|
+
opts: { root?: string; occurredAt: string },
|
|
458
|
+
): Promise<{ pushed: number; observed: number; deltasAppended: number; collections: number; refused: Array<{ actionId: string; operation: string; reason: string }> }> {
|
|
459
|
+
const push = await pushPendingMoonshotActions(execute, { occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
|
|
460
|
+
const resources = await pullMoonshotState(execute);
|
|
461
|
+
const pull = observeResources(SERVICE, resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields })), {
|
|
462
|
+
...(opts.root !== undefined ? { root: opts.root } : {}), at: opts.occurredAt, batch: `obs:${SERVICE}:${opts.occurredAt}`,
|
|
463
|
+
});
|
|
464
|
+
return { pushed: push.pushed, observed: pull.observed, deltasAppended: pull.appended, collections: COLLECTIONS.length + 1, refused: push.refused };
|
|
465
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Moonshot model catalog — the static `GET /v1/models` surface. The real platform serves a list
|
|
2
|
+
// of OpenAI-shaped model objects; the twin returns a faithful, deterministic slice so
|
|
3
|
+
// `client.models.list()` / `client.models.retrieve(id)` round-trip like the vendor.
|
|
4
|
+
//
|
|
5
|
+
// PROVENANCE (what is sourced, and what is not — §9 discipline):
|
|
6
|
+
// • the model ids are the exact strings Moonshot's own OpenAPI enumerates per request schema:
|
|
7
|
+
// KimiK3ChatRequest['model'] = 'kimi-k3'; KimiK27CodeChatRequest['model'] =
|
|
8
|
+
// 'kimi-k2.7-code' | 'kimi-k2.7-code-highspeed'; KimiK26ChatRequest['model'] = 'kimi-k2.6';
|
|
9
|
+
// EstimateTokenRequest['model'] repeats the same four. (platform.kimi.ai/docs/openapi.json,
|
|
10
|
+
// read 2026-09-16.)
|
|
11
|
+
// • `context_length` comes from Moonshot's published model table
|
|
12
|
+
// (platform.kimi.ai/docs/models, read 2026-09-16): kimi-k3 1M (1,048,576); kimi-k2.7-code
|
|
13
|
+
// and kimi-k2.7-code-highspeed 256k (262,144); kimi-k2.6 256k.
|
|
14
|
+
// • `created` is NOT sourced — Moonshot publishes no per-model creation timestamp. The values
|
|
15
|
+
// are deterministic placeholders and no capability asserts them beyond `typeof === number`.
|
|
16
|
+
// Calling that out is the point: the rest of this table is sourced, this column is not.
|
|
17
|
+
// • RETIRED ids are deliberately ABSENT: moonshot-v1-*, kimi-latest (2026-01-28) and the
|
|
18
|
+
// kimi-k2 series (2026-05-25) are shut down per the same models page. A twin that still
|
|
19
|
+
// served them would let a stale client "work" locally and then 404 against the vendor.
|
|
20
|
+
//
|
|
21
|
+
// Moonshot has NO embeddings, audio, or image-generation models — chat, Responses, Messages,
|
|
22
|
+
// files, batches, token counting, signatures and web-search tools are the whole surface.
|
|
23
|
+
export type MoonshotModel = {
|
|
24
|
+
id: string;
|
|
25
|
+
object: 'model';
|
|
26
|
+
created: number;
|
|
27
|
+
owned_by: string;
|
|
28
|
+
/** The OpenAPI request schema's enum for this id — which endpoints accept it. */
|
|
29
|
+
supports: {
|
|
30
|
+
/** POST /v1/chat/completions (all four). */
|
|
31
|
+
chat: boolean;
|
|
32
|
+
/** POST /v1/responses — kimi-k3 only ("This endpoint currently supports kimi-k3"). */
|
|
33
|
+
responses: boolean;
|
|
34
|
+
/** POST /anthropic/v1/messages — kimi-k3 only (MessagesRequest['model'] enum). */
|
|
35
|
+
messages: boolean;
|
|
36
|
+
/** POST /v1/batches — only kimi-k2.7-code(-highspeed) and kimi-k2.6, NOT kimi-k3. */
|
|
37
|
+
batch: boolean;
|
|
38
|
+
/** Vision (image_url parts in chat messages). kimi-k3 and kimi-k2.6 are vision models. */
|
|
39
|
+
vision: boolean;
|
|
40
|
+
};
|
|
41
|
+
context_length: number;
|
|
42
|
+
/** Max output tokens. kimi-k3: default 131072, up to 1048576 (ChatRequestCommon
|
|
43
|
+
* max_completion_tokens description). The k2.6/k2.7-code models share the 256k window. */
|
|
44
|
+
max_completion_tokens: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const K3_CTX = 1_048_576;
|
|
48
|
+
const K2_CTX = 262_144;
|
|
49
|
+
|
|
50
|
+
export const MOONSHOT_MODELS: MoonshotModel[] = [
|
|
51
|
+
{
|
|
52
|
+
id: 'kimi-k3', object: 'model', created: 1_760_000_000, owned_by: 'moonshot',
|
|
53
|
+
supports: { chat: true, responses: true, messages: true, batch: false, vision: true },
|
|
54
|
+
context_length: K3_CTX, max_completion_tokens: 1_048_576,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'kimi-k2.7-code', object: 'model', created: 1_760_000_001, owned_by: 'moonshot',
|
|
58
|
+
supports: { chat: true, responses: false, messages: false, batch: true, vision: false },
|
|
59
|
+
context_length: K2_CTX, max_completion_tokens: K2_CTX,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'kimi-k2.7-code-highspeed', object: 'model', created: 1_760_000_002, owned_by: 'moonshot',
|
|
63
|
+
supports: { chat: true, responses: false, messages: false, batch: true, vision: false },
|
|
64
|
+
context_length: K2_CTX, max_completion_tokens: K2_CTX,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'kimi-k2.6', object: 'model', created: 1_760_000_003, owned_by: 'moonshot',
|
|
68
|
+
supports: { chat: true, responses: false, messages: false, batch: true, vision: true },
|
|
69
|
+
context_length: K2_CTX, max_completion_tokens: K2_CTX,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/** Resolve a model by id, or undefined if the twin doesn't model it. */
|
|
74
|
+
export function findModel(id: string): MoonshotModel | undefined {
|
|
75
|
+
return MOONSHOT_MODELS.find((m) => m.id === id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The ids that accept a batch job's chat/completions requests. */
|
|
79
|
+
export const BATCH_MODELS = new Set(MOONSHOT_MODELS.filter((m) => m.supports.batch).map((m) => m.id));
|
|
80
|
+
|
|
81
|
+
/** kimi-k3's `reasoning_effort` — a CLOSED documented set (KimiK3ChatRequest). */
|
|
82
|
+
export const REASONING_EFFORTS = ['low', 'high', 'max'] as const;
|
|
83
|
+
export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
84
|
+
|
|
85
|
+
/** kimi-k2.6 `thinking.type` — enabled and disabled are BOTH supported. */
|
|
86
|
+
export const K26_THINKING_TYPES = ['enabled', 'disabled'] as const;
|
|
87
|
+
/** kimi-k2.7-code `thinking.type` — ONLY 'enabled'; 'disabled' returns an error (OpenAPI:
|
|
88
|
+
* "Unlike kimi-k2.6, `\"disabled\"` is NOT supported — passing it returns an error."). */
|
|
89
|
+
export const K27_THINKING_TYPES = ['enabled'] as const;
|