@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,720 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { applyTwinWrite, deployableEntries, projectResources } from '@volter/world-core';
|
|
6
|
+
import { checkCapabilities, verifyBoundary, isInfrastructureError, harnessError } from '@volter/world-tooling';
|
|
7
|
+
import { handleOpenRouterTwinRequest } from "./openrouter-twin.js";
|
|
8
|
+
import { buildOpenRouterMirrorClient, createOpenRouterMirrorServer } from "./openrouter-mirror-ui.js";
|
|
9
|
+
import { pullOpenRouterState, pushPendingOpenRouterActions } from "./openrouter-connector.js";
|
|
10
|
+
async function withRoot(fn) {
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), 'openrouter-cap-'));
|
|
12
|
+
const h = (step) => handleOpenRouterTwinRequest({
|
|
13
|
+
method: step.m,
|
|
14
|
+
path: step.p,
|
|
15
|
+
body: step.b === undefined ? undefined : JSON.stringify(step.b),
|
|
16
|
+
root,
|
|
17
|
+
...(step.sseSink ? { sseSink: step.sseSink } : {}),
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
return await verifyBoundary('openrouter.withRoot', () => fn(h));
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
rmSync(root, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function handleOpenRouterTwinRequestRO(method, path, body) {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), 'openrouter-cap-ro-'));
|
|
28
|
+
try {
|
|
29
|
+
return await handleOpenRouterTwinRequest({ method, path, body: JSON.stringify(body), readOnly: true, root });
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rmSync(root, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
|
|
36
|
+
const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
|
|
37
|
+
const CHAT = (extra = {}) => ({
|
|
38
|
+
model: 'google/gemini-3-flash-preview',
|
|
39
|
+
messages: [{ role: 'user', content: 'hello openrouter twin' }],
|
|
40
|
+
...extra,
|
|
41
|
+
});
|
|
42
|
+
// UI verifies assert markers against the BUILT (minified) mirror bundle — the same bytes the
|
|
43
|
+
// browser runs — not the .tsx source, so a marker that the bundler erases can't false-green.
|
|
44
|
+
let bundle = null;
|
|
45
|
+
const mirrorBundle = () => (bundle ??= buildOpenRouterMirrorClient());
|
|
46
|
+
async function withMirror(fn) {
|
|
47
|
+
const root = mkdtempSync(join(tmpdir(), 'openrouter-mirror-cap-'));
|
|
48
|
+
const server = await createOpenRouterMirrorServer({ root });
|
|
49
|
+
try {
|
|
50
|
+
return await fn(`http://127.0.0.1:${server.port}`);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (isInfrastructureError(err))
|
|
54
|
+
throw harnessError('openrouter.withMirror', err);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
server.stop();
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const OPENROUTER_CAPABILITIES = [
|
|
63
|
+
done('openrouter.chat.create', 'chat', 'Chat completions: create returns OpenAI-compatible envelope', 'api', 'core', () => withRoot(async (h) => {
|
|
64
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
65
|
+
const b = r.body;
|
|
66
|
+
return r.status === 200 && b.object === 'chat.completion' && b.model === 'google/gemini-3-flash-preview' &&
|
|
67
|
+
typeof b.id === 'string' && b.choices?.[0]?.message?.role === 'assistant' && typeof b.usage?.cost === 'number';
|
|
68
|
+
})),
|
|
69
|
+
done('openrouter.chat.stub_labeled', 'chat', 'Chat output is clearly labeled as a twin stub', 'api', 'core', () => withRoot(async (h) => {
|
|
70
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
71
|
+
const text = r.body.choices?.[0]?.message?.content;
|
|
72
|
+
return r.status === 200 && text.includes('[twin-stub:openrouter') && text.includes('hello openrouter twin');
|
|
73
|
+
})),
|
|
74
|
+
done('openrouter.chat.validation', 'chat', 'Chat rejects empty or malformed messages with vendor-shaped error', 'api', 'core', () => withRoot(async (h) => {
|
|
75
|
+
const empty = await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'openai/gpt-4o', messages: [] } });
|
|
76
|
+
const malformed = await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'openai/gpt-4o', messages: [{ content: 'x' }] } });
|
|
77
|
+
const noContent = await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'openai/gpt-4o', messages: [{ role: 'user' }] } });
|
|
78
|
+
const unknownModel = await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'not-a/provider', messages: [{ role: 'user', content: 'x' }] } });
|
|
79
|
+
return empty.status === 400 && malformed.status === 400 && noContent.status === 400 && unknownModel.status === 404 &&
|
|
80
|
+
!!empty.body.error?.code && unknownModel.body.error?.code === 'not_found';
|
|
81
|
+
})),
|
|
82
|
+
done('openrouter.chat.default_model', 'chat', 'Chat defaults model when omitted', 'api', 'common', () => withRoot(async (h) => {
|
|
83
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: { messages: [{ role: 'user', content: 'default' }] } });
|
|
84
|
+
return r.status === 200 && r.body.model === 'openai/gpt-4o';
|
|
85
|
+
})),
|
|
86
|
+
done('openrouter.chat.multiple_choices', 'chat', 'Chat supports n choices with stable indexes', 'api', 'common', () => withRoot(async (h) => {
|
|
87
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ n: 3 }) });
|
|
88
|
+
const choices = r.body.choices;
|
|
89
|
+
return r.status === 200 && choices.length === 3 && choices[0].index === 0 && choices[2].index === 2;
|
|
90
|
+
})),
|
|
91
|
+
done('openrouter.chat.max_tokens', 'chat', 'Chat honors max_tokens/max_completion_tokens as a length cap', 'api', 'common', () => withRoot(async (h) => {
|
|
92
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ max_tokens: 1 }) });
|
|
93
|
+
return r.status === 200 && r.body.choices?.[0]?.finish_reason === 'length';
|
|
94
|
+
})),
|
|
95
|
+
done('openrouter.chat.tool_calls', 'chat', 'Chat returns OpenAI-compatible tool_call envelope', 'api', 'common', () => withRoot(async (h) => {
|
|
96
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ tools: [{ type: 'function', function: { name: 'lookup' } }] }) });
|
|
97
|
+
const choice = r.body.choices?.[0];
|
|
98
|
+
return r.status === 200 && choice?.finish_reason === 'tool_calls' && choice?.message?.tool_calls?.[0]?.function?.name === 'lookup';
|
|
99
|
+
})),
|
|
100
|
+
done('openrouter.chat.usage_details', 'chat', 'Chat usage includes OpenRouter-style cost and token detail fields', 'api', 'core', () => withRoot(async (h) => {
|
|
101
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
102
|
+
const usage = r.body.usage;
|
|
103
|
+
return r.status === 200 && typeof usage.cost === 'number' && usage.prompt_tokens_details.cached_tokens === 0 &&
|
|
104
|
+
usage.completion_tokens_details.reasoning_tokens === 0;
|
|
105
|
+
})),
|
|
106
|
+
done('openrouter.chat.v1_alias', 'chat', 'Chat accepts /v1 alias as well as /api/v1', 'api', 'common', () => withRoot(async (h) => {
|
|
107
|
+
const r = await h({ m: 'POST', p: '/v1/chat/completions', b: CHAT() });
|
|
108
|
+
return r.status === 200 && r.body.object === 'chat.completion';
|
|
109
|
+
})),
|
|
110
|
+
done('openrouter.streaming.chunks', 'streaming', 'Streaming emits chat.completion.chunk events and [DONE]', 'api', 'core', () => withRoot(async (h) => {
|
|
111
|
+
const events = [];
|
|
112
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ stream: true }), sseSink: (event) => events.push(event) });
|
|
113
|
+
return r.status === 200 && events.some((event) => event.data?.object === 'chat.completion.chunk') && events.at(-1)?.done === true;
|
|
114
|
+
})),
|
|
115
|
+
done('openrouter.streaming.reconstruct', 'streaming', 'Streaming deltas reconstruct final content', 'api', 'common', () => withRoot(async (h) => {
|
|
116
|
+
const events = [];
|
|
117
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ stream: true }), sseSink: (event) => events.push(event) });
|
|
118
|
+
const streamed = events.filter((event) => event.data).map((event) => event.data.choices[0]?.delta?.content ?? '').join('');
|
|
119
|
+
const full = r.body.choices?.[0]?.message?.content;
|
|
120
|
+
return streamed === full && streamed.includes('[twin-stub');
|
|
121
|
+
})),
|
|
122
|
+
done('openrouter.streaming.usage', 'streaming', 'Final streaming chunk includes usage', 'api', 'common', () => withRoot(async (h) => {
|
|
123
|
+
const events = [];
|
|
124
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ stream: true }), sseSink: (event) => events.push(event) });
|
|
125
|
+
return events.some((event) => typeof event.data?.usage?.cost === 'number');
|
|
126
|
+
})),
|
|
127
|
+
done('openrouter.models.list', 'models', 'Models: list all models and properties', 'api', 'core', () => withRoot(async (h) => {
|
|
128
|
+
const r = await h({ m: 'GET', p: '/api/v1/models' });
|
|
129
|
+
const first = r.body.data?.[0];
|
|
130
|
+
return r.status === 200 && first?.id === 'openai/gpt-4o' && typeof first?.pricing?.prompt === 'string';
|
|
131
|
+
})),
|
|
132
|
+
done('openrouter.models.retrieve', 'models', 'Models: retrieve a model by slug', 'api', 'core', () => withRoot(async (h) => {
|
|
133
|
+
const r = await h({ m: 'GET', p: '/api/v1/models/openai/gpt-4o' });
|
|
134
|
+
return r.status === 200 && r.body.data?.id === 'openai/gpt-4o';
|
|
135
|
+
})),
|
|
136
|
+
done('openrouter.models.count', 'models', 'Models: total count endpoint', 'api', 'common', () => withRoot(async (h) => {
|
|
137
|
+
const r = await h({ m: 'GET', p: '/api/v1/models/count' });
|
|
138
|
+
return r.status === 200 && r.body.data?.count >= 3;
|
|
139
|
+
})),
|
|
140
|
+
done('openrouter.models.endpoints', 'models', 'Models: list endpoints for a model', 'api', 'common', () => withRoot(async (h) => {
|
|
141
|
+
const r = await h({ m: 'GET', p: '/api/v1/models/openai/gpt-4o/endpoints' });
|
|
142
|
+
return r.status === 200 && r.body.data?.endpoints?.[0]?.provider_name === 'twin';
|
|
143
|
+
})),
|
|
144
|
+
done('openrouter.models.not_found', 'models', 'Models: unknown model returns 404', 'api', 'common', () => withRoot(async (h) => {
|
|
145
|
+
const r = await h({ m: 'GET', p: '/api/v1/models/nope/nope' });
|
|
146
|
+
return r.status === 404 && !!r.body.error;
|
|
147
|
+
})),
|
|
148
|
+
done('openrouter.key.current', 'keys', 'Current API key metadata endpoint', 'api', 'core', () => withRoot(async (h) => {
|
|
149
|
+
const r = await h({ m: 'GET', p: '/api/v1/key' });
|
|
150
|
+
return r.status === 200 && r.body.data?.label === 'twin-key';
|
|
151
|
+
})),
|
|
152
|
+
done('openrouter.credits.get', 'credits', 'Credits: remaining credits endpoint', 'api', 'core', () => withRoot(async (h) => {
|
|
153
|
+
const r = await h({ m: 'GET', p: '/api/v1/credits' });
|
|
154
|
+
return r.status === 200 && r.body.data?.total_credits === 100;
|
|
155
|
+
})),
|
|
156
|
+
done('openrouter.generation.stats', 'generations', 'Generation stats are queryable by completion id', 'api', 'core', () => withRoot(async (h) => {
|
|
157
|
+
const chat = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
158
|
+
const id = chat.body.id;
|
|
159
|
+
const stats = await h({ m: 'GET', p: `/api/v1/generation?id=${encodeURIComponent(id)}` });
|
|
160
|
+
const data = stats.body.data;
|
|
161
|
+
return stats.status === 200 && data?.model === 'google/gemini-3-flash-preview' &&
|
|
162
|
+
data?.api_type === 'chat' && data?.provider_name === 'twin' && data?.request_id === id &&
|
|
163
|
+
data?.finish_reason === 'stop' && typeof data?.usage === 'number';
|
|
164
|
+
})),
|
|
165
|
+
done('openrouter.generation.list_local', 'generations', 'Local mirror lists recorded generations', 'api', 'common', () => withRoot(async (h) => {
|
|
166
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
167
|
+
const list = await h({ m: 'GET', p: '/api/v1/generations' });
|
|
168
|
+
return list.status === 200 && list.body.data?.length === 1;
|
|
169
|
+
})),
|
|
170
|
+
done('openrouter.readonly.rejects_chat', 'honesty', 'readOnly rejects local chat writes', 'api', 'common', async () => {
|
|
171
|
+
const root = mkdtempSync(join(tmpdir(), 'openrouter-cap-'));
|
|
172
|
+
try {
|
|
173
|
+
const r = await handleOpenRouterTwinRequest({ method: 'POST', path: '/api/v1/chat/completions', body: JSON.stringify(CHAT()), readOnly: true, root });
|
|
174
|
+
return r.status === 405;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
rmSync(root, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
done('openrouter.unmodeled.404', 'honesty', 'Unmodeled operations fail with 404 instead of fake success', 'api', 'core', () => withRoot(async (h) => {
|
|
181
|
+
const r = await h({ m: 'POST', p: '/api/v1/totally/unmodeled/op', b: { name: 'x' } });
|
|
182
|
+
return r.status === 404 && r.body.error?.code === 'not_found';
|
|
183
|
+
})),
|
|
184
|
+
done('openrouter.ui.mirror_markers', 'ui', 'Mirror Models catalog + Generations screens read live twin APIs', 'ui', 'common', async () => await withMirror(async (baseUrl) => {
|
|
185
|
+
// (1) the built mirror renders the three panel markers served by the mirror server …
|
|
186
|
+
const js = await mirrorBundle();
|
|
187
|
+
if (!['models-panel', 'generations-panel', 'playground-panel'].every((m) => js.includes(m)))
|
|
188
|
+
return false;
|
|
189
|
+
// the shell loads the bundle relative to its <base> (the hosted mirror mounts it under a path), and it is served
|
|
190
|
+
const shell = await fetch(`${baseUrl}/`).then((r) => r.text());
|
|
191
|
+
if (!shell.includes('src="assets/app.js"') || (await fetch(`${baseUrl}/assets/app.js`)).status !== 200)
|
|
192
|
+
return false;
|
|
193
|
+
// (2) … AND both data panels are coupled to the SAME mirror APIs the App component fetches.
|
|
194
|
+
const models = await fetch(`${baseUrl}/api/v1/models`).then((r) => r.json());
|
|
195
|
+
if (!(models.data?.length >= 1 && models.data?.[0]?.id === 'openai/gpt-4o'))
|
|
196
|
+
return false;
|
|
197
|
+
const emptyRows = await fetch(`${baseUrl}/api/v1/generations`).then((r) => r.json());
|
|
198
|
+
if (emptyRows.data?.length !== 0)
|
|
199
|
+
return false;
|
|
200
|
+
await fetch(`${baseUrl}/api/v1/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(CHAT()) });
|
|
201
|
+
const rows = await fetch(`${baseUrl}/api/v1/generations`).then((r) => r.json());
|
|
202
|
+
return rows.data?.length === 1 && rows.data?.[0]?.model === 'google/gemini-3-flash-preview';
|
|
203
|
+
})),
|
|
204
|
+
done('openrouter.ui.stub_banner', 'ui', 'Mirror playground renders the live stub completion and labels it', 'ui', 'common', () => withRoot(async (h) => {
|
|
205
|
+
// (1) the built mirror carries the stub banner copy the Playground emits …
|
|
206
|
+
const js = await mirrorBundle();
|
|
207
|
+
if (!(js.includes('Stub completion') && js.includes('no provider model was run')))
|
|
208
|
+
return false;
|
|
209
|
+
// (2) … AND the playground output is data-coupled: the same chat endpoint the Send button POSTs
|
|
210
|
+
// returns the labeled stub content the <pre> renders (not hardcoded UI text).
|
|
211
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ messages: [{ role: 'user', content: 'Summarize the OpenRouter twin.' }] }) });
|
|
212
|
+
const content = r.body.choices?.[0]?.message?.content;
|
|
213
|
+
return r.status === 200 && content.includes('[twin-stub:openrouter') && content.includes('Summarize the OpenRouter twin.');
|
|
214
|
+
})),
|
|
215
|
+
done('openrouter.connector.pull', 'connector', "Connector pulls the model catalog AND the account's provisioning keys through an injected client, idempotently — two domains, two calls", 'connector', 'common', async () => {
|
|
216
|
+
const root = mkdtempSync(join(tmpdir(), 'openrouter-connector-cap-'));
|
|
217
|
+
try {
|
|
218
|
+
let calls = 0;
|
|
219
|
+
const execute = async () => {
|
|
220
|
+
calls++;
|
|
221
|
+
return { status: 200, data: { data: [{ id: 'vendor/model-a', name: 'Vendor Model A', context_length: 4096 }] } };
|
|
222
|
+
};
|
|
223
|
+
await pullOpenRouterState(execute, { root });
|
|
224
|
+
await pullOpenRouterState(execute, { root });
|
|
225
|
+
const models = projectResources('openrouter', root).filter((r) => r.type === 'model');
|
|
226
|
+
// two domains per pull, so two pulls are four calls; the fake answers both with the same page
|
|
227
|
+
return calls === 4 && models.length === 1 && models[0]?.id === 'model_vendor/model-a';
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
rmSync(root, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
}),
|
|
233
|
+
done('openrouter.connector.push', 'connector', 'Connector confirms pending generation actions through an injected client idempotently', 'connector', 'niche', async () => {
|
|
234
|
+
const root = mkdtempSync(join(tmpdir(), 'openrouter-connector-cap-'));
|
|
235
|
+
try {
|
|
236
|
+
await applyTwinWrite('openrouter', {
|
|
237
|
+
operation: 'generation.record',
|
|
238
|
+
subjectType: 'generation',
|
|
239
|
+
subjectId: 'generation_chatcmpl-cap',
|
|
240
|
+
fields: { id: 'generation_chatcmpl-cap', object: 'generation', model: 'openai/gpt-4o' },
|
|
241
|
+
occurredAt: '1970-01-01T00:00:00.000Z',
|
|
242
|
+
actor: { kind: 'system' },
|
|
243
|
+
}, root);
|
|
244
|
+
const paths = [];
|
|
245
|
+
const execute = async (req) => {
|
|
246
|
+
paths.push(req.path);
|
|
247
|
+
return { status: 200, data: { ok: true } };
|
|
248
|
+
};
|
|
249
|
+
const first = await pushPendingOpenRouterActions(execute, { root });
|
|
250
|
+
const second = await pushPendingOpenRouterActions(execute, { root });
|
|
251
|
+
return first === 1 && second === 0 && deployableEntries('openrouter', root).length === 0 &&
|
|
252
|
+
paths.length === 1 && paths[0] === '/api/v1/generation?id=chatcmpl-cap';
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
rmSync(root, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
done('openrouter.chat.response_format', 'chat', 'Chat response_format json_object/json_schema', 'api', 'common', () => withRoot(async (h) => {
|
|
259
|
+
const ok = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ response_format: { type: 'json_object' } }) });
|
|
260
|
+
const content = ok.body.choices?.[0]?.message?.content;
|
|
261
|
+
let parsed = null;
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(content);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
parsed = null;
|
|
267
|
+
}
|
|
268
|
+
const schemaOk = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ response_format: { type: 'json_schema', json_schema: { name: 'echo', schema: { type: 'object' } } } }) });
|
|
269
|
+
const bad = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ response_format: { type: 'csv' } }) });
|
|
270
|
+
return ok.status === 200 && parsed?.twin_stub === true && schemaOk.status === 200 &&
|
|
271
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_response_format';
|
|
272
|
+
})),
|
|
273
|
+
done('openrouter.chat.reasoning', 'chat', 'Reasoning controls surface reasoning text and reasoning_tokens', 'api', 'common', () => withRoot(async (h) => {
|
|
274
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ reasoning: { effort: 'high' } }) });
|
|
275
|
+
const msg = r.body.choices?.[0]?.message;
|
|
276
|
+
const reasoned = r.body.usage?.completion_tokens_details?.reasoning_tokens;
|
|
277
|
+
const excluded = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ reasoning: { effort: 'low', exclude: true } }) });
|
|
278
|
+
const bad = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ reasoning: { effort: 'turbo' } }) });
|
|
279
|
+
return r.status === 200 && typeof msg.reasoning === 'string' && reasoned === 256 &&
|
|
280
|
+
excluded.status === 200 && excluded.body.choices?.[0]?.message?.reasoning === undefined &&
|
|
281
|
+
excluded.body.usage?.completion_tokens_details?.reasoning_tokens === 16 &&
|
|
282
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_reasoning';
|
|
283
|
+
})),
|
|
284
|
+
done('openrouter.chat.web_search', 'chat', 'Web search plugin/online suffix returns url_citation annotations', 'api', 'common', () => withRoot(async (h) => {
|
|
285
|
+
const viaPlugin = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ plugins: [{ id: 'web', max_results: 2 }] }) });
|
|
286
|
+
const ann = viaPlugin.body.choices?.[0]?.message?.annotations;
|
|
287
|
+
const viaOnline = await h({ m: 'POST', p: '/api/v1/chat/completions', b: { ...CHAT(), model: 'openai/gpt-4o:online' } });
|
|
288
|
+
const bad = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ plugins: [{ id: 'web', max_results: 0 }] }) });
|
|
289
|
+
return viaPlugin.status === 200 && ann?.length === 2 && ann[0]?.type === 'url_citation' &&
|
|
290
|
+
String(ann[0]?.url_citation?.title).includes('[twin-stub:openrouter:web_search]') &&
|
|
291
|
+
viaOnline.status === 200 && viaOnline.body.choices?.[0]?.message?.annotations?.length >= 1 &&
|
|
292
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_web_search';
|
|
293
|
+
})),
|
|
294
|
+
done('openrouter.chat.cache_control', 'chat', 'Prompt cache_control marks cached_tokens in usage', 'api', 'common', () => withRoot(async (h) => {
|
|
295
|
+
const cached = await h({ m: 'POST', p: '/api/v1/chat/completions', b: {
|
|
296
|
+
model: 'openai/gpt-4o',
|
|
297
|
+
messages: [
|
|
298
|
+
{ role: 'system', content: 'You are a long cached system prompt for the twin.', cache_control: { type: 'ephemeral' } },
|
|
299
|
+
{ role: 'user', content: 'hello' },
|
|
300
|
+
],
|
|
301
|
+
} });
|
|
302
|
+
const uncached = await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'openai/gpt-4o', messages: [{ role: 'user', content: 'hello' }] } });
|
|
303
|
+
const cachedTokens = cached.body.usage?.prompt_tokens_details?.cached_tokens;
|
|
304
|
+
const plainTokens = uncached.body.usage?.prompt_tokens_details?.cached_tokens;
|
|
305
|
+
return cached.status === 200 && cachedTokens > 0 && plainTokens === 0;
|
|
306
|
+
})),
|
|
307
|
+
done('openrouter.chat.provider_preferences', 'chat', 'Provider routing/preferences parameter is accepted and echoed', 'api', 'common', () => withRoot(async (h) => {
|
|
308
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ provider: { order: ['twin'], allow_fallbacks: false } }) });
|
|
309
|
+
const bad = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ provider: 'twin' }) });
|
|
310
|
+
return r.status === 200 && r.body.provider === 'twin' &&
|
|
311
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_provider';
|
|
312
|
+
})),
|
|
313
|
+
done('openrouter.chat.transforms', 'chat', 'Prompt transforms are validated and echoed', 'api', 'niche', () => withRoot(async (h) => {
|
|
314
|
+
const ok = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ transforms: ['middle-out'] }) });
|
|
315
|
+
const bad = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ transforms: ['nope'] }) });
|
|
316
|
+
const notArray = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ transforms: 'middle-out' }) });
|
|
317
|
+
return ok.status === 200 && JSON.stringify(ok.body.transforms) === JSON.stringify(['middle-out']) &&
|
|
318
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_transforms' &&
|
|
319
|
+
notArray.status === 400 && notArray.body.error?.code === 'invalid_transforms';
|
|
320
|
+
})),
|
|
321
|
+
done('openrouter.chat.logprobs', 'chat', 'logprobs + top_logprobs return OpenAI-shaped per-token logprobs', 'api', 'niche', () => withRoot(async (h) => {
|
|
322
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ logprobs: true, top_logprobs: 2 }) });
|
|
323
|
+
const lp = r.body.choices?.[0]?.logprobs;
|
|
324
|
+
const first = lp?.content?.[0];
|
|
325
|
+
const off = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
326
|
+
const badTop = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ logprobs: true, top_logprobs: 99 }) });
|
|
327
|
+
return r.status === 200 && Array.isArray(lp?.content) && typeof first?.logprob === 'number' &&
|
|
328
|
+
first?.top_logprobs?.length === 2 && typeof first?.top_logprobs?.[0]?.logprob === 'number' &&
|
|
329
|
+
off.body.choices?.[0]?.logprobs === undefined &&
|
|
330
|
+
badTop.status === 400 && badTop.body.error?.code === 'invalid_top_logprobs';
|
|
331
|
+
})),
|
|
332
|
+
done('openrouter.chat.modalities', 'chat', 'Image output modality attaches a labeled stub image; bad modality rejected', 'api', 'niche', () => withRoot(async (h) => {
|
|
333
|
+
const r = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ modalities: ['text', 'image'] }) });
|
|
334
|
+
const images = r.body.choices?.[0]?.message?.images;
|
|
335
|
+
const bad = await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT({ modalities: ['hologram'] }) });
|
|
336
|
+
const url = String(images?.[0]?.image_url?.url ?? '');
|
|
337
|
+
return r.status === 200 && images?.length === 1 && url.startsWith('data:image/png;base64,') &&
|
|
338
|
+
Buffer.from(url.split(',')[1] ?? '', 'base64').toString().includes('[twin-stub:openrouter:image]') &&
|
|
339
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_modalities';
|
|
340
|
+
})),
|
|
341
|
+
done('openrouter.responses.create', 'responses', 'Responses API beta: create response', 'api', 'core', () => withRoot(async (h) => {
|
|
342
|
+
const r = await h({ m: 'POST', p: '/api/v1/responses', b: { model: 'openai/gpt-4o', input: 'hello responses' } });
|
|
343
|
+
const b = r.body;
|
|
344
|
+
const textItem = b.output?.find((o) => o.type === 'message');
|
|
345
|
+
const bad = await h({ m: 'POST', p: '/api/v1/responses', b: { model: 'openai/gpt-4o' } });
|
|
346
|
+
const unknown = await h({ m: 'POST', p: '/api/v1/responses', b: { model: 'no/model', input: 'x' } });
|
|
347
|
+
return r.status === 200 && b.object === 'response' && b.status === 'completed' &&
|
|
348
|
+
String(b.output_text).includes('[twin-stub:openrouter') && textItem?.content?.[0]?.type === 'output_text' &&
|
|
349
|
+
b.usage?.input_tokens > 0 && bad.status === 400 && unknown.status === 404;
|
|
350
|
+
})),
|
|
351
|
+
done('openrouter.responses.reasoning', 'responses', 'Responses API reasoning emits a reasoning output item', 'api', 'common', () => withRoot(async (h) => {
|
|
352
|
+
const r = await h({ m: 'POST', p: '/api/v1/responses', b: { model: 'openai/gpt-4o', input: 'think', reasoning: { effort: 'medium' } } });
|
|
353
|
+
const reasoningItem = r.body.output?.find((o) => o.type === 'reasoning');
|
|
354
|
+
const reasoned = r.body.usage?.output_tokens_details?.reasoning_tokens;
|
|
355
|
+
return r.status === 200 && reasoningItem?.summary?.[0]?.type === 'summary_text' && reasoned === 64;
|
|
356
|
+
})),
|
|
357
|
+
done('openrouter.responses.tool_calling', 'responses', 'Responses API returns a function_call output item', 'api', 'common', () => withRoot(async (h) => {
|
|
358
|
+
const r = await h({ m: 'POST', p: '/api/v1/responses', b: { model: 'openai/gpt-4o', input: 'call', tools: [{ type: 'function', name: 'lookup' }] } });
|
|
359
|
+
const fc = r.body.output?.find((o) => o.type === 'function_call');
|
|
360
|
+
return r.status === 200 && fc?.name === 'lookup' && typeof fc?.call_id === 'string' && fc?.status === 'completed';
|
|
361
|
+
})),
|
|
362
|
+
done('openrouter.responses.web_search', 'responses', 'Responses API web search attaches url citations', 'api', 'common', () => withRoot(async (h) => {
|
|
363
|
+
const r = await h({ m: 'POST', p: '/api/v1/responses', b: { model: 'openai/gpt-4o', input: 'find', plugins: [{ id: 'web', max_results: 2 }] } });
|
|
364
|
+
const msg = r.body.output?.find((o) => o.type === 'message');
|
|
365
|
+
const ann = msg?.content?.[0]?.annotations;
|
|
366
|
+
return r.status === 200 && ann?.length === 2 && ann[0]?.type === 'url_citation';
|
|
367
|
+
})),
|
|
368
|
+
done('openrouter.anthropic.messages', 'anthropic_messages', 'Anthropic Messages compatibility endpoint', 'api', 'common', () => withRoot(async (h) => {
|
|
369
|
+
const r = await h({ m: 'POST', p: '/api/v1/messages', b: { model: 'anthropic/claude-sonnet-4.5', max_tokens: 64, messages: [{ role: 'user', content: 'hello claude' }] } });
|
|
370
|
+
const b = r.body;
|
|
371
|
+
const noMax = await h({ m: 'POST', p: '/api/v1/messages', b: { model: 'anthropic/claude-sonnet-4.5', messages: [{ role: 'user', content: 'x' }] } });
|
|
372
|
+
return r.status === 200 && b.type === 'message' && b.role === 'assistant' &&
|
|
373
|
+
String(b.content?.[0]?.text).includes('[twin-stub:openrouter') && b.stop_reason === 'end_turn' &&
|
|
374
|
+
typeof b.usage?.input_tokens === 'number' && noMax.status === 400 && noMax.body.error?.code === 'invalid_max_tokens';
|
|
375
|
+
})),
|
|
376
|
+
done('openrouter.embeddings.create', 'embeddings', 'Embeddings endpoint', 'api', 'core', () => withRoot(async (h) => {
|
|
377
|
+
const r = await h({ m: 'POST', p: '/api/v1/embeddings', b: { model: 'openai/text-embedding-3-small', input: ['alpha', 'beta'] } });
|
|
378
|
+
const b = r.body;
|
|
379
|
+
const first = b.data?.[0];
|
|
380
|
+
const same = await h({ m: 'POST', p: '/api/v1/embeddings', b: { model: 'openai/text-embedding-3-small', input: ['alpha'] } });
|
|
381
|
+
const deterministic = JSON.stringify(same.body.data?.[0]?.embedding) === JSON.stringify(first?.embedding);
|
|
382
|
+
const bad = await h({ m: 'POST', p: '/api/v1/embeddings', b: { model: 'openai/text-embedding-3-small' } });
|
|
383
|
+
return r.status === 200 && b.object === 'list' && b.data?.length === 2 && first?.index === 0 &&
|
|
384
|
+
Array.isArray(first?.embedding) && first.embedding.length === 8 && deterministic &&
|
|
385
|
+
b.usage?.prompt_tokens > 0 && bad.status === 400;
|
|
386
|
+
})),
|
|
387
|
+
done('openrouter.providers.list', 'providers', 'Providers: list all providers', 'api', 'common', () => withRoot(async (h) => {
|
|
388
|
+
const r = await h({ m: 'GET', p: '/api/v1/providers' });
|
|
389
|
+
const data = r.body.data;
|
|
390
|
+
return r.status === 200 && data?.length >= 3 && data.some((p) => p.slug === 'anthropic' && typeof p.may_log_prompts === 'boolean');
|
|
391
|
+
})),
|
|
392
|
+
done('openrouter.rerank.submit', 'rerank', 'Rerank: submit request returns scored results', 'api', 'common', () => withRoot(async (h) => {
|
|
393
|
+
const r = await h({ m: 'POST', p: '/api/v1/rerank', b: { model: 'twin/rerank-1', query: 'orange fruit', documents: ['a green car', 'an orange fruit basket'], top_n: 2 } });
|
|
394
|
+
const results = r.body.results;
|
|
395
|
+
const bad = await h({ m: 'POST', p: '/api/v1/rerank', b: { model: 'twin/rerank-1', query: 'x' } });
|
|
396
|
+
// index 1 mentions the query terms, index 0 does not → it must rank first.
|
|
397
|
+
return r.status === 200 && results?.length === 2 && results[0].relevance_score >= results[1].relevance_score &&
|
|
398
|
+
results[0].index === 1 && results[1].index === 0 && results[0].relevance_score > 0 && results[1].relevance_score === 0 &&
|
|
399
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_documents';
|
|
400
|
+
})),
|
|
401
|
+
done('openrouter.api_keys.list', 'api_keys', 'API keys: list keys reflects created keys', 'api', 'common', () => withRoot(async (h) => {
|
|
402
|
+
const empty = await h({ m: 'GET', p: '/api/v1/keys' });
|
|
403
|
+
await h({ m: 'POST', p: '/api/v1/keys', b: { name: 'ci key' } });
|
|
404
|
+
const list = await h({ m: 'GET', p: '/api/v1/keys' });
|
|
405
|
+
const rows = list.body.data;
|
|
406
|
+
return empty.status === 200 && empty.body.data?.length === 0 &&
|
|
407
|
+
list.status === 200 && rows?.length === 1 && rows[0]?.name === 'ci key' && !String(rows[0]?.id).startsWith('key_');
|
|
408
|
+
})),
|
|
409
|
+
done('openrouter.api_keys.create', 'api_keys', 'API keys: create key returns the secret once', 'api', 'common', () => withRoot(async (h) => {
|
|
410
|
+
const r = await h({ m: 'POST', p: '/api/v1/keys', b: { name: 'deploy', limit: 5 } });
|
|
411
|
+
const b = r.body.data;
|
|
412
|
+
const bad = await h({ m: 'POST', p: '/api/v1/keys', b: {} });
|
|
413
|
+
const ro = await handleOpenRouterTwinRequestRO('POST', '/api/v1/keys', { name: 'x' });
|
|
414
|
+
return r.status === 200 && typeof b?.key === 'string' && b.key.startsWith('sk-or-v1-twin-') &&
|
|
415
|
+
b.limit === 5 && b.disabled === false && bad.status === 400 && ro.status === 405;
|
|
416
|
+
})),
|
|
417
|
+
done('openrouter.api_keys.update', 'api_keys', 'API keys: update/delete key mutates persisted state', 'api', 'common', () => withRoot(async (h) => {
|
|
418
|
+
const created = await h({ m: 'POST', p: '/api/v1/keys', b: { name: 'rotate' } });
|
|
419
|
+
const hash = created.body.data?.hash;
|
|
420
|
+
const upd = await h({ m: 'PATCH', p: `/api/v1/keys/${hash}`, b: { disabled: true, name: 'rotated' } });
|
|
421
|
+
const list = await h({ m: 'GET', p: '/api/v1/keys' });
|
|
422
|
+
const after = list.body.data?.[0];
|
|
423
|
+
const del = await h({ m: 'DELETE', p: `/api/v1/keys/${hash}` });
|
|
424
|
+
const afterDel = (await h({ m: 'GET', p: '/api/v1/keys' })).body.data;
|
|
425
|
+
const missing = await h({ m: 'PATCH', p: '/api/v1/keys/nope', b: { name: 'x' } });
|
|
426
|
+
return upd.status === 200 && after?.disabled === true && after?.name === 'rotated' &&
|
|
427
|
+
del.status === 200 && del.body.data?.deleted === true && afterDel?.length === 0 && missing.status === 404;
|
|
428
|
+
})),
|
|
429
|
+
done('openrouter.byok.list', 'byok', 'BYOK: list provider credentials', 'api', 'common', () => withRoot(async (h) => {
|
|
430
|
+
const empty = await h({ m: 'GET', p: '/api/v1/keys/byok' });
|
|
431
|
+
await h({ m: 'POST', p: '/api/v1/keys/byok', b: { provider_name: 'anthropic', api_key: 'sk-ant-secret1234' } });
|
|
432
|
+
const list = await h({ m: 'GET', p: '/api/v1/keys/byok' });
|
|
433
|
+
const rows = list.body.data;
|
|
434
|
+
return empty.status === 200 && empty.body.data?.length === 0 &&
|
|
435
|
+
rows?.length === 1 && rows[0]?.provider_name === 'anthropic' && rows[0]?.api_key_preview === '...1234' &&
|
|
436
|
+
!String(rows[0]?.id).includes('sk-ant');
|
|
437
|
+
})),
|
|
438
|
+
done('openrouter.byok.create', 'byok', 'BYOK: create provider credential validates provider', 'api', 'common', () => withRoot(async (h) => {
|
|
439
|
+
const ok = await h({ m: 'POST', p: '/api/v1/keys/byok', b: { provider_name: 'openai', api_key: 'sk-secretkey99' } });
|
|
440
|
+
const unknown = await h({ m: 'POST', p: '/api/v1/keys/byok', b: { provider_name: 'made-up', api_key: 'x' } });
|
|
441
|
+
const missing = await h({ m: 'POST', p: '/api/v1/keys/byok', b: { provider_name: 'openai' } });
|
|
442
|
+
return ok.status === 200 && ok.body.data?.enabled === true &&
|
|
443
|
+
unknown.status === 400 && unknown.body.error?.code === 'unknown_provider' &&
|
|
444
|
+
missing.status === 400 && missing.body.error?.code === 'invalid_api_key';
|
|
445
|
+
})),
|
|
446
|
+
done('openrouter.byok.update', 'byok', 'BYOK: update/delete provider credential', 'api', 'common', () => withRoot(async (h) => {
|
|
447
|
+
await h({ m: 'POST', p: '/api/v1/keys/byok', b: { provider_name: 'openai', api_key: 'sk-orig0000' } });
|
|
448
|
+
const upd = await h({ m: 'PATCH', p: '/api/v1/keys/byok/openai', b: { enabled: false } });
|
|
449
|
+
const afterUpd = (await h({ m: 'GET', p: '/api/v1/keys/byok' })).body.data?.[0];
|
|
450
|
+
const del = await h({ m: 'DELETE', p: '/api/v1/keys/byok/openai' });
|
|
451
|
+
const afterDel = (await h({ m: 'GET', p: '/api/v1/keys/byok' })).body.data;
|
|
452
|
+
const missing = await h({ m: 'DELETE', p: '/api/v1/keys/byok/anthropic' });
|
|
453
|
+
return upd.status === 200 && afterUpd?.enabled === false && del.status === 200 &&
|
|
454
|
+
afterDel?.length === 0 && missing.status === 404;
|
|
455
|
+
})),
|
|
456
|
+
done('openrouter.presets.list', 'presets', 'Presets: list/get presets', 'api', 'common', () => withRoot(async (h) => {
|
|
457
|
+
const created = await h({ m: 'POST', p: '/api/v1/presets', b: { name: 'My Preset', model: 'openai/gpt-4o', parameters: { temperature: 0.2 } } });
|
|
458
|
+
const slug = created.body.data?.slug;
|
|
459
|
+
const list = await h({ m: 'GET', p: '/api/v1/presets' });
|
|
460
|
+
const get = await h({ m: 'GET', p: `/api/v1/presets/${slug}` });
|
|
461
|
+
const missing = await h({ m: 'GET', p: '/api/v1/presets/nope' });
|
|
462
|
+
return list.status === 200 && list.body.data?.length === 1 && slug === 'my-preset' &&
|
|
463
|
+
get.status === 200 && get.body.data?.parameters?.temperature === 0.2 && missing.status === 404;
|
|
464
|
+
})),
|
|
465
|
+
done('openrouter.presets.create_chat', 'presets', 'Presets: create from chat completions body', 'api', 'common', () => withRoot(async (h) => {
|
|
466
|
+
const r = await h({ m: 'POST', p: '/api/v1/presets', b: { name: 'Chat Preset', model: 'openai/gpt-4o' } });
|
|
467
|
+
const b = r.body.data;
|
|
468
|
+
const badModel = await h({ m: 'POST', p: '/api/v1/presets', b: { name: 'x', model: 'no/model' } });
|
|
469
|
+
const noName = await h({ m: 'POST', p: '/api/v1/presets', b: { model: 'openai/gpt-4o' } });
|
|
470
|
+
return r.status === 200 && b?.source === 'chat' && b?.model === 'openai/gpt-4o' &&
|
|
471
|
+
badModel.status === 404 && noName.status === 400;
|
|
472
|
+
})),
|
|
473
|
+
done('openrouter.files.upload', 'files', 'Files: upload returns a processed file object', 'api', 'common', () => withRoot(async (h) => {
|
|
474
|
+
const r = await h({ m: 'POST', p: '/api/v1/files', b: { filename: 'data.jsonl', purpose: 'user_data', content: 'a\nb\nc' } });
|
|
475
|
+
const b = r.body;
|
|
476
|
+
const noName = await h({ m: 'POST', p: '/api/v1/files', b: { purpose: 'user_data' } });
|
|
477
|
+
const badPurpose = await h({ m: 'POST', p: '/api/v1/files', b: { filename: 'x', purpose: 'wat' } });
|
|
478
|
+
const ro = await handleOpenRouterTwinRequestRO('POST', '/api/v1/files', { filename: 'x' });
|
|
479
|
+
return r.status === 200 && b.object === 'file' && b.filename === 'data.jsonl' && b.purpose === 'user_data' &&
|
|
480
|
+
b.bytes === 5 && b.status === 'processed' && typeof b.id === 'string' && !String(b.id).startsWith('file_') &&
|
|
481
|
+
noName.status === 400 && badPurpose.status === 400 && badPurpose.body.error?.code === 'invalid_purpose' && ro.status === 405;
|
|
482
|
+
})),
|
|
483
|
+
done('openrouter.files.retrieve', 'files', 'Files: retrieve/list/delete reflect persisted state', 'api', 'common', () => withRoot(async (h) => {
|
|
484
|
+
const created = await h({ m: 'POST', p: '/api/v1/files', b: { filename: 'doc.txt', content: 'hello' } });
|
|
485
|
+
const id = created.body.id;
|
|
486
|
+
const list = await h({ m: 'GET', p: '/api/v1/files' });
|
|
487
|
+
const get = await h({ m: 'GET', p: `/api/v1/files/${id}` });
|
|
488
|
+
const del = await h({ m: 'DELETE', p: `/api/v1/files/${id}` });
|
|
489
|
+
const afterDel = await h({ m: 'GET', p: '/api/v1/files' });
|
|
490
|
+
const missing = await h({ m: 'GET', p: '/api/v1/files/nope' });
|
|
491
|
+
return list.status === 200 && list.body.object === 'list' && list.body.data?.length === 1 &&
|
|
492
|
+
get.status === 200 && get.body.filename === 'doc.txt' &&
|
|
493
|
+
del.status === 200 && del.body.deleted === true && afterDel.body.data?.length === 0 && missing.status === 404;
|
|
494
|
+
})),
|
|
495
|
+
done('openrouter.oauth.authorization_code_create', 'oauth', 'OAuth: create authorization code with PKCE challenge + redirect', 'api', 'common', () => withRoot(async (h) => {
|
|
496
|
+
const r = await h({ m: 'POST', p: '/api/v1/auth/authorize', b: { callback_url: 'https://app.local/cb', code_challenge: 'abc', code_challenge_method: 'S256' } });
|
|
497
|
+
const b = r.body.data;
|
|
498
|
+
const noCb = await h({ m: 'POST', p: '/api/v1/auth/authorize', b: { code_challenge: 'abc' } });
|
|
499
|
+
const noChal = await h({ m: 'POST', p: '/api/v1/auth/authorize', b: { callback_url: 'https://app.local/cb' } });
|
|
500
|
+
return r.status === 200 && typeof b?.code === 'string' && String(b?.redirect_url).startsWith('https://app.local/cb?code=') &&
|
|
501
|
+
noCb.status === 400 && noCb.body.error?.code === 'invalid_callback_url' &&
|
|
502
|
+
noChal.status === 400 && noChal.body.error?.code === 'invalid_code_challenge';
|
|
503
|
+
})),
|
|
504
|
+
done('openrouter.oauth.exchange', 'oauth', 'OAuth: exchange code for API key validates PKCE verifier; single-use', 'api', 'common', () => withRoot(async (h) => {
|
|
505
|
+
// S256(verifier) must equal challenge. Use a known pair.
|
|
506
|
+
const verifier = 'twin-pkce-verifier-string';
|
|
507
|
+
const challenge = createHash('sha256').update(verifier).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
508
|
+
const auth = await h({ m: 'POST', p: '/api/v1/auth/authorize', b: { callback_url: 'https://app.local/cb', code_challenge: challenge } });
|
|
509
|
+
const code = auth.body.data?.code;
|
|
510
|
+
const wrong = await h({ m: 'POST', p: '/api/v1/auth/keys', b: { code, code_verifier: 'wrong' } });
|
|
511
|
+
const ok = await h({ m: 'POST', p: '/api/v1/auth/keys', b: { code, code_verifier: verifier } });
|
|
512
|
+
const reused = await h({ m: 'POST', p: '/api/v1/auth/keys', b: { code, code_verifier: verifier } });
|
|
513
|
+
const keys = await h({ m: 'GET', p: '/api/v1/keys' });
|
|
514
|
+
return wrong.status === 400 && wrong.body.error?.code === 'invalid_code_verifier' &&
|
|
515
|
+
ok.status === 200 && String(ok.body.key).startsWith('sk-or-v1-twin-') &&
|
|
516
|
+
reused.status === 400 && reused.body.error?.code === 'code_already_used' &&
|
|
517
|
+
keys.body.data?.length === 1;
|
|
518
|
+
})),
|
|
519
|
+
done('openrouter.speech.create', 'speech', 'Speech: returns a labeled deterministic stub audio envelope', 'api', 'niche', () => withRoot(async (h) => {
|
|
520
|
+
const r = await h({ m: 'POST', p: '/api/v1/audio/speech', b: { model: 'twin/tts-1', input: 'hello world', voice: 'nova' } });
|
|
521
|
+
const b = r.body;
|
|
522
|
+
const decoded = Buffer.from(String(b.audio), 'base64').toString();
|
|
523
|
+
const bad = await h({ m: 'POST', p: '/api/v1/audio/speech', b: { model: 'twin/tts-1' } });
|
|
524
|
+
return r.status === 200 && b.object === 'audio.speech' && b.voice === 'nova' && b.stub === true &&
|
|
525
|
+
decoded.includes('[twin-stub:openrouter:speech]') && decoded.includes('hello world') &&
|
|
526
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_input';
|
|
527
|
+
})),
|
|
528
|
+
done('openrouter.transcriptions.create', 'transcriptions', 'Transcriptions: returns a labeled deterministic stub transcript', 'api', 'niche', () => withRoot(async (h) => {
|
|
529
|
+
const r = await h({ m: 'POST', p: '/api/v1/audio/transcriptions', b: { model: 'twin/whisper-1', file: 'base64audio' } });
|
|
530
|
+
const b = r.body;
|
|
531
|
+
const bad = await h({ m: 'POST', p: '/api/v1/audio/transcriptions', b: { model: 'twin/whisper-1' } });
|
|
532
|
+
return r.status === 200 && b.object === 'audio.transcription' && String(b.text).includes('[twin-stub:openrouter:transcription]') &&
|
|
533
|
+
b.stub === true && bad.status === 400 && bad.body.error?.code === 'invalid_file';
|
|
534
|
+
})),
|
|
535
|
+
done('openrouter.analytics.activity', 'analytics', 'Analytics: activity is computed from recorded generations grouped by model', 'api', 'niche', () => withRoot(async (h) => {
|
|
536
|
+
const empty = await h({ m: 'GET', p: '/api/v1/analytics/activity' });
|
|
537
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
538
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'openai/gpt-4o', messages: [{ role: 'user', content: 'second' }] } });
|
|
539
|
+
const r = await h({ m: 'GET', p: '/api/v1/analytics/activity' });
|
|
540
|
+
const rows = r.body.data;
|
|
541
|
+
const gemini = rows.find((x) => x.model === 'google/gemini-3-flash-preview');
|
|
542
|
+
return empty.status === 200 && empty.body.data?.length === 0 &&
|
|
543
|
+
rows.length === 2 && gemini?.requests === 1 && gemini?.prompt_tokens > 0 && typeof gemini?.usage === 'number';
|
|
544
|
+
})),
|
|
545
|
+
done('openrouter.analytics.query', 'analytics', 'Analytics: metric/dimension query aggregates recorded generations', 'api', 'niche', () => withRoot(async (h) => {
|
|
546
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
547
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'openai/gpt-4o', messages: [{ role: 'user', content: 'q' }] } });
|
|
548
|
+
const byModel = await h({ m: 'GET', p: '/api/v1/analytics/query?metric=requests&dimension=model' });
|
|
549
|
+
const data = byModel.body.data;
|
|
550
|
+
const post = await h({ m: 'POST', p: '/api/v1/analytics/query', b: { metric: 'total_tokens', dimension: 'date' } });
|
|
551
|
+
const bad = await h({ m: 'GET', p: '/api/v1/analytics/query?metric=bogus' });
|
|
552
|
+
return byModel.status === 200 && data.length === 2 && data.every((d) => d.requests === 1) &&
|
|
553
|
+
post.status === 200 && post.body.data?.[0]?.total_tokens > 0 &&
|
|
554
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_metric';
|
|
555
|
+
})),
|
|
556
|
+
done('openrouter.analytics.daily_tokens', 'analytics', 'Analytics: daily token totals computed from recorded generations', 'api', 'niche', () => withRoot(async (h) => {
|
|
557
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: CHAT() });
|
|
558
|
+
await h({ m: 'POST', p: '/api/v1/chat/completions', b: { model: 'openai/gpt-4o', messages: [{ role: 'user', content: 'd' }] } });
|
|
559
|
+
const r = await h({ m: 'GET', p: '/api/v1/analytics/daily' });
|
|
560
|
+
const rows = r.body.data;
|
|
561
|
+
return r.status === 200 && rows.length === 1 && rows[0]?.requests === 2 &&
|
|
562
|
+
rows[0]?.total_tokens === rows[0]?.prompt_tokens + rows[0]?.completion_tokens && rows[0]?.total_tokens > 0;
|
|
563
|
+
})),
|
|
564
|
+
done('openrouter.presets.create_responses', 'presets', 'Presets: create from responses body tags source=responses', 'api', 'niche', () => withRoot(async (h) => {
|
|
565
|
+
const r = await h({ m: 'POST', p: '/api/v1/presets/responses', b: { name: 'Resp Preset', model: 'openai/gpt-4o', parameters: { temperature: 0.5 } } });
|
|
566
|
+
const b = r.body.data;
|
|
567
|
+
const get = await h({ m: 'GET', p: `/api/v1/presets/${b?.slug}` });
|
|
568
|
+
const badModel = await h({ m: 'POST', p: '/api/v1/presets/responses', b: { name: 'x', model: 'no/model' } });
|
|
569
|
+
return r.status === 200 && b?.source === 'responses' && b?.slug === 'resp-preset' &&
|
|
570
|
+
get.status === 200 && get.body.data?.source === 'responses' && badModel.status === 404;
|
|
571
|
+
})),
|
|
572
|
+
done('openrouter.datasets.list', 'datasets', 'Datasets: list reflects created datasets', 'api', 'niche', () => withRoot(async (h) => {
|
|
573
|
+
const empty = await h({ m: 'GET', p: '/api/v1/datasets' });
|
|
574
|
+
await h({ m: 'POST', p: '/api/v1/datasets', b: { name: 'eval set' } });
|
|
575
|
+
const list = await h({ m: 'GET', p: '/api/v1/datasets' });
|
|
576
|
+
const rows = list.body.data;
|
|
577
|
+
return empty.status === 200 && empty.body.data?.length === 0 &&
|
|
578
|
+
list.status === 200 && rows?.length === 1 && rows[0]?.name === 'eval set' && !String(rows[0]?.id).startsWith('dataset_');
|
|
579
|
+
})),
|
|
580
|
+
done('openrouter.datasets.create', 'datasets', 'Datasets: create validates name and persists', 'api', 'niche', () => withRoot(async (h) => {
|
|
581
|
+
const r = await h({ m: 'POST', p: '/api/v1/datasets', b: { name: 'My Dataset', description: 'd' } });
|
|
582
|
+
const b = r.body.data;
|
|
583
|
+
const noName = await h({ m: 'POST', p: '/api/v1/datasets', b: {} });
|
|
584
|
+
const ro = await handleOpenRouterTwinRequestRO('POST', '/api/v1/datasets', { name: 'x' });
|
|
585
|
+
return r.status === 200 && b?.name === 'My Dataset' && b?.status === 'ready' && b?.row_count === 0 &&
|
|
586
|
+
noName.status === 400 && ro.status === 405;
|
|
587
|
+
})),
|
|
588
|
+
done('openrouter.guardrails.crud', 'guardrails', 'Guardrails: create/list/get/update/delete persist through the kernel', 'api', 'common', () => withRoot(async (h) => {
|
|
589
|
+
const created = await h({ m: 'POST', p: '/api/v1/guardrails', b: { name: 'PII Filter', rules: [{ kind: 'pii' }] } });
|
|
590
|
+
const id = created.body.data?.id;
|
|
591
|
+
const list = await h({ m: 'GET', p: '/api/v1/guardrails' });
|
|
592
|
+
const upd = await h({ m: 'PATCH', p: `/api/v1/guardrails/${id}`, b: { enabled: false, name: 'PII Off' } });
|
|
593
|
+
const get = await h({ m: 'GET', p: `/api/v1/guardrails/${id}` });
|
|
594
|
+
const del = await h({ m: 'DELETE', p: `/api/v1/guardrails/${id}` });
|
|
595
|
+
const afterDel = await h({ m: 'GET', p: '/api/v1/guardrails' });
|
|
596
|
+
const noName = await h({ m: 'POST', p: '/api/v1/guardrails', b: {} });
|
|
597
|
+
return created.status === 200 && list.body.data?.length === 1 && upd.status === 200 &&
|
|
598
|
+
get.body.data?.enabled === false && get.body.data?.name === 'PII Off' &&
|
|
599
|
+
del.status === 200 && afterDel.body.data?.length === 0 && noName.status === 400;
|
|
600
|
+
})),
|
|
601
|
+
done('openrouter.guardrails.assignments', 'guardrails', 'Guardrails: set/get key assignments persist', 'api', 'niche', () => withRoot(async (h) => {
|
|
602
|
+
const created = await h({ m: 'POST', p: '/api/v1/guardrails', b: { name: 'GR' } });
|
|
603
|
+
const id = created.body.data?.id;
|
|
604
|
+
const set = await h({ m: 'PUT', p: `/api/v1/guardrails/${id}/assignments`, b: { key_hashes: ['h1', 'h2'] } });
|
|
605
|
+
const get = await h({ m: 'GET', p: `/api/v1/guardrails/${id}/assignments` });
|
|
606
|
+
const bad = await h({ m: 'PUT', p: `/api/v1/guardrails/${id}/assignments`, b: {} });
|
|
607
|
+
const missing = await h({ m: 'GET', p: '/api/v1/guardrails/nope/assignments' });
|
|
608
|
+
return set.status === 200 && JSON.stringify(get.body.data?.key_hashes) === JSON.stringify(['h1', 'h2']) &&
|
|
609
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_assignments' && missing.status === 404;
|
|
610
|
+
})),
|
|
611
|
+
done('openrouter.organization.members', 'organization', 'Organization: list members with roles', 'api', 'common', () => withRoot(async (h) => {
|
|
612
|
+
const r = await h({ m: 'GET', p: '/api/v1/organization/members' });
|
|
613
|
+
const rows = r.body.data;
|
|
614
|
+
return r.status === 200 && rows?.length >= 2 && rows.some((m) => m.role === 'owner') && rows.every((m) => typeof m.email === 'string');
|
|
615
|
+
})),
|
|
616
|
+
done('openrouter.workspaces.crud', 'workspaces', 'Workspaces: create/list/get/update/delete persist through the kernel', 'api', 'common', () => withRoot(async (h) => {
|
|
617
|
+
const created = await h({ m: 'POST', p: '/api/v1/workspaces', b: { name: 'Team A' } });
|
|
618
|
+
const id = created.body.data?.id;
|
|
619
|
+
const list = await h({ m: 'GET', p: '/api/v1/workspaces' });
|
|
620
|
+
const upd = await h({ m: 'PATCH', p: `/api/v1/workspaces/${id}`, b: { description: 'updated' } });
|
|
621
|
+
const get = await h({ m: 'GET', p: `/api/v1/workspaces/${id}` });
|
|
622
|
+
const del = await h({ m: 'DELETE', p: `/api/v1/workspaces/${id}` });
|
|
623
|
+
const afterDel = await h({ m: 'GET', p: '/api/v1/workspaces' });
|
|
624
|
+
const noName = await h({ m: 'POST', p: '/api/v1/workspaces', b: {} });
|
|
625
|
+
return created.status === 200 && created.body.data?.slug === 'team-a' && list.body.data?.length === 1 &&
|
|
626
|
+
upd.status === 200 && get.body.data?.description === 'updated' &&
|
|
627
|
+
del.status === 200 && afterDel.body.data?.length === 0 && noName.status === 400;
|
|
628
|
+
})),
|
|
629
|
+
done('openrouter.workspaces.members', 'workspaces', 'Workspaces: bulk add/remove members mutates membership set', 'api', 'niche', () => withRoot(async (h) => {
|
|
630
|
+
const created = await h({ m: 'POST', p: '/api/v1/workspaces', b: { name: 'WS' } });
|
|
631
|
+
const id = created.body.data?.id;
|
|
632
|
+
const add = await h({ m: 'POST', p: `/api/v1/workspaces/${id}/members`, b: { add: ['u1', 'u2'] } });
|
|
633
|
+
const remove = await h({ m: 'POST', p: `/api/v1/workspaces/${id}/members`, b: { remove: ['u1'] } });
|
|
634
|
+
const get = await h({ m: 'GET', p: `/api/v1/workspaces/${id}` });
|
|
635
|
+
const bad = await h({ m: 'POST', p: `/api/v1/workspaces/${id}/members`, b: {} });
|
|
636
|
+
return add.status === 200 && JSON.stringify(add.body.data?.members) === JSON.stringify(['u1', 'u2']) &&
|
|
637
|
+
remove.status === 200 && JSON.stringify(remove.body.data?.members) === JSON.stringify(['u2']) &&
|
|
638
|
+
JSON.stringify(get.body.data?.members) === JSON.stringify(['u2']) &&
|
|
639
|
+
bad.status === 400 && bad.body.error?.code === 'invalid_members';
|
|
640
|
+
})),
|
|
641
|
+
done('openrouter.video.submit', 'video', 'Video generation: submit returns a completed generation object', 'api', 'niche', () => withRoot(async (h) => {
|
|
642
|
+
const r = await h({ m: 'POST', p: '/api/v1/video', b: { model: 'twin/video-gen-1', prompt: 'a cat surfing' } });
|
|
643
|
+
const b = r.body;
|
|
644
|
+
const noPrompt = await h({ m: 'POST', p: '/api/v1/video', b: { model: 'twin/video-gen-1' } });
|
|
645
|
+
return r.status === 200 && b.object === 'video.generation' && b.status === 'completed' && b.stub === true &&
|
|
646
|
+
typeof b.id === 'string' && String(b.download_url).includes('/content') &&
|
|
647
|
+
noPrompt.status === 400 && noPrompt.body.error?.code === 'invalid_prompt';
|
|
648
|
+
})),
|
|
649
|
+
done('openrouter.video.poll_download', 'video', 'Video generation: poll status, download labeled stub, list models', 'api', 'niche', () => withRoot(async (h) => {
|
|
650
|
+
const sub = await h({ m: 'POST', p: '/api/v1/video', b: { model: 'twin/video-gen-1', prompt: 'a dog skating' } });
|
|
651
|
+
const id = sub.body.id;
|
|
652
|
+
const poll = await h({ m: 'GET', p: `/api/v1/video/${id}` });
|
|
653
|
+
const dl = await h({ m: 'GET', p: `/api/v1/video/${id}/content` });
|
|
654
|
+
const models = await h({ m: 'GET', p: '/api/v1/video/models' });
|
|
655
|
+
const missing = await h({ m: 'GET', p: '/api/v1/video/nope' });
|
|
656
|
+
const decoded = Buffer.from(String(dl.body.content), 'base64').toString();
|
|
657
|
+
return poll.status === 200 && poll.body.status === 'completed' &&
|
|
658
|
+
dl.status === 200 && decoded.includes('[twin-stub:openrouter:video]') &&
|
|
659
|
+
models.body.data?.length >= 1 && missing.status === 404;
|
|
660
|
+
})),
|
|
661
|
+
done('openrouter.observability.destinations', 'observability', 'Observability destinations: list/create/update/delete', 'api', 'niche', () => withRoot(async (h) => {
|
|
662
|
+
const created = await h({ m: 'POST', p: '/api/v1/observability/destinations', b: { type: 'webhook', name: 'hook', config: { url: 'https://x' } } });
|
|
663
|
+
const id = created.body.data?.id;
|
|
664
|
+
const list = await h({ m: 'GET', p: '/api/v1/observability/destinations' });
|
|
665
|
+
const upd = await h({ m: 'PATCH', p: `/api/v1/observability/destinations/${id}`, b: { enabled: false } });
|
|
666
|
+
const del = await h({ m: 'DELETE', p: `/api/v1/observability/destinations/${id}` });
|
|
667
|
+
const afterDel = await h({ m: 'GET', p: '/api/v1/observability/destinations' });
|
|
668
|
+
const badType = await h({ m: 'POST', p: '/api/v1/observability/destinations', b: { type: 'carrier-pigeon' } });
|
|
669
|
+
return created.status === 200 && created.body.data?.type === 'webhook' && list.body.data?.length === 1 &&
|
|
670
|
+
upd.status === 200 && upd.body.data?.enabled === false &&
|
|
671
|
+
del.status === 200 && afterDel.body.data?.length === 0 &&
|
|
672
|
+
badType.status === 400 && badType.body.error?.code === 'invalid_type';
|
|
673
|
+
})),
|
|
674
|
+
done('openrouter.rankings.apps', 'rankings', 'Rankings: top apps by token usage ranked descending', 'api', 'niche', () => withRoot(async (h) => {
|
|
675
|
+
const r = await h({ m: 'GET', p: '/api/v1/rankings/apps' });
|
|
676
|
+
const rows = r.body.data;
|
|
677
|
+
return r.status === 200 && rows?.length >= 3 && rows[0]?.rank === 1 &&
|
|
678
|
+
rows[0]?.total_tokens >= rows[1]?.total_tokens && typeof rows[0]?.title === 'string';
|
|
679
|
+
})),
|
|
680
|
+
done('openrouter.rankings.benchmarks', 'rankings', 'Rankings: Artificial Analysis / Design Arena benchmark catalog', 'api', 'niche', () => withRoot(async (h) => {
|
|
681
|
+
const all = await h({ m: 'GET', p: '/api/v1/rankings/benchmarks' });
|
|
682
|
+
const filtered = await h({ m: 'GET', p: '/api/v1/rankings/benchmarks?source=design-arena' });
|
|
683
|
+
const rows = all.body.data;
|
|
684
|
+
const fr = filtered.body.data;
|
|
685
|
+
return all.status === 200 && rows?.length >= 3 && rows.some((b) => b.source === 'artificial-analysis') &&
|
|
686
|
+
fr.length >= 1 && fr.every((b) => b.source === 'design-arena');
|
|
687
|
+
})),
|
|
688
|
+
done('openrouter.ui.generation_detail', 'ui', 'Mirror generation detail view is data-coupled to the generation API', 'ui', 'common', async () => await withMirror(async (baseUrl) => {
|
|
689
|
+
// (1) the built mirror carries the generation-detail panel markers the App renders …
|
|
690
|
+
const js = await mirrorBundle();
|
|
691
|
+
if (!(js.includes('generation-detail') && js.includes('Generation detail')))
|
|
692
|
+
return false;
|
|
693
|
+
// (2) … AND it is coupled to the SAME single-generation API the detail view fetches.
|
|
694
|
+
const chat = await fetch(`${baseUrl}/api/v1/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(CHAT()) }).then((r) => r.json());
|
|
695
|
+
const id = chat.id;
|
|
696
|
+
const detail = await fetch(`${baseUrl}/api/v1/generation?id=${encodeURIComponent(id)}`).then((r) => r.json());
|
|
697
|
+
const missing = await fetch(`${baseUrl}/api/v1/generation?id=does-not-exist`);
|
|
698
|
+
return detail.data?.request_id === id && detail.data?.model === 'google/gemini-3-flash-preview' &&
|
|
699
|
+
typeof detail.data?.total_cost === 'number' && detail.data?.provider_name === 'twin' && missing.status === 404;
|
|
700
|
+
})),
|
|
701
|
+
done('openrouter.ui.model_detail', 'ui', 'Mirror model detail view is data-coupled to the model endpoints API', 'ui', 'common', async () => await withMirror(async (baseUrl) => {
|
|
702
|
+
// (1) the built mirror carries the model-detail panel markers the App renders …
|
|
703
|
+
const js = await mirrorBundle();
|
|
704
|
+
if (!(js.includes('model-detail') && js.includes('Model detail') && js.includes('Endpoints')))
|
|
705
|
+
return false;
|
|
706
|
+
// (2) … AND it is coupled to the SAME model-endpoints API the detail view fetches.
|
|
707
|
+
const detail = await fetch(`${baseUrl}/api/v1/models/openai/gpt-4o/endpoints`).then((r) => r.json());
|
|
708
|
+
const missing = await fetch(`${baseUrl}/api/v1/models/nope/nope/endpoints`);
|
|
709
|
+
return detail.data?.id === 'openai/gpt-4o' && detail.data?.endpoints?.[0]?.provider_name === 'twin' &&
|
|
710
|
+
typeof detail.data?.pricing?.prompt === 'string' && missing.status === 404;
|
|
711
|
+
})),
|
|
712
|
+
// ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos, which is
|
|
713
|
+
// what the demand-ordered build list is drawn from. See pull-audit.json (repo root) for the
|
|
714
|
+
// per-pack pull-vs-read-surface evidence.
|
|
715
|
+
todo('openrouter.connector.pull_key', 'connector', 'Connector: pull current API key metadata (GET /api/v1/key) from the real account', 'connector', 'core'),
|
|
716
|
+
todo('openrouter.connector.pull_credits', 'connector', 'Connector: pull remaining credits (GET /api/v1/credits) from the real account', 'connector', 'core'),
|
|
717
|
+
];
|
|
718
|
+
export async function openrouterCapabilities() {
|
|
719
|
+
return checkCapabilities('openrouter', OPENROUTER_CAPABILITIES);
|
|
720
|
+
}
|