@bytesbrains/cruise-worker 0.12.0 → 0.13.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/emulator/worker.js +506 -0
- package/index.d.ts +1 -0
- package/index.mjs +2 -0
- package/package.json +3 -2
- package/worker/VERSION +1 -1
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// exchange.ts
|
|
5
|
+
var RESET = { kind: "reset" };
|
|
6
|
+
var ABANDONED = { kind: "abandoned" };
|
|
7
|
+
var respond = /* @__PURE__ */ __name((rendered, streamed = false) => ({ kind: "respond", rendered, streamed }), "respond");
|
|
8
|
+
var json = /* @__PURE__ */ __name((status, body) => respond({ status, headers: { "content-type": "application/json" }, body: JSON.stringify(body) }), "json");
|
|
9
|
+
var badJson = /* @__PURE__ */ __name(() => json(400, { error: { message: "invalid JSON body", type: "invalid_request_error" } }), "badJson");
|
|
10
|
+
function stall(ms, signal) {
|
|
11
|
+
if (signal.aborted) return Promise.resolve("abandoned");
|
|
12
|
+
return new Promise((settled) => {
|
|
13
|
+
const finish = /* @__PURE__ */ __name((how) => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
signal.removeEventListener("abort", onAbort);
|
|
16
|
+
settled(how);
|
|
17
|
+
}, "finish");
|
|
18
|
+
const onAbort = /* @__PURE__ */ __name(() => finish("abandoned"), "onAbort");
|
|
19
|
+
const timer = setTimeout(() => finish("elapsed"), ms);
|
|
20
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
__name(stall, "stall");
|
|
24
|
+
function framesOf(body) {
|
|
25
|
+
return body.split(/(?<=\n\n)/).filter((frame) => frame.length > 0);
|
|
26
|
+
}
|
|
27
|
+
__name(framesOf, "framesOf");
|
|
28
|
+
var FRAME_GAP_MS = 5;
|
|
29
|
+
var isStream = /* @__PURE__ */ __name((rendered) => rendered.headers["content-type"] === "text/event-stream", "isStream");
|
|
30
|
+
|
|
31
|
+
// scenarios.ts
|
|
32
|
+
var DEFAULT_USAGE = { prompt_tokens: 12, completion_tokens: 8, total_tokens: 20 };
|
|
33
|
+
var CATALOGUE = {
|
|
34
|
+
/** The uncontroversial case. */
|
|
35
|
+
ok: { mode: "json", content: "Four.", usage: DEFAULT_USAGE },
|
|
36
|
+
/** Streaming, with usage where OpenAI puts it: on the frame before `[DONE]`. */
|
|
37
|
+
"ok-stream": { mode: "sse", content: "Four, and here is why.", chunks: 5, usage: DEFAULT_USAGE },
|
|
38
|
+
/**
|
|
39
|
+
* A stream that ends without usage. Cruise's only spend signal on a
|
|
40
|
+
* logging-off gateway is that late chunk, so this is the case where it has to
|
|
41
|
+
* fall back to its reservation rather than record a free request.
|
|
42
|
+
*/
|
|
43
|
+
"stream-no-usage": { mode: "sse", content: "Answer.", chunks: 3, usageOnLastChunk: false, usage: null },
|
|
44
|
+
/** HTTP 200, no usage at all, non-streaming. */
|
|
45
|
+
"no-usage": { mode: "json", content: "Answer.", usage: null },
|
|
46
|
+
/** A refusal. Well formed, HTTP 200, and typically unbilled. */
|
|
47
|
+
refusal: {
|
|
48
|
+
mode: "json",
|
|
49
|
+
content: "",
|
|
50
|
+
refusal: "I can't help with that.",
|
|
51
|
+
finishReason: "stop",
|
|
52
|
+
usage: { prompt_tokens: 14, completion_tokens: 0, total_tokens: 14 }
|
|
53
|
+
},
|
|
54
|
+
/** Content-free with a stop reason — distinct from a refusal and from a tool call. */
|
|
55
|
+
empty: { mode: "json", content: "", finishReason: "stop", usage: DEFAULT_USAGE },
|
|
56
|
+
/**
|
|
57
|
+
* A tool call has no content either, and must not be counted as `empty`.
|
|
58
|
+
* Three of them, which is what DeepSeek and Mistral actually return where
|
|
59
|
+
* llama returns one (#10).
|
|
60
|
+
*/
|
|
61
|
+
"tool-calls": {
|
|
62
|
+
mode: "json",
|
|
63
|
+
content: "",
|
|
64
|
+
finishReason: "tool_calls",
|
|
65
|
+
toolCalls: [
|
|
66
|
+
{ id: "call_1", type: "function", function: { name: "get_weather", arguments: '{"city":"Paris"}' } },
|
|
67
|
+
{ id: "call_2", type: "function", function: { name: "get_weather", arguments: '{"city":"Berlin"}' } },
|
|
68
|
+
{ id: "call_3", type: "function", function: { name: "get_weather", arguments: '{"city":"Madrid"}' } }
|
|
69
|
+
],
|
|
70
|
+
usage: { prompt_tokens: 96, completion_tokens: 64, total_tokens: 160 }
|
|
71
|
+
},
|
|
72
|
+
/** Reasoning tokens are completion tokens that produced no content. */
|
|
73
|
+
reasoning: {
|
|
74
|
+
mode: "json",
|
|
75
|
+
content: "42.",
|
|
76
|
+
usage: {
|
|
77
|
+
prompt_tokens: 20,
|
|
78
|
+
completion_tokens: 300,
|
|
79
|
+
total_tokens: 320,
|
|
80
|
+
completion_tokens_details: { reasoning_tokens: 288 }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
/**
|
|
84
|
+
* Reasoning that consumed the whole output budget (#142): a 200, a bill,
|
|
85
|
+
* `finish_reason: length`, and not one content token. 31% of one day's
|
|
86
|
+
* production spend was this shape, and the caller was told nothing — now
|
|
87
|
+
* `x-cruise-warning: reasoning_exhausted`, which a demo caller can ask to
|
|
88
|
+
* see (#329).
|
|
89
|
+
*/
|
|
90
|
+
"reasoning-exhausted": {
|
|
91
|
+
mode: "json",
|
|
92
|
+
content: "",
|
|
93
|
+
finishReason: "length",
|
|
94
|
+
usage: {
|
|
95
|
+
prompt_tokens: 20,
|
|
96
|
+
completion_tokens: 256,
|
|
97
|
+
total_tokens: 276,
|
|
98
|
+
completion_tokens_details: { reasoning_tokens: 256 }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
/** Provider-side prompt caching, billed at its own rate where one is known. */
|
|
102
|
+
"cached-input": {
|
|
103
|
+
mode: "json",
|
|
104
|
+
content: "Cached.",
|
|
105
|
+
usage: {
|
|
106
|
+
prompt_tokens: 4e3,
|
|
107
|
+
completion_tokens: 10,
|
|
108
|
+
total_tokens: 4010,
|
|
109
|
+
prompt_tokens_details: { cached_tokens: 3900 }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
/**
|
|
113
|
+
* A prompt template the caller never sees. Measured on `devstral`: 1,230
|
|
114
|
+
* prompt tokens for a ten-token message. Metering the request rather than the
|
|
115
|
+
* response would under-count this by two orders of magnitude.
|
|
116
|
+
*/
|
|
117
|
+
"inflated-prompt": { mode: "json", content: "Hi.", usage: { prompt_tokens: 1230, completion_tokens: 6, total_tokens: 1236 } },
|
|
118
|
+
/** The three statuses a chain may fall over on. */
|
|
119
|
+
"rate-limited": { status: 429, headers: { "retry-after": "30" } },
|
|
120
|
+
timeout: { status: 408 },
|
|
121
|
+
"server-error": { status: 503 },
|
|
122
|
+
/** And the two it must not: a client error is not another provider's problem. */
|
|
123
|
+
"bad-request": { status: 400 },
|
|
124
|
+
/** Mistral's rejection of the OpenAI `user` field, which is why it is dropped. */
|
|
125
|
+
"unprocessable": { status: 422, body: JSON.stringify({ object: "error", message: "Extra inputs are not permitted: user" }) },
|
|
126
|
+
/** No reply at all: the socket dies before headers. */
|
|
127
|
+
"transport-reset": { mode: "reset" },
|
|
128
|
+
/** 200 with a body that will not parse. */
|
|
129
|
+
garbage: { mode: "garbage" },
|
|
130
|
+
/** Answers to a different name than it was called by — the alias drift #10 exists for. */
|
|
131
|
+
"aliased-model": { mode: "json", content: "Hi.", usage: DEFAULT_USAGE, reportedModel: "deepseek-v4-pro" },
|
|
132
|
+
/** Slow enough to observe, not slow enough to hang a suite. */
|
|
133
|
+
slow: { mode: "json", content: "Eventually.", usage: DEFAULT_USAGE, delayMs: 1500 }
|
|
134
|
+
};
|
|
135
|
+
function scenarioNamed(name) {
|
|
136
|
+
const found = Object.hasOwn(CATALOGUE, name) ? CATALOGUE[name] : void 0;
|
|
137
|
+
if (found === void 0) {
|
|
138
|
+
throw new Error(`No scenario '${name}'. Known: ${Object.keys(CATALOGUE).sort().join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
__name(scenarioNamed, "scenarioNamed");
|
|
143
|
+
function splitInto(text, parts) {
|
|
144
|
+
if (parts <= 1 || text.length === 0) return [text];
|
|
145
|
+
const size = Math.ceil(text.length / parts);
|
|
146
|
+
const out = [];
|
|
147
|
+
for (let at = 0; at < text.length; at += size) out.push(text.slice(at, at + size));
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
__name(splitInto, "splitInto");
|
|
151
|
+
function completion(scenario, model, sequence) {
|
|
152
|
+
const message = { role: "assistant", content: scenario.content ?? "" };
|
|
153
|
+
if (scenario.refusal !== void 0) message["refusal"] = scenario.refusal;
|
|
154
|
+
if (scenario.toolCalls !== void 0) message["tool_calls"] = scenario.toolCalls;
|
|
155
|
+
const body = {
|
|
156
|
+
id: `chatcmpl-emu-${sequence}`,
|
|
157
|
+
object: "chat.completion",
|
|
158
|
+
created: 176e7 + sequence,
|
|
159
|
+
model: scenario.reportedModel ?? model,
|
|
160
|
+
choices: [{ index: 0, message, finish_reason: scenario.finishReason ?? "stop", logprobs: null }]
|
|
161
|
+
};
|
|
162
|
+
if (scenario.usage !== null && scenario.usage !== void 0) body["usage"] = scenario.usage;
|
|
163
|
+
return body;
|
|
164
|
+
}
|
|
165
|
+
__name(completion, "completion");
|
|
166
|
+
function eventStream(scenario, model, sequence) {
|
|
167
|
+
const id = `chatcmpl-emu-${sequence}`;
|
|
168
|
+
const created = 176e7 + sequence;
|
|
169
|
+
const reported = scenario.reportedModel ?? model;
|
|
170
|
+
const frames = [];
|
|
171
|
+
const frame = /* @__PURE__ */ __name((choice, extra = {}) => {
|
|
172
|
+
frames.push(
|
|
173
|
+
`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: reported, choices: [choice], ...extra })}
|
|
174
|
+
|
|
175
|
+
`
|
|
176
|
+
);
|
|
177
|
+
}, "frame");
|
|
178
|
+
frame({ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null });
|
|
179
|
+
if (scenario.toolCalls !== void 0) {
|
|
180
|
+
scenario.toolCalls.forEach((call, index) => {
|
|
181
|
+
frame({ index: 0, delta: { tool_calls: [{ index, ...call }] }, finish_reason: null });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
for (const piece of splitInto(scenario.content ?? "", scenario.chunks ?? 3)) {
|
|
185
|
+
if (piece.length > 0) frame({ index: 0, delta: { content: piece }, finish_reason: null });
|
|
186
|
+
}
|
|
187
|
+
const usage = scenario.usage;
|
|
188
|
+
const withUsage = (scenario.usageOnLastChunk ?? true) && usage !== null && usage !== void 0;
|
|
189
|
+
frame(
|
|
190
|
+
{ index: 0, delta: {}, finish_reason: scenario.finishReason ?? "stop" },
|
|
191
|
+
withUsage ? { usage } : {}
|
|
192
|
+
);
|
|
193
|
+
frames.push("data: [DONE]\n\n");
|
|
194
|
+
return frames.join("");
|
|
195
|
+
}
|
|
196
|
+
__name(eventStream, "eventStream");
|
|
197
|
+
function render(scenario, model, sequence) {
|
|
198
|
+
const status = scenario.status ?? 200;
|
|
199
|
+
const headers = { ...scenario.headers };
|
|
200
|
+
if (scenario.body !== void 0) {
|
|
201
|
+
headers["content-type"] ??= "application/json";
|
|
202
|
+
return { status, headers, body: scenario.body };
|
|
203
|
+
}
|
|
204
|
+
if (status !== 200) {
|
|
205
|
+
headers["content-type"] = "application/json";
|
|
206
|
+
return {
|
|
207
|
+
status,
|
|
208
|
+
headers,
|
|
209
|
+
body: JSON.stringify({ error: { message: `emulated ${status}`, type: "emulated_error", code: null } })
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (scenario.mode === "garbage") {
|
|
213
|
+
headers["content-type"] = "application/json";
|
|
214
|
+
return { status, headers, body: "<html>upstream had a moment</html>" };
|
|
215
|
+
}
|
|
216
|
+
if (scenario.mode === "sse") {
|
|
217
|
+
headers["content-type"] = "text/event-stream";
|
|
218
|
+
headers["cache-control"] = "no-cache";
|
|
219
|
+
return { status, headers, body: eventStream(scenario, model, sequence) };
|
|
220
|
+
}
|
|
221
|
+
headers["content-type"] = "application/json";
|
|
222
|
+
return { status, headers, body: JSON.stringify(completion(scenario, model, sequence)) };
|
|
223
|
+
}
|
|
224
|
+
__name(render, "render");
|
|
225
|
+
|
|
226
|
+
// state.ts
|
|
227
|
+
var MAX_CACHE_ENTRIES = 500;
|
|
228
|
+
function createState() {
|
|
229
|
+
return {
|
|
230
|
+
config: { cacheTtl: 0, requireAuth: true, token: null },
|
|
231
|
+
assignments: /* @__PURE__ */ new Map(),
|
|
232
|
+
fallbackAssignment: "ok",
|
|
233
|
+
requests: [],
|
|
234
|
+
cache: /* @__PURE__ */ new Map(),
|
|
235
|
+
sequence: 0
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
__name(createState, "createState");
|
|
239
|
+
var state = createState();
|
|
240
|
+
function nextScenario(state2, model) {
|
|
241
|
+
const assignment = state2.assignments.get(model) ?? state2.fallbackAssignment;
|
|
242
|
+
let chosen;
|
|
243
|
+
if (Array.isArray(assignment)) {
|
|
244
|
+
if (assignment.length === 0) {
|
|
245
|
+
chosen = "ok";
|
|
246
|
+
} else {
|
|
247
|
+
chosen = assignment.length === 1 ? assignment[0] : assignment.shift();
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
chosen = assignment;
|
|
251
|
+
}
|
|
252
|
+
return typeof chosen === "string" ? { scenario: scenarioNamed(chosen), name: chosen } : { scenario: chosen, name: "inline" };
|
|
253
|
+
}
|
|
254
|
+
__name(nextScenario, "nextScenario");
|
|
255
|
+
|
|
256
|
+
// compat.ts
|
|
257
|
+
async function handleCompat(request, state2, account, gateway, direct = false) {
|
|
258
|
+
const { body: raw, headers } = request;
|
|
259
|
+
let body = {};
|
|
260
|
+
try {
|
|
261
|
+
body = JSON.parse(raw);
|
|
262
|
+
} catch {
|
|
263
|
+
return badJson();
|
|
264
|
+
}
|
|
265
|
+
const model = typeof body["model"] === "string" ? body["model"] : "";
|
|
266
|
+
const streamed = body["stream"] === true;
|
|
267
|
+
const record = {
|
|
268
|
+
at: Date.now(),
|
|
269
|
+
account,
|
|
270
|
+
gateway,
|
|
271
|
+
model,
|
|
272
|
+
headers,
|
|
273
|
+
body,
|
|
274
|
+
raw,
|
|
275
|
+
scenario: "",
|
|
276
|
+
cache: "OFF",
|
|
277
|
+
status: 0
|
|
278
|
+
};
|
|
279
|
+
state2.requests.push(record);
|
|
280
|
+
if (state2.config.requireAuth && !direct) {
|
|
281
|
+
const presented = /^Bearer\s+(.+)$/i.exec(headers["cf-aig-authorization"] ?? "")?.[1];
|
|
282
|
+
if (presented === void 0 || state2.config.token !== null && presented !== state2.config.token) {
|
|
283
|
+
record.status = 401;
|
|
284
|
+
return json(401, { error: { message: "missing or invalid cf-aig-authorization" } });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const ttl = Number(headers["cf-aig-cache-ttl"] ?? state2.config.cacheTtl);
|
|
288
|
+
const skipped = headers["cf-aig-skip-cache"] === "true";
|
|
289
|
+
const caching = ttl > 0 && !skipped && !direct;
|
|
290
|
+
if (skipped) record.cache = "SKIP";
|
|
291
|
+
const key = headers["cf-aig-cache-key"] ?? `${account}/${gateway}:${raw}`;
|
|
292
|
+
if (caching) {
|
|
293
|
+
const hit = state2.cache.get(key);
|
|
294
|
+
if (hit !== void 0 && hit.storedAt + hit.ttl * 1e3 > Date.now()) {
|
|
295
|
+
record.scenario = "cache-hit";
|
|
296
|
+
record.cache = "HIT";
|
|
297
|
+
record.status = hit.status;
|
|
298
|
+
const rendered2 = { status: hit.status, headers: { ...hit.headers, "cf-aig-cache-status": "HIT" }, body: hit.body };
|
|
299
|
+
return respond(rendered2, isStream(rendered2));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const { scenario, name } = nextScenario(state2, model);
|
|
303
|
+
record.scenario = name;
|
|
304
|
+
if (scenario.delayMs !== void 0 && scenario.delayMs > 0) {
|
|
305
|
+
if (await stall(scenario.delayMs, request.signal) === "abandoned") {
|
|
306
|
+
record.status = 0;
|
|
307
|
+
record.scenario = `${name}(abandoned)`;
|
|
308
|
+
return ABANDONED;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (scenario.mode === "reset") {
|
|
312
|
+
record.status = 0;
|
|
313
|
+
return RESET;
|
|
314
|
+
}
|
|
315
|
+
state2.sequence += 1;
|
|
316
|
+
const shape = scenario.mode === "garbage" ? scenario.mode : streamed ? "sse" : "json";
|
|
317
|
+
const rendered = render({ ...scenario, mode: shape }, model, state2.sequence);
|
|
318
|
+
record.status = rendered.status;
|
|
319
|
+
if (caching && rendered.status === 200) {
|
|
320
|
+
for (const [existing, entry] of state2.cache) {
|
|
321
|
+
if (entry.storedAt + entry.ttl * 1e3 <= Date.now()) state2.cache.delete(existing);
|
|
322
|
+
}
|
|
323
|
+
while (state2.cache.size >= MAX_CACHE_ENTRIES) {
|
|
324
|
+
const oldest = state2.cache.keys().next();
|
|
325
|
+
if (oldest.done === true) break;
|
|
326
|
+
state2.cache.delete(oldest.value);
|
|
327
|
+
}
|
|
328
|
+
state2.cache.set(key, { ...rendered, storedAt: Date.now(), ttl });
|
|
329
|
+
record.cache = "MISS";
|
|
330
|
+
}
|
|
331
|
+
const out = { ...rendered, headers: { ...rendered.headers, ...caching ? { "cf-aig-cache-status": "MISS" } : {} } };
|
|
332
|
+
return respond(out, isStream(out));
|
|
333
|
+
}
|
|
334
|
+
__name(handleCompat, "handleCompat");
|
|
335
|
+
|
|
336
|
+
// images.ts
|
|
337
|
+
var PIXEL = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
|
338
|
+
var OUTPUT_TOKENS = { "1024x1024": 100, "1536x1024": 400 };
|
|
339
|
+
function handleImages(request, state2, account, gateway, provider) {
|
|
340
|
+
const { body: raw, headers } = request;
|
|
341
|
+
let body = {};
|
|
342
|
+
try {
|
|
343
|
+
body = JSON.parse(raw);
|
|
344
|
+
} catch {
|
|
345
|
+
return badJson();
|
|
346
|
+
}
|
|
347
|
+
const model = typeof body["model"] === "string" ? body["model"] : "";
|
|
348
|
+
const record = {
|
|
349
|
+
at: Date.now(),
|
|
350
|
+
account,
|
|
351
|
+
// Recorded as the gateway plus the provider segment, because on this route
|
|
352
|
+
// the provider is part of the address and a test that could not see it
|
|
353
|
+
// could not tell `/compat` from `/openai`.
|
|
354
|
+
gateway: `${gateway}/${provider}`,
|
|
355
|
+
model,
|
|
356
|
+
headers,
|
|
357
|
+
body,
|
|
358
|
+
raw,
|
|
359
|
+
scenario: "image",
|
|
360
|
+
cache: "OFF",
|
|
361
|
+
status: 0
|
|
362
|
+
};
|
|
363
|
+
state2.requests.push(record);
|
|
364
|
+
if (state2.config.requireAuth) {
|
|
365
|
+
const presented = /^Bearer\s+(.+)$/i.exec(headers["cf-aig-authorization"] ?? "")?.[1];
|
|
366
|
+
if (presented === void 0 || state2.config.token !== null && presented !== state2.config.token) {
|
|
367
|
+
record.status = 401;
|
|
368
|
+
return json(401, { error: { message: "missing or invalid cf-aig-authorization" } });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const size = typeof body["size"] === "string" ? body["size"] : "1024x1024";
|
|
372
|
+
const n = body["n"] === void 0 ? 1 : body["n"];
|
|
373
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1 || n > 10) {
|
|
374
|
+
record.status = 400;
|
|
375
|
+
return json(400, {
|
|
376
|
+
error: { message: "n must be an integer between 1 and 10", type: "invalid_request_error", param: "n" }
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const perImage = OUTPUT_TOKENS[size] ?? 100;
|
|
380
|
+
const inputTokens = String(body["prompt"] ?? "").length;
|
|
381
|
+
record.status = 200;
|
|
382
|
+
return json(200, {
|
|
383
|
+
created: Math.floor(Date.now() / 1e3),
|
|
384
|
+
data: Array.from({ length: n }, () => ({ b64_json: PIXEL })),
|
|
385
|
+
usage: {
|
|
386
|
+
total_tokens: inputTokens + perImage * n,
|
|
387
|
+
input_tokens: inputTokens,
|
|
388
|
+
output_tokens: perImage * n,
|
|
389
|
+
input_tokens_details: { text_tokens: inputTokens, image_tokens: 0 }
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
__name(handleImages, "handleImages");
|
|
394
|
+
|
|
395
|
+
// routes.ts
|
|
396
|
+
var CONTROL_PREFIX = "/_emu/";
|
|
397
|
+
var COMPAT = /^\/v1\/([^/]+)\/([^/]+)\/compat\/chat\/completions$/;
|
|
398
|
+
var DIRECT = /^\/direct\/chat\/completions$/;
|
|
399
|
+
var IMAGES = /^\/v1\/([^/]+)\/([^/]+)\/([^/]+)\/images\/generations$/;
|
|
400
|
+
async function route(request, state2) {
|
|
401
|
+
const { pathname, method } = request;
|
|
402
|
+
const compat = COMPAT.exec(pathname);
|
|
403
|
+
if (compat !== null && method === "POST") {
|
|
404
|
+
return handleCompat(request, state2, compat[1], compat[2]);
|
|
405
|
+
}
|
|
406
|
+
if (DIRECT.test(pathname) && method === "POST") {
|
|
407
|
+
return handleCompat(request, state2, "direct", "direct", true);
|
|
408
|
+
}
|
|
409
|
+
const image = IMAGES.exec(pathname);
|
|
410
|
+
if (image !== null && method === "POST") {
|
|
411
|
+
return handleImages(request, state2, image[1], image[2], image[3]);
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
__name(route, "route");
|
|
416
|
+
var notFound = /* @__PURE__ */ __name((request) => json(404, { error: `no route for ${request.method} ${request.pathname}` }), "notFound");
|
|
417
|
+
|
|
418
|
+
// worker.ts
|
|
419
|
+
var SCENARIO_HEADER = "x-emu-scenario";
|
|
420
|
+
function headerRecord(request) {
|
|
421
|
+
const out = {};
|
|
422
|
+
for (const [name, value] of request.headers) out[name.toLowerCase()] = value;
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
__name(headerRecord, "headerRecord");
|
|
426
|
+
function streamOf(body) {
|
|
427
|
+
const encoder = new TextEncoder();
|
|
428
|
+
const frames = framesOf(body);
|
|
429
|
+
let next = 0;
|
|
430
|
+
return new ReadableStream({
|
|
431
|
+
async pull(controller) {
|
|
432
|
+
if (next >= frames.length) {
|
|
433
|
+
controller.close();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
controller.enqueue(encoder.encode(frames[next]));
|
|
437
|
+
next += 1;
|
|
438
|
+
if (next < frames.length) await new Promise((done) => setTimeout(done, FRAME_GAP_MS));
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
__name(streamOf, "streamOf");
|
|
443
|
+
function toResponse(outcome) {
|
|
444
|
+
if (outcome.kind !== "respond") return new Response(null, { status: 502 });
|
|
445
|
+
const { rendered, streamed } = outcome;
|
|
446
|
+
const headers = new Headers(rendered.headers);
|
|
447
|
+
if (streamed) return new Response(streamOf(rendered.body), { status: rendered.status, headers });
|
|
448
|
+
return new Response(rendered.body, { status: rendered.status, headers });
|
|
449
|
+
}
|
|
450
|
+
__name(toResponse, "toResponse");
|
|
451
|
+
var worker_default = {
|
|
452
|
+
async fetch(request, env) {
|
|
453
|
+
const url = new URL(request.url);
|
|
454
|
+
if (url.pathname === `${CONTROL_PREFIX}health`) {
|
|
455
|
+
return Response.json({ ok: true, worker: true });
|
|
456
|
+
}
|
|
457
|
+
if (url.pathname.startsWith(CONTROL_PREFIX)) {
|
|
458
|
+
return Response.json(
|
|
459
|
+
{ error: "the demo emulator has no control API; name a scenario in x-emu-scenario instead" },
|
|
460
|
+
{ status: 404 }
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const emu = {
|
|
464
|
+
method: request.method,
|
|
465
|
+
pathname: url.pathname,
|
|
466
|
+
headers: headerRecord(request),
|
|
467
|
+
// Read whatever the method, so the two adapters build the same
|
|
468
|
+
// `EmuRequest` from the same request — the node one has always read
|
|
469
|
+
// unconditionally. Nothing but POST reaches a handler, so this changes no
|
|
470
|
+
// behaviour; what it removes is an asymmetry between the adapters, which
|
|
471
|
+
// is the one thing this file exists to avoid. Raised by review of PR #332.
|
|
472
|
+
body: await request.text(),
|
|
473
|
+
signal: request.signal
|
|
474
|
+
};
|
|
475
|
+
const state2 = createState();
|
|
476
|
+
state2.config.token = env.EMULATOR_TOKEN ?? null;
|
|
477
|
+
state2.config.requireAuth = env.EMULATOR_TOKEN !== void 0;
|
|
478
|
+
const asked = emu.headers[SCENARIO_HEADER];
|
|
479
|
+
if (asked !== void 0 && asked !== "") {
|
|
480
|
+
if (!Object.hasOwn(CATALOGUE, asked)) {
|
|
481
|
+
return Response.json(
|
|
482
|
+
{
|
|
483
|
+
error: {
|
|
484
|
+
message: `No scenario '${asked}'. Known: ${Object.keys(CATALOGUE).sort().join(", ")}.`,
|
|
485
|
+
type: "invalid_request_error",
|
|
486
|
+
param: SCENARIO_HEADER,
|
|
487
|
+
code: "unknown_scenario"
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{ status: 400 }
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
state2.fallbackAssignment = asked;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
return toResponse(await route(emu, state2) ?? notFound(emu));
|
|
497
|
+
} catch (cause) {
|
|
498
|
+
console.error("emulator error", { pathname: url.pathname, cause: String(cause) });
|
|
499
|
+
return Response.json({ error: { message: "emulator failure", type: "api_error" } }, { status: 500 });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
export {
|
|
504
|
+
worker_default as default
|
|
505
|
+
};
|
|
506
|
+
//# sourceMappingURL=worker.js.map
|
package/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare const packageDir: string;
|
|
|
2
2
|
export declare const workerDir: string;
|
|
3
3
|
export declare const mainModule: string;
|
|
4
4
|
export declare const migrationsDir: string;
|
|
5
|
+
export declare const emulatorModule: string;
|
|
5
6
|
export declare const compatibilityDate: string;
|
|
6
7
|
export declare const compatibilityFlags: readonly string[];
|
|
7
8
|
export declare const bindings: {
|
package/index.mjs
CHANGED
|
@@ -19,6 +19,7 @@ var root = existsSync(path.join(here, "package.json")) ? here : path.join(here,
|
|
|
19
19
|
var packageDir = root;
|
|
20
20
|
var workerDir = path.join(root, "worker");
|
|
21
21
|
var mainModule = path.join(root, "worker", "index.js");
|
|
22
|
+
var emulatorModule = path.join(root, "emulator", "worker.js");
|
|
22
23
|
var migrationsDir = path.join(root, "migrations");
|
|
23
24
|
var compatibilityDate = COMPATIBILITY_DATE;
|
|
24
25
|
var compatibilityFlags = COMPATIBILITY_FLAGS;
|
|
@@ -34,6 +35,7 @@ export {
|
|
|
34
35
|
bindings,
|
|
35
36
|
compatibilityDate,
|
|
36
37
|
compatibilityFlags,
|
|
38
|
+
emulatorModule,
|
|
37
39
|
mainModule,
|
|
38
40
|
migrationsDir,
|
|
39
41
|
packageDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytesbrains/cruise-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The Cruise Worker, built and ready to deploy: one OpenAI-compatible endpoint in front of every model provider, with budgets that stop.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"index.mjs",
|
|
16
16
|
"index.d.ts",
|
|
17
17
|
"worker",
|
|
18
|
+
"emulator",
|
|
18
19
|
"migrations",
|
|
19
20
|
"wrangler.template.jsonc",
|
|
20
21
|
"README.md",
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
},
|
|
26
27
|
"repository": {
|
|
27
28
|
"type": "git",
|
|
28
|
-
"url": "https://gitlab.com/bytesbrains-oss/cruise.git",
|
|
29
|
+
"url": "git+https://gitlab.com/bytesbrains-oss/cruise.git",
|
|
29
30
|
"directory": "packages/worker"
|
|
30
31
|
},
|
|
31
32
|
"homepage": "https://gitlab.com/bytesbrains-oss/cruise",
|
package/worker/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.13.0+659ef1b0
|