@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,1506 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { applyTwinWrite, projectResources } from '@volter/world-core';
|
|
3
|
+
import { OPENROUTER_MODELS, OPENROUTER_PROVIDERS, findOpenRouterModel } from "./openrouter-models.js";
|
|
4
|
+
import { countPromptTokens, estimateTokens, firstToolCall, lastUserText, stableHash, stubAssistantText } from "./openrouter-stub.js";
|
|
5
|
+
import { realizeOpenRouterRespond } from "./openrouter-scenario.js";
|
|
6
|
+
const SERVICE = 'openrouter';
|
|
7
|
+
const FIXED_NOW = '1970-01-01T00:00:00.000Z';
|
|
8
|
+
function nowEpoch(occurredAt) {
|
|
9
|
+
return Math.floor(Date.parse(occurredAt ?? FIXED_NOW) / 1000);
|
|
10
|
+
}
|
|
11
|
+
function nowIso(occurredAt) {
|
|
12
|
+
return new Date(Date.parse(occurredAt ?? FIXED_NOW)).toISOString();
|
|
13
|
+
}
|
|
14
|
+
function parseJson(body) {
|
|
15
|
+
if (!body?.trim())
|
|
16
|
+
return {};
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(body);
|
|
19
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function error(status, code, message, type = 'invalid_request_error') {
|
|
26
|
+
return { status, body: { error: { message, type, code } } };
|
|
27
|
+
}
|
|
28
|
+
function apiPath(path) {
|
|
29
|
+
const url = new URL(path, 'http://twin.local');
|
|
30
|
+
return url.pathname.replace(/^\/api\/v1/, '/v1');
|
|
31
|
+
}
|
|
32
|
+
function rowsOfType(type, root) {
|
|
33
|
+
return projectResources(SERVICE, root)
|
|
34
|
+
.filter((r) => r.type === type)
|
|
35
|
+
.map((r) => {
|
|
36
|
+
const { type: _type, updatedAt: _updatedAt, ...rest } = r;
|
|
37
|
+
return rest;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function generationRows(root) {
|
|
41
|
+
return rowsOfType('generation', root);
|
|
42
|
+
}
|
|
43
|
+
function hasMessageContent(message) {
|
|
44
|
+
if (typeof message.content === 'string')
|
|
45
|
+
return message.content.length > 0;
|
|
46
|
+
if (Array.isArray(message.content))
|
|
47
|
+
return message.content.length > 0;
|
|
48
|
+
return message.tool_calls !== undefined;
|
|
49
|
+
}
|
|
50
|
+
function parseResponseFormat(raw) {
|
|
51
|
+
if (raw === undefined)
|
|
52
|
+
return { value: {} };
|
|
53
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
54
|
+
return { error: error(400, 'invalid_response_format', 'response_format must be an object') };
|
|
55
|
+
}
|
|
56
|
+
const rf = raw;
|
|
57
|
+
if (rf.type !== 'json_object' && rf.type !== 'json_schema' && rf.type !== 'text') {
|
|
58
|
+
return { error: error(400, 'invalid_response_format', 'response_format.type must be one of text, json_object, json_schema') };
|
|
59
|
+
}
|
|
60
|
+
if (rf.type === 'json_schema') {
|
|
61
|
+
const schema = rf.json_schema;
|
|
62
|
+
if (!schema || typeof schema !== 'object' || typeof schema.name !== 'string') {
|
|
63
|
+
return { error: error(400, 'invalid_response_format', 'json_schema requires a json_schema.name') };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { value: rf };
|
|
67
|
+
}
|
|
68
|
+
function parseReasoning(raw) {
|
|
69
|
+
if (raw === undefined)
|
|
70
|
+
return { value: {} };
|
|
71
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
72
|
+
return { error: error(400, 'invalid_reasoning', 'reasoning must be an object') };
|
|
73
|
+
}
|
|
74
|
+
const r = raw;
|
|
75
|
+
if (r.effort !== undefined && !['low', 'medium', 'high', 'minimal'].includes(String(r.effort))) {
|
|
76
|
+
return { error: error(400, 'invalid_reasoning', 'reasoning.effort must be one of minimal, low, medium, high') };
|
|
77
|
+
}
|
|
78
|
+
if (r.max_tokens !== undefined && (!Number.isInteger(Number(r.max_tokens)) || Number(r.max_tokens) < 0)) {
|
|
79
|
+
return { error: error(400, 'invalid_reasoning', 'reasoning.max_tokens must be a non-negative integer') };
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
value: {
|
|
83
|
+
...(r.effort !== undefined ? { effort: String(r.effort) } : {}),
|
|
84
|
+
...(r.max_tokens !== undefined ? { max_tokens: Number(r.max_tokens) } : {}),
|
|
85
|
+
...(r.exclude !== undefined ? { exclude: r.exclude === true } : {}),
|
|
86
|
+
...(r.enabled !== undefined ? { enabled: r.enabled === true } : {}),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseWebSearch(params) {
|
|
91
|
+
// OpenRouter enables web search via the `:online` model suffix, a `web` plugin entry, or `web_search_options`.
|
|
92
|
+
const plugins = Array.isArray(params.plugins) ? params.plugins : [];
|
|
93
|
+
const webPlugin = plugins.find((p) => p && typeof p === 'object' && p.id === 'web');
|
|
94
|
+
const opts = params.web_search_options && typeof params.web_search_options === 'object' ? params.web_search_options : undefined;
|
|
95
|
+
const online = typeof params.model === 'string' && params.model.endsWith(':online');
|
|
96
|
+
if (!webPlugin && !opts && !online)
|
|
97
|
+
return { value: undefined };
|
|
98
|
+
const rawMax = (webPlugin?.max_results ?? opts?.max_results);
|
|
99
|
+
const maxResults = rawMax === undefined ? 5 : Number(rawMax);
|
|
100
|
+
if (!Number.isInteger(maxResults) || maxResults < 1)
|
|
101
|
+
return { error: error(400, 'invalid_web_search', 'web search max_results must be an integer >= 1') };
|
|
102
|
+
const engine = typeof webPlugin?.engine === 'string' ? webPlugin.engine : 'native';
|
|
103
|
+
return { value: { maxResults, engine } };
|
|
104
|
+
}
|
|
105
|
+
// `transforms` is OpenRouter's prompt-transform list (e.g. "middle-out"). Validate it is an
|
|
106
|
+
// array of known transform ids so the plumbing is faithful even though the stub doesn't reshape.
|
|
107
|
+
const KNOWN_TRANSFORMS = ['middle-out'];
|
|
108
|
+
function parseTransforms(raw) {
|
|
109
|
+
if (raw === undefined)
|
|
110
|
+
return { value: undefined };
|
|
111
|
+
if (!Array.isArray(raw))
|
|
112
|
+
return { error: error(400, 'invalid_transforms', 'transforms must be an array of transform ids') };
|
|
113
|
+
for (const t of raw) {
|
|
114
|
+
if (typeof t !== 'string' || !KNOWN_TRANSFORMS.includes(t)) {
|
|
115
|
+
return { error: error(400, 'invalid_transforms', `unknown transform: ${String(t)}`) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { value: raw };
|
|
119
|
+
}
|
|
120
|
+
function parseLogprobs(params) {
|
|
121
|
+
if (params.logprobs === undefined || params.logprobs === false)
|
|
122
|
+
return { value: undefined };
|
|
123
|
+
if (params.logprobs !== true)
|
|
124
|
+
return { error: error(400, 'invalid_logprobs', 'logprobs must be a boolean') };
|
|
125
|
+
const topRaw = params.top_logprobs;
|
|
126
|
+
const top = topRaw === undefined ? 0 : Number(topRaw);
|
|
127
|
+
if (!Number.isInteger(top) || top < 0 || top > 20) {
|
|
128
|
+
return { error: error(400, 'invalid_top_logprobs', 'top_logprobs must be an integer between 0 and 20') };
|
|
129
|
+
}
|
|
130
|
+
return { value: { topLogprobs: top } };
|
|
131
|
+
}
|
|
132
|
+
function parseModalities(raw) {
|
|
133
|
+
if (raw === undefined)
|
|
134
|
+
return { value: undefined };
|
|
135
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
136
|
+
return { error: error(400, 'invalid_modalities', 'modalities must be a non-empty array') };
|
|
137
|
+
for (const m of raw) {
|
|
138
|
+
if (m !== 'text' && m !== 'image' && m !== 'audio')
|
|
139
|
+
return { error: error(400, 'invalid_modalities', `unsupported modality: ${String(m)}`) };
|
|
140
|
+
}
|
|
141
|
+
return { value: raw };
|
|
142
|
+
}
|
|
143
|
+
// cache_control on a message part marks a prefix for caching; the twin deterministically
|
|
144
|
+
// reports those marked prompt tokens as cached on a (modeled) cache hit.
|
|
145
|
+
function cachedPromptTokens(messages) {
|
|
146
|
+
let cached = 0;
|
|
147
|
+
for (const message of messages) {
|
|
148
|
+
const marked = message.cache_control !== undefined ||
|
|
149
|
+
(Array.isArray(message.content) && message.content.some((p) => p && typeof p === 'object' && 'cache_control' in p));
|
|
150
|
+
if (marked)
|
|
151
|
+
cached += estimateTokens(`${message.role}: ${typeof message.content === 'string' ? message.content : JSON.stringify(message.content)}`);
|
|
152
|
+
}
|
|
153
|
+
return cached;
|
|
154
|
+
}
|
|
155
|
+
function validateChat(params) {
|
|
156
|
+
let model = typeof params.model === 'string' && params.model ? params.model : 'openai/gpt-4o';
|
|
157
|
+
if (model.endsWith(':online'))
|
|
158
|
+
model = model.slice(0, -':online'.length);
|
|
159
|
+
if (!findOpenRouterModel(model))
|
|
160
|
+
return { response: error(404, 'not_found', `Model not found: ${model}`, 'not_found_error') };
|
|
161
|
+
if (!Array.isArray(params.messages) || params.messages.length === 0) {
|
|
162
|
+
return { response: error(400, 'invalid_messages', 'messages must be a non-empty array') };
|
|
163
|
+
}
|
|
164
|
+
for (const message of params.messages) {
|
|
165
|
+
if (!message || typeof message !== 'object' || typeof message.role !== 'string') {
|
|
166
|
+
return { response: error(400, 'invalid_message', 'each message must include a role') };
|
|
167
|
+
}
|
|
168
|
+
const role = message.role;
|
|
169
|
+
if (!['system', 'developer', 'user', 'assistant', 'tool'].includes(role)) {
|
|
170
|
+
return { response: error(400, 'invalid_message_role', `unsupported message role: ${role}`) };
|
|
171
|
+
}
|
|
172
|
+
if (!hasMessageContent(message)) {
|
|
173
|
+
return { response: error(400, 'invalid_message_content', 'each message must include content or tool_calls') };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const n = params.n === undefined ? 1 : Number(params.n);
|
|
177
|
+
if (!Number.isInteger(n) || n < 1)
|
|
178
|
+
return { response: error(400, 'invalid_n', 'n must be an integer >= 1') };
|
|
179
|
+
const rawMax = params.max_completion_tokens ?? params.max_tokens;
|
|
180
|
+
const maxTokens = rawMax === undefined ? undefined : Number(rawMax);
|
|
181
|
+
if (maxTokens !== undefined && (!Number.isInteger(maxTokens) || maxTokens < 1)) {
|
|
182
|
+
return { response: error(400, 'invalid_max_tokens', 'max_tokens must be an integer >= 1') };
|
|
183
|
+
}
|
|
184
|
+
const rf = parseResponseFormat(params.response_format);
|
|
185
|
+
if ('error' in rf)
|
|
186
|
+
return { response: rf.error };
|
|
187
|
+
const reasoning = parseReasoning(params.reasoning);
|
|
188
|
+
if ('error' in reasoning)
|
|
189
|
+
return { response: reasoning.error };
|
|
190
|
+
const web = parseWebSearch(params);
|
|
191
|
+
if ('error' in web)
|
|
192
|
+
return { response: web.error };
|
|
193
|
+
if (params.provider !== undefined && (!params.provider || typeof params.provider !== 'object' || Array.isArray(params.provider))) {
|
|
194
|
+
return { response: error(400, 'invalid_provider', 'provider preferences must be an object') };
|
|
195
|
+
}
|
|
196
|
+
const transforms = parseTransforms(params.transforms);
|
|
197
|
+
if ('error' in transforms)
|
|
198
|
+
return { response: transforms.error };
|
|
199
|
+
const logprobs = parseLogprobs(params);
|
|
200
|
+
if ('error' in logprobs)
|
|
201
|
+
return { response: logprobs.error };
|
|
202
|
+
const modalities = parseModalities(params.modalities);
|
|
203
|
+
if ('error' in modalities)
|
|
204
|
+
return { response: modalities.error };
|
|
205
|
+
const messages = params.messages;
|
|
206
|
+
const cached = cachedPromptTokens(messages);
|
|
207
|
+
return {
|
|
208
|
+
args: {
|
|
209
|
+
model,
|
|
210
|
+
messages,
|
|
211
|
+
n,
|
|
212
|
+
stream: params.stream === true,
|
|
213
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
214
|
+
...(params.tools !== undefined ? { tools: params.tools } : {}),
|
|
215
|
+
...(Object.keys(rf.value).length ? { responseFormat: rf.value } : {}),
|
|
216
|
+
...(Object.keys(reasoning.value).length ? { reasoning: reasoning.value } : {}),
|
|
217
|
+
...(web.value ? { webSearch: web.value } : {}),
|
|
218
|
+
...(params.provider !== undefined ? { provider: params.provider } : {}),
|
|
219
|
+
...(cached > 0 ? { cachedTokens: cached } : {}),
|
|
220
|
+
...(transforms.value ? { transforms: transforms.value } : {}),
|
|
221
|
+
...(logprobs.value ? { logprobs: logprobs.value } : {}),
|
|
222
|
+
...(modalities.value ? { modalities: modalities.value } : {}),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function reasoningTokens(args) {
|
|
227
|
+
if (!args.reasoning || args.reasoning.enabled === false)
|
|
228
|
+
return 0;
|
|
229
|
+
if (args.reasoning.max_tokens !== undefined)
|
|
230
|
+
return args.reasoning.max_tokens;
|
|
231
|
+
return { minimal: 4, low: 16, medium: 64, high: 256 }[args.reasoning.effort ?? 'medium'] ?? 64;
|
|
232
|
+
}
|
|
233
|
+
function schemaType(schema) {
|
|
234
|
+
const type = schema.type;
|
|
235
|
+
if (typeof type === 'string')
|
|
236
|
+
return type;
|
|
237
|
+
if (Array.isArray(type))
|
|
238
|
+
return type.find((item) => item !== 'null' && typeof item === 'string');
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
function schemaString(key) {
|
|
242
|
+
if (key.toLowerCase().includes('timestamp'))
|
|
243
|
+
return '2026-06-16T00:00:00.000Z';
|
|
244
|
+
if (key.toLowerCase().includes('url'))
|
|
245
|
+
return 'http://127.0.0.1/twin';
|
|
246
|
+
if (key.toLowerCase().includes('summary'))
|
|
247
|
+
return 'Twin structured-output stub completed the simulated QA session.';
|
|
248
|
+
if (key.toLowerCase().includes('description'))
|
|
249
|
+
return 'The tester completed a deterministic twin-simulated QA phase and reported that the page loaded successfully.';
|
|
250
|
+
if (key.toLowerCase().includes('feedback'))
|
|
251
|
+
return '';
|
|
252
|
+
return 'twin stub';
|
|
253
|
+
}
|
|
254
|
+
function synthesizeJsonSchemaValue(schema, key = 'value') {
|
|
255
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
256
|
+
return {};
|
|
257
|
+
const s = schema;
|
|
258
|
+
if (Array.isArray(s.enum) && s.enum.length > 0)
|
|
259
|
+
return s.enum[0];
|
|
260
|
+
if (Array.isArray(s.const))
|
|
261
|
+
return s.const[0];
|
|
262
|
+
if (s.const !== undefined)
|
|
263
|
+
return s.const;
|
|
264
|
+
if (Array.isArray(s.anyOf) && s.anyOf.length > 0)
|
|
265
|
+
return synthesizeJsonSchemaValue(s.anyOf[0], key);
|
|
266
|
+
if (Array.isArray(s.oneOf) && s.oneOf.length > 0)
|
|
267
|
+
return synthesizeJsonSchemaValue(s.oneOf[0], key);
|
|
268
|
+
const type = schemaType(s);
|
|
269
|
+
if (type === 'object' || (!type && s.properties && typeof s.properties === 'object')) {
|
|
270
|
+
const out = {};
|
|
271
|
+
const props = (s.properties && typeof s.properties === 'object' && !Array.isArray(s.properties))
|
|
272
|
+
? s.properties
|
|
273
|
+
: {};
|
|
274
|
+
for (const [propKey, propSchema] of Object.entries(props)) {
|
|
275
|
+
out[propKey] = synthesizeJsonSchemaValue(propSchema, propKey);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
if (type === 'array')
|
|
280
|
+
return [];
|
|
281
|
+
if (type === 'string')
|
|
282
|
+
return schemaString(key);
|
|
283
|
+
if (type === 'number' || type === 'integer')
|
|
284
|
+
return 0;
|
|
285
|
+
if (type === 'boolean')
|
|
286
|
+
return true;
|
|
287
|
+
if (type === 'null')
|
|
288
|
+
return null;
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
function structuredJsonText(args) {
|
|
292
|
+
if (args.responseFormat?.type === 'json_schema') {
|
|
293
|
+
const jsonSchema = args.responseFormat.json_schema;
|
|
294
|
+
const schema = jsonSchema && typeof jsonSchema === 'object'
|
|
295
|
+
? jsonSchema.schema
|
|
296
|
+
: undefined;
|
|
297
|
+
return JSON.stringify(synthesizeJsonSchemaValue(schema));
|
|
298
|
+
}
|
|
299
|
+
return JSON.stringify({ twin_stub: true, echo: lastUserText(args.messages) || 'empty prompt', model: args.model });
|
|
300
|
+
}
|
|
301
|
+
/** Resolve any scripted assistant turn for THIS generation request. Called exactly once per
|
|
302
|
+
* request (never once per `n` choice, which would tick the engine's call counter n times);
|
|
303
|
+
* a miss returns the teaching suffix the labeled stub carries, so an unmatched request says
|
|
304
|
+
* where to author the handler instead of failing silently. */
|
|
305
|
+
async function decideScripted(args, engine, signal) {
|
|
306
|
+
signal?.throwIfAborted();
|
|
307
|
+
if (!engine)
|
|
308
|
+
return { scripted: null, missTeach: '', matched: false };
|
|
309
|
+
const decision = await engine.serve({ model: args.model, messages: args.messages, ...(args.tools !== undefined ? { tools: args.tools } : {}) }, signal);
|
|
310
|
+
signal?.throwIfAborted();
|
|
311
|
+
if (decision.kind === 'fault')
|
|
312
|
+
return { scripted: null, missTeach: '', matched: true, fault: decision.result };
|
|
313
|
+
if (decision.kind === 'handler')
|
|
314
|
+
return { scripted: decision.respond === undefined ? null : realizeOpenRouterRespond(decision.respond), missTeach: '', matched: true };
|
|
315
|
+
return {
|
|
316
|
+
scripted: null,
|
|
317
|
+
matched: false,
|
|
318
|
+
missTeach: `\n[twin-scenario miss — no handler matched. Author one in the world dir's handlers/openrouter.json (GET /twin explains; GET /twin/scenario lists handlers + misses). Features seen: ${JSON.stringify(decision.miss.features)}]`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** A fired handler's assistant turn, in the OpenAI-shaped choice envelope OpenRouter serves. */
|
|
322
|
+
function scriptedChoice(scripted, index) {
|
|
323
|
+
const message = scripted.toolCalls.length
|
|
324
|
+
? { role: 'assistant', content: scripted.text, tool_calls: scripted.toolCalls }
|
|
325
|
+
: { role: 'assistant', content: scripted.text ?? '' };
|
|
326
|
+
return {
|
|
327
|
+
choice: { index, message, finish_reason: scripted.finishReason, native_finish_reason: scripted.finishReason },
|
|
328
|
+
completionTokens: estimateTokens(JSON.stringify(scripted.toolCalls.length ? scripted.toolCalls : scripted.text ?? '')),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function buildChoice(args, index) {
|
|
332
|
+
const tool = firstToolCall(args.tools);
|
|
333
|
+
const annotations = args.webSearch ? webSearchAnnotations(args) : undefined;
|
|
334
|
+
if (tool) {
|
|
335
|
+
return {
|
|
336
|
+
choice: {
|
|
337
|
+
index,
|
|
338
|
+
message: { role: 'assistant', content: null, tool_calls: [tool], ...(annotations ? { annotations } : {}) },
|
|
339
|
+
finish_reason: 'tool_calls',
|
|
340
|
+
native_finish_reason: 'tool_calls',
|
|
341
|
+
},
|
|
342
|
+
completionTokens: estimateTokens(JSON.stringify(tool)),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
let text = stubAssistantText(args.messages, args.model);
|
|
346
|
+
if (args.responseFormat && (args.responseFormat.type === 'json_object' || args.responseFormat.type === 'json_schema')) {
|
|
347
|
+
text = structuredJsonText(args);
|
|
348
|
+
}
|
|
349
|
+
let finish = 'stop';
|
|
350
|
+
if (args.maxTokens !== undefined && estimateTokens(text) > args.maxTokens) {
|
|
351
|
+
text = text.slice(0, args.maxTokens * 4);
|
|
352
|
+
finish = 'length';
|
|
353
|
+
}
|
|
354
|
+
const message = { role: 'assistant', content: text };
|
|
355
|
+
if (annotations)
|
|
356
|
+
message.annotations = annotations;
|
|
357
|
+
const reasoned = reasoningTokens(args);
|
|
358
|
+
if (args.reasoning && reasoned > 0 && args.reasoning.exclude !== true) {
|
|
359
|
+
message.reasoning = `[twin-stub:openrouter:reasoning] deterministic reasoning trace for ${args.model}`;
|
|
360
|
+
message.reasoning_details = [{ type: 'reasoning.text', text: message.reasoning, format: 'unknown' }];
|
|
361
|
+
}
|
|
362
|
+
// Image output modality: attach a clearly-labeled deterministic stub image so the multimodal
|
|
363
|
+
// output envelope (message.images[]) is faithful without producing real pixels.
|
|
364
|
+
if (args.modalities?.includes('image')) {
|
|
365
|
+
message.images = [{
|
|
366
|
+
type: 'image_url',
|
|
367
|
+
image_url: { url: `data:image/png;base64,${Buffer.from(`[twin-stub:openrouter:image] ${args.model}`).toString('base64')}` },
|
|
368
|
+
}];
|
|
369
|
+
}
|
|
370
|
+
const choice = { index, message, finish_reason: finish, native_finish_reason: finish };
|
|
371
|
+
if (args.logprobs)
|
|
372
|
+
choice.logprobs = buildLogprobs(text, args.logprobs.topLogprobs);
|
|
373
|
+
return { choice, completionTokens: estimateTokens(text) };
|
|
374
|
+
}
|
|
375
|
+
// Deterministic logprobs: token-by-token with a stable pseudo-logprob and `top_logprobs`
|
|
376
|
+
// alternatives, faithful to the OpenAI/OpenRouter `choices[].logprobs.content[]` shape.
|
|
377
|
+
function buildLogprobs(text, topLogprobs) {
|
|
378
|
+
const tokens = text.match(/\S+\s*/g) ?? [text];
|
|
379
|
+
const content = tokens.map((tok) => {
|
|
380
|
+
const lp = -Number(((parseInt(stableHash(tok), 16) % 500) / 100 + 0.01).toFixed(4));
|
|
381
|
+
const top = Array.from({ length: topLogprobs }, (_u, i) => ({
|
|
382
|
+
token: i === 0 ? tok : `${tok}#${i}`,
|
|
383
|
+
logprob: Number((lp - i * 0.5).toFixed(4)),
|
|
384
|
+
bytes: Array.from(Buffer.from(tok)),
|
|
385
|
+
}));
|
|
386
|
+
return { token: tok, logprob: lp, bytes: Array.from(Buffer.from(tok)), top_logprobs: top };
|
|
387
|
+
});
|
|
388
|
+
return { content };
|
|
389
|
+
}
|
|
390
|
+
function webSearchAnnotations(args) {
|
|
391
|
+
const query = lastUserText(args.messages) || 'twin web search';
|
|
392
|
+
const count = Math.min(args.webSearch.maxResults, 3);
|
|
393
|
+
return Array.from({ length: count }, (_unused, i) => ({
|
|
394
|
+
type: 'url_citation',
|
|
395
|
+
url_citation: {
|
|
396
|
+
url: `https://twin.local/search?q=${encodeURIComponent(query)}&r=${i}`,
|
|
397
|
+
title: `[twin-stub:openrouter:web_search] result ${i + 1}`,
|
|
398
|
+
content: `Deterministic web-search stub result ${i + 1} via ${args.webSearch.engine} engine.`,
|
|
399
|
+
start_index: 0,
|
|
400
|
+
end_index: 0,
|
|
401
|
+
},
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
async function recordGeneration(args, id, usage, root, occurredAt, streamed = false, finishReason = 'stop') {
|
|
405
|
+
const cost = Number(((usage.prompt_tokens * 0.0000005) + (usage.completion_tokens * 0.000003)).toFixed(8));
|
|
406
|
+
const stat = {
|
|
407
|
+
id: `generation_${id}`,
|
|
408
|
+
object: 'generation',
|
|
409
|
+
api_type: 'chat',
|
|
410
|
+
model: args.model,
|
|
411
|
+
provider_name: 'twin',
|
|
412
|
+
created_at: nowIso(occurredAt),
|
|
413
|
+
request_id: id,
|
|
414
|
+
finish_reason: finishReason,
|
|
415
|
+
native_finish_reason: finishReason,
|
|
416
|
+
total_cost: cost,
|
|
417
|
+
tokens_prompt: usage.prompt_tokens,
|
|
418
|
+
tokens_completion: usage.completion_tokens,
|
|
419
|
+
native_tokens_prompt: usage.prompt_tokens,
|
|
420
|
+
native_tokens_completion: usage.completion_tokens,
|
|
421
|
+
usage: cost,
|
|
422
|
+
app_id: null,
|
|
423
|
+
streamed,
|
|
424
|
+
};
|
|
425
|
+
await applyTwinWrite(SERVICE, {
|
|
426
|
+
operation: 'generation.record',
|
|
427
|
+
subjectType: 'generation',
|
|
428
|
+
subjectId: stat.id,
|
|
429
|
+
fields: stat,
|
|
430
|
+
occurredAt: occurredAt ?? FIXED_NOW,
|
|
431
|
+
actor: { kind: 'system' },
|
|
432
|
+
}, root);
|
|
433
|
+
}
|
|
434
|
+
async function chatCompletion(req, params, localGeneration) {
|
|
435
|
+
const validated = validateChat(params);
|
|
436
|
+
if ('response' in validated)
|
|
437
|
+
return validated.response;
|
|
438
|
+
const args = validated.args;
|
|
439
|
+
const choices = [];
|
|
440
|
+
let completionTokens = 0;
|
|
441
|
+
// Scenario handlers: a fired handler scripts the assistant turn; a miss teaches in the stub.
|
|
442
|
+
const { scripted, missTeach, matched, fault } = await decideScripted(args, req.scenarioEngine, req.signal);
|
|
443
|
+
if (fault)
|
|
444
|
+
return fault;
|
|
445
|
+
if (!matched && localGeneration)
|
|
446
|
+
return localGeneration();
|
|
447
|
+
for (let i = 0; i < args.n; i++) {
|
|
448
|
+
const built = scripted ? scriptedChoice(scripted, i) : buildChoice(args, i);
|
|
449
|
+
if (!scripted && missTeach) {
|
|
450
|
+
const message = built.choice.message;
|
|
451
|
+
if (typeof message.content === 'string')
|
|
452
|
+
message.content += missTeach;
|
|
453
|
+
}
|
|
454
|
+
choices.push(built.choice);
|
|
455
|
+
completionTokens += built.completionTokens;
|
|
456
|
+
}
|
|
457
|
+
const promptTokens = countPromptTokens(args.messages);
|
|
458
|
+
const cachedTokens = Math.min(args.cachedTokens ?? 0, promptTokens);
|
|
459
|
+
const usage = {
|
|
460
|
+
prompt_tokens: promptTokens,
|
|
461
|
+
completion_tokens: completionTokens,
|
|
462
|
+
total_tokens: promptTokens + completionTokens,
|
|
463
|
+
};
|
|
464
|
+
const reasoned = reasoningTokens(args);
|
|
465
|
+
const id = `chatcmpl-or-twin-${stableHash({ args, usage })}`;
|
|
466
|
+
const body = {
|
|
467
|
+
id,
|
|
468
|
+
object: 'chat.completion',
|
|
469
|
+
created: nowEpoch(req.occurredAt),
|
|
470
|
+
model: args.model,
|
|
471
|
+
choices,
|
|
472
|
+
usage: {
|
|
473
|
+
...usage,
|
|
474
|
+
prompt_tokens_details: { cached_tokens: cachedTokens },
|
|
475
|
+
completion_tokens_details: { reasoning_tokens: reasoned },
|
|
476
|
+
cost: Number(((usage.prompt_tokens * 0.0000005) + (usage.completion_tokens * 0.000003)).toFixed(8)),
|
|
477
|
+
},
|
|
478
|
+
system_fingerprint: 'fp_openrouter_twin',
|
|
479
|
+
};
|
|
480
|
+
if (args.provider)
|
|
481
|
+
body.provider = 'twin';
|
|
482
|
+
if (args.transforms)
|
|
483
|
+
body.transforms = args.transforms;
|
|
484
|
+
req.signal?.throwIfAborted();
|
|
485
|
+
await recordGeneration(args, id, usage, req.root, req.occurredAt, args.stream, String(choices[0]?.finish_reason ?? 'stop'));
|
|
486
|
+
req.signal?.throwIfAborted();
|
|
487
|
+
if (args.stream && req.sseSink)
|
|
488
|
+
streamChat(body, req.sseSink);
|
|
489
|
+
return { status: 200, body };
|
|
490
|
+
}
|
|
491
|
+
function streamChat(body, sink) {
|
|
492
|
+
const base = { id: body.id, object: 'chat.completion.chunk', created: body.created, model: body.model, system_fingerprint: body.system_fingerprint };
|
|
493
|
+
sink({ data: { ...base, choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }] } });
|
|
494
|
+
const choice = body.choices[0];
|
|
495
|
+
if (choice.message.tool_calls) {
|
|
496
|
+
const call = choice.message.tool_calls[0];
|
|
497
|
+
sink({ data: { ...base, choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: call.id, type: 'function', function: { name: call.function.name } }] }, finish_reason: null }] } });
|
|
498
|
+
sink({ data: { ...base, choices: [{ index: 0, delta: { tool_calls: [{ index: 0, function: { arguments: call.function.arguments } }] }, finish_reason: null }] } });
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
const text = String(choice.message.content ?? '');
|
|
502
|
+
for (let i = 0; i < text.length; i += 24) {
|
|
503
|
+
sink({ data: { ...base, choices: [{ index: 0, delta: { content: text.slice(i, i + 24) }, finish_reason: null }] } });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
sink({ data: { ...base, choices: [{ index: 0, delta: {}, finish_reason: choice.finish_reason }], usage: body.usage } });
|
|
507
|
+
sink({ done: true });
|
|
508
|
+
}
|
|
509
|
+
function responsesInputToMessages(input) {
|
|
510
|
+
if (typeof input === 'string')
|
|
511
|
+
return [{ role: 'user', content: input }];
|
|
512
|
+
if (!Array.isArray(input))
|
|
513
|
+
return null;
|
|
514
|
+
const messages = [];
|
|
515
|
+
for (const item of input) {
|
|
516
|
+
if (!item || typeof item !== 'object')
|
|
517
|
+
return null;
|
|
518
|
+
const role = typeof item.role === 'string' ? item.role : 'user';
|
|
519
|
+
let content = item.content;
|
|
520
|
+
if (Array.isArray(content)) {
|
|
521
|
+
content = content.map((p) => (p && typeof p === 'object' && 'text' in p ? String(p.text ?? '') : '')).join(' ').trim();
|
|
522
|
+
}
|
|
523
|
+
messages.push({ role, content });
|
|
524
|
+
}
|
|
525
|
+
return messages.length ? messages : null;
|
|
526
|
+
}
|
|
527
|
+
async function createResponse(req, params, localGeneration) {
|
|
528
|
+
const model = typeof params.model === 'string' && params.model ? params.model : 'openai/gpt-4o';
|
|
529
|
+
if (!findOpenRouterModel(model.replace(/:online$/, '')))
|
|
530
|
+
return error(404, 'not_found', `Model not found: ${model}`, 'not_found_error');
|
|
531
|
+
if (params.input === undefined && params.messages === undefined)
|
|
532
|
+
return error(400, 'invalid_input', 'input is required');
|
|
533
|
+
const messages = responsesInputToMessages(params.input ?? params.messages);
|
|
534
|
+
if (!messages)
|
|
535
|
+
return error(400, 'invalid_input', 'input could not be parsed into messages');
|
|
536
|
+
const reasoning = parseReasoning(params.reasoning);
|
|
537
|
+
if ('error' in reasoning)
|
|
538
|
+
return reasoning.error;
|
|
539
|
+
const web = parseWebSearch({ ...params, plugins: params.plugins, web_search_options: params.web_search_options });
|
|
540
|
+
if ('error' in web)
|
|
541
|
+
return web.error;
|
|
542
|
+
const tools = Array.isArray(params.tools) ? params.tools : undefined;
|
|
543
|
+
const args = {
|
|
544
|
+
model: model.replace(/:online$/, ''),
|
|
545
|
+
messages,
|
|
546
|
+
n: 1,
|
|
547
|
+
stream: false,
|
|
548
|
+
...(tools ? { tools } : {}),
|
|
549
|
+
...(Object.keys(reasoning.value).length ? { reasoning: reasoning.value } : {}),
|
|
550
|
+
...(web.value ? { webSearch: web.value } : {}),
|
|
551
|
+
};
|
|
552
|
+
const { scripted, missTeach, matched, fault } = await decideScripted(args, req.scenarioEngine, req.signal);
|
|
553
|
+
if (fault)
|
|
554
|
+
return fault;
|
|
555
|
+
if (!matched && localGeneration)
|
|
556
|
+
return localGeneration();
|
|
557
|
+
const built = scripted ? scriptedChoice(scripted, 0) : buildChoice(args, 0);
|
|
558
|
+
const msg = built.choice.message;
|
|
559
|
+
if (!scripted && missTeach && typeof msg.content === 'string')
|
|
560
|
+
msg.content += missTeach;
|
|
561
|
+
const promptTokens = countPromptTokens(messages);
|
|
562
|
+
const reasoned = reasoningTokens(args);
|
|
563
|
+
const id = `resp_or_twin_${stableHash({ args })}`;
|
|
564
|
+
const output = [];
|
|
565
|
+
if (msg.reasoning) {
|
|
566
|
+
output.push({ id: `rs_${stableHash(msg.reasoning).slice(0, 8)}`, type: 'reasoning', summary: [{ type: 'summary_text', text: msg.reasoning }] });
|
|
567
|
+
}
|
|
568
|
+
if (msg.tool_calls) {
|
|
569
|
+
const call = msg.tool_calls[0];
|
|
570
|
+
output.push({ id: `fc_${call.id}`, type: 'function_call', call_id: call.id, name: call.function.name, arguments: call.function.arguments, status: 'completed' });
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
output.push({
|
|
574
|
+
id: `msg_${stableHash(String(msg.content)).slice(0, 8)}`,
|
|
575
|
+
type: 'message',
|
|
576
|
+
role: 'assistant',
|
|
577
|
+
status: 'completed',
|
|
578
|
+
content: [{ type: 'output_text', text: String(msg.content ?? ''), annotations: msg.annotations ?? [] }],
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
const status = built.choice.finish_reason === 'tool_calls' ? 'completed' : 'completed';
|
|
582
|
+
const body = {
|
|
583
|
+
id,
|
|
584
|
+
object: 'response',
|
|
585
|
+
created_at: nowEpoch(req.occurredAt),
|
|
586
|
+
status,
|
|
587
|
+
model: args.model,
|
|
588
|
+
output,
|
|
589
|
+
output_text: msg.tool_calls ? '' : String(msg.content ?? ''),
|
|
590
|
+
usage: {
|
|
591
|
+
input_tokens: promptTokens,
|
|
592
|
+
output_tokens: built.completionTokens,
|
|
593
|
+
total_tokens: promptTokens + built.completionTokens,
|
|
594
|
+
output_tokens_details: { reasoning_tokens: reasoned },
|
|
595
|
+
},
|
|
596
|
+
system_fingerprint: 'fp_openrouter_twin',
|
|
597
|
+
};
|
|
598
|
+
req.signal?.throwIfAborted();
|
|
599
|
+
return { status: 200, body };
|
|
600
|
+
}
|
|
601
|
+
// ── Embeddings ──────────────────────────────────────────────────────────────
|
|
602
|
+
const EMBED_DIM = 8;
|
|
603
|
+
function embedVector(text) {
|
|
604
|
+
// Deterministic pseudo-embedding seeded from the text hash; L2-normalized.
|
|
605
|
+
const seed = parseInt(stableHash(text), 16) || 1;
|
|
606
|
+
let x = seed;
|
|
607
|
+
const raw = Array.from({ length: EMBED_DIM }, () => {
|
|
608
|
+
x = (Math.imul(x, 1103515245) + 12345) & 0x7fffffff;
|
|
609
|
+
return (x / 0x7fffffff) * 2 - 1;
|
|
610
|
+
});
|
|
611
|
+
const norm = Math.sqrt(raw.reduce((s, v) => s + v * v, 0)) || 1;
|
|
612
|
+
return raw.map((v) => Number((v / norm).toFixed(6)));
|
|
613
|
+
}
|
|
614
|
+
function createEmbeddings(req, params) {
|
|
615
|
+
const model = typeof params.model === 'string' && params.model ? params.model : 'openai/text-embedding-3-small';
|
|
616
|
+
if (params.input === undefined)
|
|
617
|
+
return error(400, 'invalid_input', 'input is required');
|
|
618
|
+
const inputs = Array.isArray(params.input) ? params.input : [params.input];
|
|
619
|
+
if (inputs.length === 0)
|
|
620
|
+
return error(400, 'invalid_input', 'input must be a non-empty string or array');
|
|
621
|
+
for (const i of inputs)
|
|
622
|
+
if (typeof i !== 'string')
|
|
623
|
+
return error(400, 'invalid_input', 'each embedding input must be a string');
|
|
624
|
+
const data = inputs.map((text, index) => ({ object: 'embedding', index, embedding: embedVector(text) }));
|
|
625
|
+
const promptTokens = inputs.reduce((s, t) => s + estimateTokens(t), 0);
|
|
626
|
+
return {
|
|
627
|
+
status: 200,
|
|
628
|
+
body: { object: 'list', data, model, usage: { prompt_tokens: promptTokens, total_tokens: promptTokens } },
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
// ── Anthropic Messages compatibility ─────────────────────────────────────────
|
|
632
|
+
function anthropicContentToText(content) {
|
|
633
|
+
if (typeof content === 'string')
|
|
634
|
+
return content;
|
|
635
|
+
if (Array.isArray(content)) {
|
|
636
|
+
return content.map((p) => (p && typeof p === 'object' && p.type === 'text' ? String(p.text ?? '') : '')).filter(Boolean).join(' ');
|
|
637
|
+
}
|
|
638
|
+
return '';
|
|
639
|
+
}
|
|
640
|
+
function anthropicMessages(req, params) {
|
|
641
|
+
const model = typeof params.model === 'string' && params.model ? params.model : 'anthropic/claude-sonnet-4.5';
|
|
642
|
+
if (!findOpenRouterModel(model))
|
|
643
|
+
return error(404, 'not_found', `Model not found: ${model}`, 'not_found_error');
|
|
644
|
+
if (!Array.isArray(params.messages) || params.messages.length === 0)
|
|
645
|
+
return error(400, 'invalid_messages', 'messages must be a non-empty array');
|
|
646
|
+
if (params.max_tokens === undefined)
|
|
647
|
+
return error(400, 'invalid_max_tokens', 'max_tokens is required for the Anthropic Messages API');
|
|
648
|
+
const messages = params.messages.map((m) => ({ role: String(m.role ?? 'user'), content: anthropicContentToText(m.content) }));
|
|
649
|
+
if (typeof params.system === 'string' && params.system)
|
|
650
|
+
messages.unshift({ role: 'system', content: params.system });
|
|
651
|
+
const text = stubAssistantText(messages, model);
|
|
652
|
+
const inputTokens = countPromptTokens(messages);
|
|
653
|
+
const outputTokens = estimateTokens(text);
|
|
654
|
+
return {
|
|
655
|
+
status: 200,
|
|
656
|
+
body: {
|
|
657
|
+
id: `msg_or_twin_${stableHash({ model, messages }).slice(0, 12)}`,
|
|
658
|
+
type: 'message',
|
|
659
|
+
role: 'assistant',
|
|
660
|
+
model,
|
|
661
|
+
content: [{ type: 'text', text }],
|
|
662
|
+
stop_reason: 'end_turn',
|
|
663
|
+
stop_sequence: null,
|
|
664
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
// ── Rerank ───────────────────────────────────────────────────────────────────
|
|
669
|
+
function rerank(req, params) {
|
|
670
|
+
const model = typeof params.model === 'string' && params.model ? params.model : 'twin/rerank-1';
|
|
671
|
+
if (typeof params.query !== 'string' || !params.query)
|
|
672
|
+
return error(400, 'invalid_query', 'query is required');
|
|
673
|
+
if (!Array.isArray(params.documents) || params.documents.length === 0)
|
|
674
|
+
return error(400, 'invalid_documents', 'documents must be a non-empty array');
|
|
675
|
+
for (const d of params.documents)
|
|
676
|
+
if (typeof d !== 'string')
|
|
677
|
+
return error(400, 'invalid_documents', 'each document must be a string');
|
|
678
|
+
const topN = params.top_n === undefined ? params.documents.length : Number(params.top_n);
|
|
679
|
+
if (!Number.isInteger(topN) || topN < 1)
|
|
680
|
+
return error(400, 'invalid_top_n', 'top_n must be an integer >= 1');
|
|
681
|
+
const queryTokens = new Set(params.query.toLowerCase().split(/\W+/).filter(Boolean));
|
|
682
|
+
const scored = params.documents.map((doc, index) => {
|
|
683
|
+
const docTokens = doc.toLowerCase().split(/\W+/).filter(Boolean);
|
|
684
|
+
const overlap = docTokens.filter((t) => queryTokens.has(t)).length;
|
|
685
|
+
const score = Number((overlap / Math.max(1, docTokens.length)).toFixed(6));
|
|
686
|
+
return { index, relevance_score: score, document: { text: doc } };
|
|
687
|
+
});
|
|
688
|
+
scored.sort((a, b) => b.relevance_score - a.relevance_score || a.index - b.index);
|
|
689
|
+
return { status: 200, body: { id: `rerank_or_twin_${stableHash(params).slice(0, 10)}`, model, results: scored.slice(0, topN) } };
|
|
690
|
+
}
|
|
691
|
+
// ── Generic CRUD resource families (api keys, byok, presets) ─────────────────
|
|
692
|
+
function genId(prefix, seed) {
|
|
693
|
+
return `${prefix}-${stableHash(seed).slice(0, 12)}`;
|
|
694
|
+
}
|
|
695
|
+
async function createApiKey(req, params) {
|
|
696
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
697
|
+
return error(400, 'invalid_name', 'name is required');
|
|
698
|
+
const hash = genId('hash', { name: params.name, at: req.occurredAt });
|
|
699
|
+
const limit = params.limit === undefined ? null : Number(params.limit);
|
|
700
|
+
if (limit !== null && (!Number.isFinite(limit) || limit < 0))
|
|
701
|
+
return error(400, 'invalid_limit', 'limit must be a non-negative number');
|
|
702
|
+
const key = {
|
|
703
|
+
id: `key_${hash}`,
|
|
704
|
+
hash,
|
|
705
|
+
name: params.name,
|
|
706
|
+
label: `sk-or-v1-...${hash.slice(-4)}`,
|
|
707
|
+
disabled: false,
|
|
708
|
+
limit,
|
|
709
|
+
usage: 0,
|
|
710
|
+
created_at: nowIso(req.occurredAt),
|
|
711
|
+
updated_at: nowIso(req.occurredAt),
|
|
712
|
+
...(typeof params.include_byok_in_limit === 'boolean' ? { include_byok_in_limit: params.include_byok_in_limit } : { include_byok_in_limit: false }),
|
|
713
|
+
};
|
|
714
|
+
await applyTwinWrite(SERVICE, { operation: 'api_key.create', subjectType: 'api_key', subjectId: key.id, fields: key, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
715
|
+
return { status: 200, body: { data: { ...stripPrefix(key), key: `sk-or-v1-twin-${hash}` } } };
|
|
716
|
+
}
|
|
717
|
+
function stripPrefix(row) {
|
|
718
|
+
return { ...row, id: String(row.id).replace(/^[a-z_]+_/, '') };
|
|
719
|
+
}
|
|
720
|
+
function listApiKeys(req) {
|
|
721
|
+
return { status: 200, body: { data: rowsOfType('api_key', req.root).filter((r) => !r.deleted).map(stripPrefix) } };
|
|
722
|
+
}
|
|
723
|
+
async function updateApiKey(req, hash, params, del) {
|
|
724
|
+
const row = rowsOfType('api_key', req.root).find((r) => r.hash === hash || String(r.id) === `key_${hash}`);
|
|
725
|
+
if (!row)
|
|
726
|
+
return error(404, 'not_found', 'API key not found', 'not_found_error');
|
|
727
|
+
if (del) {
|
|
728
|
+
await applyTwinWrite(SERVICE, { operation: 'api_key.delete', subjectType: 'api_key', subjectId: String(row.id), fields: { disabled: true, deleted: true }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
729
|
+
return { status: 200, body: { data: { ...stripPrefix({ ...row, disabled: true }), deleted: true } } };
|
|
730
|
+
}
|
|
731
|
+
const patch = { updated_at: nowIso(req.occurredAt) };
|
|
732
|
+
if (typeof params.name === 'string')
|
|
733
|
+
patch.name = params.name;
|
|
734
|
+
if (typeof params.disabled === 'boolean')
|
|
735
|
+
patch.disabled = params.disabled;
|
|
736
|
+
if (params.limit !== undefined)
|
|
737
|
+
patch.limit = params.limit === null ? null : Number(params.limit);
|
|
738
|
+
await applyTwinWrite(SERVICE, { operation: 'api_key.update', subjectType: 'api_key', subjectId: String(row.id), fields: patch, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
739
|
+
return { status: 200, body: { data: stripPrefix({ ...row, ...patch }) } };
|
|
740
|
+
}
|
|
741
|
+
async function createByok(req, params) {
|
|
742
|
+
if (typeof params.provider_name !== 'string' || !params.provider_name)
|
|
743
|
+
return error(400, 'invalid_provider_name', 'provider_name is required');
|
|
744
|
+
if (typeof params.api_key !== 'string' || !params.api_key)
|
|
745
|
+
return error(400, 'invalid_api_key', 'api_key is required');
|
|
746
|
+
if (!OPENROUTER_PROVIDERS.some((p) => p.slug === params.provider_name))
|
|
747
|
+
return error(400, 'unknown_provider', `unknown provider: ${params.provider_name}`);
|
|
748
|
+
const id = `byok_${genId('cred', { p: params.provider_name, at: req.occurredAt })}`;
|
|
749
|
+
const row = {
|
|
750
|
+
id,
|
|
751
|
+
provider_name: params.provider_name,
|
|
752
|
+
enabled: params.enabled === undefined ? true : params.enabled === true,
|
|
753
|
+
api_key_preview: `...${String(params.api_key).slice(-4)}`,
|
|
754
|
+
created_at: nowIso(req.occurredAt),
|
|
755
|
+
updated_at: nowIso(req.occurredAt),
|
|
756
|
+
};
|
|
757
|
+
await applyTwinWrite(SERVICE, { operation: 'byok.create', subjectType: 'byok', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
758
|
+
return { status: 200, body: { data: stripPrefix(row) } };
|
|
759
|
+
}
|
|
760
|
+
function listByok(req) {
|
|
761
|
+
return { status: 200, body: { data: rowsOfType('byok', req.root).filter((r) => !r.deleted).map(stripPrefix) } };
|
|
762
|
+
}
|
|
763
|
+
async function updateByok(req, provider, params, del) {
|
|
764
|
+
const row = rowsOfType('byok', req.root).find((r) => r.provider_name === provider && !r.deleted);
|
|
765
|
+
if (!row)
|
|
766
|
+
return error(404, 'not_found', 'BYOK credential not found', 'not_found_error');
|
|
767
|
+
if (del) {
|
|
768
|
+
await applyTwinWrite(SERVICE, { operation: 'byok.delete', subjectType: 'byok', subjectId: String(row.id), fields: { deleted: true, enabled: false }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
769
|
+
return { status: 200, body: { data: { deleted: true, provider_name: provider } } };
|
|
770
|
+
}
|
|
771
|
+
const patch = { updated_at: nowIso(req.occurredAt) };
|
|
772
|
+
if (typeof params.enabled === 'boolean')
|
|
773
|
+
patch.enabled = params.enabled;
|
|
774
|
+
if (typeof params.api_key === 'string' && params.api_key)
|
|
775
|
+
patch.api_key_preview = `...${params.api_key.slice(-4)}`;
|
|
776
|
+
await applyTwinWrite(SERVICE, { operation: 'byok.update', subjectType: 'byok', subjectId: String(row.id), fields: patch, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
777
|
+
return { status: 200, body: { data: stripPrefix({ ...row, ...patch }) } };
|
|
778
|
+
}
|
|
779
|
+
async function createPreset(req, params) {
|
|
780
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
781
|
+
return error(400, 'invalid_name', 'name is required');
|
|
782
|
+
const model = typeof params.model === 'string' ? params.model : 'openai/gpt-4o';
|
|
783
|
+
if (!findOpenRouterModel(model.replace(/:online$/, '')))
|
|
784
|
+
return error(404, 'not_found', `Model not found: ${model}`, 'not_found_error');
|
|
785
|
+
const id = `preset_${genId('preset', { name: params.name, at: req.occurredAt })}`;
|
|
786
|
+
const row = {
|
|
787
|
+
id,
|
|
788
|
+
name: params.name,
|
|
789
|
+
slug: String(params.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''),
|
|
790
|
+
description: typeof params.description === 'string' ? params.description : '',
|
|
791
|
+
model,
|
|
792
|
+
parameters: params.parameters && typeof params.parameters === 'object' ? params.parameters : {},
|
|
793
|
+
source: 'chat',
|
|
794
|
+
created_at: nowIso(req.occurredAt),
|
|
795
|
+
updated_at: nowIso(req.occurredAt),
|
|
796
|
+
};
|
|
797
|
+
await applyTwinWrite(SERVICE, { operation: 'preset.create', subjectType: 'preset', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
798
|
+
return { status: 200, body: { data: stripPrefix(row) } };
|
|
799
|
+
}
|
|
800
|
+
function listPresets(req) {
|
|
801
|
+
return { status: 200, body: { data: rowsOfType('preset', req.root).map(stripPrefix) } };
|
|
802
|
+
}
|
|
803
|
+
function getPreset(req, slug) {
|
|
804
|
+
const row = rowsOfType('preset', req.root).find((r) => r.slug === slug || String(r.id) === `preset_${slug}`);
|
|
805
|
+
return row ? { status: 200, body: { data: stripPrefix(row) } } : error(404, 'not_found', 'Preset not found', 'not_found_error');
|
|
806
|
+
}
|
|
807
|
+
async function createPresetFromResponses(req, params) {
|
|
808
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
809
|
+
return error(400, 'invalid_name', 'name is required');
|
|
810
|
+
const model = typeof params.model === 'string' ? params.model : 'openai/gpt-4o';
|
|
811
|
+
if (!findOpenRouterModel(model.replace(/:online$/, '')))
|
|
812
|
+
return error(404, 'not_found', `Model not found: ${model}`, 'not_found_error');
|
|
813
|
+
const id = `preset_${genId('preset', { name: params.name, src: 'responses', at: req.occurredAt })}`;
|
|
814
|
+
const row = {
|
|
815
|
+
id,
|
|
816
|
+
name: params.name,
|
|
817
|
+
slug: String(params.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''),
|
|
818
|
+
description: typeof params.description === 'string' ? params.description : '',
|
|
819
|
+
model,
|
|
820
|
+
parameters: params.parameters && typeof params.parameters === 'object' ? params.parameters : {},
|
|
821
|
+
source: 'responses',
|
|
822
|
+
created_at: nowIso(req.occurredAt),
|
|
823
|
+
updated_at: nowIso(req.occurredAt),
|
|
824
|
+
};
|
|
825
|
+
await applyTwinWrite(SERVICE, { operation: 'preset.create', subjectType: 'preset', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
826
|
+
return { status: 200, body: { data: stripPrefix(row) } };
|
|
827
|
+
}
|
|
828
|
+
// ── Files ─────────────────────────────────────────────────────────────────────
|
|
829
|
+
async function uploadFile(req, params) {
|
|
830
|
+
const filename = typeof params.filename === 'string' && params.filename ? params.filename : undefined;
|
|
831
|
+
if (!filename)
|
|
832
|
+
return error(400, 'invalid_filename', 'filename is required');
|
|
833
|
+
const purpose = typeof params.purpose === 'string' && params.purpose ? params.purpose : 'user_data';
|
|
834
|
+
if (!['user_data', 'fine-tune', 'assistants', 'batch', 'vision'].includes(purpose)) {
|
|
835
|
+
return error(400, 'invalid_purpose', `unsupported purpose: ${purpose}`);
|
|
836
|
+
}
|
|
837
|
+
const content = typeof params.content === 'string' ? params.content : '';
|
|
838
|
+
const id = `file_${genId('file', { filename, at: req.occurredAt, content })}`;
|
|
839
|
+
const row = {
|
|
840
|
+
id,
|
|
841
|
+
object: 'file',
|
|
842
|
+
filename,
|
|
843
|
+
purpose,
|
|
844
|
+
bytes: Buffer.byteLength(content, 'utf8'),
|
|
845
|
+
created_at: nowEpoch(req.occurredAt),
|
|
846
|
+
status: 'processed',
|
|
847
|
+
};
|
|
848
|
+
await applyTwinWrite(SERVICE, { operation: 'file.upload', subjectType: 'file', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
849
|
+
return { status: 200, body: stripPrefix(row) };
|
|
850
|
+
}
|
|
851
|
+
function listFiles(req) {
|
|
852
|
+
return { status: 200, body: { object: 'list', data: rowsOfType('file', req.root).filter((r) => !r.deleted).map(stripPrefix) } };
|
|
853
|
+
}
|
|
854
|
+
function getFile(req, fileId) {
|
|
855
|
+
const row = rowsOfType('file', req.root).find((r) => String(r.id) === `file_${fileId}` && !r.deleted);
|
|
856
|
+
return row ? { status: 200, body: stripPrefix(row) } : error(404, 'not_found', 'File not found', 'not_found_error');
|
|
857
|
+
}
|
|
858
|
+
async function deleteFile(req, fileId) {
|
|
859
|
+
const row = rowsOfType('file', req.root).find((r) => String(r.id) === `file_${fileId}` && !r.deleted);
|
|
860
|
+
if (!row)
|
|
861
|
+
return error(404, 'not_found', 'File not found', 'not_found_error');
|
|
862
|
+
await applyTwinWrite(SERVICE, { operation: 'file.delete', subjectType: 'file', subjectId: String(row.id), fields: { deleted: true }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
863
|
+
return { status: 200, body: { id: fileId, object: 'file', deleted: true } };
|
|
864
|
+
}
|
|
865
|
+
// ── OAuth (PKCE) ───────────────────────────────────────────────────────────────
|
|
866
|
+
// OpenRouter's OAuth PKCE flow: the app sends the user to /auth?callback_url&code_challenge,
|
|
867
|
+
// the user approves, OpenRouter redirects back with a `code`, and the app POSTs the code to
|
|
868
|
+
// /api/v1/auth/keys with the code_verifier to exchange it for a real API key.
|
|
869
|
+
function base64url(input) {
|
|
870
|
+
return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
871
|
+
}
|
|
872
|
+
async function oauthAuthorize(req, params) {
|
|
873
|
+
const callback = typeof params.callback_url === 'string' && params.callback_url ? params.callback_url : undefined;
|
|
874
|
+
if (!callback)
|
|
875
|
+
return error(400, 'invalid_callback_url', 'callback_url is required');
|
|
876
|
+
const challenge = typeof params.code_challenge === 'string' ? params.code_challenge : undefined;
|
|
877
|
+
if (!challenge)
|
|
878
|
+
return error(400, 'invalid_code_challenge', 'code_challenge is required');
|
|
879
|
+
const method = typeof params.code_challenge_method === 'string' ? params.code_challenge_method : 'S256';
|
|
880
|
+
if (!['S256', 'plain'].includes(method))
|
|
881
|
+
return error(400, 'invalid_code_challenge_method', 'code_challenge_method must be S256 or plain');
|
|
882
|
+
const code = genId('orc', { challenge, callback, at: req.occurredAt });
|
|
883
|
+
const row = {
|
|
884
|
+
id: `oauthcode_${code}`,
|
|
885
|
+
code,
|
|
886
|
+
code_challenge: challenge,
|
|
887
|
+
code_challenge_method: method,
|
|
888
|
+
callback_url: callback,
|
|
889
|
+
consumed: false,
|
|
890
|
+
created_at: nowIso(req.occurredAt),
|
|
891
|
+
};
|
|
892
|
+
await applyTwinWrite(SERVICE, { operation: 'oauth_code.create', subjectType: 'oauth_code', subjectId: row.id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
893
|
+
const redirect = `${callback}${callback.includes('?') ? '&' : '?'}code=${code}`;
|
|
894
|
+
return { status: 200, body: { data: { code, redirect_url: redirect } } };
|
|
895
|
+
}
|
|
896
|
+
async function oauthExchange(req, params) {
|
|
897
|
+
const code = typeof params.code === 'string' ? params.code : undefined;
|
|
898
|
+
if (!code)
|
|
899
|
+
return error(400, 'invalid_code', 'code is required');
|
|
900
|
+
const verifier = typeof params.code_verifier === 'string' ? params.code_verifier : undefined;
|
|
901
|
+
const row = rowsOfType('oauth_code', req.root).find((r) => r.code === code);
|
|
902
|
+
if (!row)
|
|
903
|
+
return error(400, 'invalid_code', 'authorization code not found', 'invalid_request_error');
|
|
904
|
+
if (row.consumed)
|
|
905
|
+
return error(400, 'code_already_used', 'authorization code already exchanged');
|
|
906
|
+
const method = row.code_challenge_method ?? 'S256';
|
|
907
|
+
if (row.code_challenge) {
|
|
908
|
+
if (!verifier)
|
|
909
|
+
return error(400, 'invalid_code_verifier', 'code_verifier is required');
|
|
910
|
+
const computed = method === 'plain' ? verifier : base64url(createHash('sha256').update(verifier).digest());
|
|
911
|
+
if (computed !== row.code_challenge)
|
|
912
|
+
return error(400, 'invalid_code_verifier', 'code_verifier does not match code_challenge');
|
|
913
|
+
}
|
|
914
|
+
const keyHash = genId('hash', { code, at: req.occurredAt });
|
|
915
|
+
const key = {
|
|
916
|
+
id: `key_${keyHash}`,
|
|
917
|
+
hash: keyHash,
|
|
918
|
+
name: 'OAuth provisioned key',
|
|
919
|
+
label: `sk-or-v1-...${keyHash.slice(-4)}`,
|
|
920
|
+
disabled: false,
|
|
921
|
+
limit: null,
|
|
922
|
+
usage: 0,
|
|
923
|
+
include_byok_in_limit: false,
|
|
924
|
+
created_at: nowIso(req.occurredAt),
|
|
925
|
+
updated_at: nowIso(req.occurredAt),
|
|
926
|
+
};
|
|
927
|
+
await applyTwinWrite(SERVICE, { operation: 'api_key.create', subjectType: 'api_key', subjectId: key.id, fields: key, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
928
|
+
await applyTwinWrite(SERVICE, { operation: 'oauth_code.consume', subjectType: 'oauth_code', subjectId: String(row.id), fields: { consumed: true }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
929
|
+
return { status: 200, body: { key: `sk-or-v1-twin-${keyHash}`, user_id: 'user_twin' } };
|
|
930
|
+
}
|
|
931
|
+
// ── Guardrails ─────────────────────────────────────────────────────────────────
|
|
932
|
+
async function createGuardrail(req, params) {
|
|
933
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
934
|
+
return error(400, 'invalid_name', 'name is required');
|
|
935
|
+
const id = `guardrail_${genId('gr', { name: params.name, at: req.occurredAt })}`;
|
|
936
|
+
const row = {
|
|
937
|
+
id,
|
|
938
|
+
name: params.name,
|
|
939
|
+
description: typeof params.description === 'string' ? params.description : '',
|
|
940
|
+
enabled: params.enabled === undefined ? true : params.enabled === true,
|
|
941
|
+
rules: Array.isArray(params.rules) ? params.rules : [],
|
|
942
|
+
created_at: nowIso(req.occurredAt),
|
|
943
|
+
updated_at: nowIso(req.occurredAt),
|
|
944
|
+
};
|
|
945
|
+
await applyTwinWrite(SERVICE, { operation: 'guardrail.create', subjectType: 'guardrail', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
946
|
+
return { status: 200, body: { data: stripPrefix(row) } };
|
|
947
|
+
}
|
|
948
|
+
function listGuardrails(req) {
|
|
949
|
+
return { status: 200, body: { data: rowsOfType('guardrail', req.root).filter((r) => !r.deleted).map(stripPrefix) } };
|
|
950
|
+
}
|
|
951
|
+
function getGuardrail(req, id) {
|
|
952
|
+
const row = rowsOfType('guardrail', req.root).find((r) => String(r.id) === `guardrail_${id}` && !r.deleted);
|
|
953
|
+
return row ? { status: 200, body: { data: stripPrefix(row) } } : error(404, 'not_found', 'Guardrail not found', 'not_found_error');
|
|
954
|
+
}
|
|
955
|
+
async function updateGuardrail(req, id, params, del) {
|
|
956
|
+
const row = rowsOfType('guardrail', req.root).find((r) => String(r.id) === `guardrail_${id}` && !r.deleted);
|
|
957
|
+
if (!row)
|
|
958
|
+
return error(404, 'not_found', 'Guardrail not found', 'not_found_error');
|
|
959
|
+
if (del) {
|
|
960
|
+
await applyTwinWrite(SERVICE, { operation: 'guardrail.delete', subjectType: 'guardrail', subjectId: String(row.id), fields: { deleted: true }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
961
|
+
return { status: 200, body: { data: { id, deleted: true } } };
|
|
962
|
+
}
|
|
963
|
+
const patch = { updated_at: nowIso(req.occurredAt) };
|
|
964
|
+
if (typeof params.name === 'string')
|
|
965
|
+
patch.name = params.name;
|
|
966
|
+
if (typeof params.description === 'string')
|
|
967
|
+
patch.description = params.description;
|
|
968
|
+
if (typeof params.enabled === 'boolean')
|
|
969
|
+
patch.enabled = params.enabled;
|
|
970
|
+
if (Array.isArray(params.rules))
|
|
971
|
+
patch.rules = params.rules;
|
|
972
|
+
await applyTwinWrite(SERVICE, { operation: 'guardrail.update', subjectType: 'guardrail', subjectId: String(row.id), fields: patch, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
973
|
+
return { status: 200, body: { data: stripPrefix({ ...row, ...patch }) } };
|
|
974
|
+
}
|
|
975
|
+
async function setGuardrailAssignments(req, id, params) {
|
|
976
|
+
const row = rowsOfType('guardrail', req.root).find((r) => String(r.id) === `guardrail_${id}` && !r.deleted);
|
|
977
|
+
if (!row)
|
|
978
|
+
return error(404, 'not_found', 'Guardrail not found', 'not_found_error');
|
|
979
|
+
const keyHashes = Array.isArray(params.key_hashes) ? params.key_hashes.map(String) : undefined;
|
|
980
|
+
if (!keyHashes)
|
|
981
|
+
return error(400, 'invalid_assignments', 'key_hashes must be an array');
|
|
982
|
+
await applyTwinWrite(SERVICE, { operation: 'guardrail.assign', subjectType: 'guardrail', subjectId: String(row.id), fields: { key_hashes: keyHashes, updated_at: nowIso(req.occurredAt) }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
983
|
+
return { status: 200, body: { data: { id, key_hashes: keyHashes } } };
|
|
984
|
+
}
|
|
985
|
+
function getGuardrailAssignments(req, id) {
|
|
986
|
+
const row = rowsOfType('guardrail', req.root).find((r) => String(r.id) === `guardrail_${id}` && !r.deleted);
|
|
987
|
+
if (!row)
|
|
988
|
+
return error(404, 'not_found', 'Guardrail not found', 'not_found_error');
|
|
989
|
+
return { status: 200, body: { data: { id, key_hashes: Array.isArray(row.key_hashes) ? row.key_hashes : [] } } };
|
|
990
|
+
}
|
|
991
|
+
// ── Organization members ───────────────────────────────────────────────────────
|
|
992
|
+
const ORG_MEMBERS = [
|
|
993
|
+
{ id: 'user_twin_owner', name: 'Twin Owner', email: 'owner@twin.local', role: 'owner' },
|
|
994
|
+
{ id: 'user_twin_admin', name: 'Twin Admin', email: 'admin@twin.local', role: 'admin' },
|
|
995
|
+
{ id: 'user_twin_member', name: 'Twin Member', email: 'member@twin.local', role: 'member' },
|
|
996
|
+
];
|
|
997
|
+
function listOrgMembers() {
|
|
998
|
+
return { status: 200, body: { data: ORG_MEMBERS } };
|
|
999
|
+
}
|
|
1000
|
+
// ── Workspaces ─────────────────────────────────────────────────────────────────
|
|
1001
|
+
async function createWorkspace(req, params) {
|
|
1002
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
1003
|
+
return error(400, 'invalid_name', 'name is required');
|
|
1004
|
+
const id = `workspace_${genId('ws', { name: params.name, at: req.occurredAt })}`;
|
|
1005
|
+
const row = {
|
|
1006
|
+
id,
|
|
1007
|
+
name: params.name,
|
|
1008
|
+
slug: String(params.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''),
|
|
1009
|
+
description: typeof params.description === 'string' ? params.description : '',
|
|
1010
|
+
members: [],
|
|
1011
|
+
created_at: nowIso(req.occurredAt),
|
|
1012
|
+
updated_at: nowIso(req.occurredAt),
|
|
1013
|
+
};
|
|
1014
|
+
await applyTwinWrite(SERVICE, { operation: 'workspace.create', subjectType: 'workspace', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1015
|
+
return { status: 200, body: { data: stripPrefix(row) } };
|
|
1016
|
+
}
|
|
1017
|
+
function listWorkspaces(req) {
|
|
1018
|
+
return { status: 200, body: { data: rowsOfType('workspace', req.root).filter((r) => !r.deleted).map(stripPrefix) } };
|
|
1019
|
+
}
|
|
1020
|
+
function getWorkspace(req, id) {
|
|
1021
|
+
const row = rowsOfType('workspace', req.root).find((r) => String(r.id) === `workspace_${id}` && !r.deleted);
|
|
1022
|
+
return row ? { status: 200, body: { data: stripPrefix(row) } } : error(404, 'not_found', 'Workspace not found', 'not_found_error');
|
|
1023
|
+
}
|
|
1024
|
+
async function updateWorkspace(req, id, params, del) {
|
|
1025
|
+
const row = rowsOfType('workspace', req.root).find((r) => String(r.id) === `workspace_${id}` && !r.deleted);
|
|
1026
|
+
if (!row)
|
|
1027
|
+
return error(404, 'not_found', 'Workspace not found', 'not_found_error');
|
|
1028
|
+
if (del) {
|
|
1029
|
+
await applyTwinWrite(SERVICE, { operation: 'workspace.delete', subjectType: 'workspace', subjectId: String(row.id), fields: { deleted: true }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1030
|
+
return { status: 200, body: { data: { id, deleted: true } } };
|
|
1031
|
+
}
|
|
1032
|
+
const patch = { updated_at: nowIso(req.occurredAt) };
|
|
1033
|
+
if (typeof params.name === 'string')
|
|
1034
|
+
patch.name = params.name;
|
|
1035
|
+
if (typeof params.description === 'string')
|
|
1036
|
+
patch.description = params.description;
|
|
1037
|
+
await applyTwinWrite(SERVICE, { operation: 'workspace.update', subjectType: 'workspace', subjectId: String(row.id), fields: patch, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1038
|
+
return { status: 200, body: { data: stripPrefix({ ...row, ...patch }) } };
|
|
1039
|
+
}
|
|
1040
|
+
async function updateWorkspaceMembers(req, id, params) {
|
|
1041
|
+
const row = rowsOfType('workspace', req.root).find((r) => String(r.id) === `workspace_${id}` && !r.deleted);
|
|
1042
|
+
if (!row)
|
|
1043
|
+
return error(404, 'not_found', 'Workspace not found', 'not_found_error');
|
|
1044
|
+
const add = Array.isArray(params.add) ? params.add.map(String) : [];
|
|
1045
|
+
const remove = Array.isArray(params.remove) ? params.remove.map(String) : [];
|
|
1046
|
+
if (add.length === 0 && remove.length === 0)
|
|
1047
|
+
return error(400, 'invalid_members', 'add or remove must be a non-empty array');
|
|
1048
|
+
const current = new Set((Array.isArray(row.members) ? row.members : []).map(String));
|
|
1049
|
+
for (const m of add)
|
|
1050
|
+
current.add(m);
|
|
1051
|
+
for (const m of remove)
|
|
1052
|
+
current.delete(m);
|
|
1053
|
+
const members = [...current];
|
|
1054
|
+
await applyTwinWrite(SERVICE, { operation: 'workspace.members', subjectType: 'workspace', subjectId: String(row.id), fields: { members, updated_at: nowIso(req.occurredAt) }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1055
|
+
return { status: 200, body: { data: { id, members } } };
|
|
1056
|
+
}
|
|
1057
|
+
// ── Datasets ───────────────────────────────────────────────────────────────────
|
|
1058
|
+
async function createDataset(req, params) {
|
|
1059
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
1060
|
+
return error(400, 'invalid_name', 'name is required');
|
|
1061
|
+
const id = `dataset_${genId('ds', { name: params.name, at: req.occurredAt })}`;
|
|
1062
|
+
const row = {
|
|
1063
|
+
id,
|
|
1064
|
+
name: params.name,
|
|
1065
|
+
description: typeof params.description === 'string' ? params.description : '',
|
|
1066
|
+
row_count: 0,
|
|
1067
|
+
status: 'ready',
|
|
1068
|
+
created_at: nowIso(req.occurredAt),
|
|
1069
|
+
updated_at: nowIso(req.occurredAt),
|
|
1070
|
+
};
|
|
1071
|
+
await applyTwinWrite(SERVICE, { operation: 'dataset.create', subjectType: 'dataset', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1072
|
+
return { status: 200, body: { data: stripPrefix(row) } };
|
|
1073
|
+
}
|
|
1074
|
+
function listDatasets(req) {
|
|
1075
|
+
return { status: 200, body: { data: rowsOfType('dataset', req.root).filter((r) => !r.deleted).map(stripPrefix) } };
|
|
1076
|
+
}
|
|
1077
|
+
// ── Analytics (computed from recorded generations) ──────────────────────────────
|
|
1078
|
+
function dayOf(iso) {
|
|
1079
|
+
return iso.slice(0, 10);
|
|
1080
|
+
}
|
|
1081
|
+
function analyticsActivity(req) {
|
|
1082
|
+
// Group recorded generations by (date, model) like OpenRouter's activity endpoint.
|
|
1083
|
+
const groups = new Map();
|
|
1084
|
+
for (const g of generationRows(req.root)) {
|
|
1085
|
+
const date = dayOf(g.created_at);
|
|
1086
|
+
const key = `${date}::${g.model}`;
|
|
1087
|
+
const cur = groups.get(key) ?? { date, model: g.model, requests: 0, prompt_tokens: 0, completion_tokens: 0, usage: 0 };
|
|
1088
|
+
cur.requests += 1;
|
|
1089
|
+
cur.prompt_tokens += g.tokens_prompt;
|
|
1090
|
+
cur.completion_tokens += g.tokens_completion;
|
|
1091
|
+
cur.usage = Number((cur.usage + g.total_cost).toFixed(8));
|
|
1092
|
+
groups.set(key, cur);
|
|
1093
|
+
}
|
|
1094
|
+
const data = [...groups.values()].sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : a.model < b.model ? -1 : 1));
|
|
1095
|
+
return { status: 200, body: { data } };
|
|
1096
|
+
}
|
|
1097
|
+
function analyticsDailyTokens(req) {
|
|
1098
|
+
const groups = new Map();
|
|
1099
|
+
for (const g of generationRows(req.root)) {
|
|
1100
|
+
const date = dayOf(g.created_at);
|
|
1101
|
+
const cur = groups.get(date) ?? { date, prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, requests: 0, usage: 0 };
|
|
1102
|
+
cur.prompt_tokens += g.tokens_prompt;
|
|
1103
|
+
cur.completion_tokens += g.tokens_completion;
|
|
1104
|
+
cur.total_tokens += g.tokens_prompt + g.tokens_completion;
|
|
1105
|
+
cur.requests += 1;
|
|
1106
|
+
cur.usage = Number((cur.usage + g.total_cost).toFixed(8));
|
|
1107
|
+
groups.set(date, cur);
|
|
1108
|
+
}
|
|
1109
|
+
const data = [...groups.values()].sort((a, b) => (a.date < b.date ? -1 : 1));
|
|
1110
|
+
return { status: 200, body: { data } };
|
|
1111
|
+
}
|
|
1112
|
+
function analyticsQuery(req, params) {
|
|
1113
|
+
// A small metric/dimension query over recorded generations.
|
|
1114
|
+
const metric = typeof params.metric === 'string' ? params.metric : 'requests';
|
|
1115
|
+
if (!['requests', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'usage'].includes(metric)) {
|
|
1116
|
+
return error(400, 'invalid_metric', `unsupported metric: ${metric}`);
|
|
1117
|
+
}
|
|
1118
|
+
const dimension = typeof params.dimension === 'string' ? params.dimension : 'model';
|
|
1119
|
+
if (!['model', 'date'].includes(dimension))
|
|
1120
|
+
return error(400, 'invalid_dimension', `unsupported dimension: ${dimension}`);
|
|
1121
|
+
const groups = new Map();
|
|
1122
|
+
for (const g of generationRows(req.root)) {
|
|
1123
|
+
const keyVal = dimension === 'model' ? g.model : dayOf(g.created_at);
|
|
1124
|
+
const total = g.tokens_prompt + g.tokens_completion;
|
|
1125
|
+
const value = metric === 'requests' ? 1
|
|
1126
|
+
: metric === 'prompt_tokens' ? g.tokens_prompt
|
|
1127
|
+
: metric === 'completion_tokens' ? g.tokens_completion
|
|
1128
|
+
: metric === 'total_tokens' ? total
|
|
1129
|
+
: g.total_cost;
|
|
1130
|
+
groups.set(keyVal, Number(((groups.get(keyVal) ?? 0) + value).toFixed(8)));
|
|
1131
|
+
}
|
|
1132
|
+
const data = [...groups.entries()].map(([key, value]) => ({ [dimension]: key, [metric]: value })).sort((a, b) => (Number(b[metric]) - Number(a[metric])));
|
|
1133
|
+
return { status: 200, body: { metric, dimension, data } };
|
|
1134
|
+
}
|
|
1135
|
+
// ── Rankings (deterministic local catalog) ──────────────────────────────────────
|
|
1136
|
+
function rankingsApps(req) {
|
|
1137
|
+
// Top apps by token usage, derived from any locally recorded generations that carry an app_id,
|
|
1138
|
+
// plus a deterministic seed so the endpoint is meaningful even on a fresh store.
|
|
1139
|
+
const byApp = new Map();
|
|
1140
|
+
for (const g of generationRows(req.root)) {
|
|
1141
|
+
if (g.app_id != null)
|
|
1142
|
+
byApp.set(String(g.app_id), (byApp.get(String(g.app_id)) ?? 0) + g.tokens_prompt + g.tokens_completion);
|
|
1143
|
+
}
|
|
1144
|
+
const seeded = [
|
|
1145
|
+
{ app_id: 'twin-app-1', title: 'Twin Playground', total_tokens: 1_000_000 },
|
|
1146
|
+
{ app_id: 'twin-app-2', title: 'Twin Agents', total_tokens: 750_000 },
|
|
1147
|
+
{ app_id: 'twin-app-3', title: 'Twin Search', total_tokens: 500_000 },
|
|
1148
|
+
];
|
|
1149
|
+
for (const s of seeded)
|
|
1150
|
+
s.total_tokens += byApp.get(s.app_id) ?? 0;
|
|
1151
|
+
const data = seeded.sort((a, b) => b.total_tokens - a.total_tokens).map((s, i) => ({ rank: i + 1, ...s }));
|
|
1152
|
+
return { status: 200, body: { data } };
|
|
1153
|
+
}
|
|
1154
|
+
const BENCHMARKS = [
|
|
1155
|
+
{ source: 'artificial-analysis', model: 'openai/gpt-4o', intelligence_index: 71, output_speed_tps: 95, latency_s: 0.45 },
|
|
1156
|
+
{ source: 'artificial-analysis', model: 'anthropic/claude-sonnet-4.5', intelligence_index: 74, output_speed_tps: 88, latency_s: 0.6 },
|
|
1157
|
+
{ source: 'design-arena', model: 'google/gemini-3-flash-preview', elo: 1240, votes: 12000 },
|
|
1158
|
+
];
|
|
1159
|
+
function rankingsBenchmarks(req) {
|
|
1160
|
+
const source = new URL(req.path, 'http://twin.local').searchParams.get('source');
|
|
1161
|
+
const data = source ? BENCHMARKS.filter((b) => b.source === source) : BENCHMARKS;
|
|
1162
|
+
return { status: 200, body: { data } };
|
|
1163
|
+
}
|
|
1164
|
+
// ── Observability destinations (CRUD) ───────────────────────────────────────────
|
|
1165
|
+
async function createDestination(req, params) {
|
|
1166
|
+
if (typeof params.type !== 'string' || !params.type)
|
|
1167
|
+
return error(400, 'invalid_type', 'type is required');
|
|
1168
|
+
if (!['datadog', 'webhook', 's3', 'langfuse'].includes(params.type))
|
|
1169
|
+
return error(400, 'invalid_type', `unsupported destination type: ${params.type}`);
|
|
1170
|
+
const id = `destination_${genId('dest', { type: params.type, at: req.occurredAt })}`;
|
|
1171
|
+
const row = {
|
|
1172
|
+
id,
|
|
1173
|
+
type: params.type,
|
|
1174
|
+
name: typeof params.name === 'string' ? params.name : String(params.type),
|
|
1175
|
+
enabled: params.enabled === undefined ? true : params.enabled === true,
|
|
1176
|
+
config: params.config && typeof params.config === 'object' ? params.config : {},
|
|
1177
|
+
created_at: nowIso(req.occurredAt),
|
|
1178
|
+
updated_at: nowIso(req.occurredAt),
|
|
1179
|
+
};
|
|
1180
|
+
await applyTwinWrite(SERVICE, { operation: 'destination.create', subjectType: 'destination', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1181
|
+
return { status: 200, body: { data: stripPrefix(row) } };
|
|
1182
|
+
}
|
|
1183
|
+
function listDestinations(req) {
|
|
1184
|
+
return { status: 200, body: { data: rowsOfType('destination', req.root).filter((r) => !r.deleted).map(stripPrefix) } };
|
|
1185
|
+
}
|
|
1186
|
+
async function updateDestination(req, id, params, del) {
|
|
1187
|
+
const row = rowsOfType('destination', req.root).find((r) => String(r.id) === `destination_${id}` && !r.deleted);
|
|
1188
|
+
if (!row)
|
|
1189
|
+
return error(404, 'not_found', 'Destination not found', 'not_found_error');
|
|
1190
|
+
if (del) {
|
|
1191
|
+
await applyTwinWrite(SERVICE, { operation: 'destination.delete', subjectType: 'destination', subjectId: String(row.id), fields: { deleted: true }, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1192
|
+
return { status: 200, body: { data: { id, deleted: true } } };
|
|
1193
|
+
}
|
|
1194
|
+
const patch = { updated_at: nowIso(req.occurredAt) };
|
|
1195
|
+
if (typeof params.name === 'string')
|
|
1196
|
+
patch.name = params.name;
|
|
1197
|
+
if (typeof params.enabled === 'boolean')
|
|
1198
|
+
patch.enabled = params.enabled;
|
|
1199
|
+
if (params.config && typeof params.config === 'object')
|
|
1200
|
+
patch.config = params.config;
|
|
1201
|
+
await applyTwinWrite(SERVICE, { operation: 'destination.update', subjectType: 'destination', subjectId: String(row.id), fields: patch, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1202
|
+
return { status: 200, body: { data: stripPrefix({ ...row, ...patch }) } };
|
|
1203
|
+
}
|
|
1204
|
+
// ── Audio: speech + transcription (protocol stubs) ──────────────────────────────
|
|
1205
|
+
function createSpeech(req, params) {
|
|
1206
|
+
const model = typeof params.model === 'string' && params.model ? params.model : undefined;
|
|
1207
|
+
if (!model)
|
|
1208
|
+
return error(400, 'invalid_model', 'model is required');
|
|
1209
|
+
if (typeof params.input !== 'string' || !params.input)
|
|
1210
|
+
return error(400, 'invalid_input', 'input is required');
|
|
1211
|
+
const voice = typeof params.voice === 'string' ? params.voice : 'alloy';
|
|
1212
|
+
const format = typeof params.response_format === 'string' ? params.response_format : 'mp3';
|
|
1213
|
+
// Deterministic, clearly-labeled stub audio payload — the API surface is the request/response
|
|
1214
|
+
// envelope and the (base64) audio container, not real waveform synthesis.
|
|
1215
|
+
const marker = `[twin-stub:openrouter:speech] model=${model} voice=${voice} text=${params.input}`;
|
|
1216
|
+
const audio = Buffer.from(marker).toString('base64');
|
|
1217
|
+
return { status: 200, body: { model, voice, format, audio, object: 'audio.speech', stub: true } };
|
|
1218
|
+
}
|
|
1219
|
+
function createTranscription(req, params) {
|
|
1220
|
+
const model = typeof params.model === 'string' && params.model ? params.model : undefined;
|
|
1221
|
+
if (!model)
|
|
1222
|
+
return error(400, 'invalid_model', 'model is required');
|
|
1223
|
+
if (params.file === undefined && params.audio === undefined)
|
|
1224
|
+
return error(400, 'invalid_file', 'file is required');
|
|
1225
|
+
const text = `[twin-stub:openrouter:transcription] deterministic transcript for ${model}`;
|
|
1226
|
+
return { status: 200, body: { text, model, object: 'audio.transcription', stub: true } };
|
|
1227
|
+
}
|
|
1228
|
+
// ── Video generation (protocol stub: submit + poll/download) ─────────────────────
|
|
1229
|
+
async function submitVideo(req, params) {
|
|
1230
|
+
const model = typeof params.model === 'string' && params.model ? params.model : undefined;
|
|
1231
|
+
if (!model)
|
|
1232
|
+
return error(400, 'invalid_model', 'model is required');
|
|
1233
|
+
if (typeof params.prompt !== 'string' || !params.prompt)
|
|
1234
|
+
return error(400, 'invalid_prompt', 'prompt is required');
|
|
1235
|
+
const id = `video_${genId('vid', { model, prompt: params.prompt, at: req.occurredAt })}`;
|
|
1236
|
+
const row = {
|
|
1237
|
+
id,
|
|
1238
|
+
object: 'video.generation',
|
|
1239
|
+
model,
|
|
1240
|
+
prompt: params.prompt,
|
|
1241
|
+
status: 'completed', // deterministic: the twin completes immediately
|
|
1242
|
+
progress: 100,
|
|
1243
|
+
created_at: nowEpoch(req.occurredAt),
|
|
1244
|
+
download_url: `http://twin.local/v1/video/${id.replace(/^video_/, '')}/content`,
|
|
1245
|
+
stub: true,
|
|
1246
|
+
};
|
|
1247
|
+
await applyTwinWrite(SERVICE, { operation: 'video.submit', subjectType: 'video', subjectId: id, fields: row, occurredAt: req.occurredAt ?? FIXED_NOW, actor: { kind: 'system' } }, req.root);
|
|
1248
|
+
return { status: 200, body: stripPrefix(row) };
|
|
1249
|
+
}
|
|
1250
|
+
function pollVideo(req, id) {
|
|
1251
|
+
const row = rowsOfType('video', req.root).find((r) => String(r.id) === `video_${id}`);
|
|
1252
|
+
return row ? { status: 200, body: stripPrefix(row) } : error(404, 'not_found', 'Video generation not found', 'not_found_error');
|
|
1253
|
+
}
|
|
1254
|
+
function downloadVideo(req, id) {
|
|
1255
|
+
const row = rowsOfType('video', req.root).find((r) => String(r.id) === `video_${id}`);
|
|
1256
|
+
if (!row)
|
|
1257
|
+
return error(404, 'not_found', 'Video generation not found', 'not_found_error');
|
|
1258
|
+
const content = Buffer.from(`[twin-stub:openrouter:video] ${row.model} :: ${row.prompt}`).toString('base64');
|
|
1259
|
+
return { status: 200, body: { id, object: 'video.content', content, stub: true } };
|
|
1260
|
+
}
|
|
1261
|
+
const VIDEO_MODELS = [
|
|
1262
|
+
{ id: 'twin/video-gen-1', name: 'Twin Video Gen 1', max_duration_s: 8 },
|
|
1263
|
+
{ id: 'twin/video-gen-pro', name: 'Twin Video Gen Pro', max_duration_s: 16 },
|
|
1264
|
+
];
|
|
1265
|
+
function listVideoModels() {
|
|
1266
|
+
return { status: 200, body: { data: VIDEO_MODELS } };
|
|
1267
|
+
}
|
|
1268
|
+
/** The legacy handler remains JSON-shaped; only the fetch adapter can select a local handoff. */
|
|
1269
|
+
export async function handleOpenRouterTwinRequest(req) {
|
|
1270
|
+
const result = await handleOpenRouterTwinRequestWithGeneration(req);
|
|
1271
|
+
if (result instanceof Response)
|
|
1272
|
+
throw new Error('Unexpected local generation in the legacy twin handler');
|
|
1273
|
+
return result;
|
|
1274
|
+
}
|
|
1275
|
+
export async function handleOpenRouterTwinRequestWithGeneration(req, localGeneration) {
|
|
1276
|
+
const method = req.method.toUpperCase();
|
|
1277
|
+
const path = apiPath(req.path);
|
|
1278
|
+
const url = new URL(req.path, 'http://twin.local');
|
|
1279
|
+
if (method === 'GET' && path === '/v1/models')
|
|
1280
|
+
return { status: 200, body: { data: OPENROUTER_MODELS } };
|
|
1281
|
+
if (method === 'GET' && path === '/v1/models/count')
|
|
1282
|
+
return { status: 200, body: { data: { count: OPENROUTER_MODELS.length } } };
|
|
1283
|
+
if (method === 'GET' && path.startsWith('/v1/models/') && path.endsWith('/endpoints')) {
|
|
1284
|
+
const modelId = decodeURIComponent(path.slice('/v1/models/'.length, -'/endpoints'.length));
|
|
1285
|
+
const model = findOpenRouterModel(modelId);
|
|
1286
|
+
return model ? { status: 200, body: { data: { ...model, endpoints: [{ name: 'OpenRouter Twin', provider_name: 'twin', context_length: model.context_length }] } } } : error(404, 'not_found', 'Model not found');
|
|
1287
|
+
}
|
|
1288
|
+
if (method === 'GET' && path.startsWith('/v1/models/')) {
|
|
1289
|
+
const model = findOpenRouterModel(decodeURIComponent(path.slice('/v1/models/'.length)));
|
|
1290
|
+
return model ? { status: 200, body: { data: model } } : error(404, 'not_found', 'Model not found');
|
|
1291
|
+
}
|
|
1292
|
+
if (method === 'GET' && path === '/v1/providers') {
|
|
1293
|
+
return { status: 200, body: { data: OPENROUTER_PROVIDERS } };
|
|
1294
|
+
}
|
|
1295
|
+
if (method === 'GET' && path === '/v1/key') {
|
|
1296
|
+
return { status: 200, body: { data: { label: 'twin-key', limit: null, limit_reset: null, limit_remaining: null, include_byok_in_limit: false, usage: 0 } } };
|
|
1297
|
+
}
|
|
1298
|
+
if (method === 'GET' && path === '/v1/credits') {
|
|
1299
|
+
return { status: 200, body: { data: { total_credits: 100, total_usage: 0 } } };
|
|
1300
|
+
}
|
|
1301
|
+
if (method === 'GET' && path === '/v1/generation') {
|
|
1302
|
+
const id = url.searchParams.get('id');
|
|
1303
|
+
const generationId = id?.startsWith('generation_') ? id : `generation_${id ?? ''}`;
|
|
1304
|
+
const found = generationRows(req.root).find((g) => g.id === generationId);
|
|
1305
|
+
return found ? { status: 200, body: { data: found } } : error(404, 'not_found', 'Generation not found');
|
|
1306
|
+
}
|
|
1307
|
+
if (method === 'GET' && path === '/v1/generations') {
|
|
1308
|
+
return { status: 200, body: { data: generationRows(req.root) } };
|
|
1309
|
+
}
|
|
1310
|
+
// BYOK provider credentials (more-specific than /v1/keys, so route first)
|
|
1311
|
+
if (path === '/v1/keys/byok' || path === '/v1/byok') {
|
|
1312
|
+
if (method === 'GET')
|
|
1313
|
+
return listByok(req);
|
|
1314
|
+
if (method === 'POST')
|
|
1315
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createByok(req, parseJson(req.body));
|
|
1316
|
+
}
|
|
1317
|
+
if (path.startsWith('/v1/keys/byok/') || path.startsWith('/v1/byok/')) {
|
|
1318
|
+
const provider = decodeURIComponent(path.replace(/^\/v1\/(keys\/)?byok\//, ''));
|
|
1319
|
+
if (method === 'PATCH')
|
|
1320
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateByok(req, provider, parseJson(req.body), false);
|
|
1321
|
+
if (method === 'DELETE')
|
|
1322
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateByok(req, provider, {}, true);
|
|
1323
|
+
}
|
|
1324
|
+
// API keys
|
|
1325
|
+
if (path === '/v1/keys') {
|
|
1326
|
+
if (method === 'GET')
|
|
1327
|
+
return listApiKeys(req);
|
|
1328
|
+
if (method === 'POST')
|
|
1329
|
+
return req.readOnly ? error(405, 'read_only', 'This OpenRouter twin was started read-only') : createApiKey(req, parseJson(req.body));
|
|
1330
|
+
}
|
|
1331
|
+
if (path.startsWith('/v1/keys/')) {
|
|
1332
|
+
const hash = decodeURIComponent(path.slice('/v1/keys/'.length));
|
|
1333
|
+
if (method === 'GET') {
|
|
1334
|
+
const row = rowsOfType('api_key', req.root).find((r) => r.hash === hash || String(r.id) === `key_${hash}`);
|
|
1335
|
+
return row ? { status: 200, body: { data: stripPrefix(row) } } : error(404, 'not_found', 'API key not found', 'not_found_error');
|
|
1336
|
+
}
|
|
1337
|
+
if (method === 'PATCH')
|
|
1338
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateApiKey(req, hash, parseJson(req.body), false);
|
|
1339
|
+
if (method === 'DELETE')
|
|
1340
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateApiKey(req, hash, {}, true);
|
|
1341
|
+
}
|
|
1342
|
+
// Presets (responses-sourced create is more specific than the slug getter, so route it first)
|
|
1343
|
+
if (method === 'POST' && path === '/v1/presets/responses') {
|
|
1344
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createPresetFromResponses(req, parseJson(req.body));
|
|
1345
|
+
}
|
|
1346
|
+
if (path === '/v1/presets') {
|
|
1347
|
+
if (method === 'GET')
|
|
1348
|
+
return listPresets(req);
|
|
1349
|
+
if (method === 'POST')
|
|
1350
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createPreset(req, parseJson(req.body));
|
|
1351
|
+
}
|
|
1352
|
+
if (path.startsWith('/v1/presets/')) {
|
|
1353
|
+
if (method === 'GET')
|
|
1354
|
+
return getPreset(req, decodeURIComponent(path.slice('/v1/presets/'.length)));
|
|
1355
|
+
}
|
|
1356
|
+
// Files
|
|
1357
|
+
if (path === '/v1/files') {
|
|
1358
|
+
if (method === 'GET')
|
|
1359
|
+
return listFiles(req);
|
|
1360
|
+
if (method === 'POST')
|
|
1361
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : uploadFile(req, parseJson(req.body));
|
|
1362
|
+
}
|
|
1363
|
+
if (path.startsWith('/v1/files/')) {
|
|
1364
|
+
const fileId = decodeURIComponent(path.slice('/v1/files/'.length));
|
|
1365
|
+
if (method === 'GET')
|
|
1366
|
+
return getFile(req, fileId);
|
|
1367
|
+
if (method === 'DELETE')
|
|
1368
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : deleteFile(req, fileId);
|
|
1369
|
+
}
|
|
1370
|
+
// OAuth (PKCE)
|
|
1371
|
+
if (method === 'POST' && path === '/v1/auth/authorize') {
|
|
1372
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : oauthAuthorize(req, parseJson(req.body));
|
|
1373
|
+
}
|
|
1374
|
+
if (method === 'POST' && (path === '/v1/auth/keys' || path === '/v1/auth/exchange')) {
|
|
1375
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : oauthExchange(req, parseJson(req.body));
|
|
1376
|
+
}
|
|
1377
|
+
// Guardrails
|
|
1378
|
+
if (path === '/v1/guardrails') {
|
|
1379
|
+
if (method === 'GET')
|
|
1380
|
+
return listGuardrails(req);
|
|
1381
|
+
if (method === 'POST')
|
|
1382
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createGuardrail(req, parseJson(req.body));
|
|
1383
|
+
}
|
|
1384
|
+
if (path.startsWith('/v1/guardrails/') && path.endsWith('/assignments')) {
|
|
1385
|
+
const id = decodeURIComponent(path.slice('/v1/guardrails/'.length, -'/assignments'.length));
|
|
1386
|
+
if (method === 'GET')
|
|
1387
|
+
return getGuardrailAssignments(req, id);
|
|
1388
|
+
if (method === 'PUT' || method === 'POST')
|
|
1389
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : setGuardrailAssignments(req, id, parseJson(req.body));
|
|
1390
|
+
}
|
|
1391
|
+
if (path.startsWith('/v1/guardrails/')) {
|
|
1392
|
+
const id = decodeURIComponent(path.slice('/v1/guardrails/'.length));
|
|
1393
|
+
if (method === 'GET')
|
|
1394
|
+
return getGuardrail(req, id);
|
|
1395
|
+
if (method === 'PATCH')
|
|
1396
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateGuardrail(req, id, parseJson(req.body), false);
|
|
1397
|
+
if (method === 'DELETE')
|
|
1398
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateGuardrail(req, id, {}, true);
|
|
1399
|
+
}
|
|
1400
|
+
// Organization
|
|
1401
|
+
if (method === 'GET' && (path === '/v1/organization/members' || path === '/v1/me/members')) {
|
|
1402
|
+
return listOrgMembers();
|
|
1403
|
+
}
|
|
1404
|
+
// Workspaces
|
|
1405
|
+
if (path === '/v1/workspaces') {
|
|
1406
|
+
if (method === 'GET')
|
|
1407
|
+
return listWorkspaces(req);
|
|
1408
|
+
if (method === 'POST')
|
|
1409
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createWorkspace(req, parseJson(req.body));
|
|
1410
|
+
}
|
|
1411
|
+
if (path.startsWith('/v1/workspaces/') && path.endsWith('/members')) {
|
|
1412
|
+
const id = decodeURIComponent(path.slice('/v1/workspaces/'.length, -'/members'.length));
|
|
1413
|
+
if (method === 'POST' || method === 'PATCH')
|
|
1414
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateWorkspaceMembers(req, id, parseJson(req.body));
|
|
1415
|
+
}
|
|
1416
|
+
if (path.startsWith('/v1/workspaces/')) {
|
|
1417
|
+
const id = decodeURIComponent(path.slice('/v1/workspaces/'.length));
|
|
1418
|
+
if (method === 'GET')
|
|
1419
|
+
return getWorkspace(req, id);
|
|
1420
|
+
if (method === 'PATCH')
|
|
1421
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateWorkspace(req, id, parseJson(req.body), false);
|
|
1422
|
+
if (method === 'DELETE')
|
|
1423
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateWorkspace(req, id, {}, true);
|
|
1424
|
+
}
|
|
1425
|
+
// Datasets
|
|
1426
|
+
if (path === '/v1/datasets') {
|
|
1427
|
+
if (method === 'GET')
|
|
1428
|
+
return listDatasets(req);
|
|
1429
|
+
if (method === 'POST')
|
|
1430
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createDataset(req, parseJson(req.body));
|
|
1431
|
+
}
|
|
1432
|
+
// Analytics (computed from recorded generations)
|
|
1433
|
+
if (method === 'GET' && path === '/v1/analytics/activity')
|
|
1434
|
+
return analyticsActivity(req);
|
|
1435
|
+
if (method === 'GET' && (path === '/v1/analytics/daily' || path === '/v1/analytics/daily-tokens'))
|
|
1436
|
+
return analyticsDailyTokens(req);
|
|
1437
|
+
if ((method === 'GET' || method === 'POST') && path === '/v1/analytics/query') {
|
|
1438
|
+
return analyticsQuery(req, method === 'POST' ? parseJson(req.body) : Object.fromEntries(url.searchParams.entries()));
|
|
1439
|
+
}
|
|
1440
|
+
// Rankings
|
|
1441
|
+
if (method === 'GET' && path === '/v1/rankings/apps')
|
|
1442
|
+
return rankingsApps(req);
|
|
1443
|
+
if (method === 'GET' && path === '/v1/rankings/benchmarks')
|
|
1444
|
+
return rankingsBenchmarks(req);
|
|
1445
|
+
// Observability destinations
|
|
1446
|
+
if (path === '/v1/observability/destinations') {
|
|
1447
|
+
if (method === 'GET')
|
|
1448
|
+
return listDestinations(req);
|
|
1449
|
+
if (method === 'POST')
|
|
1450
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createDestination(req, parseJson(req.body));
|
|
1451
|
+
}
|
|
1452
|
+
if (path.startsWith('/v1/observability/destinations/')) {
|
|
1453
|
+
const id = decodeURIComponent(path.slice('/v1/observability/destinations/'.length));
|
|
1454
|
+
if (method === 'PATCH')
|
|
1455
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateDestination(req, id, parseJson(req.body), false);
|
|
1456
|
+
if (method === 'DELETE')
|
|
1457
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : updateDestination(req, id, {}, true);
|
|
1458
|
+
}
|
|
1459
|
+
// Audio (protocol stubs)
|
|
1460
|
+
if (method === 'POST' && path === '/v1/audio/speech') {
|
|
1461
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createSpeech(req, parseJson(req.body));
|
|
1462
|
+
}
|
|
1463
|
+
if (method === 'POST' && path === '/v1/audio/transcriptions') {
|
|
1464
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : createTranscription(req, parseJson(req.body));
|
|
1465
|
+
}
|
|
1466
|
+
// Video generation (protocol stub)
|
|
1467
|
+
if (method === 'GET' && path === '/v1/video/models')
|
|
1468
|
+
return listVideoModels();
|
|
1469
|
+
if (method === 'POST' && path === '/v1/video') {
|
|
1470
|
+
return req.readOnly ? error(405, 'read_only', 'read-only') : submitVideo(req, parseJson(req.body));
|
|
1471
|
+
}
|
|
1472
|
+
if (path.startsWith('/v1/video/') && path.endsWith('/content')) {
|
|
1473
|
+
const id = decodeURIComponent(path.slice('/v1/video/'.length, -'/content'.length));
|
|
1474
|
+
if (method === 'GET')
|
|
1475
|
+
return downloadVideo(req, id);
|
|
1476
|
+
}
|
|
1477
|
+
if (method === 'GET' && path.startsWith('/v1/video/')) {
|
|
1478
|
+
return pollVideo(req, decodeURIComponent(path.slice('/v1/video/'.length)));
|
|
1479
|
+
}
|
|
1480
|
+
if (method === 'POST' && path === '/v1/chat/completions') {
|
|
1481
|
+
if (req.readOnly)
|
|
1482
|
+
return error(405, 'read_only', 'This OpenRouter twin was started read-only');
|
|
1483
|
+
return chatCompletion(req, parseJson(req.body), localGeneration);
|
|
1484
|
+
}
|
|
1485
|
+
if (method === 'POST' && path === '/v1/responses') {
|
|
1486
|
+
if (req.readOnly)
|
|
1487
|
+
return error(405, 'read_only', 'This OpenRouter twin was started read-only');
|
|
1488
|
+
return createResponse(req, parseJson(req.body), localGeneration);
|
|
1489
|
+
}
|
|
1490
|
+
if (method === 'POST' && path === '/v1/embeddings') {
|
|
1491
|
+
if (req.readOnly)
|
|
1492
|
+
return error(405, 'read_only', 'This OpenRouter twin was started read-only');
|
|
1493
|
+
return createEmbeddings(req, parseJson(req.body));
|
|
1494
|
+
}
|
|
1495
|
+
if (method === 'POST' && (path === '/v1/messages' || path === '/v1/anthropic/v1/messages')) {
|
|
1496
|
+
if (req.readOnly)
|
|
1497
|
+
return error(405, 'read_only', 'This OpenRouter twin was started read-only');
|
|
1498
|
+
return anthropicMessages(req, parseJson(req.body));
|
|
1499
|
+
}
|
|
1500
|
+
if (method === 'POST' && path === '/v1/rerank') {
|
|
1501
|
+
if (req.readOnly)
|
|
1502
|
+
return error(405, 'read_only', 'This OpenRouter twin was started read-only');
|
|
1503
|
+
return rerank(req, parseJson(req.body));
|
|
1504
|
+
}
|
|
1505
|
+
return error(404, 'not_found', `OpenRouter operation not modeled: ${method} ${path}`, 'not_found_error');
|
|
1506
|
+
}
|