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