Repository navigation
Expand file tree
/
Copy pathfake-openrouter.mjs
More file actions
495 lines (469 loc) · 25.1 KB
/
Copy pathfake-openrouter.mjs
File metadata and controls
495 lines (469 loc) · 25.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
// Deterministic stand-in for OpenRouter used by the full-stack browser gate.
// The real `ai` and `ai-stream` Edge Functions call it through
// OPENROUTER_API_URL, so their auth, quota, source loading, prompt building,
// and output validation all run for real. Unknown prompts fail loudly: a prompt
// change must update this file rather than silently receive a generic answer.
//
// With UPSTREAM set, it becomes a recording proxy for the live gate instead:
// every chat call goes to the real provider with the model pinned to
// LIVE_MODEL, and calls past MAX_UPSTREAM_CALLS are refused to bound cost.
// LIVE_MODEL=as-requested forwards the model the Edge Function chose.
import { createServer } from "node:http";
const port = Number(process.env.PORT ?? 8787);
const upstream = process.env.UPSTREAM;
const liveModel = process.env.LIVE_MODEL;
const chatModel = (requested) => (liveModel === "as-requested" ? requested : liveModel);
// Claude runs only on DebateOwl's Anthropic key (BYOK in OpenRouter): every
// Claude call leaves pinned to the Anthropic provider with fallbacks off, and
// any `models` fallback list is dropped. A rewrite to another family (only
// as-requested reaches one) keeps just the options that still apply to it.
const ANTHROPIC_ONLY = { only: ["anthropic"], allow_fallbacks: false };
const liveChatBody = (body) => {
const model = chatModel(body.model);
const { models: _models, ...rest } = body;
const provider = body.provider && typeof body.provider === "object" ? body.provider : {};
if (model.startsWith("anthropic/")) return { ...rest, model, provider: { ...provider, ...ANTHROPIC_ONLY } };
if (model === body.model || !body.provider || typeof body.provider !== "object") return { ...rest, model };
const { only: _only, allow_fallbacks: _allowFallbacks, ...others } = provider;
return { ...rest, model, provider: others };
};
const maxUpstreamCalls = Number(process.env.MAX_UPSTREAM_CALLS ?? 200);
if (upstream && !liveModel) throw new Error("LIVE_MODEL is required with UPSTREAM");
if (upstream && liveModel !== "as-requested" && !liveModel.startsWith("anthropic/")) {
throw new Error(`LIVE_MODEL must be an anthropic/ model (Anthropic BYOK) or as-requested, not ${liveModel}`);
}
const requests = [];
// Subscription state the fake Stripe returns, set by tests through /__subscription.
const subscriptions = new Map();
let failures = [];
// Replies cut off at max_tokens (finish_reason "length"), set through /__truncate.
let truncations = [];
let upstreamCalls = 0;
// Delay between streamed chunks; a test can slow it to keep a draft on screen.
let streamDelayMs;
const MARK = "FAKE-PROVIDER";
const STREAM_CHUNK_CHARS = 24;
const STREAM_CHUNK_DELAY_MS = 10;
const usage = { prompt_tokens: 120, completion_tokens: 80, total_tokens: 200, cost: 0.0001 };
const contention = (index) => ({
id: String(index + 1),
title: `Fares shut people out ${index + 1}`,
claim: `${MARK} contention ${index + 1}: fares block the riders who need transit most`,
reasoning: "Fares are a regressive barrier concentrated on low-income riders.",
evidence: "UITP reported in 2024 that comparable cities gained riders after removing fares.",
needsSource: false,
// The prompt asks for whyItMatters; the Edge Function files it under impact.
whyItMatters: "More people reach work, school, and healthcare.",
});
const counter = (index) => ({
targetArgument: [
"The lost farebox revenue has to come from somewhere.",
"Free buses will be overcrowded and dirty.",
"People do not value what they do not pay for.",
"Drivers will not leave their cars anyway.",
"Only tourists and the well-off benefit.",
"Cities cannot afford the extra buses.",
"Fares already have discounts for people who need them.",
"Free transport has been tried and dropped elsewhere.",
"Better buses would help more than free buses.",
"Fare-free cities still have empty buses at night.",
"Taxpayers who never ride would pay for everyone else.",
"Free fares crowd out walking and cycling.",
][index],
counterpoint: `${MARK} answer ${index + 1}: fare collection itself costs a large share of what it brings in, so the real gap is far smaller.`,
...(index % 2 === 0 ? { source: "UITP, 2024" } : {}),
});
// R2's answers are full spoken blocks the speaker underlines and builds on.
const r2Answer = (index) => ({
...counter(index),
counterpoint: `${MARK} answer ${index + 1}: that is not what the numbers show. Fare collection itself costs a large share of what it brings in, so the real gap is far smaller than they say (UITP, 2024). Low-income households already spend more of their budget on transport than anyone else (Eurostat, 2023). For a family on a low income, that difference is the bus to work or the walk. If the money were really the problem, why do cities that dropped fares keep them dropped? This is exactly our first argument: fares shut out the people who need transit most.`,
source: "UITP, 2024",
});
// The role decides the shape, as the real prompt asks: the Introductor may
// define an unclear term, R2 backs up the team's arguments and then answers
// theirs, and the Conclusor brings reasons why we win. Neither R2 nor the
// Conclusor brings new arguments.
const generate = (user) => {
// R2 is planned, then written in parts (supabase/functions/ai/r2-case.ts).
const planCount = /PLANNING STEP[\s\S]*?theirClaims: exactly (\d+)/.exec(user)?.[1];
if (planCount) {
return {
ourArguments: [
{ title: "Fares shut people out", claim: "Fares keep the riders who need transit most off the bus." },
{ title: "More people ride", claim: "Free fares bring more passengers onto public transport." },
],
theirClaims: Array.from({ length: Number(planCount) }, (_, index) => `Free transit claim ${index + 1}: it costs the city too much.`),
};
}
const part = /=== WRITING PART (\d+) OF \d+: claims (\d+)-(\d+) ===/.exec(user);
if (part && part[1] !== "1") {
const [first, last] = [Number(part[2]), Number(part[3])];
return {
counters: Array.from({ length: last - first + 1 }, (_, index) => r2Answer(first - 1 + index)),
sources: [{ org: "UITP", year: "2024", claim: "Ridership rose after fares were removed", url: "https://www.uitp.org/free-fares-2024" }],
};
}
const length = /^Length: (\w+)/m.exec(user)?.[1] ?? "standard";
const role = /^Role: (\w+)/m.exec(user)?.[1] ?? "r1";
const count = { brief: 2, standard: 3, comprehensive: 4 }[length] ?? 3;
const conc = role === "conc";
const r2 = role === "r2";
// R2: one full answer for each thing they are likely to say; part 1 of a
// split R2 case writes only its own share of them.
const answers = part ? Number(part[3]) - Number(part[2]) + 1 : length === "brief" ? 10 : 12;
return {
opening: {
hook: `${MARK} opening hook: picture a nurse who skips the night bus because the fare is a tenth of her shift's pay.`,
...(role === "intro" ? { definition: "By free public transport we mean no fare at the point of use, paid for from public budgets." } : {}),
},
caseStructure: {
thesis: conc
? `${MARK} case thesis: here is why free transit won this debate`
: r2
? `${MARK} case thesis: I'm the R2 of my team, and I'm here to show that free transit is a net public good`
: `${MARK} case thesis: today we will show ${count} arguments that prove free transit is a net public good`,
},
contentions: conc || r2 ? [] : Array.from({ length: count }, (_, index) => contention(index)),
counters: conc ? [] : Array.from({ length: r2 ? answers : 3 }, (_, index) => (r2 ? r2Answer(index) : counter(index))),
...(r2 ? {
reinforce: [
{ argument: "Our first argument: fares shut people out", text: `${MARK} backing up: our first argument was that fares shut out the riders who need transit most. Eurostat found in 2023 that low-income households spend more of their budget on transport. If fares were fair, those numbers would not look like this.` },
{ argument: "Our second argument: more people ride", text: `${MARK} backing up: our second argument was that free fares bring more passengers. UITP reported in 2024 that cities gained riders after removing fares. That is the policy working.` },
],
} : {}),
...(conc ? {
whyWeWin: [
"Their funding worry never answered our point that fare collection eats most of the fare.",
"Only our side showed who actually gets to work, school, and the doctor.",
"Even if some buses get busier, a full bus is the policy working.",
].slice(0, length === "brief" ? 2 : 3),
} : {}),
closingLine: r2
? "That is why it is clear that free public transport pays for itself in the lives it opens up. Thank you."
: "That is why it is clear that free public transport pays for itself in the lives it opens up.",
sources: [
{ org: "UITP", year: "2024", claim: "Ridership rose after fares were removed", url: "https://www.uitp.org/free-fares-2024" },
{ org: "Eurostat", year: "2023", claim: "Low-income households spend more of their budget on transport" },
],
metadata: { side: "pro", format: "custom", generatedAt: new Date().toISOString() },
};
};
const attack = () => ({
overallAssessment: {
score: 62,
summary: `${MARK} attack summary: the central warrant is underdefended.`,
strengthAreas: ["Clear thesis"],
},
weaknesses: [{
id: "w1",
severity: "major",
type: "evidential",
description: `${MARK} weakness: the congestion claim has no comparative evidence.`,
whyWeak: "Correlation between fare policy and congestion is asserted, not shown.",
howOpponentAttacks: "Name a city where ridership rose and congestion did not fall.",
improvedVersion: "Cite a before/after study with a control corridor.",
}],
fallaciesDetected: [{ name: "Hasty generalization", location: "Second contention", explanation: "One city generalized to all." }],
crossExQuestions: [{ question: "Which funding line replaces the farebox?", purpose: "Force a concrete mechanism.", expectedTrap: "A vague appeal to general taxation." }],
weighingAnalysis: {
currentWeighing: "Access is asserted to outweigh cost without comparison.",
missingComparisons: ["Cost per additional rider versus targeted subsidy."],
improvementSuggestions: ["Weigh against the strongest alternative."],
},
});
const idea = (index, side) => ({
id: `i${index}`,
title: `${MARK} idea ${index}: access as a precondition for participation`,
description: "Mobility gates work, education, and civic life.",
side,
strength: "strong",
tradeoffs: ["Requires a durable funding source."],
relatedTo: [],
});
const brainstorm = () => ({
needsMoreInfo: false,
topFive: [1, 2, 3, 4, 5].map((rank) => ({ rank, idea: idea(rank, rank % 2 ? "pro" : "con"), whyTop: "It reaches the widest impact." })),
nextSteps: ["Find a before/after ridership study."],
warningsAndTradeoffs: ["Do not overclaim the congestion effect."],
});
const factoids = () => ({
factoids: [],
searchSuggestions: [`${MARK} search suggestion`],
warnings: [],
verificationStatus: "complete",
});
const ask = () => ({
response: `${MARK} answer: weigh access against cost by naming who bears each.`,
followUpQuestions: ["What is the strongest alternative policy?"],
uncertainties: [],
});
// In a voice round the prompt allows audio tags. The fake uses one allowed tag
// and one invented tag, so the journey can prove neither reaches the page.
const sparring = (user, system) => /^SPARRING ACTION: score/m.test(user)
? {
action: "score",
scorecard: {
userScore: 78,
aiScore: 74,
categories: ["Logic", "Evidence", "Delivery", "Strategy"].map((name) => ({ name, userScore: 20, aiScore: 18, feedback: `${name} feedback.` })),
overallFeedback: "The user won a narrow edge by weighing the main mechanism.",
keyMoments: ["The rebuttal answered the strongest objection."],
improvementAreas: ["Compare the alternative policy explicitly."],
winner: "user",
verdict: `${MARK} verdict: the user had the edge.`,
},
}
: {
action: "respond",
speech: {
content: system.includes("SPOKEN ROUND:")
? `[sighs] ${MARK} opponent speech: the policy needs a stronger comparative case. [dramatic gasp] It is not there yet.`
: `${MARK} opponent speech: the policy needs a stronger comparative case.`,
strategy: "Test the warrant, then weigh the alternative.",
targetedPoints: ["The comparative mechanism is underdeveloped."],
},
};
const crossfire = (user) => {
if (user.startsWith("CROSSFIRE DEBRIEF")) {
return {
concessionsSecured: [],
controlledQuestions: [],
weakOrRepetitiveQuestions: [],
evasiveOrIncompleteAnswers: [],
missedFollowUps: [],
betterFollowUpQuestions: [],
strongestExchange: null,
};
}
const type = /must be type "(question|answer)"/.exec(user)?.[1];
if (!type) return null;
const tag = user.includes("SPOKEN EXCHANGE:") ? "[scoffs] " : "";
return { type, text: `${tag}${MARK} crossfire ${type}: what evidence links fewer cars to safer streets?` };
};
// Order matters only where one marker could appear inside another prompt.
const routes = [
["research-decision", "You decide whether a debate coaching answer needs live web research", () => ({ shouldSearch: false, query: "" })],
["research-queries", "generate 2-3 follow-up search queries", () => ({ queries: [] })],
["generate", "Generate a tournament-ready debate case", (user) => generate(user)],
["attack", "Analyze the opponent's case clearly", attack],
["brainstorm", "Brainstorm arguments like experienced competitive debaters", brainstorm],
["factoids", "You are a debate research specialist", factoids],
["sparring", "live debate sparring opponent", (user, system) => sparring(user, system)],
["crossfire", "You are DebateOwl's crossfire coach", (user) => crossfire(user)],
["ask", "You are a debate coach chat bot", ask],
];
// A cacheable system prompt arrives as text parts instead of a plain string.
function messageText(message) {
const content = message?.content;
if (Array.isArray(content)) return content.map((part) => part?.text ?? "").join("");
return String(content ?? "");
}
function classify(body) {
const messages = Array.isArray(body.messages) ? body.messages : [];
const systemMessage = messages.find((message) => message.role === "system");
const system = messageText(systemMessage);
const user = String(messages.at(-1)?.content ?? "");
const shape = {
messageCount: messages.length,
cacheBreakpoint: Array.isArray(systemMessage?.content) && systemMessage.content.some((part) => part?.cache_control),
};
if (body.stream === true && system.includes("Answer in clear Markdown that can stream smoothly")) {
return { feature: "ask-stream", system, user, ...shape };
}
const route = routes.find(([, marker]) => system.includes(marker));
if (!route) return { feature: null, system, user, ...shape };
return { feature: route[0], system, user, ...shape, output: route[2](user, system) };
}
function jevAnswers(questions) {
return Object.fromEntries(Object.entries(questions).map(([id, question]) => {
if (question.type === "noul") return [id, { type: "noul", noul: 0.8 }];
const labels = question.type === "choice" ? Object.keys(question.criteria) : question.criteria.map((_, index) => String(index));
const probabilities = Object.fromEntries(labels.map((label, index) => [label, index === 0 ? 0.9 : 0.1 / (labels.length - 1)]));
if (question.type === "choice") return [id, { type: "choice", choice: labels[0], confidence: 0.9, probabilities }];
const legend = Object.fromEntries(question.criteria.map((criterion, index) => [String(index), criterion]));
return [id, { type: "score", score: 0, confidence: 0.9, probabilities, legend }];
}));
}
function json(res, status, value) {
res.writeHead(status, { "content-type": "application/json" });
res.end(JSON.stringify(value));
}
function stream(res, finishReason = "stop") {
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
const chunks = [`${MARK} streamed answer: `, "weigh access ", "against cost."];
for (const content of chunks) {
res.write(`data: ${JSON.stringify({ choices: [{ delta: { content } }] })}\n\n`);
}
res.write(`data: ${JSON.stringify({ choices: [{ delta: {}, finish_reason: finishReason }] })}\n\n`);
res.write(`data: ${JSON.stringify({ choices: [], usage })}\n\n`);
res.end("data: [DONE]\n\n");
}
// Live generation: a structured answer streamed in small chunks, the way a
// model writes tokens, so the client's partial rendering runs for real.
async function streamJson(res, content, finishReason = "stop") {
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
for (let index = 0; index < content.length; index += STREAM_CHUNK_CHARS) {
res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: content.slice(index, index + STREAM_CHUNK_CHARS) } }] })}\n\n`);
await new Promise((resolve) => setTimeout(resolve, streamDelayMs ?? STREAM_CHUNK_DELAY_MS));
}
res.write(`data: ${JSON.stringify({ choices: [{ delta: {}, finish_reason: finishReason }] })}\n\n`);
res.write(`data: ${JSON.stringify({ choices: [], usage })}\n\n`);
res.end("data: [DONE]\n\n");
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
async function proxy(req, res, path) {
const body = JSON.parse(await readBody(req));
const chat = path === "/api/v1/chat/completions";
const record = chat
? { ...classify(body), requestedModel: body.model, model: chatModel(body.model), stream: body.stream === true, maxTokens: body.max_tokens }
: { feature: "jev", model: body.model };
delete record.output;
requests.push(record);
// Forced failures (fake.fail) apply in live mode too, before any spend.
const failure = chat && failures.find((item) => item.count > 0 && (!item.feature || item.feature === record.feature));
if (failure) {
failure.count -= 1;
record.status = failure.status;
return json(res, failure.status, { error: { message: `Forced ${failure.status} for ${record.feature}` } });
}
if (++upstreamCalls > maxUpstreamCalls) {
record.status = 429;
return json(res, 429, { error: { message: `Live gate stopped after ${maxUpstreamCalls} provider calls` } });
}
const started = Date.now();
// A caller that hangs up (a cancelled live run) stops the upstream call too.
// A streamed reply is complete at its finish reason; the caller may close
// right after it without reading the trailing bytes.
const hangUp = new AbortController();
let finished = false;
res.on("close", () => {
if (!res.writableEnded) hangUp.abort();
});
try {
const response = await fetch(`${upstream}${path.replace(/^\/api/, "")}`, {
method: "POST",
headers: { authorization: req.headers.authorization ?? "", "content-type": "application/json" },
body: JSON.stringify(chat ? liveChatBody(body) : body),
signal: hangUp.signal,
});
record.status = response.status;
res.writeHead(response.status, { "content-type": response.headers.get("content-type") ?? "application/json" });
// Keep the head of the reply: OpenRouter's own `model` and `provider`
// fields prove which model answered, independent of the rewrite above.
// A streamed reply names them in its first chunk, so read them as it arrives.
let head = "";
let tail = "";
for await (const chunk of response.body ?? []) {
tail = (tail + Buffer.from(chunk).toString("utf8")).slice(-4096);
if (/"finish_reason"\s*:\s*"|data: \[DONE\]/.test(tail)) finished = true;
if (head.length < 8192) {
head += Buffer.from(chunk).toString("utf8");
record.upstreamModel ??= /"model"\s*:\s*"([^"]+)"/.exec(head)?.[1];
record.upstreamProvider ??= /"provider"\s*:\s*"([^"]+)"/.exec(head)?.[1];
}
res.write(chunk);
}
res.end();
} catch (error) {
if (hangUp.signal.aborted) {
if (!finished) record.status = 499;
return;
}
record.status = 502;
const message = error instanceof Error ? error.message : "upstream connection failed";
console.error(`live provider request failed: ${message}`);
if (!res.headersSent) json(res, 502, { error: { message: "Live provider request failed" } });
else res.destroy();
} finally {
record.ms = Date.now() - started;
}
}
createServer(async (req, res) => {
const url = new URL(req.url ?? "/", "http://fake");
if (url.pathname === "/__requests" && req.method === "GET") return json(res, 200, requests);
if (url.pathname === "/__stats" && req.method === "GET") {
return json(res, 200, { mode: upstream ? "live" : "fake", liveModel: liveModel ?? null, upstreamCalls, maxUpstreamCalls });
}
if (url.pathname === "/__reset" && req.method === "POST") {
requests.length = 0;
failures = [];
truncations = [];
streamDelayMs = undefined;
return json(res, 200, { ok: true });
}
if (url.pathname === "/__stream-delay" && req.method === "POST") {
const { ms } = JSON.parse(await readBody(req) || "{}");
streamDelayMs = typeof ms === "number" && ms >= 0 ? ms : undefined;
return json(res, 200, { ok: true });
}
if (url.pathname === "/__subscription" && req.method === "POST") {
const subscription = JSON.parse(await readBody(req) || "{}");
subscriptions.set(subscription.id, subscription);
return json(res, 200, { ok: true });
}
if (url.pathname === "/__truncate" && req.method === "POST") {
const { feature, count = 1 } = JSON.parse(await readBody(req) || "{}");
truncations.push({ feature, count });
return json(res, 200, { ok: true });
}
if (url.pathname === "/__fail" && req.method === "POST") {
const { feature, count = 1, status = 500 } = JSON.parse(await readBody(req) || "{}");
failures.push({ feature, count, status });
return json(res, 200, { ok: true });
}
if (upstream && req.method === "POST" && (url.pathname === "/api/v1/chat/completions" || url.pathname === "/api/alpha/decisions")) {
return proxy(req, res, url.pathname);
}
if (url.pathname === "/api/alpha/decisions" && req.method === "POST") {
// Confident, schema-valid answers so Jev-dependent paths (Crossfire
// signals, source ranking) run end to end instead of falling back.
const body = JSON.parse(await readBody(req));
requests.push({ feature: "jev", model: body.model, status: 200 });
return json(res, 200, { model: body.model, answers: jevAnswers(body.questions ?? {}), usage: { input_tokens: 40, output_tokens: 8, cost: 0.00001 } });
}
const subscriptionPath = url.pathname.match(/^\/v1\/subscriptions\/([^/]+)$/);
if (subscriptionPath && req.method === "GET") {
// The webhook applies Stripe's current subscription state, not the event's.
const subscription = subscriptions.get(decodeURIComponent(subscriptionPath[1]));
if (!subscription) return json(res, 404, { error: { type: "invalid_request_error", message: "No such subscription" } });
requests.push({ feature: "stripe-subscription", user: subscription.id });
return json(res, 200, subscription);
}
if (url.pathname === "/v1/customers/search" && req.method === "GET") {
// Account deletion searches Stripe (through STRIPE_API_URL) for the user's
// customers. Test accounts have none.
requests.push({ feature: "stripe-customer-search", user: url.searchParams.get("query") ?? "" });
return json(res, 200, { object: "search_result", data: [], has_more: false, next_page: null, url: "/v1/customers/search" });
}
if (url.pathname !== "/api/v1/chat/completions" || req.method !== "POST") {
return json(res, 404, { error: { message: `Unexpected ${req.method} ${url.pathname}` } });
}
const body = JSON.parse(await readBody(req));
const { output, ...record } = classify(body);
const { feature } = record;
requests.push({ ...record, model: body.model, stream: body.stream === true, maxTokens: body.max_tokens });
if (!feature) return json(res, 500, { error: { message: "Fake OpenRouter does not recognize this prompt" } });
const failure = failures.find((item) => item.count > 0 && (!item.feature || item.feature === feature));
if (failure) {
failure.count -= 1;
return json(res, failure.status, { error: { message: `Fake ${feature} failure` } });
}
// A cut-off reply stops halfway with finish_reason "length", as a model does at max_tokens.
const truncation = truncations.find((item) => item.count > 0 && (!item.feature || item.feature === feature));
if (truncation) truncation.count -= 1;
const finishReason = truncation ? "length" : "stop";
if (feature === "ask-stream") return stream(res, finishReason);
if (output === null) return json(res, 500, { error: { message: `Fake ${feature} could not parse the request` } });
const content = truncation ? JSON.stringify(output).slice(0, Math.floor(JSON.stringify(output).length / 2)) : JSON.stringify(output);
if (body.stream === true) return streamJson(res, content, finishReason);
return json(res, 200, {
id: `fake-${requests.length}`,
model: body.model,
choices: [{ index: 0, finish_reason: finishReason, message: { role: "assistant", content } }],
usage,
});
}).listen(port, "0.0.0.0", () => console.log(`${upstream ? "live proxy" : "fake OpenRouter"} listening on ${port}`));