@volter/twin-openrouter 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 +131 -0
- package/client/openrouter-mirror.css +131 -0
- package/client/openrouter-mirror.tsx +221 -0
- package/dist/client/openrouter-mirror.bundle.js +235 -0
- package/dist/client/openrouter-mirror.css +131 -0
- package/dist/client/openrouter-mirror.d.ts +1 -0
- package/dist/client/openrouter-mirror.js +95 -0
- package/dist/client/openrouter-mirror.tsx +221 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +31 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js +66 -0
- package/dist/src/openrouter-budget.d.ts +58 -0
- package/dist/src/openrouter-budget.js +133 -0
- package/dist/src/openrouter-capabilities.d.ts +3 -0
- package/dist/src/openrouter-capabilities.js +720 -0
- package/dist/src/openrouter-conformance.d.ts +11 -0
- package/dist/src/openrouter-conformance.js +53 -0
- package/dist/src/openrouter-connector.d.ts +90 -0
- package/dist/src/openrouter-connector.js +245 -0
- package/dist/src/openrouter-local-generation.d.ts +17 -0
- package/dist/src/openrouter-local-generation.js +271 -0
- package/dist/src/openrouter-mirror-ui.d.ts +12 -0
- package/dist/src/openrouter-mirror-ui.js +78 -0
- package/dist/src/openrouter-models.d.ts +39 -0
- package/dist/src/openrouter-models.js +78 -0
- package/dist/src/openrouter-scenario.d.ts +36 -0
- package/dist/src/openrouter-scenario.js +145 -0
- package/dist/src/openrouter-server.d.ts +20 -0
- package/dist/src/openrouter-server.js +144 -0
- package/dist/src/openrouter-stub.d.ts +8 -0
- package/dist/src/openrouter-stub.js +58 -0
- package/dist/src/openrouter-twin.d.ts +4 -0
- package/dist/src/openrouter-twin.js +1506 -0
- package/dist/src/openrouter-types.d.ts +50 -0
- package/dist/src/openrouter-types.js +1 -0
- package/dist/test-fixtures/openrouter-openapi-operations.SOURCE.md +16 -0
- package/dist/test-fixtures/openrouter-openapi-operations.json +1041 -0
- package/package.json +71 -0
- package/src/cli.ts +29 -0
- package/src/index.ts +108 -0
- package/src/openrouter-budget.ts +159 -0
- package/src/openrouter-capabilities.ts +861 -0
- package/src/openrouter-conformance.ts +60 -0
- package/src/openrouter-connector.ts +264 -0
- package/src/openrouter-local-generation.ts +207 -0
- package/src/openrouter-mirror-ui.ts +84 -0
- package/src/openrouter-models.ts +118 -0
- package/src/openrouter-scenario.ts +156 -0
- package/src/openrouter-server.ts +158 -0
- package/src/openrouter-stub.ts +60 -0
- package/src/openrouter-twin.ts +1441 -0
- package/src/openrouter-types.ts +49 -0
- package/test-fixtures/openrouter-openapi-operations.SOURCE.md +16 -0
- package/test-fixtures/openrouter-openapi-operations.json +1041 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { handleOpenRouterTwinRequest } from './openrouter-twin.ts';
|
|
5
|
+
import type { SseEvent } from './openrouter-types.ts';
|
|
6
|
+
|
|
7
|
+
export type OpenRouterConformanceReport = {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
checksRun: number;
|
|
10
|
+
violations: Array<{ check: string; detail: string }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Body = Record<string, any>;
|
|
14
|
+
|
|
15
|
+
export async function checkOpenRouterConformance(opts: { root?: string } = {}): Promise<OpenRouterConformanceReport> {
|
|
16
|
+
const root = opts.root ?? mkdtempSync(join(tmpdir(), 'openrouter-conf-'));
|
|
17
|
+
const owned = opts.root === undefined;
|
|
18
|
+
const violations: Array<{ check: string; detail: string }> = [];
|
|
19
|
+
let checksRun = 0;
|
|
20
|
+
const fail = (check: string, detail: string) => violations.push({ check, detail });
|
|
21
|
+
const h = (method: string, path: string, body?: unknown, sseSink?: (event: SseEvent) => void) =>
|
|
22
|
+
handleOpenRouterTwinRequest({ method, path, body: body === undefined ? undefined : JSON.stringify(body), root, ...(sseSink ? { sseSink } : {}) });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
checksRun++;
|
|
26
|
+
const chat = await h('POST', '/api/v1/chat/completions', { model: 'openai/gpt-4o', messages: [{ role: 'user', content: 'hello' }] });
|
|
27
|
+
const cb = chat.body as Body;
|
|
28
|
+
if (chat.status !== 200 || cb.object !== 'chat.completion' || cb.choices?.[0]?.message?.role !== 'assistant') fail('chat.envelope', 'invalid chat completion envelope');
|
|
29
|
+
if (typeof cb.usage?.cost !== 'number') fail('chat.usage', 'OpenRouter usage cost missing');
|
|
30
|
+
|
|
31
|
+
checksRun++;
|
|
32
|
+
const events: SseEvent[] = [];
|
|
33
|
+
await h('POST', '/api/v1/chat/completions', { model: 'openai/gpt-4o', stream: true, messages: [{ role: 'user', content: 'stream' }] }, (event) => events.push(event));
|
|
34
|
+
if (!events.some((event) => event.data?.object === 'chat.completion.chunk') || events.at(-1)?.done !== true) fail('streaming', 'missing chunk or [DONE]');
|
|
35
|
+
|
|
36
|
+
checksRun++;
|
|
37
|
+
const models = await h('GET', '/api/v1/models');
|
|
38
|
+
if (!Array.isArray((models.body as Body).data) || (models.body as Body).data.length === 0) fail('models.list', 'missing models data');
|
|
39
|
+
const one = await h('GET', '/api/v1/models/openai/gpt-4o');
|
|
40
|
+
if ((one.body as Body).data?.id !== 'openai/gpt-4o') fail('models.retrieve', 'missing model');
|
|
41
|
+
|
|
42
|
+
checksRun++;
|
|
43
|
+
const gen = await h('GET', `/api/v1/generation?id=${encodeURIComponent(cb.id)}`);
|
|
44
|
+
if ((gen.body as Body).data?.model !== 'openai/gpt-4o') fail('generation.stats', 'generation lookup failed');
|
|
45
|
+
|
|
46
|
+
checksRun++;
|
|
47
|
+
const bad = await h('POST', '/api/v1/chat/completions', { model: 'openai/gpt-4o', messages: [] });
|
|
48
|
+
if (bad.status !== 400 || !(bad.body as Body).error) fail('error.envelope', 'bad chat request did not return error envelope');
|
|
49
|
+
|
|
50
|
+
checksRun++;
|
|
51
|
+
const resp = await h('POST', '/api/v1/responses', { model: 'openai/gpt-4o', input: 'hi' });
|
|
52
|
+
if (resp.status !== 200 || (resp.body as Body).object !== 'response') fail('responses.create', 'responses envelope invalid');
|
|
53
|
+
const embed = await h('POST', '/api/v1/embeddings', { model: 'm', input: 'hi' });
|
|
54
|
+
if (embed.status !== 200 || !Array.isArray((embed.body as Body).data?.[0]?.embedding)) fail('embeddings.create', 'embeddings envelope invalid');
|
|
55
|
+
} finally {
|
|
56
|
+
if (owned) rmSync(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { ok: violations.length === 0, checksRun, violations };
|
|
60
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// OpenRouter CONNECTOR — pull/push against an INJECTED `OpenRouterExecute`, so every test and every
|
|
2
|
+
// capability verify runs offline against a deterministic fake.
|
|
3
|
+
//
|
|
4
|
+
// `liveOpenRouterExecute` below is the pack's ONE construction site for a real, network-calling
|
|
5
|
+
// execute — the choke point the rate budget sits inside. Before it existed there was nowhere for a
|
|
6
|
+
// guard to live: the pack only ever received an execute, so a budget could only be something the
|
|
7
|
+
// caller remembered to opt into, and "the caller remembered" is exactly the assumption that cost a
|
|
8
|
+
// ~4.5-day Figma token lockout (2026-07-25). See `openrouter-budget.ts` for the numbers, and for the
|
|
9
|
+
// honest statement that none of OpenRouter's published figures governs this connector exactly.
|
|
10
|
+
//
|
|
11
|
+
// The injected contract is UNCHANGED: everything below still takes a plain `OpenRouterExecute`.
|
|
12
|
+
import { confirmAction, deployableEntries, observeResources } from '@volter/world-core';
|
|
13
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
14
|
+
import type { GenerationStat } from './openrouter-types.ts';
|
|
15
|
+
import { OpenRouterBudget, OpenRouterBudgetError, openRouterCallWeight, type OpenRouterBudgetOptions } from './openrouter-budget.ts';
|
|
16
|
+
|
|
17
|
+
export type OpenRouterExecute = (req: { method: string; path: string; body?: unknown }) => Promise<{ status: number; data: unknown }>;
|
|
18
|
+
const DEFAULT_OCCURRED_AT = '1970-01-01T00:00:00.000Z';
|
|
19
|
+
|
|
20
|
+
/** The real OpenRouter API host. The ONLY place this pack names it as a target. */
|
|
21
|
+
export const OPENROUTER_API_BASE = 'https://openrouter.ai';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* THE CHOKE POINT — the one place this pack builds an `OpenRouterExecute` that really reaches
|
|
25
|
+
* openrouter.ai, and therefore the one place the rate budget has to be enforced.
|
|
26
|
+
*
|
|
27
|
+
* Reads the API key from `OPENROUTER_API_KEY` (never a literal, never a credentials file) and sends it
|
|
28
|
+
* as `Authorization: Bearer …`, which is how OpenRouter authenticates its OpenAI-compatible surface.
|
|
29
|
+
*
|
|
30
|
+
* EVERY request is guarded: the budget is charged BEFORE it goes out (`checkBudget`, which THROWS
|
|
31
|
+
* instead of returning once the ceiling or a persisted cooldown says stop) and the response is fed
|
|
32
|
+
* back (`recordCall`) so a `Retry-After` / 429 / rate-limit-exhaustion signal becomes a persisted
|
|
33
|
+
* cooldown that makes every later call fail fast WITHOUT touching OpenRouter. There is deliberately
|
|
34
|
+
* NO option to disable the guard.
|
|
35
|
+
*
|
|
36
|
+
* No vendor SDK is imported: real-transport `fetch` keeps this pack SDK-free at runtime, which the
|
|
37
|
+
* architecture guardrail requires.
|
|
38
|
+
*/
|
|
39
|
+
export function liveOpenRouterExecute(
|
|
40
|
+
opts: {
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
baseUrl?: string;
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
/** An existing budget to share across executes. Omit and one is constructed. Cannot be null. */
|
|
45
|
+
budget?: OpenRouterBudget;
|
|
46
|
+
/** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
|
|
47
|
+
budgetOptions?: OpenRouterBudgetOptions;
|
|
48
|
+
} = {},
|
|
49
|
+
): OpenRouterExecute {
|
|
50
|
+
const apiKey = opts.apiKey ?? process.env.OPENROUTER_API_KEY ?? '';
|
|
51
|
+
if (!apiKey) throw new Error('OPENROUTER_API_KEY is not set — the live OpenRouter client needs an API key');
|
|
52
|
+
const baseUrl = opts.baseUrl ?? OPENROUTER_API_BASE;
|
|
53
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
54
|
+
// There is no value a caller can pass to end up with an unguarded execute. `null`/`undefined` (or
|
|
55
|
+
// omitting it) build the default budget; anything that is not a REAL `OpenRouterBudget` is refused
|
|
56
|
+
// loudly rather than trusted — a duck-typed stand-in with a no-op `checkBudget` would otherwise be
|
|
57
|
+
// the one clean way around the guard.
|
|
58
|
+
if (opts.budget !== undefined && opts.budget !== null && !(opts.budget instanceof OpenRouterBudget)) {
|
|
59
|
+
throw new Error('liveOpenRouterExecute: `budget` must be an OpenRouterBudget — refusing to build a live OpenRouter client around an unverified rate guard');
|
|
60
|
+
}
|
|
61
|
+
// The default ledger is keyed by a hash of THIS key — OpenRouter's limits are per key / per
|
|
62
|
+
// account, so a cwd-scoped ledger would hand the same key a fresh allowance per checkout/CI leg.
|
|
63
|
+
const budget = opts.budget instanceof OpenRouterBudget
|
|
64
|
+
? opts.budget
|
|
65
|
+
: new OpenRouterBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
|
|
66
|
+
return async (req) => {
|
|
67
|
+
requireVendorPath(req.path);
|
|
68
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
69
|
+
const weight = openRouterCallWeight(method, req.path);
|
|
70
|
+
// THROWS instead of calling. Nothing below this line runs when the budget refuses.
|
|
71
|
+
const reservation = budget.checkBudget(weight);
|
|
72
|
+
const res = await doFetch(new URL(req.path, baseUrl).toString(), {
|
|
73
|
+
method,
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${apiKey}`,
|
|
76
|
+
Accept: 'application/json',
|
|
77
|
+
...(req.body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
78
|
+
},
|
|
79
|
+
...(req.body === undefined ? {} : { body: JSON.stringify(req.body) }),
|
|
80
|
+
});
|
|
81
|
+
const headers = lowerCasedHeaders(res.headers);
|
|
82
|
+
const text = await res.text();
|
|
83
|
+
let data: unknown = null;
|
|
84
|
+
try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
|
|
85
|
+
// Settles the reservation and, on a back-off signal, arms the cooldown. May itself throw (a
|
|
86
|
+
// Retry-After beyond the cap is not something to sleep off) — the cooldown is persisted first
|
|
87
|
+
// either way, so the refusal survives the throw.
|
|
88
|
+
// recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
|
|
89
|
+
// call that louder refusal wins; an answer OpenRouter ACCEPTED is kept, so a write that landed is
|
|
90
|
+
// never recorded as failed and performed again on retry.
|
|
91
|
+
try {
|
|
92
|
+
budget.recordCall(weight, headers, { status: res.status, reservation });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!(error instanceof OpenRouterBudgetError) || !res.ok) throw error;
|
|
95
|
+
}
|
|
96
|
+
return { status: res.status, data };
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Refuse a `path` that is not a same-origin ABSOLUTE PATH.
|
|
102
|
+
*
|
|
103
|
+
* `new URL(path, base)` treats `https://evil.test/x` AND the protocol-relative `//evil.test/x` as
|
|
104
|
+
* absolute and silently retargets the host — and this factory attaches the credential
|
|
105
|
+
* unconditionally, so a caller-supplied absolute path would exfiltrate it to an arbitrary server.
|
|
106
|
+
* Figma's guarded client sidesteps this by concatenating rather than resolving; this execute takes an
|
|
107
|
+
* arbitrary `{ path }` from its caller, so it validates instead. (§9 finding, 2026-07-26.)
|
|
108
|
+
*
|
|
109
|
+
* `/twin/...` is refused for a different reason: those are the twin's OWN seed routes
|
|
110
|
+
* (`*RequestForAction` builds them), which the real vendor has never heard of. Sending one live is a
|
|
111
|
+
* guaranteed 404 that still burns budget and puts the live credential on the wire for nothing.
|
|
112
|
+
*/
|
|
113
|
+
function requireVendorPath(path: string): void {
|
|
114
|
+
if (typeof path !== 'string' || !/^\/(?!\/)/.test(path)) {
|
|
115
|
+
throw new Error(`liveOpenRouterExecute: path must be a same-origin absolute path beginning with a single "/" (got ${String(path)}) — refusing to let a caller-supplied absolute URL retarget the host with the credential attached`);
|
|
116
|
+
}
|
|
117
|
+
if (path === '/twin' || path.startsWith('/twin/')) {
|
|
118
|
+
throw new Error(`liveOpenRouterExecute: refusing to send the twin-only path ${path} to real OpenRouter — that route exists only in the local twin`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Response headers as a plain lower-cased record — what the kernel's back-off reader expects. */
|
|
123
|
+
function lowerCasedHeaders(h: Headers): Record<string, string> {
|
|
124
|
+
const out: Record<string, string> = {};
|
|
125
|
+
h.forEach((v: string, k: string) => { out[k.toLowerCase()] = v; });
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Fetch OpenRouter's model catalog and provisioning keys through the injected client, mapped to SyncResource[] (no fold). */
|
|
130
|
+
async function collectOpenRouterState(execute: OpenRouterExecute): Promise<SyncResource[]> {
|
|
131
|
+
const resources: SyncResource[] = [];
|
|
132
|
+
|
|
133
|
+
const models = await execute({ method: 'GET', path: '/api/v1/models' });
|
|
134
|
+
const catalog = (models.data as { data?: Array<Record<string, unknown>> }).data ?? [];
|
|
135
|
+
for (const model of catalog) {
|
|
136
|
+
if (typeof model.id === 'string') resources.push({ type: 'model', id: `model_${model.id}`, fields: model });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The PROVISIONING keys (`GET /api/v1/keys`) — the one part of an OpenRouter account a world both
|
|
140
|
+
// WRITES and can read back, so it is the pull that makes the mirror a mirror rather than a
|
|
141
|
+
// catalog copy. The vendor addresses a key by its `hash` and never returns the prefixed form, so
|
|
142
|
+
// the subject id is rebuilt exactly as `createApiKey` mints it.
|
|
143
|
+
const keys = await execute({ method: 'GET', path: '/api/v1/keys' });
|
|
144
|
+
for (const key of (keys.data as { data?: Array<Record<string, unknown>> }).data ?? []) {
|
|
145
|
+
const hash = typeof key.hash === 'string' ? key.hash : typeof key.id === 'string' ? key.id : null;
|
|
146
|
+
if (!hash) continue;
|
|
147
|
+
resources.push({ type: 'api_key', id: `key_${hash}`, fields: { ...key, id: `key_${hash}`, hash } });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return resources;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function pullOpenRouterState(execute: OpenRouterExecute, opts: { root?: string; occurredAt?: string } = {}): Promise<void> {
|
|
154
|
+
const resources = await collectOpenRouterState(execute);
|
|
155
|
+
const at = opts.occurredAt ?? DEFAULT_OCCURRED_AT;
|
|
156
|
+
observeResources('openrouter', resources, { ...(opts.root !== undefined ? { root: opts.root } : {}), at, batch: `obs:openrouter:${at}` });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* D7 consumer-facing pull entry point: gather all OpenRouter domains (the model catalog and the account's provisioning keys) and
|
|
161
|
+
* fold them into the twin as ONE observation, returning the standard result shape. Idempotent:
|
|
162
|
+
* a re-pull of identical state appends no new deltas.
|
|
163
|
+
*/
|
|
164
|
+
export async function syncOpenRouterFromReal(
|
|
165
|
+
execute: OpenRouterExecute,
|
|
166
|
+
opts: { root?: string; occurredAt?: string } = {},
|
|
167
|
+
): Promise<{ observed: number; deltasAppended: number }> {
|
|
168
|
+
const resources = await collectOpenRouterState(execute);
|
|
169
|
+
const at = opts.occurredAt ?? DEFAULT_OCCURRED_AT;
|
|
170
|
+
const result = observeResources('openrouter', resources, { ...(opts.root !== undefined ? { root: opts.root } : {}), at, batch: `obs:openrouter:${at}` });
|
|
171
|
+
return { observed: resources.length, deltasAppended: result.appended };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function openRouterRequestForAction(action: TwinAction): { method: string; path: string; body?: unknown } | null {
|
|
175
|
+
if (action.operation === 'generation.record') {
|
|
176
|
+
const stat = action.fields as unknown as GenerationStat;
|
|
177
|
+
return { method: 'GET', path: `/api/v1/generation?id=${encodeURIComponent(stat.id.replace(/^generation_/, ''))}` };
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function pushOpenRouterAction(action: TwinAction, execute: OpenRouterExecute, opts: { root?: string; occurredAt?: string } = {}): Promise<boolean> {
|
|
183
|
+
const req = openRouterRequestForAction(action);
|
|
184
|
+
if (!req) return false;
|
|
185
|
+
const result = await execute(req);
|
|
186
|
+
if (result.status < 200 || result.status >= 300) return false;
|
|
187
|
+
// The vendor's OWN id for the subject, and the receipt that says the crossing happened — protocol
|
|
188
|
+
// 2's confirmation carries both, so a later read can tell a deployed subject from a local one.
|
|
189
|
+
const answered = (result.data as { data?: Record<string, unknown> } | undefined)?.data;
|
|
190
|
+
const vendorSubjectId = typeof answered?.id === 'string' ? answered.id : action.subject.id;
|
|
191
|
+
confirmAction({
|
|
192
|
+
service: 'openrouter',
|
|
193
|
+
actionId: action.id,
|
|
194
|
+
subject: action.subject,
|
|
195
|
+
fields: action.fields ?? {},
|
|
196
|
+
occurredAt: opts.occurredAt ?? DEFAULT_OCCURRED_AT,
|
|
197
|
+
vendorSubjectId,
|
|
198
|
+
receipt: { status: 'deployed' },
|
|
199
|
+
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
200
|
+
});
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function pushPendingOpenRouterActions(execute: OpenRouterExecute, opts: { root?: string; occurredAt?: string } = {}): Promise<number> {
|
|
205
|
+
let pushed = 0;
|
|
206
|
+
for (const action of deployableEntries('openrouter', opts.root)) {
|
|
207
|
+
if (await pushOpenRouterAction(action, execute, opts)) pushed++;
|
|
208
|
+
}
|
|
209
|
+
return pushed;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── PROTOCOL 2: the pack's half of the real state system ────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* An `OpenRouterExecute` over the kernel's executor. At a REAL boundary the kernel sets the sealed
|
|
216
|
+
* credential over these headers (executor.ts); at the twin's own wire any credential is one.
|
|
217
|
+
*
|
|
218
|
+
* The paths carry OpenRouter's `/api` prefix, which `collectOpenRouterState` already uses and the
|
|
219
|
+
* twin's own router accepts alongside the bare `/v1` form.
|
|
220
|
+
*/
|
|
221
|
+
export function openRouterExecuteOver(execute: RemoteExecute): OpenRouterExecute {
|
|
222
|
+
return async (req) => {
|
|
223
|
+
const res = await execute({
|
|
224
|
+
method: req.method,
|
|
225
|
+
path: req.path,
|
|
226
|
+
headers: { accept: 'application/json', 'content-type': 'application/json', authorization: 'Bearer twin' },
|
|
227
|
+
...(req.body === undefined ? {} : { body: JSON.stringify(req.body) }),
|
|
228
|
+
});
|
|
229
|
+
let data: unknown = {};
|
|
230
|
+
try { data = JSON.parse(res.body || '{}'); } catch { data = {}; }
|
|
231
|
+
return { status: res.status, data };
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The refresh adapter: pull the account's state through the executor and fold it into the root. */
|
|
236
|
+
export async function syncOpenRouterFromRemote(
|
|
237
|
+
execute: RemoteExecute,
|
|
238
|
+
opts: { root?: string; origin?: string; occurredAt?: string } = {},
|
|
239
|
+
): Promise<{ observed: number; deltasAppended: number }> {
|
|
240
|
+
return syncOpenRouterFromReal(openRouterExecuteOver(execute), {
|
|
241
|
+
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
242
|
+
occurredAt: opts.occurredAt ?? new Date().toISOString(),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The perform adapter. OpenRouter's provisioning surface is what a world can really write:
|
|
248
|
+
* `pushOpenRouterAction` maps the one crossing operation it models and answers `false` for the
|
|
249
|
+
* rest, which are the twin's OWN record (a generation is a completion this twin produced; there is
|
|
250
|
+
* nothing at OpenRouter to create). Saying so is the honest outcome, never a silent success.
|
|
251
|
+
*/
|
|
252
|
+
export async function performOpenRouterAction(execute: RemoteExecute, action: TwinAction, _ctx: PerformContext): Promise<PushOutcome> {
|
|
253
|
+
const client = openRouterExecuteOver(execute);
|
|
254
|
+
const req = openRouterRequestForAction(action);
|
|
255
|
+
if (!req) {
|
|
256
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
257
|
+
return { externalId: action.subject.id, data: { performed: false, reason: `${op} is the twin's own record — nothing at OpenRouter to write` } };
|
|
258
|
+
}
|
|
259
|
+
const result = await client(req);
|
|
260
|
+
if (result.status < 200 || result.status >= 300) {
|
|
261
|
+
throw new Error(`openrouter ${req.method} ${req.path} refused: HTTP ${result.status}`);
|
|
262
|
+
}
|
|
263
|
+
return { externalId: action.subject.id, data: (result.data ?? {}) as Record<string, unknown> };
|
|
264
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/** Optional pack-owned transport to a separately World-owned loopback generation service. */
|
|
2
|
+
|
|
3
|
+
export const LOCAL_GENERATION_BODY_LIMIT = 8 * 1024 * 1024;
|
|
4
|
+
export type LocalGenerationFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
export function localGenerationOrigin(value: string): string {
|
|
7
|
+
const match = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})\/?$/.exec(value);
|
|
8
|
+
const port = match ? Number(match[1]) : 0;
|
|
9
|
+
if (!port || port > 65535) {
|
|
10
|
+
throw new Error('local_generation_url_invalid: expected http://127.0.0.1:<nonzero-port> with no credentials, path, query, or fragment');
|
|
11
|
+
}
|
|
12
|
+
return `http://127.0.0.1:${port}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read only the opt-in generation request body; ordinary twin routes retain their old path. */
|
|
16
|
+
export async function readLocalGenerationBody(request: Request): Promise<{ bytes: Uint8Array<ArrayBuffer>; text: string }> {
|
|
17
|
+
request.signal.throwIfAborted();
|
|
18
|
+
const reader = request.body?.getReader();
|
|
19
|
+
if (!reader) return { bytes: new Uint8Array(0), text: '' };
|
|
20
|
+
const parts: Uint8Array[] = [];
|
|
21
|
+
let length = 0;
|
|
22
|
+
let readFailed = false;
|
|
23
|
+
let done = false;
|
|
24
|
+
let primary: unknown;
|
|
25
|
+
let failed = false;
|
|
26
|
+
let cleanupFailure: unknown;
|
|
27
|
+
let cleanupFailed = false;
|
|
28
|
+
let result: { bytes: Uint8Array<ArrayBuffer>; text: string } | undefined;
|
|
29
|
+
let abortCleanup: Promise<void> | undefined;
|
|
30
|
+
const onAbort = () => {
|
|
31
|
+
abortCleanup ??= reader.cancel(request.signal.reason);
|
|
32
|
+
// The request's finally block observes the same promise even if cancellation rejects first.
|
|
33
|
+
void abortCleanup.catch(() => {});
|
|
34
|
+
};
|
|
35
|
+
request.signal.addEventListener('abort', onAbort, { once: true });
|
|
36
|
+
if (request.signal.aborted) onAbort();
|
|
37
|
+
try {
|
|
38
|
+
while (true) {
|
|
39
|
+
request.signal.throwIfAborted();
|
|
40
|
+
let part;
|
|
41
|
+
try { part = await reader.read(); }
|
|
42
|
+
catch (error) { readFailed = true; throw error; }
|
|
43
|
+
request.signal.throwIfAborted();
|
|
44
|
+
if (part.done) { done = true; break; }
|
|
45
|
+
length += part.value.byteLength;
|
|
46
|
+
if (length > LOCAL_GENERATION_BODY_LIMIT) throw new Error('local_generation_body_too_large: 8 MiB limit');
|
|
47
|
+
parts.push(part.value);
|
|
48
|
+
}
|
|
49
|
+
const bytes = new Uint8Array(length);
|
|
50
|
+
let offset = 0;
|
|
51
|
+
for (const part of parts) { bytes.set(part, offset); offset += part.byteLength; }
|
|
52
|
+
try { result = { bytes, text: new TextDecoder('utf-8', { fatal: true }).decode(bytes) }; }
|
|
53
|
+
catch (error) { throw new Error('local_generation_invalid_utf8: request body is not valid UTF-8', { cause: error }); }
|
|
54
|
+
} catch (error) {
|
|
55
|
+
primary = error;
|
|
56
|
+
failed = true;
|
|
57
|
+
} finally {
|
|
58
|
+
request.signal.removeEventListener('abort', onAbort);
|
|
59
|
+
if (!done && (!readFailed || abortCleanup)) {
|
|
60
|
+
try { await (abortCleanup ?? reader.cancel(primary)); }
|
|
61
|
+
catch (cleanup) { cleanupFailure = cleanup; cleanupFailed = true; }
|
|
62
|
+
}
|
|
63
|
+
try { reader.releaseLock(); }
|
|
64
|
+
catch (cleanup) {
|
|
65
|
+
cleanupFailure = cleanupFailed ? new AggregateError([cleanupFailure, cleanup], 'local_generation_body_cleanup_failed') : cleanup;
|
|
66
|
+
cleanupFailed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (cleanupFailed) throw new AggregateError(failed ? [primary, cleanupFailure] : [cleanupFailure], 'local_generation_body_cleanup_failed');
|
|
70
|
+
if (failed) throw primary;
|
|
71
|
+
if (!result) throw new Error('local_generation_body_read_failed: no completed body');
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function localError(status: number, code: string, message: string): Response {
|
|
76
|
+
return Response.json({ error: { message, type: 'local_generation_error', code } }, { status });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One pull from the caller means at most one read from the local response. */
|
|
80
|
+
export async function forwardLocalGeneration(args: {
|
|
81
|
+
origin: string;
|
|
82
|
+
request: Request;
|
|
83
|
+
pathname: string;
|
|
84
|
+
body: Uint8Array<ArrayBuffer>;
|
|
85
|
+
fetchImpl?: LocalGenerationFetch;
|
|
86
|
+
}): Promise<Response> {
|
|
87
|
+
const relay = new AbortController();
|
|
88
|
+
const abort = () => relay.abort(args.request.signal.reason);
|
|
89
|
+
args.request.signal.addEventListener('abort', abort, { once: true });
|
|
90
|
+
if (args.request.signal.aborted) abort();
|
|
91
|
+
const headers = new Headers();
|
|
92
|
+
for (const key of ['content-type', 'accept']) {
|
|
93
|
+
const value = args.request.headers.get(key);
|
|
94
|
+
if (value !== null) headers.set(key, value);
|
|
95
|
+
}
|
|
96
|
+
let upstream: Response;
|
|
97
|
+
try {
|
|
98
|
+
upstream = await (args.fetchImpl ?? fetch)(`${args.origin}${args.pathname}`, {
|
|
99
|
+
method: 'POST', headers, body: args.body, redirect: 'manual', signal: relay.signal,
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
args.request.signal.removeEventListener('abort', abort);
|
|
103
|
+
if (args.request.signal.aborted) throw error;
|
|
104
|
+
return localError(503, 'local_generation_unavailable', `Local generation service unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
105
|
+
}
|
|
106
|
+
if (upstream.status >= 300 && upstream.status < 400) {
|
|
107
|
+
try { await upstream.body?.cancel(); }
|
|
108
|
+
catch (error) {
|
|
109
|
+
args.request.signal.removeEventListener('abort', abort);
|
|
110
|
+
return localError(502, 'local_generation_cleanup_failed', `Local generation redirect body cleanup failed: ${String(error)}`);
|
|
111
|
+
}
|
|
112
|
+
args.request.signal.removeEventListener('abort', abort);
|
|
113
|
+
return localError(502, 'local_generation_redirect_refused', 'Local generation service redirect refused');
|
|
114
|
+
}
|
|
115
|
+
if (args.request.signal.aborted) {
|
|
116
|
+
args.request.signal.removeEventListener('abort', abort);
|
|
117
|
+
try { await upstream.body?.cancel(args.request.signal.reason); }
|
|
118
|
+
catch (cleanup) { throw new AggregateError([args.request.signal.reason, cleanup], 'local_generation_cleanup_failed'); }
|
|
119
|
+
throw args.request.signal.reason;
|
|
120
|
+
}
|
|
121
|
+
const responseHeaders = new Headers();
|
|
122
|
+
for (const key of ['content-type', 'retry-after', 'retry-after-ms']) {
|
|
123
|
+
const value = upstream.headers.get(key);
|
|
124
|
+
if (value !== null) responseHeaders.set(key, value);
|
|
125
|
+
}
|
|
126
|
+
if (!upstream.body) {
|
|
127
|
+
args.request.signal.removeEventListener('abort', abort);
|
|
128
|
+
return new Response(null, { status: upstream.status, headers: responseHeaders });
|
|
129
|
+
}
|
|
130
|
+
const reader = upstream.body.getReader();
|
|
131
|
+
let terminal = false;
|
|
132
|
+
let cancelPromise: Promise<void> | undefined;
|
|
133
|
+
let failure: unknown;
|
|
134
|
+
let abortCleanup: Promise<void> | undefined;
|
|
135
|
+
let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
136
|
+
let readerTerminal: 'pending' | 'closed' | 'errored' = 'pending';
|
|
137
|
+
void reader.closed.then(() => { readerTerminal = 'closed'; }, () => { readerTerminal = 'errored'; });
|
|
138
|
+
const cancelReader = (reason?: unknown): Promise<void> => {
|
|
139
|
+
if (cancelPromise) return cancelPromise;
|
|
140
|
+
cancelPromise = (async () => {
|
|
141
|
+
let primary: unknown;
|
|
142
|
+
let failed = false;
|
|
143
|
+
await Promise.resolve();
|
|
144
|
+
if (readerTerminal === 'pending') {
|
|
145
|
+
try { await reader.cancel(reason); } catch (error) { primary = error; failed = true; }
|
|
146
|
+
}
|
|
147
|
+
try { reader.releaseLock(); }
|
|
148
|
+
catch (cleanup) {
|
|
149
|
+
primary = failed ? new AggregateError([primary, cleanup], 'local_generation_cleanup_failed') : cleanup;
|
|
150
|
+
failed = true;
|
|
151
|
+
}
|
|
152
|
+
if (failed) throw primary;
|
|
153
|
+
})();
|
|
154
|
+
return cancelPromise;
|
|
155
|
+
};
|
|
156
|
+
const finish = () => {
|
|
157
|
+
if (terminal) return;
|
|
158
|
+
terminal = true;
|
|
159
|
+
args.request.signal.removeEventListener('abort', onAbort);
|
|
160
|
+
};
|
|
161
|
+
const onAbort = () => {
|
|
162
|
+
relay.abort(args.request.signal.reason);
|
|
163
|
+
failure = args.request.signal.reason;
|
|
164
|
+
abortCleanup = cancelReader(failure);
|
|
165
|
+
void abortCleanup.then(
|
|
166
|
+
() => { finish(); try { streamController?.error(failure); } catch { /* an active pull already reported the abort */ } },
|
|
167
|
+
(cleanup) => {
|
|
168
|
+
failure = new AggregateError([failure, cleanup], 'local_generation_cleanup_failed');
|
|
169
|
+
finish();
|
|
170
|
+
try { streamController?.error(failure); } catch { /* an active pull already reported the failure */ }
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
args.request.signal.removeEventListener('abort', abort);
|
|
175
|
+
args.request.signal.addEventListener('abort', onAbort, { once: true });
|
|
176
|
+
if (args.request.signal.aborted) onAbort();
|
|
177
|
+
const body = new ReadableStream<Uint8Array>({
|
|
178
|
+
start(controller) {
|
|
179
|
+
streamController = controller;
|
|
180
|
+
if (failure !== undefined && !abortCleanup) controller.error(failure);
|
|
181
|
+
},
|
|
182
|
+
async pull(controller) {
|
|
183
|
+
if (failure !== undefined) {
|
|
184
|
+
if (abortCleanup) await abortCleanup.then(() => {}, () => {});
|
|
185
|
+
finish();
|
|
186
|
+
try { controller.error(failure); } catch { /* abort callback already reported it */ }
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const part = await reader.read();
|
|
191
|
+
if (abortCleanup) { await abortCleanup.then(() => {}, () => {}); throw failure; }
|
|
192
|
+
if (part.done) { finish(); reader.releaseLock(); controller.close(); }
|
|
193
|
+
else controller.enqueue(part.value);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
finish();
|
|
196
|
+
try { reader.releaseLock(); } catch { /* read/cancel race; retain original stream error */ }
|
|
197
|
+
try { controller.error(failure ?? error); } catch { /* already errored by abort callback */ }
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
async cancel(reason) {
|
|
201
|
+
finish();
|
|
202
|
+
relay.abort(reason);
|
|
203
|
+
await cancelReader(reason);
|
|
204
|
+
},
|
|
205
|
+
}, { highWaterMark: 0 });
|
|
206
|
+
return new Response(body, { status: upstream.status, headers: responseHeaders });
|
|
207
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// OpenRouter MIRROR UI — a models/playground/generations console served as a React/TSX app
|
|
2
|
+
// (bundled by Bun).
|
|
3
|
+
//
|
|
4
|
+
// PURE FRONTEND (R3): the mirror imports no handler module and no twin internals — it MOUNTS the
|
|
5
|
+
// pack's OWN fetch adapter (`createOpenRouterTwinFetch`) as its API backend and reads every byte of
|
|
6
|
+
// state back over the wire. The client fetches OPENROUTER'S REAL VENDOR PATHS (`/api/v1/models`,
|
|
7
|
+
// `/api/v1/chat/completions`, `/api/v1/generations`, `/api/v1/generation?id=…`) on the same origin,
|
|
8
|
+
// so there is exactly ONE serving code path and API<->UI parity cannot drift: the screen shows real
|
|
9
|
+
// twin state or nothing. Mounting the adapter also hands the mirror the streaming lane, the world
|
|
10
|
+
// clock and an honorable `readOnly` for free — the hand-rolled dispatch this replaced used
|
|
11
|
+
// `new Date()` and ignored `readOnly` (the airtable §9 round-two finding 13 drift, verbatim).
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { bundleClient, fileResponse } from '@volter/world-core';
|
|
14
|
+
import { serveHttp } from '@volter/world-core';
|
|
15
|
+
import { createOpenRouterTwinFetch } from './openrouter-server.ts';
|
|
16
|
+
|
|
17
|
+
const CLIENT_ENTRY = () => new URL('../client/openrouter-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
|
|
18
|
+
const CLIENT_CSS = () => new URL('../client/openrouter-mirror.css', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
|
|
19
|
+
|
|
20
|
+
const APP_SHELL = `<!doctype html>
|
|
21
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
22
|
+
<base href="/"><title>OpenRouter (twin)</title><link rel="stylesheet" href="assets/styles.css"></head>
|
|
23
|
+
<body><div id="root"></div><script type="module" src="assets/app.js"></script></body></html>`;
|
|
24
|
+
|
|
25
|
+
let clientBundle: Promise<string> | null = null;
|
|
26
|
+
|
|
27
|
+
export function buildOpenRouterMirrorClient(): Promise<string> {
|
|
28
|
+
if (!clientBundle) {
|
|
29
|
+
clientBundle = bundleClient(CLIENT_ENTRY())
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
clientBundle = null;
|
|
32
|
+
throw error;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return clientBundle;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function createOpenRouterMirrorServer(options: { root?: string; port?: number; readOnly?: boolean } = {}): Promise<{ port: number; stop: () => void }> {
|
|
39
|
+
// Built ONCE, above the socket — never per request: the adapter is a closure over the twin's
|
|
40
|
+
// options and is the very same one `createOpenRouterTwinServer` serves.
|
|
41
|
+
const twin = createOpenRouterTwinFetch(options);
|
|
42
|
+
const server = await serveHttp({
|
|
43
|
+
// LOOPBACK-SPECIFIC bind (2026-08-20, the roving ui-verify flake): with the default
|
|
44
|
+
// wildcard hostname, `port: 0` can be handed a port some long-running app already LISTENS
|
|
45
|
+
// on at 127.0.0.1 (SO_REUSEADDR allows the overlapping non-identical bind), and the more
|
|
46
|
+
// specific loopback listener then shadows this server for every 127.0.0.1 fetch — the
|
|
47
|
+
// verify talks to a STRANGER (captured: a desktop app's asset server answering 404s on the
|
|
48
|
+
// mirror's port). Binding 127.0.0.1 makes the kernel allocate a port that is actually free
|
|
49
|
+
// on loopback, so the verify's fetches deterministically reach THIS server.
|
|
50
|
+
hostname: '127.0.0.1',
|
|
51
|
+
port: options.port ?? 0,
|
|
52
|
+
idleTimeout: 60,
|
|
53
|
+
async fetch(request) {
|
|
54
|
+
const url = new URL(request.url);
|
|
55
|
+
if (request.method === 'GET' && url.pathname === '/assets/app.js') {
|
|
56
|
+
try {
|
|
57
|
+
return new Response(await buildOpenRouterMirrorClient(), { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return new Response(String(error), { status: 500 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (request.method === 'GET' && url.pathname === '/assets/styles.css') {
|
|
63
|
+
return fileResponse(CLIENT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
64
|
+
}
|
|
65
|
+
if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '')) {
|
|
66
|
+
return new Response(APP_SHELL, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
67
|
+
}
|
|
68
|
+
// everything else -> the twin's OWN FETCH ADAPTER (composition, R2/R3). That includes the
|
|
69
|
+
// uniform `GET /twin` door and the SSE lane; nothing about the mirror port may differ from
|
|
70
|
+
// the API port.
|
|
71
|
+
return twin(request);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function openrouterMirrorHtml(): string {
|
|
78
|
+
return APP_SHELL;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
|
|
82
|
+
export function openrouterMirrorStyles(): Promise<string> {
|
|
83
|
+
return readFile(CLIENT_CSS(), 'utf8');
|
|
84
|
+
}
|