opencode2api-proxy 0.0.0-stage → 1.0.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.
@@ -0,0 +1,2621 @@
1
+ // src/config/defaults.ts
2
+ var SOTA_MODELS = [
3
+ "deepseek-v4.1-flash",
4
+ "glm-5.3",
5
+ "grok-4.6",
6
+ "kimi-k3",
7
+ "qwen3.8-max"
8
+ ];
9
+ var V1_BASE = "https://opencode.ai/zen";
10
+ var DEFAULT_CONFIG = {
11
+ listen: "127.0.0.1:8080",
12
+ server_keys: [],
13
+ server_key_metadata: {},
14
+ zen_keys: [],
15
+ go_keys: [],
16
+ anonymous: false,
17
+ proxies: [],
18
+ proxyfile: "",
19
+ upstream: {
20
+ zen: V1_BASE,
21
+ go: "https://opencode.ai/zen/go",
22
+ anonymous: "https://opencode.ai/inference"
23
+ },
24
+ retry: {
25
+ max_attempts: 3,
26
+ timeout_seconds: 300
27
+ },
28
+ models: {
29
+ refresh_seconds: 300,
30
+ protocols: {},
31
+ free_only: false
32
+ },
33
+ performance: {
34
+ max_idle_conns: 2048,
35
+ max_idle_conns_per_host: 256,
36
+ max_conns_per_host: 0,
37
+ idle_conn_timeout_seconds: 120,
38
+ connect_timeout_seconds: 5,
39
+ failure_cooldown_seconds: 15,
40
+ attempt_timeout_seconds: 0,
41
+ first_event_timeout_seconds: 0
42
+ },
43
+ logging: {
44
+ level: "info",
45
+ ring_size: 2e3,
46
+ dump_request_bodies: false
47
+ },
48
+ webui: {
49
+ enabled: true,
50
+ listen: "0.0.0.0:8081",
51
+ username: "admin",
52
+ session_ttl_minutes: 720
53
+ },
54
+ prefer: "go",
55
+ reasoning: {
56
+ effort: "",
57
+ effort_by_model: {}
58
+ },
59
+ rotation: {
60
+ sota: { alias: "sota", order: [...SOTA_MODELS] },
61
+ sweet: { alias: "sweet", order: [] },
62
+ first_output_seconds: 30
63
+ }
64
+ };
65
+
66
+ // src/protocol/paths.ts
67
+ function protocolPath(protocol) {
68
+ switch (protocol) {
69
+ case "responses":
70
+ return "/v1/responses";
71
+ case "anthropic":
72
+ return "/v1/messages";
73
+ case "systemone":
74
+ return "/v1/systemone";
75
+ default:
76
+ return "/v1/chat/completions";
77
+ }
78
+ }
79
+
80
+ // src/protocol/lanes.ts
81
+ var V2_USER_AGENT = "opencode/latest/2.0.32/cli";
82
+ var V1_RESPONSES_SDK = "4.0.40";
83
+ var V1_ANTHROPIC_SDK = "4.0.46";
84
+ var V1_CHAT_SDK = "4.0.23";
85
+ function isV1Upstream(baseURL) {
86
+ const normalized = baseURL.trim().toLowerCase().replace(/\/+$/, "");
87
+ return normalized === V1_BASE || normalized.endsWith("/zen");
88
+ }
89
+ function v1ProviderUtils(protocol) {
90
+ switch (protocol) {
91
+ case "responses":
92
+ return V1_RESPONSES_SDK;
93
+ case "anthropic":
94
+ return V1_ANTHROPIC_SDK;
95
+ default:
96
+ return V1_CHAT_SDK;
97
+ }
98
+ }
99
+ function v1UserAgent(providerUtils) {
100
+ return `opencode/1.19.3 ai-sdk/provider-utils/${providerUtils} runtime/bun/1.3.14`;
101
+ }
102
+ function upstreamURL(baseURL, protocol) {
103
+ return baseURL.replace(/\/+$/, "") + protocolPath(protocol);
104
+ }
105
+
106
+ // src/protocol/sha256.ts
107
+ var K = new Uint32Array([
108
+ 1116352408,
109
+ 1899447441,
110
+ 3049323471,
111
+ 3921009573,
112
+ 961987163,
113
+ 1508970993,
114
+ 2453635748,
115
+ 2870763221,
116
+ 3624381080,
117
+ 310598401,
118
+ 607225278,
119
+ 1426881987,
120
+ 1925078388,
121
+ 2162078206,
122
+ 2614888103,
123
+ 3248222580,
124
+ 3835390401,
125
+ 4022224774,
126
+ 264347078,
127
+ 604807628,
128
+ 770255983,
129
+ 1249150122,
130
+ 1555081692,
131
+ 1996064986,
132
+ 2554220882,
133
+ 2821834349,
134
+ 2952996808,
135
+ 3210313671,
136
+ 3336571891,
137
+ 3584528711,
138
+ 113926993,
139
+ 338241895,
140
+ 666307205,
141
+ 773529912,
142
+ 1294757372,
143
+ 1396182291,
144
+ 1695183700,
145
+ 1986661051,
146
+ 2177026350,
147
+ 2456956037,
148
+ 2730485921,
149
+ 2820302411,
150
+ 3259730800,
151
+ 3345764771,
152
+ 3516065817,
153
+ 3600352804,
154
+ 4094571909,
155
+ 275423344,
156
+ 430227734,
157
+ 506948616,
158
+ 659060556,
159
+ 883997877,
160
+ 958139571,
161
+ 1322822218,
162
+ 1537002063,
163
+ 1747873779,
164
+ 1955562222,
165
+ 2024104815,
166
+ 2227730452,
167
+ 2361852424,
168
+ 2428436474,
169
+ 2756734187,
170
+ 3204031479,
171
+ 3329325298
172
+ ]);
173
+ function sha256(input) {
174
+ const h = new Uint32Array([
175
+ 1779033703,
176
+ 3144134277,
177
+ 1013904242,
178
+ 2773480762,
179
+ 1359893119,
180
+ 2600822924,
181
+ 528734635,
182
+ 1541459225
183
+ ]);
184
+ const bitLength = input.length * 8;
185
+ const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
186
+ const block = new Uint8Array(paddedLength);
187
+ block.set(input);
188
+ block[input.length] = 128;
189
+ const view = new DataView(block.buffer);
190
+ view.setUint32(paddedLength - 4, bitLength >>> 0, false);
191
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296), false);
192
+ const w = new Uint32Array(64);
193
+ for (let offset = 0; offset < paddedLength; offset += 64) {
194
+ for (let i = 0; i < 16; i += 1) {
195
+ w[i] = view.getUint32(offset + i * 4, false);
196
+ }
197
+ for (let i = 16; i < 64; i += 1) {
198
+ const s0 = rotr(w[i - 15] ?? 0, 7) ^ (rotr(w[i - 15] ?? 0, 18) ^ (w[i - 15] ?? 0) >>> 3);
199
+ const s1 = rotr(w[i - 2] ?? 0, 17) ^ (rotr(w[i - 2] ?? 0, 19) ^ (w[i - 2] ?? 0) >>> 10);
200
+ w[i] = (w[i - 16] ?? 0) + s0 + (w[i - 7] ?? 0) + s1 >>> 0;
201
+ }
202
+ let a = h[0] ?? 0;
203
+ let b = h[1] ?? 0;
204
+ let c = h[2] ?? 0;
205
+ let d = h[3] ?? 0;
206
+ let e = h[4] ?? 0;
207
+ let f = h[5] ?? 0;
208
+ let g = h[6] ?? 0;
209
+ let hh = h[7] ?? 0;
210
+ for (let i = 0; i < 64; i += 1) {
211
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
212
+ const ch = e & f ^ ~e & g;
213
+ const temp1 = hh + S1 + ch + (K[i] ?? 0) + (w[i] ?? 0) >>> 0;
214
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
215
+ const maj = a & b ^ a & c ^ b & c;
216
+ const temp2 = S0 + maj >>> 0;
217
+ hh = g;
218
+ g = f;
219
+ f = e;
220
+ e = d + temp1 >>> 0;
221
+ d = c;
222
+ c = b;
223
+ b = a;
224
+ a = temp1 + temp2 >>> 0;
225
+ }
226
+ h[0] = (h[0] ?? 0) + a >>> 0;
227
+ h[1] = (h[1] ?? 0) + b >>> 0;
228
+ h[2] = (h[2] ?? 0) + c >>> 0;
229
+ h[3] = (h[3] ?? 0) + d >>> 0;
230
+ h[4] = (h[4] ?? 0) + e >>> 0;
231
+ h[5] = (h[5] ?? 0) + f >>> 0;
232
+ h[6] = (h[6] ?? 0) + g >>> 0;
233
+ h[7] = (h[7] ?? 0) + hh >>> 0;
234
+ }
235
+ const digest = new Uint8Array(32);
236
+ const digestView = new DataView(digest.buffer);
237
+ for (let i = 0; i < 8; i += 1) {
238
+ digestView.setUint32(i * 4, h[i] ?? 0, false);
239
+ }
240
+ return digest;
241
+ }
242
+ function rotr(value, bits) {
243
+ return (value >>> bits | value << 32 - bits) >>> 0;
244
+ }
245
+ function sha256Bytes(input) {
246
+ return sha256(utf8(input));
247
+ }
248
+ function toHex(bytes) {
249
+ let out = "";
250
+ for (const byte of bytes) {
251
+ out += byte.toString(16).padStart(2, "0");
252
+ }
253
+ return out;
254
+ }
255
+ function utf8(input) {
256
+ return new TextEncoder().encode(input);
257
+ }
258
+ function randomBytes(size) {
259
+ const bytes = new Uint8Array(size);
260
+ crypto.getRandomValues(bytes);
261
+ return bytes;
262
+ }
263
+
264
+ // src/protocol/identity.ts
265
+ var BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
266
+ var CANONICAL_SESSION_PATTERN = /^ses_[0-9a-f]{12}[0-9A-Za-z]{14}$/;
267
+ var DESCENDING_TIME_PART_PATTERN = /^[0-9a-f]{9}f[c-f][0-9a-f]$/;
268
+ var CLAUDE_CODE_SESSION_PATTERN = /_session_([0-9A-Fa-f-]{36})$/;
269
+ function isOfficialSessionID(signal) {
270
+ return CANONICAL_SESSION_PATTERN.test(signal) && DESCENDING_TIME_PART_PATTERN.test(signal.slice(4, 16));
271
+ }
272
+ function canonicalSessionID(signal) {
273
+ if (isOfficialSessionID(signal)) {
274
+ return signal;
275
+ }
276
+ const sum = sha256Bytes(`ses\0${signal}`);
277
+ const timePart = descendingTimePart(sum.subarray(0, 6));
278
+ const randomPart = base62Fixed(sum.subarray(6, 16), 14);
279
+ return `ses_${timePart}${randomPart}`;
280
+ }
281
+ function descendingTimePart(hash) {
282
+ const current = (BigInt(hash[0] ?? 0) << 28n | BigInt(hash[1] ?? 0) << 20n | BigInt(hash[2] ?? 0) << 12n | BigInt(hash[3] ?? 0) << 4n | BigInt(hash[4] ?? 0) >> 4n) << 12n;
283
+ const withCounter = current | 1n;
284
+ const inverted = ~withCounter & 0xffffffffffffn;
285
+ return inverted.toString(16).padStart(12, "0");
286
+ }
287
+ function base62Fixed(bytes, width) {
288
+ let value = 0n;
289
+ for (const byte of bytes) {
290
+ value = value << 8n | BigInt(byte);
291
+ }
292
+ const base = 62n;
293
+ const out = new Array(width);
294
+ for (let i = width - 1; i >= 0; i--) {
295
+ const remainder = value % base;
296
+ value /= base;
297
+ out[i] = BASE62[Number(remainder)] ?? "0";
298
+ }
299
+ return out.join("");
300
+ }
301
+ function randomBase62(length) {
302
+ const bytes = randomBytes(length);
303
+ let out = "";
304
+ for (const byte of bytes) {
305
+ out += BASE62[byte % 62];
306
+ }
307
+ return out;
308
+ }
309
+ var ascendingLast = 0n;
310
+ var ascendingCounter = 0n;
311
+ function ascendingID(prefix, now = Date.now()) {
312
+ const ms = BigInt(now);
313
+ if (ms !== ascendingLast) {
314
+ ascendingLast = ms;
315
+ ascendingCounter = 0n;
316
+ }
317
+ ascendingCounter += 1n;
318
+ const value = BigInt.asUintN(48, ms << 12n | ascendingCounter);
319
+ const timePart = value.toString(16).padStart(12, "0");
320
+ return `${prefix}_${timePart}${randomBase62(14)}`;
321
+ }
322
+ function randomID(prefix, size) {
323
+ return `${prefix}_${toHex(randomBytes(size))}`;
324
+ }
325
+ function claudeCodeSessionID(userId) {
326
+ if (!userId) {
327
+ return "";
328
+ }
329
+ const match = CLAUDE_CODE_SESSION_PATTERN.exec(userId);
330
+ if (match?.[1]) {
331
+ return match[1];
332
+ }
333
+ try {
334
+ const value = JSON.parse(userId);
335
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.session_id === "string") {
336
+ return value.session_id;
337
+ }
338
+ } catch {
339
+ }
340
+ return "";
341
+ }
342
+ function scopedSessionID(scope, signal) {
343
+ if (isOfficialSessionID(signal)) {
344
+ return signal;
345
+ }
346
+ const seeded = scope ? `${scope}\0${signal}` : signal;
347
+ return canonicalSessionID(seeded);
348
+ }
349
+
350
+ // src/protocol/json-util.ts
351
+ function isPlainObject(value) {
352
+ return typeof value === "object" && value !== null && !Array.isArray(value);
353
+ }
354
+ function stringAt(value, ...path) {
355
+ let current = value;
356
+ for (const key of path) {
357
+ if (!isPlainObject(current)) {
358
+ return "";
359
+ }
360
+ current = current[key];
361
+ }
362
+ return typeof current === "string" ? current : "";
363
+ }
364
+ function mapAt(value, ...path) {
365
+ let current = value;
366
+ for (const key of path) {
367
+ if (!isPlainObject(current)) {
368
+ return {};
369
+ }
370
+ current = current[key];
371
+ }
372
+ return isPlainObject(current) ? current : {};
373
+ }
374
+ function sliceAt(value, ...path) {
375
+ let current = value;
376
+ for (const key of path) {
377
+ if (!isPlainObject(current)) {
378
+ return [];
379
+ }
380
+ current = current[key];
381
+ }
382
+ return Array.isArray(current) ? current : [];
383
+ }
384
+ function firstString(...values) {
385
+ for (const value of values) {
386
+ if (value) {
387
+ return value.trim();
388
+ }
389
+ }
390
+ return "";
391
+ }
392
+
393
+ // src/protocol/anthropic-betas.ts
394
+ var TOOL_BETAS = {
395
+ code_execution_20250522: "code-execution-2025-05-22",
396
+ code_execution_20250825: "code-execution-2025-08-25",
397
+ computer_20241022: "computer-use-2024-10-22",
398
+ computer_20250124: "computer-use-2025-01-24",
399
+ computer_20251124: "computer-use-2025-11-24",
400
+ text_editor_20241022: "computer-use-2024-10-22",
401
+ text_editor_20250124: "computer-use-2025-01-24",
402
+ text_editor_20250429: "computer-use-2025-01-24",
403
+ bash_20241022: "computer-use-2024-10-22",
404
+ bash_20250124: "computer-use-2025-01-24",
405
+ memory_20250818: "context-management-2025-06-27",
406
+ web_fetch_20250910: "web-fetch-2025-09-10",
407
+ web_fetch_20260209: "code-execution-web-tools-2026-02-09",
408
+ web_search_20260209: "code-execution-web-tools-2026-02-09",
409
+ advisor_20260301: "advisor-tool-2026-03-01"
410
+ };
411
+ var CLAUDE_3_OR_OLDER = /claude-(?:instant(?:-|$)|v?2(?:$|[-.:])|3(?:$|[-.]))/;
412
+ var STRUCTURED_OUTPUT_MODELS = [
413
+ "claude-opus-5",
414
+ "claude-opus-4-8",
415
+ "claude-opus-4-7",
416
+ "claude-fable-5",
417
+ "claude-sonnet-5",
418
+ "claude-sonnet-4-6",
419
+ "claude-opus-4-6",
420
+ "claude-sonnet-4-5",
421
+ "claude-opus-4-5",
422
+ "claude-haiku-4-5",
423
+ "claude-sonnet-4-1"
424
+ ];
425
+ function v1AnthropicBetas(body) {
426
+ let payload;
427
+ try {
428
+ payload = JSON.parse(body);
429
+ } catch {
430
+ return "";
431
+ }
432
+ if (!isPlainObject(payload)) {
433
+ return "";
434
+ }
435
+ const betas = [];
436
+ const add = (beta) => {
437
+ if (!betas.includes(beta)) {
438
+ betas.push(beta);
439
+ }
440
+ };
441
+ if (sliceAt(payload, "mcp_servers").length > 0) {
442
+ add("mcp-client-2025-04-04");
443
+ }
444
+ const management = mapAt(payload, "context_management");
445
+ if (Object.keys(management).length > 0) {
446
+ add("context-management-2025-06-27");
447
+ for (const raw of sliceAt(management, "edits")) {
448
+ if (stringAt(raw, "type") === "compact_20260112") {
449
+ add("compact-2026-01-12");
450
+ }
451
+ }
452
+ }
453
+ const container = payload["container"];
454
+ if (isPlainObject(container) && sliceAt(container, "skills").length > 0) {
455
+ add("code-execution-2025-08-25");
456
+ add("skills-2025-10-02");
457
+ add("files-api-2025-04-14");
458
+ }
459
+ if (isPresentJSON(mapAt(payload, "output_config")["task_budget"])) {
460
+ add("task-budgets-2026-03-13");
461
+ }
462
+ if (payload["speed"] === "fast") {
463
+ add("fast-mode-2026-02-01");
464
+ }
465
+ const fallbacks = payload["fallbacks"];
466
+ if (fallbacks === "default") {
467
+ add("server-side-fallback-2026-07-01");
468
+ } else if (Array.isArray(fallbacks) && fallbacks.length > 0) {
469
+ add("server-side-fallback-2026-06-01");
470
+ }
471
+ if (isPresentJSON(mapAt(payload, "thinking")["block_binding"])) {
472
+ add("thinking-binding-controls-2026-08-01");
473
+ }
474
+ const model = stringAt(payload, "model");
475
+ for (const rawTool of sliceAt(payload, "tools")) {
476
+ const tool = isPlainObject(rawTool) ? rawTool : {};
477
+ const type = stringAt(tool, "type");
478
+ if (type === "" || type === "custom") {
479
+ if (anthropicSupportsStructuredOutput(model)) {
480
+ add("structured-outputs-2025-11-13");
481
+ }
482
+ if (isPresentJSON(tool["input_examples"]) || isPresentJSON(tool["allowed_callers"])) {
483
+ add("advanced-tool-use-2025-11-20");
484
+ }
485
+ continue;
486
+ }
487
+ const beta = TOOL_BETAS[type];
488
+ if (beta !== void 0) {
489
+ add(beta);
490
+ }
491
+ }
492
+ return betas.join(",");
493
+ }
494
+ function isPresentJSON(value) {
495
+ return value !== void 0 && value !== null;
496
+ }
497
+ function anthropicSupportsStructuredOutput(model) {
498
+ for (const supported of STRUCTURED_OUTPUT_MODELS) {
499
+ if (model.includes(supported)) {
500
+ return true;
501
+ }
502
+ }
503
+ if (model.includes("claude-sonnet-4-") || model.includes("claude-opus-4-") || model.includes("claude-3-haiku") || CLAUDE_3_OR_OLDER.test(model)) {
504
+ return false;
505
+ }
506
+ return model.includes("claude-");
507
+ }
508
+
509
+ // src/protocol/upstream-headers.ts
510
+ var V2_ANTHROPIC_BETAS = "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14";
511
+ var GIT_COMMIT = /^[0-9a-f]{40}$/;
512
+ function v1ProjectID(project) {
513
+ if (project === "global" || GIT_COMMIT.test(project)) {
514
+ return project;
515
+ }
516
+ return v2ProjectID(project);
517
+ }
518
+ function v2ProjectID(project) {
519
+ const sum = sha256Bytes(`opencode-project\0${project}`);
520
+ return toHex(sum.subarray(0, 20));
521
+ }
522
+ function setV1Headers(headers, protocol, body, ids) {
523
+ headers.set("Accept", "*/*");
524
+ headers.set("User-Agent", v1UserAgent(v1ProviderUtils(protocol)));
525
+ headers.set("x-opencode-client", "cli");
526
+ headers.set("x-opencode-session", ids.session);
527
+ headers.set("x-opencode-session-id", ids.session);
528
+ headers.set("x-opencode-request", ids.message);
529
+ headers.set("x-opencode-project", v1ProjectID(ids.project));
530
+ if (ids.parentSession !== "") {
531
+ const parent = canonicalSessionID(ids.parentSession);
532
+ headers.set("x-opencode-parent-session-id", parent);
533
+ headers.set("x-parent-session-id", parent);
534
+ }
535
+ if (protocol === "anthropic") {
536
+ const betas = v1AnthropicBetas(body);
537
+ if (betas !== "") {
538
+ headers.set("anthropic-beta", betas);
539
+ }
540
+ }
541
+ }
542
+ function setV2Headers(headers, protocol, ids) {
543
+ headers.set("Accept", "application/json, text/event-stream");
544
+ headers.set("x-opencode-client", "cli");
545
+ headers.set("x-opencode-session", ids.session);
546
+ headers.set("x-session-affinity", ids.session);
547
+ headers.set("X-Session-Id", ids.session);
548
+ headers.set("User-Agent", V2_USER_AGENT);
549
+ headers.set("x-opencode-session-id", ids.session);
550
+ headers.set("x-opencode-project", v2ProjectID(ids.project));
551
+ if (ids.parentSession !== "") {
552
+ headers.set("x-parent-session-id", ids.parentSession);
553
+ }
554
+ if (protocol === "anthropic") {
555
+ headers.set("anthropic-beta", V2_ANTHROPIC_BETAS);
556
+ }
557
+ }
558
+
559
+ // src/protocol/upstream-request.ts
560
+ function buildUpstreamRequest(baseURL, protocol, body, ids, key) {
561
+ const headers = new Headers();
562
+ headers.set("Content-Type", "application/json");
563
+ if (isV1Upstream(baseURL)) {
564
+ setV1Headers(headers, protocol, body, ids);
565
+ } else {
566
+ setV2Headers(headers, protocol, ids);
567
+ }
568
+ if (protocol === "anthropic") {
569
+ headers.set("x-api-key", key);
570
+ headers.set("anthropic-version", "2023-06-01");
571
+ } else {
572
+ headers.set("Authorization", `Bearer ${key}`);
573
+ }
574
+ return { url: upstreamURL(baseURL, protocol), headers, body };
575
+ }
576
+
577
+ // src/protocol/request-identity.ts
578
+ function headerValue(headers, name) {
579
+ const value = headers[name];
580
+ if (Array.isArray(value)) {
581
+ return value[0] ?? "";
582
+ }
583
+ return typeof value === "string" ? value : "";
584
+ }
585
+ function conversationSeed(body) {
586
+ const prelude = [];
587
+ for (const field of ["system", "instructions"]) {
588
+ if (body[field] !== void 0 && body[field] !== null) {
589
+ prelude.push(body[field]);
590
+ }
591
+ }
592
+ const first = firstUserTurn(body, prelude);
593
+ if (first === void 0) {
594
+ return "";
595
+ }
596
+ try {
597
+ return JSON.stringify([...prelude, first]);
598
+ } catch {
599
+ return "";
600
+ }
601
+ }
602
+ function firstUserTurn(body, prelude) {
603
+ const input = body["input"];
604
+ if (typeof input === "string" && input !== "") {
605
+ return input;
606
+ }
607
+ for (const field of ["messages", "input"]) {
608
+ for (const raw of sliceAt(body, field)) {
609
+ if (!isPlainObject(raw)) {
610
+ continue;
611
+ }
612
+ const role = stringAt(raw, "role");
613
+ if (role === "system" || role === "developer") {
614
+ prelude.push(raw["content"]);
615
+ } else if (role === "user") {
616
+ const content = raw["content"];
617
+ if (content !== void 0 && content !== null) {
618
+ return content;
619
+ }
620
+ }
621
+ }
622
+ }
623
+ return void 0;
624
+ }
625
+ function turnFingerprint(count, content) {
626
+ let encoded;
627
+ try {
628
+ encoded = JSON.stringify(content) ?? "";
629
+ } catch {
630
+ encoded = String(content);
631
+ }
632
+ const sum = sha256Bytes(encoded);
633
+ return `${count}:${toHex(sum.subarray(0, 8))}`;
634
+ }
635
+ function carriesToolResult(content) {
636
+ if (!Array.isArray(content)) {
637
+ return false;
638
+ }
639
+ return content.some(
640
+ (block) => isPlainObject(block) && stringAt(block, "type") === "tool_result"
641
+ );
642
+ }
643
+ function userTurn(body) {
644
+ const input = body["input"];
645
+ if (typeof input === "string" && input !== "") {
646
+ return turnFingerprint(1, input);
647
+ }
648
+ let count = 0;
649
+ let last;
650
+ for (const field of ["messages", "input"]) {
651
+ for (const raw of sliceAt(body, field)) {
652
+ if (!isPlainObject(raw) || stringAt(raw, "role") !== "user") {
653
+ continue;
654
+ }
655
+ if (carriesToolResult(raw["content"])) {
656
+ continue;
657
+ }
658
+ count += 1;
659
+ last = raw["content"];
660
+ }
661
+ }
662
+ if (count === 0) {
663
+ return "";
664
+ }
665
+ return turnFingerprint(count, last);
666
+ }
667
+ var MESSAGE_TTL_MS = 6 * 60 * 60 * 1e3;
668
+ var MESSAGE_STORE_LIMIT = 8192;
669
+ var MessageStore = class {
670
+ turns = /* @__PURE__ */ new Map();
671
+ latest = /* @__PURE__ */ new Map();
672
+ idFor(session, turn, now) {
673
+ const key = `${session}\0${turn}`;
674
+ let entry = turn ? this.turns.get(key) : this.latest.get(session);
675
+ if (!entry || now - entry.seen > MESSAGE_TTL_MS) {
676
+ entry = { id: ascendingID("msg", now), seen: now };
677
+ }
678
+ entry.seen = now;
679
+ if (turn) {
680
+ this.turns.set(key, entry);
681
+ }
682
+ this.latest.set(session, entry);
683
+ if (this.turns.size > MESSAGE_STORE_LIMIT || this.latest.size > MESSAGE_STORE_LIMIT) {
684
+ this.prune(now);
685
+ }
686
+ return entry.id;
687
+ }
688
+ prune(now) {
689
+ for (const [key, entry] of this.turns) {
690
+ if (now - entry.seen > MESSAGE_TTL_MS) {
691
+ this.turns.delete(key);
692
+ }
693
+ }
694
+ for (const [key, entry] of this.latest) {
695
+ if (now - entry.seen > MESSAGE_TTL_MS) {
696
+ this.latest.delete(key);
697
+ }
698
+ }
699
+ while (this.turns.size > MESSAGE_STORE_LIMIT * 3 / 4) {
700
+ const oldest = this.turns.keys().next();
701
+ if (oldest.done) {
702
+ break;
703
+ }
704
+ this.turns.delete(oldest.value);
705
+ }
706
+ while (this.latest.size > MESSAGE_STORE_LIMIT * 3 / 4) {
707
+ const oldest = this.latest.keys().next();
708
+ if (oldest.done) {
709
+ break;
710
+ }
711
+ this.latest.delete(oldest.value);
712
+ }
713
+ }
714
+ };
715
+ var userMessages = new MessageStore();
716
+ function userMessageID(headers, body, session) {
717
+ const header = headerValue(headers, "x-opencode-request");
718
+ if (MESSAGE_ID.test(header)) {
719
+ return header;
720
+ }
721
+ return userMessages.idFor(session, userTurn(body), Date.now());
722
+ }
723
+ var MESSAGE_ID = /^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/;
724
+ function deriveRequestIDs(headers, body, scope) {
725
+ const signal = firstString(
726
+ headerValue(headers, "x-opencode-session"),
727
+ headerValue(headers, "x-opencode-session-id"),
728
+ headerValue(headers, "x-session-affinity"),
729
+ headerValue(headers, "x-session-id"),
730
+ headerValue(headers, "session_id"),
731
+ headerValue(headers, "conversation-id"),
732
+ headerValue(headers, "conversation_id"),
733
+ stringAt(body, "conversation_id"),
734
+ stringAt(body, "metadata", "session_id"),
735
+ claudeCodeSessionID(stringAt(body, "metadata", "user_id"))
736
+ );
737
+ let effective = signal;
738
+ if (!effective) {
739
+ effective = conversationSeed(body);
740
+ }
741
+ if (!effective) {
742
+ effective = stringAt(body, "previous_response_id");
743
+ }
744
+ if (!effective || effective === "{}") {
745
+ effective = randomID("fallback", 16);
746
+ }
747
+ const session = scopedSessionID(scope, effective);
748
+ let projectSignal = firstString(
749
+ headerValue(headers, "x-opencode-project"),
750
+ stringAt(body, "metadata", "project_id")
751
+ );
752
+ if (!projectSignal) {
753
+ projectSignal = "opencode2api:default-project";
754
+ if (scope) {
755
+ projectSignal += `:${scope}`;
756
+ }
757
+ }
758
+ const parentSession = firstString(
759
+ headerValue(headers, "x-parent-session-id"),
760
+ stringAt(body, "metadata", "parent_session_id")
761
+ );
762
+ return {
763
+ session,
764
+ request: randomID("req", 16),
765
+ message: userMessageID(headers, body, session),
766
+ project: projectSignal,
767
+ parentSession
768
+ };
769
+ }
770
+
771
+ // src/protocol/tools/opencode_tools.json
772
+ var opencode_tools_default = {
773
+ edit: {
774
+ description: "Edit the contents of a file by finding and replacing exact text. When editing text from Read output, preserve the exact indentation (tabs or spaces) and omit the line-number prefix, such as `1: `. Never include the prefix in oldString or newString. Write line breaks as \\n; the file's existing CRLF or LF line endings are matched and preserved. To change line endings themselves, make oldString and newString differ only in \\r; that edit applies to the exact bytes. If there is no exact match, the edit retries while ignoring typographic quote, dash, and space variants, then trailing whitespace on each line. The edit fails if oldString is not found. By default, oldString must identify a UNIQUE location. Multiple matches FAIL unless replaceAll is true. Add more surrounding context to disambiguate, or set replaceAll to true to replace every occurrence. Use replaceAll when the change should apply to every occurrence, such as renaming a variable.",
775
+ parameters: {
776
+ additionalProperties: false,
777
+ properties: {
778
+ newString: {
779
+ description: "Text to replace oldString with (must differ from oldString)",
780
+ type: "string"
781
+ },
782
+ oldString: {
783
+ description: "Exact text to find and replace",
784
+ type: "string"
785
+ },
786
+ path: {
787
+ description: "File to edit",
788
+ type: "string"
789
+ },
790
+ replaceAll: {
791
+ description: "Whether to replace every occurrence of oldString. When false, oldString must match exactly once. Defaults to false.",
792
+ type: "boolean"
793
+ }
794
+ },
795
+ required: ["path", "oldString", "newString"],
796
+ type: "object"
797
+ }
798
+ },
799
+ execute: {
800
+ description: 'Run JavaScript in a confined Code Mode runtime to script tool calls and HTTP requests and compose their results.\n`fetch` is available for HTTP requests. Imports, direct filesystem access, and timers are unavailable; all other external access goes through `tools`.\nWithin `{ code }`, the only callable tools are those explicitly listed in the Code Mode catalog instructions or returned by the `search` function. Inside `{ code }`, ignore tools shown outside the Code Mode catalog. They are not available in the Code Mode runtime.\nCall tools through `tools` using only exact paths and signatures from the catalog. Do not infer or normalize tool names; preserve bracket notation such as `tools.<namespace>["tool-name"](input)`.\nPrefer an explicit `return`; if omitted, the final top-level expression becomes the result.\nAwait every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.allSettled`.',
801
+ parameters: {
802
+ additionalProperties: false,
803
+ properties: {
804
+ code: {
805
+ type: "string"
806
+ }
807
+ },
808
+ required: ["code"],
809
+ type: "object"
810
+ }
811
+ },
812
+ glob: {
813
+ description: 'Search file paths using a glob pattern (examples: "**/*.ts", "src/**/*.tsx").',
814
+ parameters: {
815
+ additionalProperties: false,
816
+ properties: {
817
+ hidden: {
818
+ description: "Include hidden files and directories (default: false).",
819
+ type: "boolean"
820
+ },
821
+ limit: {
822
+ description: "Maximum number of matching files to return (default: 100)",
823
+ exclusiveMinimum: 0,
824
+ type: "integer"
825
+ },
826
+ path: {
827
+ description: "Directory to search. Defaults to the current working directory.",
828
+ type: "string"
829
+ },
830
+ pattern: {
831
+ description: "Glob pattern to match files against",
832
+ type: "string"
833
+ }
834
+ },
835
+ required: ["pattern"],
836
+ type: "object"
837
+ }
838
+ },
839
+ grep: {
840
+ description: "Search file contents using ripgrep's regular expression syntax or literal text matching. Use it to locate specific code, symbols, or text patterns, and narrow searches with `path` or `include`. Returns matching file paths, line numbers, and line previews.",
841
+ parameters: {
842
+ additionalProperties: false,
843
+ properties: {
844
+ caseSensitive: {
845
+ description: "Use case-sensitive matching (default: true).",
846
+ type: "boolean"
847
+ },
848
+ include: {
849
+ description: 'Glob pattern to filter files (for example, "*.js" or "*.{ts,tsx}")',
850
+ type: "string"
851
+ },
852
+ limit: {
853
+ description: "Maximum number of matching lines to return (default: 100)",
854
+ exclusiveMinimum: 0,
855
+ type: "integer"
856
+ },
857
+ literal: {
858
+ description: "Treat `pattern` as exact text instead of a regular expression (default: false).",
859
+ type: "boolean"
860
+ },
861
+ path: {
862
+ description: "File or directory to search. Defaults to the current working directory.",
863
+ type: "string"
864
+ },
865
+ pattern: {
866
+ description: "Regular expression or literal text to match in file contents.",
867
+ minLength: 1,
868
+ type: "string"
869
+ }
870
+ },
871
+ required: ["pattern"],
872
+ type: "object"
873
+ }
874
+ },
875
+ question: {
876
+ description: 'Use this tool when you need to ask the user questions during execution. This allows you to:\n1. Gather user preferences or requirements\n2. Clarify ambiguous instructions\n3. Get decisions on implementation choices as you work\n4. Offer choices to the user about what direction to take.\n\nUsage notes:\n- A "Type your own answer" option is added automatically; don\'t include a separate option for free form answers\n- Set `multiple: true` to allow selecting more than one option\n- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label',
877
+ parameters: {
878
+ additionalProperties: false,
879
+ properties: {
880
+ questions: {
881
+ description: "Questions to ask",
882
+ items: {
883
+ additionalProperties: false,
884
+ properties: {
885
+ header: {
886
+ description: "Very short label (max 30 chars)",
887
+ type: "string"
888
+ },
889
+ multiple: {
890
+ type: "boolean"
891
+ },
892
+ options: {
893
+ description: "Available choices",
894
+ items: {
895
+ additionalProperties: false,
896
+ properties: {
897
+ description: {
898
+ description: "Explanation of choice",
899
+ type: "string"
900
+ },
901
+ label: {
902
+ description: "Display text (1-5 words, concise)",
903
+ type: "string"
904
+ }
905
+ },
906
+ required: ["label", "description"],
907
+ type: "object"
908
+ },
909
+ type: "array"
910
+ },
911
+ question: {
912
+ description: "Complete question",
913
+ type: "string"
914
+ }
915
+ },
916
+ required: ["question", "header", "options"],
917
+ type: "object"
918
+ },
919
+ minItems: 1,
920
+ type: "array"
921
+ }
922
+ },
923
+ required: ["questions"],
924
+ type: "object"
925
+ }
926
+ },
927
+ read: {
928
+ description: "Read the contents of a file or directory. Supports text files, images, and PDFs. Images and PDFs are presented directly to the model. Each text line is prefixed by its 1-based line number as <line>: <content>. The prefix is for reference and is not part of the file content. Directory entries are returned one per line. Use offset and limit to read large files or directories in sections. Prefer one larger read over many small slices, and use grep to find specific content in large files.",
929
+ parameters: {
930
+ additionalProperties: false,
931
+ properties: {
932
+ limit: {
933
+ description: "The maximum number of lines or directory entries to read (defaults to and capped at 2000)",
934
+ minimum: 0,
935
+ type: "integer"
936
+ },
937
+ offset: {
938
+ description: "The line or directory entry to start reading from (1-based)",
939
+ minimum: 0,
940
+ type: "integer"
941
+ },
942
+ path: {
943
+ description: "File or directory to read",
944
+ type: "string"
945
+ }
946
+ },
947
+ required: ["path"],
948
+ type: "object"
949
+ }
950
+ },
951
+ shell: {
952
+ description: "Execute a shell command and return its output. Commands run on Linux using bash. Quote file paths containing spaces or special characters. Prefer dedicated tools over shell commands when possible. When output is large, the full result is saved to a file and a truncated preview is returned. Rely on automatic truncation unless filtering the output is more useful. Commands accept an optional timeout, background commands have no timeout by default. Background commands return immediately, and you will be notified when they complete.",
953
+ parameters: {
954
+ additionalProperties: false,
955
+ properties: {
956
+ background: {
957
+ description: "Run the command in the background and return immediately (useful for dev servers and long-running builds). You do not need to use '&' at the end of the command when using this parameter. You will be notified when it completes. DO NOT poll for completion.",
958
+ type: "boolean"
959
+ },
960
+ command: {
961
+ description: "Shell command string to execute",
962
+ type: "string"
963
+ },
964
+ timeout: {
965
+ description: "Timeout in milliseconds. Set to 0 to disable the timeout. Defaults to 120000 for foreground commands. Background commands have no timeout by default.",
966
+ minimum: 0,
967
+ type: "integer"
968
+ },
969
+ workdir: {
970
+ description: "Working directory to execute the command in. Defaults to the current working directory. When possible, avoid changing directories in the command and set the working directory here instead.",
971
+ type: "string"
972
+ }
973
+ },
974
+ required: ["command"],
975
+ type: "object"
976
+ }
977
+ },
978
+ skill: {
979
+ description: "Load a specialized skill's instructions and resources into the current conversation when the task at hand matches its description.\n\nThe skill ID must match an available skill or a skill explicitly referenced by the user.",
980
+ parameters: {
981
+ additionalProperties: false,
982
+ properties: {
983
+ id: {
984
+ description: "The ID of an available skill or a skill explicitly referenced by the user",
985
+ type: "string"
986
+ }
987
+ },
988
+ required: ["id"],
989
+ type: "object"
990
+ }
991
+ },
992
+ subagent: {
993
+ description: `Spawns an agent in a child session to work on the specified task.
994
+ The output includes a sessionID you can pass back later to continue that specific conversation with the subagent.
995
+ New child sessions start with fresh context, so include all relevant context and instructions when you don't pass a sessionID.
996
+ Foreground (default) runs the subagent to completion and returns its final response.
997
+ Background mode (background=true) launches it asynchronously and returns immediately; you are notified when it finishes.
998
+ Use background only for independent work that can run while you continue elsewhere.
999
+
1000
+ Available subagents:
1001
+ - explore: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.
1002
+ - general: General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
1003
+ parameters: {
1004
+ additionalProperties: false,
1005
+ properties: {
1006
+ agent: {
1007
+ description: "The type of specialized agent to use for this task. If the user asks for a subagent by a name that is not one of the available subagents, they most likely mean a model: pick a suitable agent and pass the name through the model parameter instead.",
1008
+ type: "string"
1009
+ },
1010
+ background: {
1011
+ description: "Run the subagent in the background and return immediately. You will be notified when it completes. DO NOT sleep, poll, or proactively check on its progress.",
1012
+ type: "boolean"
1013
+ },
1014
+ description: {
1015
+ description: "A short 3-5 word label for the task, displayed to the user",
1016
+ type: "string"
1017
+ },
1018
+ model: {
1019
+ description: 'NEVER set this unless the user explicitly asks for a particular model or variant. The value is written as "providerID/modelID", or "providerID/modelID#variant" to include a variant. Do not guess the ID: look the model up with the models tool, filtering to your own provider first.',
1020
+ type: "string"
1021
+ },
1022
+ prompt: {
1023
+ description: "The task for the subagent to perform",
1024
+ type: "string"
1025
+ },
1026
+ sessionID: {
1027
+ description: "Continue a specific previous subagent conversation by passing its sessionID. Calls without a sessionID start a new conversation.",
1028
+ pattern: "^ses",
1029
+ type: "string"
1030
+ }
1031
+ },
1032
+ required: ["agent", "description", "prompt"],
1033
+ type: "object"
1034
+ }
1035
+ },
1036
+ webfetch: {
1037
+ description: "Fetch content from an HTTP or HTTPS URL and return it as text, markdown, or HTML. Markdown is the default.\n\nUse a more targeted tool when one is available. This tool is read-only. Large text results may be replaced with a preview while the complete output is retained in managed storage.",
1038
+ parameters: {
1039
+ additionalProperties: false,
1040
+ properties: {
1041
+ format: {
1042
+ description: "The format to return the content in. Defaults to markdown.",
1043
+ enum: ["text", "markdown", "html"],
1044
+ type: "string"
1045
+ },
1046
+ timeout: {
1047
+ description: "Optional timeout in seconds (maximum: 120)",
1048
+ exclusiveMinimum: 0,
1049
+ maximum: 120,
1050
+ type: "number"
1051
+ },
1052
+ url: {
1053
+ description: "The HTTP or HTTPS URL to fetch content from",
1054
+ type: "string"
1055
+ }
1056
+ },
1057
+ required: ["url"],
1058
+ type: "object"
1059
+ }
1060
+ },
1061
+ websearch: {
1062
+ description: "Search the web using the user's selected search integration. Use this for current information beyond knowledge cutoff.\n\nThe current year is 2026. Use this year when searching for recent information or current events.",
1063
+ parameters: {
1064
+ additionalProperties: false,
1065
+ properties: {
1066
+ query: {
1067
+ description: "Websearch query",
1068
+ type: "string"
1069
+ }
1070
+ },
1071
+ required: ["query"],
1072
+ type: "object"
1073
+ }
1074
+ },
1075
+ write: {
1076
+ description: "Writes a file to the local filesystem, overwriting if one exists.\n\nMissing parent directories are created automatically.\n\nUse this tool to create new files or overwrite existing files. For partial changes, use the edit tool instead.",
1077
+ parameters: {
1078
+ additionalProperties: false,
1079
+ properties: {
1080
+ content: {
1081
+ description: "Content to write to the file",
1082
+ type: "string"
1083
+ },
1084
+ path: {
1085
+ description: "Path to the file to write to",
1086
+ type: "string"
1087
+ }
1088
+ },
1089
+ required: ["path", "content"],
1090
+ type: "object"
1091
+ }
1092
+ }
1093
+ };
1094
+
1095
+ // src/protocol/tools.ts
1096
+ var V1_READ_TOOL = "read";
1097
+ var V2_CORE_TOOLS = ["shell", "edit", "glob", "grep", "read"];
1098
+ var V1_DEPRECATED_READ_DESCRIPTION = "Deprecated. This tool is provided for compatibility only. Do not call it.";
1099
+ var KNOWN_TOOLS = loadKnownTools();
1100
+ function loadKnownTools() {
1101
+ const map = /* @__PURE__ */ new Map();
1102
+ try {
1103
+ const raw = opencode_tools_default;
1104
+ if (isPlainObject(raw)) {
1105
+ for (const [name, value] of Object.entries(raw)) {
1106
+ if (!isPlainObject(value)) {
1107
+ continue;
1108
+ }
1109
+ const description = typeof value.description === "string" ? value.description : "";
1110
+ const parameters = isPlainObject(value.parameters) ? value.parameters : { type: "object", properties: {} };
1111
+ map.set(name, { description, parameters });
1112
+ }
1113
+ }
1114
+ } catch {
1115
+ }
1116
+ return map;
1117
+ }
1118
+ function knownOpencodeTool(name) {
1119
+ return KNOWN_TOOLS.get(name);
1120
+ }
1121
+ function toolName(protocol, item) {
1122
+ if (protocol === "chat") {
1123
+ return stringAt(item, "function", "name");
1124
+ }
1125
+ return stringAt(item, "name");
1126
+ }
1127
+ function setToolName(protocol, item, name) {
1128
+ if (!isPlainObject(item)) {
1129
+ return;
1130
+ }
1131
+ if (protocol === "chat") {
1132
+ const fn = item.function;
1133
+ if (isPlainObject(fn)) {
1134
+ fn.name = name;
1135
+ }
1136
+ return;
1137
+ }
1138
+ item.name = name;
1139
+ }
1140
+ function v2Tool(protocol, name) {
1141
+ const definition = knownOpencodeTool(name);
1142
+ const description = definition?.description ?? `Agent tool ${name}`;
1143
+ const parameters = definition?.parameters ?? {
1144
+ type: "object",
1145
+ properties: {}
1146
+ };
1147
+ if (protocol === "chat") {
1148
+ return {
1149
+ type: "function",
1150
+ function: { name, description, parameters }
1151
+ };
1152
+ }
1153
+ if (protocol === "anthropic") {
1154
+ return { name, description, input_schema: parameters };
1155
+ }
1156
+ return { type: "function", name, description, parameters };
1157
+ }
1158
+ function v2Toolset(protocol, names = V2_CORE_TOOLS) {
1159
+ return names.map((name) => v2Tool(protocol, name));
1160
+ }
1161
+ function isV1ReadToolCandidate(name) {
1162
+ const lower = name.trim().toLowerCase();
1163
+ if (lower === "") {
1164
+ return false;
1165
+ }
1166
+ if (lower === V1_READ_TOOL) {
1167
+ return true;
1168
+ }
1169
+ const normalized = lower.replace(/[_\-.]/g, "");
1170
+ if ([
1171
+ "readfile",
1172
+ "fileread",
1173
+ "readfiles",
1174
+ "readtext",
1175
+ "readtextfile",
1176
+ "readcontent",
1177
+ "viewfile"
1178
+ ].includes(normalized)) {
1179
+ return true;
1180
+ }
1181
+ return lower.startsWith("read_") || lower.startsWith("read-") || lower.endsWith("_read") || lower.endsWith("-read");
1182
+ }
1183
+ function v1DeprecatedReadTool(protocol) {
1184
+ const parameters = { type: "object", properties: {} };
1185
+ if (protocol === "chat") {
1186
+ return {
1187
+ type: "function",
1188
+ function: {
1189
+ name: V1_READ_TOOL,
1190
+ description: V1_DEPRECATED_READ_DESCRIPTION,
1191
+ parameters
1192
+ }
1193
+ };
1194
+ }
1195
+ if (protocol === "anthropic") {
1196
+ return {
1197
+ name: V1_READ_TOOL,
1198
+ description: V1_DEPRECATED_READ_DESCRIPTION,
1199
+ input_schema: parameters
1200
+ };
1201
+ }
1202
+ return {
1203
+ type: "function",
1204
+ name: V1_READ_TOOL,
1205
+ description: V1_DEPRECATED_READ_DESCRIPTION,
1206
+ parameters
1207
+ };
1208
+ }
1209
+ function rewriteV1ToolReferences(payload, protocol, fromName, toName) {
1210
+ if (fromName === "" || fromName === toName) {
1211
+ return;
1212
+ }
1213
+ if (protocol === "chat") {
1214
+ const choice2 = payload.tool_choice;
1215
+ if (isPlainObject(choice2) && isPlainObject(choice2.function)) {
1216
+ if (choice2.function.name === fromName) {
1217
+ choice2.function.name = toName;
1218
+ }
1219
+ }
1220
+ for (const raw of sliceAt(payload, "messages")) {
1221
+ for (const rawCall of sliceAt(raw, "tool_calls")) {
1222
+ if (isPlainObject(rawCall) && isPlainObject(rawCall.function)) {
1223
+ if (rawCall.function.name === fromName) {
1224
+ rawCall.function.name = toName;
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+ return;
1230
+ }
1231
+ if (protocol === "responses") {
1232
+ const choice2 = payload.tool_choice;
1233
+ if (isPlainObject(choice2) && choice2.name === fromName) {
1234
+ choice2.name = toName;
1235
+ }
1236
+ for (const item of sliceAt(payload, "input")) {
1237
+ if (isPlainObject(item) && item.type === "function_call" && item.name === fromName) {
1238
+ item.name = toName;
1239
+ }
1240
+ }
1241
+ return;
1242
+ }
1243
+ const choice = payload.tool_choice;
1244
+ if (isPlainObject(choice) && choice.name === fromName) {
1245
+ choice.name = toName;
1246
+ }
1247
+ for (const msg of sliceAt(payload, "messages")) {
1248
+ for (const block of sliceAt(msg, "content")) {
1249
+ if (isPlainObject(block) && block.type === "tool_use" && block.name === fromName) {
1250
+ block.name = toName;
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ function ensureV1ReadTool(payload, protocol) {
1256
+ const raw = payload.tools;
1257
+ if (raw === void 0) {
1258
+ payload.tools = [v1DeprecatedReadTool(protocol)];
1259
+ return { alias: "", changed: true };
1260
+ }
1261
+ if (!Array.isArray(raw)) {
1262
+ return { alias: "", changed: false };
1263
+ }
1264
+ const items = raw;
1265
+ let candidateIndex = -1;
1266
+ let candidateName = "";
1267
+ let caseFoldCandidate = false;
1268
+ for (const [index, item] of items.entries()) {
1269
+ const name = toolName(protocol, item);
1270
+ if (name === "") {
1271
+ continue;
1272
+ }
1273
+ if (name === V1_READ_TOOL) {
1274
+ return { alias: "", changed: false };
1275
+ }
1276
+ if (name.toLowerCase() === V1_READ_TOOL) {
1277
+ if (!caseFoldCandidate) {
1278
+ candidateIndex = index;
1279
+ candidateName = name;
1280
+ caseFoldCandidate = true;
1281
+ }
1282
+ continue;
1283
+ }
1284
+ if (candidateIndex < 0 && isV1ReadToolCandidate(name)) {
1285
+ candidateIndex = index;
1286
+ candidateName = name;
1287
+ }
1288
+ }
1289
+ if (candidateIndex >= 0) {
1290
+ setToolName(protocol, items[candidateIndex], V1_READ_TOOL);
1291
+ rewriteV1ToolReferences(payload, protocol, candidateName, V1_READ_TOOL);
1292
+ payload.tools = items;
1293
+ return { alias: candidateName, changed: true };
1294
+ }
1295
+ items.push(v1DeprecatedReadTool(protocol));
1296
+ payload.tools = items;
1297
+ return { alias: "", changed: true };
1298
+ }
1299
+ function ensureV2Tools(payload, protocol) {
1300
+ const raw = payload.tools;
1301
+ if (raw === void 0) {
1302
+ payload.tools = v2Toolset(protocol);
1303
+ return true;
1304
+ }
1305
+ if (!Array.isArray(raw)) {
1306
+ return false;
1307
+ }
1308
+ const items = raw;
1309
+ let changed = false;
1310
+ const present = /* @__PURE__ */ new Set();
1311
+ for (const [index, item] of items.entries()) {
1312
+ const name = toolName(protocol, item);
1313
+ if (name === "") {
1314
+ continue;
1315
+ }
1316
+ present.add(name);
1317
+ if (knownOpencodeTool(name)) {
1318
+ items[index] = v2Tool(protocol, name);
1319
+ changed = true;
1320
+ }
1321
+ }
1322
+ const missing = V2_CORE_TOOLS.filter((name) => !present.has(name));
1323
+ if (missing.length > 0) {
1324
+ items.push(...v2Toolset(protocol, missing));
1325
+ changed = true;
1326
+ }
1327
+ if (!changed) {
1328
+ return false;
1329
+ }
1330
+ payload.tools = items;
1331
+ return true;
1332
+ }
1333
+ function ensureAnonymousChatUsage(payload, protocol) {
1334
+ if (protocol !== "chat") {
1335
+ return false;
1336
+ }
1337
+ const options = payload.stream_options;
1338
+ if (!isPlainObject(options)) {
1339
+ payload.stream_options = { include_usage: true };
1340
+ return true;
1341
+ }
1342
+ if (options.include_usage === true) {
1343
+ return false;
1344
+ }
1345
+ options.include_usage = true;
1346
+ return true;
1347
+ }
1348
+ function prepareAnonymousBody(rawBody, protocol, v1) {
1349
+ if (protocol === "systemone") {
1350
+ return { body: rawBody, alias: "", shaped: false };
1351
+ }
1352
+ let payload;
1353
+ try {
1354
+ payload = JSON.parse(rawBody);
1355
+ } catch {
1356
+ return { body: rawBody, alias: "", shaped: false };
1357
+ }
1358
+ if (!isPlainObject(payload)) {
1359
+ return { body: rawBody, alias: "", shaped: false };
1360
+ }
1361
+ let changed = false;
1362
+ if (payload.stream !== true) {
1363
+ payload.stream = true;
1364
+ changed = true;
1365
+ }
1366
+ if (ensureAnonymousChatUsage(payload, protocol)) {
1367
+ changed = true;
1368
+ }
1369
+ let alias = "";
1370
+ if (v1) {
1371
+ const result = ensureV1ReadTool(payload, protocol);
1372
+ alias = result.alias;
1373
+ if (result.changed) {
1374
+ changed = true;
1375
+ }
1376
+ } else if (ensureV2Tools(payload, protocol)) {
1377
+ changed = true;
1378
+ }
1379
+ if (!changed) {
1380
+ return { body: rawBody, alias, shaped: false };
1381
+ }
1382
+ return { body: JSON.stringify(payload), alias, shaped: true };
1383
+ }
1384
+
1385
+ // src/protocol/error.ts
1386
+ function buildErrorEnvelope(protocol, status, message, kind) {
1387
+ if (protocol === "anthropic") {
1388
+ return {
1389
+ status,
1390
+ body: { type: "error", error: { type: kind, message } }
1391
+ };
1392
+ }
1393
+ return {
1394
+ status,
1395
+ body: {
1396
+ error: { message, type: kind, param: null, code: null }
1397
+ }
1398
+ };
1399
+ }
1400
+
1401
+ // src/webui/theme.ts
1402
+ var THEME_TOKENS = `:root {
1403
+ --bg: #0a0a0a;
1404
+ --bg-subtle: #171717;
1405
+ --surface: #121212;
1406
+ --surface-2: #0d0d0d;
1407
+ --text-1: #ededed;
1408
+ --text-2: #a0a0a0;
1409
+ --text-3: #787878;
1410
+ --border-1: #262626;
1411
+ --border-2: #3a3a3a;
1412
+ --inverse-bg: #ededed;
1413
+ --inverse-text: #0a0a0a;
1414
+ --inverse-hover: #cfcfcf;
1415
+ --focus-ring: rgba(237, 237, 237, 0.18);
1416
+ --overlay: rgba(0, 0, 0, 0.6);
1417
+ --ok: #3fb950;
1418
+ --warn: #d29922;
1419
+ --warn-bg: rgba(210, 153, 34, 0.15);
1420
+ --bad: #f85149;
1421
+ --bad-bg: rgba(248, 81, 73, 0.15);
1422
+ --fs-xs: 12px;
1423
+ --fs-sm: 13px;
1424
+ --fs-md: 14px;
1425
+ --fs-lg: 16px;
1426
+ --fs-xl: 20px;
1427
+ --fs-2xl: 24px;
1428
+ --fs-3xl: 30px;
1429
+ --sp-1: 4px;
1430
+ --sp-2: 8px;
1431
+ --sp-3: 12px;
1432
+ --sp-4: 16px;
1433
+ --sp-5: 20px;
1434
+ --sp-6: 24px;
1435
+ --sp-8: 32px;
1436
+ --sp-10: 40px;
1437
+ --sp-12: 48px;
1438
+ --r-sm: 4px;
1439
+ --r-md: 6px;
1440
+ --r-lg: 10px;
1441
+ --sidebar-w: 232px;
1442
+ --content-max: 1280px;
1443
+ }`;
1444
+
1445
+ // src/webui/styles.ts
1446
+ var WEBUI_STYLES = `
1447
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1448
+
1449
+ html, body {
1450
+ height: 100%;
1451
+ background: var(--bg);
1452
+ color: var(--text-1);
1453
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
1454
+ "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif;
1455
+ font-size: var(--fs-md);
1456
+ line-height: 1.55;
1457
+ -webkit-font-smoothing: antialiased;
1458
+ }
1459
+
1460
+ a { color: var(--text-1); text-decoration: none; }
1461
+ a:hover { color: var(--inverse-hover); }
1462
+
1463
+ code, pre, .mono {
1464
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
1465
+ "Liberation Mono", monospace;
1466
+ font-size: var(--fs-sm);
1467
+ }
1468
+
1469
+ #layout { display: flex; min-height: 100%; }
1470
+
1471
+ #sidebar {
1472
+ width: var(--sidebar-w);
1473
+ flex: 0 0 var(--sidebar-w);
1474
+ background: var(--surface);
1475
+ border-right: 1px solid var(--border-1);
1476
+ padding: var(--sp-5) var(--sp-3);
1477
+ position: sticky;
1478
+ top: 0;
1479
+ height: 100vh;
1480
+ overflow-y: auto;
1481
+ }
1482
+
1483
+ #brand {
1484
+ display: flex;
1485
+ align-items: center;
1486
+ gap: var(--sp-2);
1487
+ padding: 0 var(--sp-3) var(--sp-5);
1488
+ font-size: var(--fs-lg);
1489
+ font-weight: 600;
1490
+ letter-spacing: -0.01em;
1491
+ }
1492
+
1493
+ #brand .dot {
1494
+ width: 8px;
1495
+ height: 8px;
1496
+ border-radius: 50%;
1497
+ background: var(--text-1);
1498
+ }
1499
+
1500
+ .nav-group { margin-bottom: var(--sp-5); }
1501
+ .nav-group h3 {
1502
+ font-size: var(--fs-xs);
1503
+ font-weight: 500;
1504
+ text-transform: uppercase;
1505
+ letter-spacing: 0.08em;
1506
+ color: var(--text-3);
1507
+ padding: 0 var(--sp-3) var(--sp-2);
1508
+ }
1509
+
1510
+ .nav-item {
1511
+ display: block;
1512
+ padding: var(--sp-2) var(--sp-3);
1513
+ border-radius: var(--r-md);
1514
+ color: var(--text-2);
1515
+ font-size: var(--fs-sm);
1516
+ cursor: pointer;
1517
+ border: 1px solid transparent;
1518
+ }
1519
+
1520
+ .nav-item:hover { background: var(--bg-subtle); color: var(--text-1); }
1521
+ .nav-item.active {
1522
+ background: var(--surface-2);
1523
+ color: var(--text-1);
1524
+ border-color: var(--border-1);
1525
+ }
1526
+
1527
+ #content {
1528
+ flex: 1 1 auto;
1529
+ max-width: var(--content-max);
1530
+ padding: var(--sp-8) var(--sp-8) var(--sp-12);
1531
+ min-width: 0;
1532
+ }
1533
+
1534
+ .page-head {
1535
+ display: flex;
1536
+ align-items: baseline;
1537
+ justify-content: space-between;
1538
+ gap: var(--sp-4);
1539
+ margin-bottom: var(--sp-6);
1540
+ flex-wrap: wrap;
1541
+ }
1542
+
1543
+ .page-head h1 { font-size: var(--fs-2xl); font-weight: 600; letter-spacing: -0.02em; }
1544
+ .page-head p { color: var(--text-3); font-size: var(--fs-sm); }
1545
+
1546
+ .card {
1547
+ background: var(--surface);
1548
+ border: 1px solid var(--border-1);
1549
+ border-radius: var(--r-lg);
1550
+ padding: var(--sp-5);
1551
+ margin-bottom: var(--sp-4);
1552
+ }
1553
+
1554
+ .card h2 {
1555
+ font-size: var(--fs-lg);
1556
+ font-weight: 600;
1557
+ margin-bottom: var(--sp-4);
1558
+ }
1559
+
1560
+ .grid {
1561
+ display: grid;
1562
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1563
+ gap: var(--sp-4);
1564
+ }
1565
+
1566
+ .stat {
1567
+ background: var(--surface);
1568
+ border: 1px solid var(--border-1);
1569
+ border-radius: var(--r-lg);
1570
+ padding: var(--sp-4) var(--sp-5);
1571
+ }
1572
+
1573
+ .stat .label {
1574
+ color: var(--text-3);
1575
+ font-size: var(--fs-xs);
1576
+ text-transform: uppercase;
1577
+ letter-spacing: 0.06em;
1578
+ margin-bottom: var(--sp-2);
1579
+ }
1580
+
1581
+ .stat .value { font-size: var(--fs-3xl); font-weight: 600; letter-spacing: -0.03em; }
1582
+ .stat .sub { color: var(--text-3); font-size: var(--fs-xs); margin-top: var(--sp-1); }
1583
+
1584
+ table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
1585
+ th {
1586
+ text-align: left;
1587
+ font-weight: 500;
1588
+ color: var(--text-3);
1589
+ font-size: var(--fs-xs);
1590
+ text-transform: uppercase;
1591
+ letter-spacing: 0.06em;
1592
+ padding: var(--sp-2) var(--sp-3);
1593
+ border-bottom: 1px solid var(--border-1);
1594
+ }
1595
+ td { padding: var(--sp-3); border-bottom: 1px solid var(--border-1); }
1596
+ tr:last-child td { border-bottom: none; }
1597
+ tbody tr:hover { background: var(--bg-subtle); }
1598
+
1599
+ button, .btn {
1600
+ appearance: none;
1601
+ border: 1px solid var(--border-2);
1602
+ background: var(--surface-2);
1603
+ color: var(--text-1);
1604
+ border-radius: var(--r-md);
1605
+ padding: var(--sp-2) var(--sp-4);
1606
+ font-size: var(--fs-sm);
1607
+ font-family: inherit;
1608
+ cursor: pointer;
1609
+ transition: background 120ms ease, border-color 120ms ease;
1610
+ }
1611
+
1612
+ button:hover, .btn:hover { background: var(--bg-subtle); border-color: var(--text-3); }
1613
+ button:focus-visible, .btn:focus-visible, input:focus-visible,
1614
+ select:focus-visible, textarea:focus-visible {
1615
+ outline: 2px solid var(--focus-ring);
1616
+ outline-offset: 2px;
1617
+ }
1618
+
1619
+ button.primary {
1620
+ background: var(--inverse-bg);
1621
+ color: var(--inverse-text);
1622
+ border-color: var(--inverse-bg);
1623
+ font-weight: 500;
1624
+ }
1625
+ button.primary:hover { background: var(--inverse-hover); border-color: var(--inverse-hover); }
1626
+ button.ghost { background: transparent; border-color: var(--border-1); }
1627
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
1628
+
1629
+ input, select, textarea {
1630
+ width: 100%;
1631
+ background: var(--surface-2);
1632
+ border: 1px solid var(--border-2);
1633
+ border-radius: var(--r-md);
1634
+ color: var(--text-1);
1635
+ padding: var(--sp-2) var(--sp-3);
1636
+ font-size: var(--fs-sm);
1637
+ font-family: inherit;
1638
+ }
1639
+
1640
+ textarea { min-height: 140px; resize: vertical; font-family: inherit; }
1641
+ input::placeholder, textarea::placeholder { color: var(--text-3); }
1642
+
1643
+ .field { margin-bottom: var(--sp-4); }
1644
+ .field label {
1645
+ display: block;
1646
+ font-size: var(--fs-xs);
1647
+ text-transform: uppercase;
1648
+ letter-spacing: 0.06em;
1649
+ color: var(--text-3);
1650
+ margin-bottom: var(--sp-2);
1651
+ }
1652
+ .field .hint { color: var(--text-3); font-size: var(--fs-xs); margin-top: var(--sp-1); }
1653
+
1654
+ .row { display: flex; gap: var(--sp-3); flex-wrap: wrap; align-items: center; }
1655
+ .row > * { flex: 1 1 200px; }
1656
+ .row > .shrink { flex: 0 0 auto; }
1657
+
1658
+ .badge {
1659
+ display: inline-block;
1660
+ padding: 1px var(--sp-2);
1661
+ border-radius: var(--r-sm);
1662
+ font-size: var(--fs-xs);
1663
+ border: 1px solid var(--border-2);
1664
+ color: var(--text-2);
1665
+ white-space: nowrap;
1666
+ }
1667
+
1668
+ .badge.ok { color: var(--ok); border-color: var(--ok); }
1669
+ .badge.warn { color: var(--warn); border-color: var(--warn); background: var(--warn-bg); }
1670
+ .badge.bad { color: var(--bad); border-color: var(--bad); background: var(--bad-bg); }
1671
+
1672
+ #toast {
1673
+ position: fixed;
1674
+ right: var(--sp-6);
1675
+ bottom: var(--sp-6);
1676
+ background: var(--surface);
1677
+ border: 1px solid var(--border-2);
1678
+ border-radius: var(--r-md);
1679
+ padding: var(--sp-3) var(--sp-5);
1680
+ font-size: var(--fs-sm);
1681
+ max-width: 380px;
1682
+ display: none;
1683
+ z-index: 40;
1684
+ }
1685
+
1686
+ #toast.show { display: block; }
1687
+ #toast.error { border-color: var(--bad); color: var(--bad); }
1688
+
1689
+ .log-line {
1690
+ display: grid;
1691
+ grid-template-columns: 92px 64px 1fr;
1692
+ gap: var(--sp-3);
1693
+ padding: var(--sp-1) 0;
1694
+ border-bottom: 1px solid var(--border-1);
1695
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1696
+ font-size: var(--fs-xs);
1697
+ }
1698
+
1699
+ .log-line .time { color: var(--text-3); }
1700
+ .log-line .level { text-transform: uppercase; }
1701
+ .log-line .level.debug { color: var(--text-3); }
1702
+ .log-line .level.info { color: var(--text-1); }
1703
+ .log-line .level.warn { color: var(--warn); }
1704
+ .log-line .level.error { color: var(--bad); }
1705
+ .log-line .message { color: var(--text-2); word-break: break-word; }
1706
+
1707
+ #login {
1708
+ min-height: 100vh;
1709
+ display: flex;
1710
+ align-items: center;
1711
+ justify-content: center;
1712
+ padding: var(--sp-6);
1713
+ }
1714
+
1715
+ #login .card { width: 100%; max-width: 380px; margin: 0; }
1716
+ #login h1 { font-size: var(--fs-xl); margin-bottom: var(--sp-2); }
1717
+ #login p { color: var(--text-3); font-size: var(--fs-sm); margin-bottom: var(--sp-5); }
1718
+
1719
+ .hidden { display: none !important; }
1720
+
1721
+ .bar {
1722
+ height: 6px;
1723
+ background: var(--surface-2);
1724
+ border-radius: 3px;
1725
+ overflow: hidden;
1726
+ }
1727
+ .bar > span { display: block; height: 100%; background: var(--text-2); }
1728
+
1729
+ .empty {
1730
+ color: var(--text-3);
1731
+ font-size: var(--fs-sm);
1732
+ padding: var(--sp-6);
1733
+ text-align: center;
1734
+ }
1735
+ `;
1736
+
1737
+ // src/webui/script-core.ts
1738
+ var WEBUI_CORE = `
1739
+ const state = { csrf: "", page: "overview", config: null, monitor: null };
1740
+
1741
+ const PAGES = [
1742
+ { group: "Monitor", id: "overview", label: "Overview" },
1743
+ { group: "Monitor", id: "models", label: "Models" },
1744
+ { group: "Monitor", id: "keys", label: "API Keys" },
1745
+ { group: "Monitor", id: "quota", label: "Quota" },
1746
+ { group: "Tools", id: "playground", label: "Playground" },
1747
+ { group: "Tools", id: "rotation", label: "Rotation" },
1748
+ { group: "Settings", id: "config", label: "Configuration" },
1749
+ { group: "Settings", id: "account", label: "Account" },
1750
+ { group: "Settings", id: "logs", label: "Logs" },
1751
+ ];
1752
+
1753
+ const el = (id) => document.getElementById(id);
1754
+
1755
+ function toast(message, isError) {
1756
+ const node = el("toast");
1757
+ node.textContent = message;
1758
+ node.className = "show" + (isError ? " error" : "");
1759
+ clearTimeout(node._timer);
1760
+ node._timer = setTimeout(() => { node.className = ""; }, 4000);
1761
+ }
1762
+
1763
+ async function api(path, options) {
1764
+ const settings = Object.assign({ credentials: "same-origin" }, options || {});
1765
+ settings.headers = Object.assign(
1766
+ { "Content-Type": "application/json" },
1767
+ settings.headers || {},
1768
+ );
1769
+ if (state.csrf && settings.method && settings.method !== "GET") {
1770
+ settings.headers["X-CSRF-Token"] = state.csrf;
1771
+ }
1772
+ const response = await fetch(path, settings);
1773
+ if (response.status === 401) {
1774
+ showLogin();
1775
+ throw new Error("session expired");
1776
+ }
1777
+ const text = await response.text();
1778
+ let payload = null;
1779
+ if (text) {
1780
+ try { payload = JSON.parse(text); } catch { payload = text; }
1781
+ }
1782
+ if (!response.ok) {
1783
+ const message = payload && payload.error
1784
+ ? payload.error.message
1785
+ : "request failed with HTTP " + response.status;
1786
+ throw new Error(message);
1787
+ }
1788
+ return payload;
1789
+ }
1790
+
1791
+ function showLogin() {
1792
+ el("app").classList.add("hidden");
1793
+ el("login").classList.remove("hidden");
1794
+ }
1795
+
1796
+ function showApp() {
1797
+ el("login").classList.add("hidden");
1798
+ el("app").classList.remove("hidden");
1799
+ }
1800
+
1801
+ function renderNav() {
1802
+ const groups = [];
1803
+ for (const page of PAGES) {
1804
+ let group = groups.find((entry) => entry.name === page.group);
1805
+ if (!group) {
1806
+ group = { name: page.group, items: [] };
1807
+ groups.push(group);
1808
+ }
1809
+ group.items.push(page);
1810
+ }
1811
+ el("nav").innerHTML = groups.map((group) => (
1812
+ '<div class="nav-group"><h3>' + group.name + '</h3>' +
1813
+ group.items.map((page) => (
1814
+ '<a class="nav-item" data-page="' + page.id + '">' + page.label + '</a>'
1815
+ )).join("") +
1816
+ '</div>'
1817
+ )).join("");
1818
+ for (const node of document.querySelectorAll(".nav-item")) {
1819
+ node.addEventListener("click", () => {
1820
+ location.hash = "#" + node.dataset.page;
1821
+ });
1822
+ }
1823
+ }
1824
+
1825
+ function markActive() {
1826
+ for (const node of document.querySelectorAll(".nav-item")) {
1827
+ node.classList.toggle("active", node.dataset.page === state.page);
1828
+ }
1829
+ }
1830
+
1831
+ function fmt(n) {
1832
+ if (n === undefined || n === null) return "0";
1833
+ return Number(n).toLocaleString("en-US");
1834
+ }
1835
+
1836
+ function pct(value) {
1837
+ return (Math.round(value * 1000) / 10).toFixed(1) + "%";
1838
+ }
1839
+
1840
+ function escapeHtml(value) {
1841
+ return String(value)
1842
+ .replace(/&/g, "&amp;")
1843
+ .replace(/</g, "&lt;")
1844
+ .replace(/>/g, "&gt;")
1845
+ .replace(/"/g, "&quot;");
1846
+ }
1847
+
1848
+ function stat(label, value, sub) {
1849
+ return '<div class="stat"><div class="label">' + escapeHtml(label) +
1850
+ '</div><div class="value">' + escapeHtml(value) +
1851
+ '</div><div class="sub">' + escapeHtml(sub) + '</div></div>';
1852
+ }
1853
+ `;
1854
+
1855
+ // src/webui/script-monitor.ts
1856
+ var WEBUI_MONITOR_PAGES = `
1857
+ async function loadOverview() {
1858
+ const data = await api("/api/monitor");
1859
+ state.monitor = data;
1860
+ const metrics = data.metrics || {};
1861
+ const lifetime = metrics.lifetime || {};
1862
+ const window = metrics.last_hour || {};
1863
+ const usage = metrics.usage || {};
1864
+ const upstream = metrics.upstream || {};
1865
+ el("content").innerHTML = (
1866
+ '<div class="page-head"><div><h1>Overview</h1>' +
1867
+ '<p>Live traffic, token usage and upstream health</p></div></div>' +
1868
+ '<div class="grid">' +
1869
+ stat("Total requests", fmt(lifetime.total), fmt(lifetime.errors) + " errors") +
1870
+ stat("Success rate", pct(lifetime.success_rate || 0), "lifetime") +
1871
+ stat("Active requests", fmt(metrics.active_requests), fmt(metrics.active_streams) + " streaming") +
1872
+ stat("Uptime", formatUptime(metrics.uptime_seconds), "since start") +
1873
+ stat("Input tokens", fmt(usageLifetime(usage, "input_tokens")), "lifetime") +
1874
+ stat("Output tokens", fmt(usageLifetime(usage, "output_tokens")), "lifetime") +
1875
+ stat("Last hour", fmt(window.total), "requests") +
1876
+ stat("Upstream attempts", fmt((upstream.lifetime || {}).total), "total") +
1877
+ '</div>' +
1878
+ '<div class="card"><h2>Requests per minute</h2>' +
1879
+ seriesChart(metrics.series || []) +
1880
+ '</div>' +
1881
+ '<div class="card"><h2>Models</h2>' +
1882
+ countTable(metrics.models || {}) +
1883
+ '</div>' +
1884
+ '<div class="card"><h2>Recent upstream requests</h2>' +
1885
+ upstreamTable(upstream.requests || []) +
1886
+ '</div>'
1887
+ );
1888
+ }
1889
+
1890
+ function usageLifetime(usage, field) {
1891
+ const lifetime = usage.lifetime || {};
1892
+ const tokens = lifetime.tokens || {};
1893
+ return tokens[field] || 0;
1894
+ }
1895
+
1896
+ function formatUptime(seconds) {
1897
+ const total = Math.max(0, Math.floor(seconds || 0));
1898
+ const hours = Math.floor(total / 3600);
1899
+ const minutes = Math.floor((total % 3600) / 60);
1900
+ if (hours > 0) return hours + "h " + minutes + "m";
1901
+ return minutes + "m " + (total % 60) + "s";
1902
+ }
1903
+
1904
+ function seriesChart(series) {
1905
+ if (!series.length) return '<div class="empty">No traffic recorded yet</div>';
1906
+ const max = series.reduce((acc, point) => Math.max(acc, point.total), 0) || 1;
1907
+ const bars = series.slice(-30).map((point) => {
1908
+ const height = Math.round((point.total / max) * 100);
1909
+ const label = new Date(point.minute).toLocaleTimeString("en-US", {
1910
+ hour: "2-digit", minute: "2-digit",
1911
+ });
1912
+ return '<div title="' + escapeHtml(label) + ": " + point.total + " requests, " +
1913
+ point.errors + ' errors" style="flex:1 1 0;height:100%;display:flex;' +
1914
+ 'flex-direction:column;justify-content:flex-end">' +
1915
+ '<div style="height:' + Math.max(height, 1) + '%;background:var(--text-2);' +
1916
+ 'border-radius:2px 2px 0 0"></div></div>';
1917
+ }).join("");
1918
+ return '<div style="display:flex;align-items:flex-end;gap:2px;height:140px">' +
1919
+ bars + '</div>';
1920
+ }
1921
+
1922
+ function countTable(counts) {
1923
+ const entries = Object.entries(counts || {});
1924
+ if (!entries.length) return '<div class="empty">No data yet</div>';
1925
+ entries.sort((a, b) => b[1] - a[1]);
1926
+ return '<table><thead><tr><th>Name</th><th>Count</th></tr></thead><tbody>' +
1927
+ entries.map(([name, count]) =>
1928
+ '<tr><td class="mono">' + escapeHtml(name) + '</td><td>' + fmt(count) + '</td></tr>'
1929
+ ).join("") + '</tbody></table>';
1930
+ }
1931
+
1932
+ function upstreamTable(requests) {
1933
+ if (!requests.length) return '<div class="empty">No upstream requests yet</div>';
1934
+ return '<table><thead><tr><th>Time</th><th>Model</th><th>Tier</th>' +
1935
+ '<th>Status</th><th>Duration</th><th>Outcome</th></tr></thead><tbody>' +
1936
+ requests.slice().reverse().slice(0, 50).map((entry) =>
1937
+ '<tr><td class="mono">' + escapeHtml(new Date(entry.time).toLocaleTimeString()) +
1938
+ '</td><td class="mono">' + escapeHtml(entry.model) +
1939
+ '</td><td>' + escapeHtml(entry.tier || "-") +
1940
+ '</td><td>' + escapeHtml(entry.status) +
1941
+ '</td><td>' + fmt(entry.duration_ms) + " ms" +
1942
+ '</td><td>' + escapeHtml(entry.outcome || "-") + '</td></tr>'
1943
+ ).join("") + '</tbody></table>';
1944
+ }
1945
+
1946
+ async function loadModels() {
1947
+ const data = await api("/api/debug/models");
1948
+ const models = data.models || [];
1949
+ const catalog = data.catalog || {};
1950
+ el("content").innerHTML = (
1951
+ '<div class="page-head"><div><h1>Models</h1>' +
1952
+ '<p>' + fmt(catalog.total) + " discovered, " + fmt(catalog.exposed) +
1953
+ " exposed" + (catalog.stale ? " (stale)" : "") + '</p></div></div>' +
1954
+ '<div class="card"><h2>Catalog</h2>' +
1955
+ '<div class="grid">' +
1956
+ stat("Zen models", fmt(catalog.zen), "") +
1957
+ stat("Go models", fmt(catalog.go), "") +
1958
+ stat("Total", fmt(catalog.total), "") +
1959
+ stat("Exposed", fmt(catalog.exposed), "") +
1960
+ '</div></div>' +
1961
+ '<div class="card"><h2>Routing</h2>' +
1962
+ modelsTable(models) +
1963
+ '</div>'
1964
+ );
1965
+ }
1966
+
1967
+ function modelsTable(models) {
1968
+ if (!models.length) return '<div class="empty">No models discovered yet</div>';
1969
+ return '<table><thead><tr><th>Model</th><th>Protocol</th><th>Tier</th>' +
1970
+ '<th>Anonymous</th><th>Source</th><th>Error</th></tr></thead><tbody>' +
1971
+ models.map((entry) =>
1972
+ '<tr><td class="mono">' + escapeHtml(entry.model) +
1973
+ '</td><td>' + escapeHtml(entry.native_protocol || "-") +
1974
+ '</td><td>' + escapeHtml(entry.tier || "-") +
1975
+ '</td><td>' + (entry.anonymous ? "yes" : "no") +
1976
+ '</td><td>' + escapeHtml(entry.protocol_source || "-") +
1977
+ '</td><td>' + (entry.route_error
1978
+ ? '<span class="badge bad">' + escapeHtml(entry.route_error) + '</span>'
1979
+ : '<span class="badge ok">ok</span>') +
1980
+ '</td></tr>'
1981
+ ).join("") + '</tbody></table>';
1982
+ }
1983
+
1984
+ async function loadKeys() {
1985
+ const data = await api("/api/api-keys");
1986
+ const keys = data.keys || [];
1987
+ el("content").innerHTML = (
1988
+ '<div class="page-head"><div><h1>API Keys</h1>' +
1989
+ '<p>Local keys your clients authenticate with</p></div>' +
1990
+ '<button class="primary shrink" id="create-key">Create key</button></div>' +
1991
+ '<div class="card"><h2>Keys</h2>' + keysTable(keys) + '</div>'
1992
+ );
1993
+ el("create-key").addEventListener("click", createKey);
1994
+ }
1995
+
1996
+ function keysTable(keys) {
1997
+ if (!keys.length) return '<div class="empty">No keys configured</div>';
1998
+ return '<table><thead><tr><th>Name</th><th>Value</th><th>State</th>' +
1999
+ '<th>Requests</th><th>Created</th><th></th></tr></thead><tbody>' +
2000
+ keys.map((entry) =>
2001
+ '<tr><td>' + escapeHtml(entry.name) +
2002
+ '</td><td class="mono">' + escapeHtml(entry.display) +
2003
+ '</td><td>' + (entry.enabled
2004
+ ? '<span class="badge ok">enabled</span>'
2005
+ : '<span class="badge bad">disabled</span>') +
2006
+ '</td><td>' + fmt((entry.usage || {}).lifetime
2007
+ ? entry.usage.lifetime.requests : 0) +
2008
+ '</td><td class="mono">' + escapeHtml(
2009
+ entry.created_at ? new Date(entry.created_at).toLocaleDateString() : "-") +
2010
+ '</td><td><button class="ghost" data-toggle="' + escapeHtml(entry.id) +
2011
+ '">Toggle</button> ' +
2012
+ '<button class="ghost" data-delete="' + escapeHtml(entry.id) +
2013
+ '">Delete</button></td></tr>'
2014
+ ).join("") + '</tbody></table>';
2015
+ }
2016
+
2017
+ async function createKey() {
2018
+ const name = prompt("Name for the new API key");
2019
+ if (!name) return;
2020
+ try {
2021
+ const result = await api("/api/api-keys", {
2022
+ method: "POST",
2023
+ body: JSON.stringify({ name }),
2024
+ });
2025
+ toast("Key created: " + result.key);
2026
+ await loadKeys();
2027
+ } catch (error) {
2028
+ toast(error.message, true);
2029
+ }
2030
+ }
2031
+
2032
+ async function loadQuota() {
2033
+ const data = await api("/api/model-quotas");
2034
+ const accounts = data.accounts || [];
2035
+ el("content").innerHTML = (
2036
+ '<div class="page-head"><div><h1>Quota</h1>' +
2037
+ '<p>' + escapeHtml(data.note || "") + '</p></div></div>' +
2038
+ '<div class="card"><h2>Accounts</h2>' + quotaTable(accounts) + '</div>'
2039
+ );
2040
+ }
2041
+
2042
+ function quotaTable(accounts) {
2043
+ if (!accounts.length) return '<div class="empty">No Go keys configured</div>';
2044
+ return '<table><thead><tr><th>Key</th><th>Status</th><th>Window</th>' +
2045
+ '<th>Used</th></tr></thead><tbody>' +
2046
+ accounts.map((entry) => {
2047
+ const windows = entry.windows || {};
2048
+ const rows = Object.keys(windows).map((name) => {
2049
+ const win = windows[name] || {};
2050
+ return "<tr><td>" + escapeHtml(entry.display) + '</td><td>' +
2051
+ escapeHtml(entry.status) + '</td><td>' + escapeHtml(name) +
2052
+ '</td><td>' + pct(win.used_percent || 0) + '</td></tr>';
2053
+ }).join("");
2054
+ return rows || '<tr><td>' + escapeHtml(entry.display) +
2055
+ '</td><td>' + escapeHtml(entry.status) + '</td><td>-</td><td>-</td></tr>';
2056
+ }).join("") + '</tbody></table>';
2057
+ }
2058
+ `;
2059
+
2060
+ // src/webui/script-settings.ts
2061
+ var WEBUI_SETTINGS_PAGES = `
2062
+ async function loadPlayground() {
2063
+ el("content").innerHTML = (
2064
+ '<div class="page-head"><div><h1>Playground</h1>' +
2065
+ '<p>Send a test request through the proxy</p></div></div>' +
2066
+ '<div class="card"><h2>Request</h2>' +
2067
+ '<div class="row">' +
2068
+ '<div class="field"><label>Protocol</label><select id="pg-protocol">' +
2069
+ '<option value="chat">chat</option>' +
2070
+ '<option value="responses">responses</option>' +
2071
+ '<option value="anthropic">anthropic</option>' +
2072
+ '</select></div>' +
2073
+ '<div class="field"><label>Model</label><input id="pg-model" placeholder="model id"></div>' +
2074
+ '</div>' +
2075
+ '<div class="field"><label>Body</label><textarea id="pg-body">' +
2076
+ '{"model":"","messages":[{"role":"user","content":"Hello"}]}' +
2077
+ '</textarea><div class="hint">The model field is filled in from the box above.</div></div>' +
2078
+ '<button class="primary" id="pg-send">Send request</button>' +
2079
+ '</div>' +
2080
+ '<div class="card"><h2>Response</h2><pre id="pg-result" class="mono">' +
2081
+ 'No request sent yet.</pre></div>'
2082
+ );
2083
+ el("pg-send").addEventListener("click", sendPlayground);
2084
+ }
2085
+
2086
+ async function sendPlayground() {
2087
+ const protocol = el("pg-protocol").value;
2088
+ const model = el("pg-model").value.trim();
2089
+ let body;
2090
+ try {
2091
+ body = JSON.parse(el("pg-body").value);
2092
+ } catch {
2093
+ toast("The body must be valid JSON", true);
2094
+ return;
2095
+ }
2096
+ body.model = model;
2097
+ el("pg-result").textContent = "Sending...";
2098
+ try {
2099
+ const result = await api("/api/debug/inference", {
2100
+ method: "POST",
2101
+ body: JSON.stringify({ protocol, request: body }),
2102
+ });
2103
+ el("pg-result").textContent = JSON.stringify(result, null, 2);
2104
+ } catch (error) {
2105
+ el("pg-result").textContent = error.message;
2106
+ toast(error.message, true);
2107
+ }
2108
+ }
2109
+
2110
+ async function loadRotation() {
2111
+ const data = await api("/api/rotation");
2112
+ const groups = data.groups || [];
2113
+ el("content").innerHTML = (
2114
+ '<div class="page-head"><div><h1>Rotation</h1>' +
2115
+ '<p>Model groups that rotate between upstreams</p></div></div>' +
2116
+ groups.map((group) => (
2117
+ '<div class="card"><h2>' + escapeHtml(group.alias || group.id) +
2118
+ ' <span class="badge">current: ' + escapeHtml(group.current || "-") +
2119
+ '</span></h2>' +
2120
+ '<table><thead><tr><th>Model</th><th>State</th><th></th></tr></thead><tbody>' +
2121
+ (group.order || []).map((model) => (
2122
+ '<tr><td class="mono">' + escapeHtml(model) + '</td><td>' +
2123
+ (group.current === model
2124
+ ? '<span class="badge ok">current</span>'
2125
+ : '<span class="badge">idle</span>') +
2126
+ '</td><td><button class="ghost" data-group="' + escapeHtml(group.id) +
2127
+ '" data-model="' + escapeHtml(model) + '">Use</button></td></tr>'
2128
+ )).join("") + '</tbody></table></div>'
2129
+ )).join("")
2130
+ );
2131
+ for (const button of document.querySelectorAll("[data-group]")) {
2132
+ button.addEventListener("click", () => selectRotation(
2133
+ button.dataset.group, button.dataset.model));
2134
+ }
2135
+ }
2136
+
2137
+ async function selectRotation(group, model) {
2138
+ try {
2139
+ await api("/api/rotation/current", {
2140
+ method: "POST",
2141
+ body: JSON.stringify({ group, model }),
2142
+ });
2143
+ toast("Switched to " + model);
2144
+ await loadRotation();
2145
+ } catch (error) {
2146
+ toast(error.message, true);
2147
+ }
2148
+ }
2149
+
2150
+ async function loadConfig() {
2151
+ const config = await api("/api/config");
2152
+ state.config = config;
2153
+ el("content").innerHTML = (
2154
+ '<div class="page-head"><div><h1>Configuration</h1>' +
2155
+ '<p>Upstream lanes, retries and server behavior</p></div>' +
2156
+ '<button class="primary shrink" id="save-config">Save</button></div>' +
2157
+ '<div class="card"><h2>Upstream</h2>' +
2158
+ '<div class="field"><label>Zen base URL</label>' +
2159
+ '<input id="cfg-zen" value="' + escapeHtml(config.upstream.zen) + '"></div>' +
2160
+ '<div class="field"><label>Go base URL</label>' +
2161
+ '<input id="cfg-go" value="' + escapeHtml(config.upstream.go) + '"></div>' +
2162
+ '<div class="field"><label>Anonymous base URL</label>' +
2163
+ '<input id="cfg-anon" value="' + escapeHtml(config.upstream.anonymous || "") + '">' +
2164
+ '</div></div>' +
2165
+ '<div class="card"><h2>Behavior</h2><div class="row">' +
2166
+ '<div class="field"><label>Prefer tier</label><select id="cfg-prefer">' +
2167
+ '<option value="go"' + (config.prefer === "go" ? " selected" : "") + '>go</option>' +
2168
+ '<option value="zen"' + (config.prefer === "zen" ? " selected" : "") + '>zen</option>' +
2169
+ '</select></div>' +
2170
+ '<div class="field"><label>Max attempts</label><input id="cfg-attempts" type="number" value="' +
2171
+ escapeHtml(config.retry.max_attempts) + '"></div>' +
2172
+ '<div class="field"><label>Timeout (seconds)</label><input id="cfg-timeout" type="number" value="' +
2173
+ escapeHtml(config.retry.timeout_seconds) + '"></div>' +
2174
+ '<div class="field"><label>Catalog refresh (seconds)</label>' +
2175
+ '<input id="cfg-refresh" type="number" value="' +
2176
+ escapeHtml(config.models.refresh_seconds) + '"></div>' +
2177
+ '</div>' +
2178
+ '<div class="row"><div class="field"><label>Log level</label><select id="cfg-level">' +
2179
+ ["debug", "info", "warn", "error"].map((level) =>
2180
+ '<option value="' + level + '"' +
2181
+ (config.logging.level === level ? " selected" : "") + '>' + level + '</option>'
2182
+ ).join("") + '</select></div>' +
2183
+ '<div class="field"><label>Log ring size</label><input id="cfg-ring" type="number" value="' +
2184
+ escapeHtml(config.logging.ring_size) + '"></div></div>' +
2185
+ '<div class="field"><label>Free models only</label>' +
2186
+ '<select id="cfg-freeonly"><option value="false"' +
2187
+ (!config.models.free_only ? " selected" : "") + '>no</option>' +
2188
+ '<option value="true"' + (config.models.free_only ? " selected" : "") +
2189
+ '>yes</option></select></div></div>' +
2190
+ (config.restart_required_fields && config.restart_required_fields.length
2191
+ ? '<div class="card"><span class="badge warn">Restart required: ' +
2192
+ escapeHtml(config.restart_required_fields.join(", ")) + '</span></div>'
2193
+ : "")
2194
+ );
2195
+ el("save-config").addEventListener("click", saveConfig);
2196
+ }
2197
+
2198
+ async function saveConfig() {
2199
+ const config = state.config;
2200
+ const body = JSON.parse(JSON.stringify(config));
2201
+ body.upstream.zen = el("cfg-zen").value.trim();
2202
+ body.upstream.go = el("cfg-go").value.trim();
2203
+ body.upstream.anonymous = el("cfg-anon").value.trim();
2204
+ body.prefer = el("cfg-prefer").value;
2205
+ body.retry.max_attempts = Number(el("cfg-attempts").value);
2206
+ body.retry.timeout_seconds = Number(el("cfg-timeout").value);
2207
+ body.models.refresh_seconds = Number(el("cfg-refresh").value);
2208
+ body.models.free_only = el("cfg-freeonly").value === "true";
2209
+ body.logging.level = el("cfg-level").value;
2210
+ body.logging.ring_size = Number(el("cfg-ring").value);
2211
+ delete body.effective;
2212
+ delete body.restart_required_fields;
2213
+ try {
2214
+ await api("/api/config", { method: "PUT", body: JSON.stringify(body) });
2215
+ toast("Configuration saved");
2216
+ await loadConfig();
2217
+ } catch (error) {
2218
+ toast(error.message, true);
2219
+ }
2220
+ }
2221
+
2222
+ async function loadAccount() {
2223
+ el("content").innerHTML = (
2224
+ '<div class="page-head"><div><h1>Account</h1>' +
2225
+ '<p>Change the WebUI credentials</p></div></div>' +
2226
+ '<div class="card"><h2>Credentials</h2>' +
2227
+ '<div class="field"><label>Current password</label>' +
2228
+ '<input id="acc-current" type="password"></div>' +
2229
+ '<div class="field"><label>New username</label>' +
2230
+ '<input id="acc-username" placeholder="leave empty to keep"></div>' +
2231
+ '<div class="field"><label>New password</label>' +
2232
+ '<input id="acc-password" type="password" placeholder="leave empty to keep"></div>' +
2233
+ '<button class="primary" id="acc-save">Update</button></div>'
2234
+ );
2235
+ el("acc-save").addEventListener("click", saveAccount);
2236
+ }
2237
+
2238
+ async function saveAccount() {
2239
+ const body = {
2240
+ current_password: el("acc-current").value,
2241
+ username: el("acc-username").value.trim(),
2242
+ new_password: el("acc-password").value,
2243
+ };
2244
+ try {
2245
+ await api("/api/account", { method: "PUT", body: JSON.stringify(body) });
2246
+ toast("Account updated, please sign in again");
2247
+ showLogin();
2248
+ } catch (error) {
2249
+ toast(error.message, true);
2250
+ }
2251
+ }
2252
+
2253
+ let logSource = null;
2254
+
2255
+ async function loadLogs() {
2256
+ el("content").innerHTML = (
2257
+ '<div class="page-head"><div><h1>Logs</h1>' +
2258
+ '<p>Live structured log stream</p></div></div>' +
2259
+ '<div class="card"><h2>Stream</h2><div id="log-lines"></div></div>'
2260
+ );
2261
+ if (logSource) { logSource.close(); logSource = null; }
2262
+ const source = new EventSource("/api/logs/stream");
2263
+ logSource = source;
2264
+ source.addEventListener("log", (event) => {
2265
+ const entry = JSON.parse(event.data);
2266
+ const container = el("log-lines");
2267
+ if (!container) return;
2268
+ const line = document.createElement("div");
2269
+ line.className = "log-line";
2270
+ line.innerHTML = '<span class="time">' +
2271
+ escapeHtml(new Date(entry.time).toLocaleTimeString()) +
2272
+ '</span><span class="level ' + escapeHtml(entry.level) + '">' +
2273
+ escapeHtml(entry.level) + '</span><span class="message">' +
2274
+ escapeHtml(entry.message) +
2275
+ (entry.component ? ' [' + escapeHtml(entry.component) + ']' : "") +
2276
+ '</span>';
2277
+ container.prepend(line);
2278
+ while (container.children.length > 500) {
2279
+ container.removeChild(container.lastChild);
2280
+ }
2281
+ });
2282
+ source.addEventListener("gap", () => {
2283
+ toast("Some log events were skipped", true);
2284
+ });
2285
+ }
2286
+
2287
+ const LOADERS = {
2288
+ overview: loadOverview,
2289
+ models: loadModels,
2290
+ keys: loadKeys,
2291
+ quota: loadQuota,
2292
+ playground: loadPlayground,
2293
+ rotation: loadRotation,
2294
+ config: loadConfig,
2295
+ account: loadAccount,
2296
+ logs: loadLogs,
2297
+ };
2298
+
2299
+ async function render() {
2300
+ const hash = location.hash.replace("#", "") || "overview";
2301
+ state.page = LOADERS[hash] ? hash : "overview";
2302
+ markActive();
2303
+ try {
2304
+ await LOADERS[state.page]();
2305
+ } catch (error) {
2306
+ el("content").innerHTML = '<div class="empty">' +
2307
+ escapeHtml(error.message) + '</div>';
2308
+ }
2309
+ }
2310
+
2311
+ async function login(event) {
2312
+ event.preventDefault();
2313
+ const username = el("login-user").value;
2314
+ const password = el("login-pass").value;
2315
+ try {
2316
+ const result = await api("/api/auth/login", {
2317
+ method: "POST",
2318
+ body: JSON.stringify({ username, password }),
2319
+ });
2320
+ state.csrf = result.csrf_token;
2321
+ showApp();
2322
+ await render();
2323
+ } catch (error) {
2324
+ toast(error.message, true);
2325
+ }
2326
+ }
2327
+
2328
+ async function boot() {
2329
+ renderNav();
2330
+ window.addEventListener("hashchange", render);
2331
+ el("login-form").addEventListener("submit", login);
2332
+ try {
2333
+ const session = await api("/api/auth/session");
2334
+ state.csrf = session.csrf_token;
2335
+ showApp();
2336
+ await render();
2337
+ } catch {
2338
+ showLogin();
2339
+ }
2340
+ }
2341
+
2342
+ boot();
2343
+ `;
2344
+
2345
+ // src/webui/document.ts
2346
+ var WEBUI_SCRIPT = `${WEBUI_CORE}${WEBUI_MONITOR_PAGES}${WEBUI_SETTINGS_PAGES}`;
2347
+ var WEBUI_BODY = `
2348
+ <div id="login" class="hidden">
2349
+ <div class="card">
2350
+ <h1>OpenCode2API</h1>
2351
+ <p>Sign in to the control panel.</p>
2352
+ <form id="login-form">
2353
+ <div class="field">
2354
+ <label for="login-user">Username</label>
2355
+ <input id="login-user" value="admin" autocomplete="username">
2356
+ </div>
2357
+ <div class="field">
2358
+ <label for="login-pass">Password</label>
2359
+ <input id="login-pass" type="password" autocomplete="current-password">
2360
+ </div>
2361
+ <button class="primary" type="submit" style="width:100%">Sign in</button>
2362
+ </form>
2363
+ </div>
2364
+ </div>
2365
+
2366
+ <div id="app" class="hidden">
2367
+ <div id="layout">
2368
+ <aside id="sidebar">
2369
+ <div id="brand"><span class="dot"></span>OpenCode2API</div>
2370
+ <nav id="nav"></nav>
2371
+ </aside>
2372
+ <main id="content"></main>
2373
+ </div>
2374
+ </div>
2375
+
2376
+ <div id="toast"></div>
2377
+ `;
2378
+ function buildWebUIHTML() {
2379
+ return `<!DOCTYPE html>
2380
+ <html lang="en">
2381
+ <head>
2382
+ <meta charset="utf-8">
2383
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2384
+ <meta name="referrer" content="no-referrer">
2385
+ <title>OpenCode2API Control Panel</title>
2386
+ <style>
2387
+ ${THEME_TOKENS}
2388
+ ${WEBUI_STYLES}
2389
+ </style>
2390
+ </head>
2391
+ <body>
2392
+ ${WEBUI_BODY}
2393
+ <script>
2394
+ ${WEBUI_SCRIPT}
2395
+ </script>
2396
+ </body>
2397
+ </html>
2398
+ `;
2399
+ }
2400
+ var WEBUI_HTML = buildWebUIHTML();
2401
+
2402
+ // src/worker/index.ts
2403
+ var CORS_HEADERS = {
2404
+ "Access-Control-Allow-Origin": "*",
2405
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
2406
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, x-opencode-session, x-opencode-session-id, x-session-affinity, x-session-id, x-opencode-request, x-opencode-project, x-parent-session-id, x-opencode-client, Accept",
2407
+ "Access-Control-Max-Age": "86400"
2408
+ };
2409
+ var DEFAULT_ZEN = "https://opencode.ai/zen";
2410
+ var DEFAULT_GO = "https://opencode.ai/zen/go";
2411
+ var index_default = {
2412
+ async fetch(request, env) {
2413
+ try {
2414
+ return await handle(request, env);
2415
+ } catch (error) {
2416
+ return jsonResponse(
2417
+ {
2418
+ error: {
2419
+ message: error instanceof Error ? error.message : "internal error",
2420
+ type: "server_error",
2421
+ param: null,
2422
+ code: null
2423
+ }
2424
+ },
2425
+ 500
2426
+ );
2427
+ }
2428
+ }
2429
+ };
2430
+ async function handle(request, env) {
2431
+ const url = new URL(request.url);
2432
+ if (request.method === "OPTIONS") {
2433
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
2434
+ }
2435
+ if (url.pathname === "/" || url.pathname === "/index.html") {
2436
+ if (!webuiEnabled(env)) {
2437
+ return jsonResponse({ error: { message: "not found" } }, 404);
2438
+ }
2439
+ return new Response(WEBUI_HTML, {
2440
+ headers: {
2441
+ "Content-Type": "text/html; charset=utf-8",
2442
+ ...CORS_HEADERS
2443
+ }
2444
+ });
2445
+ }
2446
+ if (url.pathname === "/v1/health" || url.pathname === "/healthz") {
2447
+ return jsonResponse({ status: "ok", version: "dev" });
2448
+ }
2449
+ if (url.pathname === "/v1/models") {
2450
+ if (!authorized(request, env)) {
2451
+ return unauthorized();
2452
+ }
2453
+ return modelsResponse(env);
2454
+ }
2455
+ const protocol = protocolFor(url.pathname);
2456
+ if (protocol === void 0) {
2457
+ return jsonResponse({ error: { message: "not found" } }, 404);
2458
+ }
2459
+ if (!authorized(request, env)) {
2460
+ return unauthorized();
2461
+ }
2462
+ const text = await request.text();
2463
+ let payload;
2464
+ try {
2465
+ const parsed = JSON.parse(text);
2466
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2467
+ return errorResponse(protocol, 400, "request body must be a JSON object", "invalid_request_error");
2468
+ }
2469
+ payload = parsed;
2470
+ } catch {
2471
+ return errorResponse(protocol, 400, "request body must be a JSON object", "invalid_request_error");
2472
+ }
2473
+ const model = typeof payload["model"] === "string" ? payload["model"] : "";
2474
+ if (model === "") {
2475
+ return errorResponse(protocol, 400, "model is required", "invalid_request_error");
2476
+ }
2477
+ return forward(request, env, protocol, payload);
2478
+ }
2479
+ async function forward(request, env, protocol, payload) {
2480
+ const ids = deriveRequestIDs(
2481
+ headersToDict(request.headers),
2482
+ payload,
2483
+ "worker"
2484
+ );
2485
+ const tier = pickTier(env);
2486
+ const key = tier === "go" ? firstKey(env.OPENCODE_GO_KEYS) : firstKey(env.OPENCODE_ZEN_KEYS);
2487
+ const baseURL = baseURLFor(env, tier);
2488
+ const body = shapeBody(JSON.stringify(payload), protocol, baseURL);
2489
+ const upstream = buildUpstreamRequest(baseURL, protocol, body, ids, key);
2490
+ const response = await fetch(upstream.url, {
2491
+ method: "POST",
2492
+ headers: upstream.headers,
2493
+ body: upstream.body
2494
+ });
2495
+ const headers = new Headers(response.headers);
2496
+ headers.set("x-request-id", ids.request);
2497
+ for (const [name, value] of Object.entries(CORS_HEADERS)) {
2498
+ headers.set(name, value);
2499
+ }
2500
+ return new Response(response.body, { status: response.status, headers });
2501
+ }
2502
+ function shapeBody(body, protocol, baseURL) {
2503
+ const prepared = prepareAnonymousBody(body, protocol, isV1Upstream(baseURL));
2504
+ return prepared.body;
2505
+ }
2506
+ function pickTier(env) {
2507
+ if (firstKey(env.OPENCODE_GO_KEYS) !== "") {
2508
+ return "go";
2509
+ }
2510
+ return "zen";
2511
+ }
2512
+ function baseURLFor(env, tier) {
2513
+ if (tier === "go") {
2514
+ return env.UPSTREAM_GO ?? DEFAULT_GO;
2515
+ }
2516
+ return env.UPSTREAM_ZEN ?? DEFAULT_ZEN;
2517
+ }
2518
+ function firstKey(value) {
2519
+ if (value === void 0) {
2520
+ return "";
2521
+ }
2522
+ for (const part of value.split(",")) {
2523
+ const key = part.trim();
2524
+ if (key !== "") {
2525
+ return key;
2526
+ }
2527
+ }
2528
+ return "";
2529
+ }
2530
+ function authorized(request, env) {
2531
+ const expected = env.API_KEY ?? "";
2532
+ if (expected === "") {
2533
+ return true;
2534
+ }
2535
+ const header = request.headers.get("x-api-key");
2536
+ if (header !== null && header === expected) {
2537
+ return true;
2538
+ }
2539
+ const authorization = request.headers.get("authorization");
2540
+ if (authorization === null) {
2541
+ return false;
2542
+ }
2543
+ return authorization.toLowerCase().startsWith("bearer ") && authorization.slice(7).trim() === expected;
2544
+ }
2545
+ function webuiEnabled(env) {
2546
+ return (env.WEBUI_ENABLED ?? "true").toLowerCase() !== "false";
2547
+ }
2548
+ async function modelsResponse(env) {
2549
+ const tier = pickTier(env);
2550
+ const baseURL = baseURLFor(env, tier);
2551
+ const key = tier === "go" ? firstKey(env.OPENCODE_GO_KEYS) : firstKey(env.OPENCODE_ZEN_KEYS);
2552
+ try {
2553
+ const response = await fetch(`${baseURL.replace(/\/+$/, "")}/v1/models`, {
2554
+ headers: {
2555
+ Authorization: `Bearer ${key}`,
2556
+ "User-Agent": "opencode/latest/2.0.32/cli",
2557
+ "x-opencode-client": "cli"
2558
+ }
2559
+ });
2560
+ if (!response.ok) {
2561
+ return jsonResponse({ error: { message: "models endpoint failed" } }, 502);
2562
+ }
2563
+ const payload = await response.json();
2564
+ return jsonResponse(payload);
2565
+ } catch {
2566
+ return jsonResponse({ error: { message: "models endpoint unreachable" } }, 502);
2567
+ }
2568
+ }
2569
+ function protocolFor(path) {
2570
+ switch (path) {
2571
+ case "/v1/chat/completions":
2572
+ return "chat";
2573
+ case "/v1/responses":
2574
+ return "responses";
2575
+ case "/v1/messages":
2576
+ return "anthropic";
2577
+ case "/v1/systemone":
2578
+ return "systemone";
2579
+ default:
2580
+ return void 0;
2581
+ }
2582
+ }
2583
+ function headersToDict(headers) {
2584
+ const result = {};
2585
+ headers.forEach((value, name) => {
2586
+ result[name] = value;
2587
+ });
2588
+ return result;
2589
+ }
2590
+ function jsonResponse(body, status = 200) {
2591
+ return new Response(`${JSON.stringify(body)}
2592
+ `, {
2593
+ status,
2594
+ headers: {
2595
+ "Content-Type": "application/json",
2596
+ ...CORS_HEADERS
2597
+ }
2598
+ });
2599
+ }
2600
+ function unauthorized() {
2601
+ return jsonResponse(
2602
+ {
2603
+ error: {
2604
+ message: "invalid API key",
2605
+ type: "authentication_error",
2606
+ param: null,
2607
+ code: null
2608
+ }
2609
+ },
2610
+ 401
2611
+ );
2612
+ }
2613
+ function errorResponse(protocol, status, message, kind) {
2614
+ const envelope = buildErrorEnvelope(protocol, status, message, kind);
2615
+ return jsonResponse(envelope.body, envelope.status);
2616
+ }
2617
+ export {
2618
+ canonicalSessionID,
2619
+ index_default as default,
2620
+ randomID
2621
+ };