@skydiveai/pi-server 0.1.0-beta.1145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2966 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { pino } from "pino";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { getModel } from "@earendil-works/pi-ai";
5
+ import { z } from "zod";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ //#region src/session-registry.ts
9
+ function createMapSessionRegistry() {
10
+ const sessions = /* @__PURE__ */ new Map();
11
+ return {
12
+ get(id) {
13
+ return sessions.get(id) ?? null;
14
+ },
15
+ set(id, session) {
16
+ sessions.set(id, session);
17
+ },
18
+ delete(id) {
19
+ sessions.delete(id);
20
+ }
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/trace-context.ts
25
+ /**
26
+ * Per-request trace context. Protocol entry points wrap their handlers
27
+ * in `runInTraceContext` and `requestLogger` records the request's
28
+ * traceparent here, so anything downstream (the self-trace extension,
29
+ * response headers) can read it without threading it through every
30
+ * call. Lives in its own module so both `logger.ts` and `tracing.ts`
31
+ * can import it without a cycle.
32
+ */
33
+ const traceStorage = new AsyncLocalStorage();
34
+ function runInTraceContext(fn) {
35
+ return traceStorage.run({ traceparent: null }, fn);
36
+ }
37
+ /** No-op when called outside `runInTraceContext`. */
38
+ function setCurrentTraceparent(tp) {
39
+ const store = traceStorage.getStore();
40
+ if (store) store.traceparent = tp;
41
+ }
42
+ function getCurrentTraceparent() {
43
+ return traceStorage.getStore()?.traceparent ?? null;
44
+ }
45
+ /** Extract the trace id (second segment) from a `traceparent` value. */
46
+ function parseTraceId(traceparent) {
47
+ if (!traceparent) return null;
48
+ const parts = traceparent.split("-");
49
+ return parts.length >= 2 ? parts[1] ?? null : null;
50
+ }
51
+ //#endregion
52
+ //#region src/logger.ts
53
+ /**
54
+ * Base logger for the harness. Defaults to JSON line output (one event per
55
+ * line, machine-greppable) so logs from inside the e2b sandbox can be
56
+ * tailed and joined with api/proxy logs by `trace_id`. Set
57
+ * `HARNESS_LOG_FORMAT=pretty` for human-readable output during local dev.
58
+ */
59
+ const isPretty = process.env.HARNESS_LOG_FORMAT === "pretty";
60
+ const logger = pino({
61
+ level: process.env.HARNESS_LOG_LEVEL ?? "info",
62
+ base: { service: process.env.HARNESS_LOG_SERVICE ?? "pi-server" },
63
+ ...isPretty ? { transport: {
64
+ target: "pino-pretty",
65
+ options: {
66
+ colorize: true,
67
+ translateTime: "HH:MM:ss.l"
68
+ }
69
+ } } : {}
70
+ });
71
+ /**
72
+ * W3C Trace Context: `traceparent: 00-<32 hex>-<16 hex>-<2 hex>`.
73
+ * https://www.w3.org/TR/trace-context/#traceparent-header
74
+ *
75
+ * We only support version 00. Anything malformed → mint a fresh root.
76
+ */
77
+ const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
78
+ function hex(bytes) {
79
+ return randomBytes(bytes).toString("hex");
80
+ }
81
+ function newSpanId() {
82
+ return hex(8);
83
+ }
84
+ function newTraceContext() {
85
+ const traceId = hex(16);
86
+ const spanId = newSpanId();
87
+ return {
88
+ traceId,
89
+ spanId,
90
+ parentSpanId: null,
91
+ flags: "01",
92
+ traceparent: `00-${traceId}-${spanId}-01`
93
+ };
94
+ }
95
+ /**
96
+ * Parse an incoming `traceparent` and mint a child span id under the same
97
+ * trace id. The parent span id is preserved so logs can stitch caller and
98
+ * callee. Returns a fresh root context if the header is missing or malformed.
99
+ */
100
+ function deriveTraceContext(headerValue) {
101
+ if (typeof headerValue !== "string") return newTraceContext();
102
+ const m = headerValue.match(TRACEPARENT_RE);
103
+ if (!m) return newTraceContext();
104
+ const [, , traceId, parentSpanId, flags] = m;
105
+ const spanId = newSpanId();
106
+ return {
107
+ traceId,
108
+ spanId,
109
+ parentSpanId,
110
+ flags,
111
+ traceparent: `00-${traceId}-${spanId}-${flags}`
112
+ };
113
+ }
114
+ /**
115
+ * Build a per-request child logger pre-bound with trace context. Every event
116
+ * emitted from `req` should go through this logger so it auto-inherits
117
+ * `trace_id`/`span_id`/`parent_span_id`.
118
+ */
119
+ function requestLogger(request) {
120
+ const trace = deriveTraceContext(request.headers.get("traceparent"));
121
+ setCurrentTraceparent(trace.traceparent);
122
+ return {
123
+ log: logger.child({
124
+ trace_id: trace.traceId,
125
+ span_id: trace.spanId,
126
+ parent_span_id: trace.parentSpanId
127
+ }),
128
+ trace
129
+ };
130
+ }
131
+ //#endregion
132
+ //#region src/protocols/prewarm.ts
133
+ /**
134
+ * Prewarm endpoint — POST to a configured path runs a 1-token chat
135
+ * completion so the first real request doesn't pay session/model
136
+ * cold-start. Invokes the chat-completions handler in-process (no HTTP
137
+ * loopback).
138
+ */
139
+ function createPrewarm({ paths, chatCompletions }) {
140
+ const prewarmPaths = new Set(paths);
141
+ return async (request) => {
142
+ if (prewarmPaths.size === 0) return null;
143
+ const url = new URL(request.url);
144
+ if (request.method !== "POST" || !prewarmPaths.has(url.pathname)) return null;
145
+ const start = performance.now();
146
+ try {
147
+ const warmHeaders = { "content-type": "application/json" };
148
+ const envdToken = request.headers.get("x-e2b-envd-token");
149
+ if (envdToken) warmHeaders["x-e2b-envd-token"] = envdToken;
150
+ await (await chatCompletions(new Request(new URL("/v1/chat/completions", url.origin), {
151
+ method: "POST",
152
+ headers: warmHeaders,
153
+ body: JSON.stringify({
154
+ max_tokens: 1,
155
+ stream: false,
156
+ messages: [{
157
+ role: "user",
158
+ content: "hi"
159
+ }]
160
+ })
161
+ })))?.text();
162
+ const ms = Math.round(performance.now() - start);
163
+ logger.info({
164
+ event: "prewarm_done",
165
+ ms
166
+ }, "harness prewarm complete");
167
+ return Response.json({
168
+ ok: true,
169
+ ms
170
+ });
171
+ } catch (err) {
172
+ logger.error({
173
+ err,
174
+ event: "prewarm_failed"
175
+ }, "harness prewarm failed");
176
+ return Response.json({ error: err instanceof Error ? err.message : "prewarm failed" }, { status: 500 });
177
+ }
178
+ };
179
+ }
180
+ //#endregion
181
+ //#region src/constants.ts
182
+ const DEFAULT_PROVIDER = "anthropic";
183
+ const DEFAULT_MODEL = "claude-opus-4-7";
184
+ const DEFAULT_THINKING_LEVEL = "medium";
185
+ const VALID_THINKING_LEVELS = [
186
+ "off",
187
+ "minimal",
188
+ "low",
189
+ "medium",
190
+ "high",
191
+ "xhigh"
192
+ ];
193
+ const PI_AGENT_DIR = join(homedir(), ".pi", "agent");
194
+ //#endregion
195
+ //#region src/protocols/shared.ts
196
+ /**
197
+ * Provider usage off an `agent_end` event, from the last assistant message that
198
+ * actually carries it — the final billed call saw the full accumulated context,
199
+ * so its counts are the turn's context size. A trailing assistant message
200
+ * without usage (e.g. a tool-only turn) isn't a report, so skip past it rather
201
+ * than give up. Null when no assistant message reported usage, or the report
202
+ * lacks the core counts: we don't fabricate zeros, so sizing falls back to the
203
+ * estimate instead of trusting a fake 0.
204
+ */
205
+ function extractAgentEndUsage(event) {
206
+ const u = [...event.messages ?? []].reverse().find((m) => m?.role === "assistant" && m?.usage)?.usage;
207
+ if (!u || u.input == null || u.output == null) return null;
208
+ return {
209
+ input: u.input,
210
+ output: u.output,
211
+ cacheRead: u.cacheRead ?? 0,
212
+ cacheWrite: u.cacheWrite ?? 0,
213
+ totalTokens: u.totalTokens ?? 0
214
+ };
215
+ }
216
+ const PER_REQUEST_API_KEY_HEADERS = [{
217
+ header: "x-anthropic-api-key",
218
+ provider: "anthropic"
219
+ }, {
220
+ header: "x-openai-api-key",
221
+ provider: "openai"
222
+ }];
223
+ function resolvePerRequestApiKeys(request) {
224
+ const out = {};
225
+ for (const { header, provider } of PER_REQUEST_API_KEY_HEADERS) {
226
+ const v = request.headers.get(header);
227
+ if (v) out[provider] = v;
228
+ }
229
+ if (!out.anthropic) {
230
+ const auth = request.headers.get("authorization");
231
+ if (auth && /^Bearer\s+/i.test(auth)) out.anthropic = auth.replace(/^Bearer\s+/i, "");
232
+ }
233
+ return out;
234
+ }
235
+ function parseSessionId(request) {
236
+ const raw = request.headers.get("x-session-id");
237
+ if (!raw) return {
238
+ sessionId: randomUUID(),
239
+ source: "generated",
240
+ invalid: false
241
+ };
242
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) return {
243
+ sessionId: raw,
244
+ source: "header",
245
+ invalid: false
246
+ };
247
+ return {
248
+ sessionId: randomUUID(),
249
+ source: "generated",
250
+ invalid: true
251
+ };
252
+ }
253
+ function parseShellEnv(request) {
254
+ const raw = request.headers.get("x-shell-env");
255
+ if (!raw) return null;
256
+ try {
257
+ const parsed = JSON.parse(raw);
258
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
259
+ const env = {};
260
+ for (const [k, v] of Object.entries(parsed)) if (typeof v === "string") env[k] = v;
261
+ return Object.keys(env).length > 0 ? env : null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+ function jsonResponse(body, status = 200) {
267
+ return new Response(JSON.stringify(body), {
268
+ status,
269
+ headers: { "Content-Type": "application/json" }
270
+ });
271
+ }
272
+ function jsonError(status, message) {
273
+ return jsonResponse({ error: { message } }, status);
274
+ }
275
+ const SSE_KEEPALIVE_INTERVAL_MS = 15e3;
276
+ function createSSEResponse(handler) {
277
+ const encoder = new TextEncoder();
278
+ let controller;
279
+ const stream = new ReadableStream({ start(ctrl) {
280
+ controller = ctrl;
281
+ } });
282
+ const writer = {
283
+ write(chunk) {
284
+ try {
285
+ controller.enqueue(encoder.encode(chunk));
286
+ } catch {}
287
+ },
288
+ close() {
289
+ try {
290
+ controller.close();
291
+ } catch {}
292
+ }
293
+ };
294
+ const keepalive = setInterval(() => {
295
+ writer.write(": keepalive\n\n");
296
+ }, SSE_KEEPALIVE_INTERVAL_MS);
297
+ handler(writer).then(() => {
298
+ clearInterval(keepalive);
299
+ try {
300
+ controller.close();
301
+ } catch {}
302
+ }, (err) => {
303
+ logger.error({ err }, "SSE handler crashed — stream closed without response");
304
+ clearInterval(keepalive);
305
+ try {
306
+ controller.close();
307
+ } catch {}
308
+ });
309
+ return new Response(stream, {
310
+ status: 200,
311
+ headers: {
312
+ "Content-Type": "text/event-stream",
313
+ "Cache-Control": "no-cache, no-transform",
314
+ Connection: "keep-alive",
315
+ "X-Accel-Buffering": "no"
316
+ }
317
+ });
318
+ }
319
+ function resolveModel(modelString, log) {
320
+ const raw = typeof modelString === "string" && modelString.trim() ? modelString.trim() : DEFAULT_MODEL;
321
+ let provider = DEFAULT_PROVIDER;
322
+ let name = raw;
323
+ const sep = raw.match(/[:/]/);
324
+ if (sep) [provider, name] = raw.split(sep[0], 2);
325
+ const found = getModel(provider, name);
326
+ if (found) return found;
327
+ log.warn({
328
+ event: "model_unknown",
329
+ requested_provider: provider,
330
+ requested_model: name,
331
+ fallback_provider: DEFAULT_PROVIDER,
332
+ fallback_model: DEFAULT_MODEL
333
+ }, "unknown model, falling back to default");
334
+ return getModel(DEFAULT_PROVIDER, DEFAULT_MODEL);
335
+ }
336
+ const contextWindowSchema = z.number().int().positive();
337
+ function applyContextWindowOverride(model, contextWindow) {
338
+ const parsed = contextWindowSchema.safeParse(contextWindow);
339
+ if (!parsed.success) return model;
340
+ return {
341
+ ...model,
342
+ contextWindow: parsed.data
343
+ };
344
+ }
345
+ function resolveThinkingLevel(input) {
346
+ if (typeof input === "string" && VALID_THINKING_LEVELS.includes(input)) return input;
347
+ return DEFAULT_THINKING_LEVEL;
348
+ }
349
+ async function fetchImageAsBase64({ url, userAgent }) {
350
+ const ctrl = new AbortController();
351
+ const timeout = setTimeout(() => ctrl.abort(), 15e3);
352
+ try {
353
+ const r = await fetch(url, {
354
+ headers: { "user-agent": userAgent || "pi-server/0.1" },
355
+ redirect: "follow",
356
+ signal: ctrl.signal
357
+ });
358
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
359
+ return {
360
+ type: "image",
361
+ mimeType: r.headers.get("content-type")?.split(";")[0]?.trim() || "image/png",
362
+ data: Buffer.from(await r.arrayBuffer()).toString("base64")
363
+ };
364
+ } finally {
365
+ clearTimeout(timeout);
366
+ }
367
+ }
368
+ async function runConversation({ session, prompt, images, log, postPrompt }) {
369
+ await session.prompt(prompt, images.length ? { images } : void 0);
370
+ if (postPrompt) await postPrompt({
371
+ session,
372
+ log
373
+ });
374
+ }
375
+ /**
376
+ * Hard-stop the in-flight turn for a session. Shared by every protocol's
377
+ * `/:id/abort` route: a cancel signals the stop explicitly instead of relying
378
+ * on a dropped connection. `session.abort()` interrupts the turn and resolves
379
+ * once the agent is idle, so a 200 means the agent has actually stopped (404 =
380
+ * nothing running to stop).
381
+ */
382
+ async function handleAbort({ request, sessionId, registry }) {
383
+ const { log } = requestLogger(request);
384
+ const session = registry.get(sessionId);
385
+ if (!session) return jsonError(404, "session not found or already completed");
386
+ await session.abort();
387
+ log.info({
388
+ event: "abort_accepted",
389
+ session_id: sessionId
390
+ }, "session aborted");
391
+ return jsonResponse({ ok: true });
392
+ }
393
+ //#endregion
394
+ //#region src/protocols/a2a.ts
395
+ /**
396
+ * A2A (Agent-to-Agent) protocol handler.
397
+ *
398
+ * Exports an AgentExecutor + agent card builder for use with the
399
+ * @a2a-js/sdk server. The SDK's Express restHandler handles all
400
+ * routing and proto-JSON serialization.
401
+ *
402
+ * @see https://google.github.io/A2A/specification/
403
+ */
404
+ async function extractParts(parts, log) {
405
+ let text = "";
406
+ const images = [];
407
+ for (const part of parts) {
408
+ if (part.text != null) {
409
+ text += part.text;
410
+ continue;
411
+ }
412
+ if (part.raw != null && part.mediaType?.startsWith("image/")) {
413
+ images.push({
414
+ type: "image",
415
+ mimeType: part.mediaType,
416
+ data: part.raw
417
+ });
418
+ continue;
419
+ }
420
+ if (part.url != null && part.mediaType?.startsWith("image/")) {
421
+ try {
422
+ images.push(await fetchImageAsBase64({ url: part.url }));
423
+ } catch (err) {
424
+ log.warn({
425
+ event: "a2a_image_fetch_failed",
426
+ url: part.url,
427
+ err
428
+ }, "image fetch failed");
429
+ text += `\n[image fetch failed: ${part.url}]\n`;
430
+ }
431
+ continue;
432
+ }
433
+ if (part.data != null) text += `\n${JSON.stringify(part.data)}\n`;
434
+ }
435
+ return {
436
+ text: text.trim(),
437
+ images
438
+ };
439
+ }
440
+ function createAgentExecutor(options) {
441
+ const onSessionSetup = options.onSessionSetup ?? (() => {});
442
+ const postPrompt = options.postPrompt;
443
+ const sessions = /* @__PURE__ */ new Map();
444
+ const taskSessions = /* @__PURE__ */ new Map();
445
+ const executor = {
446
+ async execute(requestContext, eventBus) {
447
+ const { log } = requestLogger(new Request("http://localhost/a2a"));
448
+ const userMessage = requestContext.userMessage;
449
+ const { text: prompt, images } = await extractParts(userMessage.parts ?? userMessage.content ?? [], log);
450
+ const taskId = requestContext.taskId;
451
+ const contextId = requestContext.contextId;
452
+ eventBus.publish({
453
+ kind: "task",
454
+ id: taskId,
455
+ contextId,
456
+ status: { state: "working" }
457
+ });
458
+ if (!prompt && images.length === 0) {
459
+ eventBus.publish({
460
+ kind: "status-update",
461
+ taskId,
462
+ contextId,
463
+ status: {
464
+ state: "failed",
465
+ message: {
466
+ kind: "message",
467
+ messageId: randomUUID(),
468
+ role: "agent",
469
+ parts: [{
470
+ kind: "text",
471
+ text: "No extractable content"
472
+ }]
473
+ }
474
+ }
475
+ });
476
+ eventBus.finished();
477
+ return;
478
+ }
479
+ const existing = sessions.get(contextId);
480
+ if (existing) {
481
+ log.info({
482
+ event: "a2a_steer",
483
+ contextId,
484
+ taskId
485
+ }, "steering existing session");
486
+ await existing.session.steer(prompt, images.length > 0 ? images : void 0);
487
+ eventBus.finished();
488
+ return;
489
+ }
490
+ const cwd = options.cwd ?? process.cwd();
491
+ const sessionResult = await options.createSession({
492
+ cwd,
493
+ sessionId: randomUUID(),
494
+ perRequestApiKeys: {},
495
+ systemPromptOverride: null,
496
+ shellEnv: null,
497
+ model: resolveModel(void 0, log),
498
+ thinkingLevel: null,
499
+ log,
500
+ onSessionSetup,
501
+ prepare: null
502
+ });
503
+ const { session } = sessionResult;
504
+ sessions.set(contextId, sessionResult);
505
+ taskSessions.set(taskId, sessionResult);
506
+ let responseText = "";
507
+ session.subscribe((event) => {
508
+ const ev = event;
509
+ if (ev.type === "message_update" && ev.assistantMessageEvent?.type === "text_delta") {
510
+ responseText += ev.assistantMessageEvent.delta ?? "";
511
+ eventBus.publish({
512
+ kind: "status-update",
513
+ taskId,
514
+ contextId,
515
+ status: {
516
+ state: "working",
517
+ message: {
518
+ kind: "message",
519
+ messageId: randomUUID(),
520
+ role: "agent",
521
+ parts: [{
522
+ kind: "text",
523
+ text: responseText
524
+ }]
525
+ }
526
+ }
527
+ });
528
+ }
529
+ if (ev.type === "tool_execution_end") {
530
+ const resultText = Array.isArray(ev.result?.content) ? ev.result.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : typeof ev.result === "string" ? ev.result : "";
531
+ eventBus.publish({
532
+ kind: "artifact-update",
533
+ taskId,
534
+ contextId,
535
+ artifact: {
536
+ artifactId: randomUUID(),
537
+ name: String(ev.toolName ?? "tool_result"),
538
+ parts: [{
539
+ kind: "data",
540
+ data: {
541
+ toolCallId: ev.toolCallId,
542
+ toolName: ev.toolName,
543
+ result: resultText,
544
+ isError: ev.isError ?? false
545
+ }
546
+ }]
547
+ },
548
+ lastChunk: true
549
+ });
550
+ }
551
+ });
552
+ try {
553
+ await runConversation({
554
+ session,
555
+ prompt,
556
+ images,
557
+ log,
558
+ postPrompt
559
+ });
560
+ } catch (err) {
561
+ log.error({
562
+ event: "a2a_executor_error",
563
+ err
564
+ }, "agent execution error");
565
+ } finally {
566
+ sessions.delete(contextId);
567
+ taskSessions.delete(taskId);
568
+ }
569
+ eventBus.publish({
570
+ kind: "status-update",
571
+ taskId,
572
+ contextId,
573
+ status: {
574
+ state: "completed",
575
+ message: {
576
+ kind: "message",
577
+ messageId: randomUUID(),
578
+ role: "agent",
579
+ parts: [{
580
+ kind: "text",
581
+ text: responseText || "(no text output)"
582
+ }]
583
+ }
584
+ }
585
+ });
586
+ eventBus.finished();
587
+ },
588
+ async cancelTask(taskId, eventBus) {
589
+ await taskSessions.get(taskId)?.session.abort();
590
+ eventBus.finished();
591
+ }
592
+ };
593
+ return {
594
+ ...executor,
595
+ execute: (requestContext, eventBus) => runInTraceContext(() => executor.execute(requestContext, eventBus))
596
+ };
597
+ }
598
+ function buildAgentCard(baseUrl, overrides) {
599
+ const o = overrides ?? {};
600
+ return {
601
+ name: o.name ?? "Pi Agent",
602
+ description: o.description ?? "A pi coding agent.",
603
+ url: baseUrl,
604
+ version: o.version ?? "0.1.0",
605
+ protocolVersion: "1.0",
606
+ preferredTransport: "HTTP+JSON",
607
+ additionalInterfaces: [{
608
+ url: baseUrl,
609
+ transport: "HTTP+JSON"
610
+ }],
611
+ capabilities: {
612
+ streaming: true,
613
+ pushNotifications: false
614
+ },
615
+ defaultInputModes: ["text/plain"],
616
+ defaultOutputModes: ["text/plain"],
617
+ skills: o.skills ?? [{
618
+ id: "code-generation",
619
+ name: "Code Generation",
620
+ description: "Generate, edit, and refactor code."
621
+ }, {
622
+ id: "tool-use",
623
+ name: "Tool Use",
624
+ description: "Execute shell commands, read/write files, and use MCP tools."
625
+ }]
626
+ };
627
+ }
628
+ //#endregion
629
+ //#region src/model-spec.ts
630
+ /**
631
+ * Runtime mirror of pi-ai's `KnownProvider` union (which is type-only). The
632
+ * `satisfies` keeps every member valid against the type; new pi-ai providers
633
+ * are admitted by adding them here.
634
+ */
635
+ const KNOWN_PI_PROVIDERS = [
636
+ "amazon-bedrock",
637
+ "anthropic",
638
+ "google",
639
+ "google-vertex",
640
+ "openai",
641
+ "azure-openai-responses",
642
+ "openai-codex",
643
+ "deepseek",
644
+ "github-copilot",
645
+ "xai",
646
+ "groq",
647
+ "cerebras",
648
+ "openrouter",
649
+ "vercel-ai-gateway",
650
+ "zai",
651
+ "mistral",
652
+ "minimax",
653
+ "minimax-cn",
654
+ "moonshotai",
655
+ "moonshotai-cn",
656
+ "huggingface",
657
+ "fireworks",
658
+ "opencode",
659
+ "opencode-go",
660
+ "kimi-coding",
661
+ "cloudflare-workers-ai",
662
+ "cloudflare-ai-gateway",
663
+ "xiaomi",
664
+ "xiaomi-token-plan-cn",
665
+ "xiaomi-token-plan-ams",
666
+ "xiaomi-token-plan-sgp"
667
+ ];
668
+ /**
669
+ * Mirrors pi-ai's `ThinkingLevelMap`. Each key is genuinely tri-state:
670
+ * absent = provider default, `null` = level unsupported, string = the
671
+ * provider-specific value for that level.
672
+ */
673
+ const thinkingLevelMapSchema = z.object({
674
+ off: z.string().nullable().optional(),
675
+ minimal: z.string().nullable().optional(),
676
+ low: z.string().nullable().optional(),
677
+ medium: z.string().nullable().optional(),
678
+ high: z.string().nullable().optional(),
679
+ xhigh: z.string().nullable().optional()
680
+ });
681
+ /**
682
+ * Per-API compat overrides, mirroring pi-ai's compat interfaces. Every field
683
+ * is genuinely tri-state: absent defers to pi-ai's baseUrl auto-detection
684
+ * (`detectCompat`), present pins the knob. Unknown keys are stripped rather
685
+ * than rejected so an already-shipped server tolerates knobs added for newer
686
+ * models instead of kicking the whole spec back to registry resolution.
687
+ */
688
+ const openaiCompletionsCompatSchema = z.object({
689
+ supportsReasoningEffort: z.boolean().optional(),
690
+ requiresThinkingAsText: z.boolean().optional(),
691
+ thinkingFormat: z.enum([
692
+ "openai",
693
+ "openrouter",
694
+ "deepseek",
695
+ "zai",
696
+ "qwen",
697
+ "qwen-chat-template"
698
+ ]).optional(),
699
+ supportsStrictMode: z.boolean().optional(),
700
+ maxTokensField: z.enum(["max_completion_tokens", "max_tokens"]).optional(),
701
+ cacheControlFormat: z.literal("anthropic").optional()
702
+ }).strip();
703
+ const anthropicMessagesCompatSchema = z.object({
704
+ supportsEagerToolInputStreaming: z.boolean().optional(),
705
+ supportsLongCacheRetention: z.boolean().optional()
706
+ }).strip();
707
+ const baseSpecShape = {
708
+ /** Exact model id the serving endpoint expects. */
709
+ id: z.string().min(1),
710
+ /** Display name shown in pi surfaces and session logs. */
711
+ name: z.string().min(1),
712
+ /** pi-ai provider key — also the auth-storage key for `apiKey`. */
713
+ provider: z.enum(KNOWN_PI_PROVIDERS),
714
+ /** Provider API root, e.g. "https://openrouter.ai/api/v1". */
715
+ baseUrl: z.string().url(),
716
+ reasoning: z.boolean(),
717
+ thinkingLevelMap: thinkingLevelMapSchema.optional(),
718
+ input: z.array(z.enum(["text", "image"])).nonempty(),
719
+ contextWindow: z.number().int().positive(),
720
+ maxTokens: z.number().int().positive(),
721
+ /**
722
+ * Optional runtime API key for `provider`, installed as a per-request key
723
+ * so the model registry's auth lookup succeeds. Deployments that resolve
724
+ * real credentials elsewhere (e.g. at an egress proxy) can pass whatever
725
+ * placeholder that layer recognizes — the value is opaque to this server.
726
+ */
727
+ apiKey: z.string().min(1).optional()
728
+ };
729
+ const modelSpecSchema = z.discriminatedUnion("api", [z.object({
730
+ ...baseSpecShape,
731
+ api: z.literal("openai-completions"),
732
+ compat: openaiCompletionsCompatSchema.optional()
733
+ }), z.object({
734
+ ...baseSpecShape,
735
+ api: z.literal("anthropic-messages"),
736
+ compat: anthropicMessagesCompatSchema.optional()
737
+ })]);
738
+ /**
739
+ * Parse a request-body `x_model`. Null when absent or invalid — never a
740
+ * request rejection, so a malformed spec degrades to the legacy body-model
741
+ * resolution instead of failing the turn.
742
+ */
743
+ function parseModelSpec(input, log) {
744
+ if (input == null) return null;
745
+ const parsed = modelSpecSchema.safeParse(input);
746
+ if (!parsed.success) {
747
+ log.warn({
748
+ event: "x_model_invalid",
749
+ issues: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
750
+ }, "ignoring invalid x_model, falling back to body model");
751
+ return null;
752
+ }
753
+ return parsed.data;
754
+ }
755
+ function buildModelFromSpec(spec) {
756
+ const base = {
757
+ id: spec.id,
758
+ name: spec.name,
759
+ provider: spec.provider,
760
+ baseUrl: spec.baseUrl,
761
+ reasoning: spec.reasoning,
762
+ ...spec.thinkingLevelMap ? { thinkingLevelMap: spec.thinkingLevelMap } : {},
763
+ input: spec.input,
764
+ cost: {
765
+ input: 0,
766
+ output: 0,
767
+ cacheRead: 0,
768
+ cacheWrite: 0
769
+ },
770
+ contextWindow: spec.contextWindow,
771
+ maxTokens: spec.maxTokens
772
+ };
773
+ switch (spec.api) {
774
+ case "openai-completions": return {
775
+ ...base,
776
+ api: spec.api,
777
+ ...spec.compat ? { compat: spec.compat } : {}
778
+ };
779
+ case "anthropic-messages": return {
780
+ ...base,
781
+ api: spec.api,
782
+ ...spec.compat ? { compat: spec.compat } : {}
783
+ };
784
+ default: throw new Error(`Unhandled x_model api: ${String(spec)}`);
785
+ }
786
+ }
787
+ /**
788
+ * Resolve the model a request should run on: a valid `x_model` wins,
789
+ * anything else falls back to registry resolution of the body model. Shared
790
+ * by every transport so they honor the spec identically.
791
+ */
792
+ function resolveRequestModel({ modelInput, modelSpecInput, defaultModel, log }) {
793
+ const modelSpec = parseModelSpec(modelSpecInput, log);
794
+ return {
795
+ modelSpec,
796
+ model: modelSpec ? buildModelFromSpec(modelSpec) : resolveModel(modelInput ?? defaultModel, log)
797
+ };
798
+ }
799
+ /**
800
+ * Install the spec's runtime API key as the per-request key for its
801
+ * provider. A caller-supplied key for the same provider (request headers)
802
+ * always wins.
803
+ */
804
+ function withSpecApiKey(keys, modelSpec) {
805
+ if (!modelSpec?.apiKey || keys[modelSpec.provider]) return keys;
806
+ return {
807
+ ...keys,
808
+ [modelSpec.provider]: modelSpec.apiKey
809
+ };
810
+ }
811
+ //#endregion
812
+ //#region src/protocols/model-provider-error.ts
813
+ function numericStatus(err) {
814
+ const s = err.status ?? err.statusCode;
815
+ return typeof s === "number" ? s : null;
816
+ }
817
+ function messageText(err) {
818
+ if (err instanceof Error) return err.message;
819
+ if (typeof err === "string") return err;
820
+ const m = err?.message;
821
+ return typeof m === "string" ? m : String(err);
822
+ }
823
+ function providerErrorType(err) {
824
+ const e = err.error;
825
+ if (e && typeof e === "object" && "type" in e) {
826
+ const t = e.type;
827
+ if (typeof t === "string") return t;
828
+ }
829
+ return null;
830
+ }
831
+ /**
832
+ * A stable, machine-readable code when the provider supplies one — preferred
833
+ * over message text since wording drifts. Covers OpenAI / Vercel AI Gateway
834
+ * (`error.code`, e.g. `context_length_exceeded`, `rate_limit_exceeded`) and
835
+ * OpenRouter (`error.metadata.error_type`, its canonical vocabulary). Anthropic
836
+ * and Gemini have no per-overflow code, so they fall through to the message
837
+ * regex; Gemini's `error.status` (e.g. `INVALID_ARGUMENT`) is too generic to map
838
+ * on its own.
839
+ */
840
+ function machineCode(err) {
841
+ const e = err.error;
842
+ if (!e || typeof e !== "object") return null;
843
+ const code = e.code;
844
+ if (typeof code === "string") return code;
845
+ const meta = e.metadata;
846
+ if (meta && typeof meta === "object") {
847
+ const errorType = meta.error_type;
848
+ if (typeof errorType === "string") return errorType;
849
+ }
850
+ return null;
851
+ }
852
+ function providerId(err, message) {
853
+ if (typeof err.provider === "string") return err.provider;
854
+ if (/anthropic|claude/i.test(message)) return "anthropic";
855
+ if (/openai|gpt-/i.test(message)) return "openai";
856
+ return null;
857
+ }
858
+ /**
859
+ * A context-overflow message from any provider. Verified shapes (see research):
860
+ * - Anthropic: "prompt is too long: 1001955 tokens > 1000000 maximum"
861
+ * - Anthropic (1M beta): "Prompt exceeds maximum token limit (191989 > 180000)"
862
+ * - OpenAI / Vercel AI Gateway: "This model's maximum context length is N tokens..."
863
+ * - OpenRouter: "This endpoint's maximum context length is N tokens..."
864
+ * - Gemini: "The input token count (N) exceeds the maximum number of tokens allowed (M)"
865
+ */
866
+ const CONTEXT_OVERFLOW_MESSAGE = /prompt is too long|context[_ ]length[_ ]exceeded|maximum context length|exceeds maximum token limit|input token count|too many tokens|exceeds the maximum/i;
867
+ /**
868
+ * Map a provider error to a stable code. Prefers the provider's machine code
869
+ * (OpenAI `error.code`, OpenRouter `error.metadata.error_type`) over message
870
+ * text, then falls back to status + message regex for providers that don't
871
+ * carry one (Anthropic, Gemini). Context overflow drives reactive compaction,
872
+ * so it's matched first and most specifically.
873
+ */
874
+ function classifyCode(status, message, code) {
875
+ if (code === "context_length_exceeded" || CONTEXT_OVERFLOW_MESSAGE.test(message)) return "context_length_exceeded";
876
+ if (status === 402 || code === "insufficient_credits" || code === "insufficient_quota" || /insufficient credits|requires more credits|credit balance is too low|payment required|purchase more credits/i.test(message)) return "provider_out_of_credits";
877
+ if (status === 429 || code === "rate_limit_exceeded" || /rate[_ ]limit|too many requests|resource[_ ]exhausted/i.test(message)) return "rate_limited";
878
+ if (status === 529 || code === "provider_overloaded" || /overloaded/i.test(message)) return "provider_overloaded";
879
+ if (status === 503 || status === 504 || code === "provider_unavailable" || /no healthy upstream|upstream request timeout|stream timeout|service unavailable|unavailable|gateway/i.test(message)) return "provider_unavailable";
880
+ if (code === "content_filter" || /finish_reason:\s*content_filter|content[_ ]filter|content[_ ]management[_ ]policy/i.test(message)) return "content_filter";
881
+ return "provider_error";
882
+ }
883
+ /**
884
+ * True when the thrown error looks like it originated from a model-provider
885
+ * call (it carries an HTTP status or a provider-shaped error body) rather than
886
+ * an arbitrary harness-internal throw. Conservative: when unsure, returns false
887
+ * so the caller keeps the existing generic handling.
888
+ */
889
+ function isModelProviderError(err) {
890
+ if (err === null || typeof err !== "object") return false;
891
+ const e = err;
892
+ if (numericStatus(e) !== null) return true;
893
+ if (e.error && typeof e.error === "object") return true;
894
+ const msg = messageText(err);
895
+ return /prompt is too long|context[_ ]length[_ ]exceeded|exceeds maximum token limit|input token count|rate[_ ]limit|overloaded|no healthy upstream|upstream request timeout|insufficient credits|requires more credits|credit balance is too low|payment required|purchase more credits|finish_reason:\s*content_filter|content[_ ]filter|content[_ ]management[_ ]policy/i.test(msg);
896
+ }
897
+ /** Build the structured, forwardable error from a thrown model-provider error. */
898
+ function toModelProviderError(err) {
899
+ const e = err ?? {};
900
+ const status = numericStatus(e);
901
+ const message = messageText(err);
902
+ return {
903
+ code: classifyCode(status, message, machineCode(e)),
904
+ message,
905
+ provider: providerId(e, message),
906
+ type: providerErrorType(e),
907
+ upstreamStatus: status
908
+ };
909
+ }
910
+ //#endregion
911
+ //#region src/protocols/anthropic-messages.ts
912
+ /**
913
+ * Anthropic Messages API–compatible protocol handler.
914
+ *
915
+ * Catch-all: returns Response on match, null on no match.
916
+ * Zero server-framework deps — just Web Standard Request/Response.
917
+ *
918
+ * Routes:
919
+ * POST /v1/messages
920
+ * POST /v1/messages/:id/abort
921
+ *
922
+ * @see https://docs.anthropic.com/en/api/messages
923
+ */
924
+ function sseEvent(writer, event, data) {
925
+ writer.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
926
+ }
927
+ async function extractContent$1(content, log) {
928
+ if (typeof content === "string") return {
929
+ text: content,
930
+ images: []
931
+ };
932
+ if (!Array.isArray(content)) return {
933
+ text: "",
934
+ images: []
935
+ };
936
+ let text = "";
937
+ const images = [];
938
+ for (const block of content) {
939
+ if (block?.type === "text") {
940
+ text += block.text ?? "";
941
+ continue;
942
+ }
943
+ if (block?.type === "image") {
944
+ const src = block.source;
945
+ if (src?.type === "base64" && src.media_type && src.data) images.push({
946
+ type: "image",
947
+ mimeType: src.media_type,
948
+ data: src.data
949
+ });
950
+ else if (src?.type === "url" && src.url) try {
951
+ images.push(await fetchImageAsBase64({ url: src.url }));
952
+ } catch (err) {
953
+ log.warn({
954
+ event: "image_fetch_failed",
955
+ url: src.url,
956
+ err
957
+ }, "image fetch failed");
958
+ text += `\n[image fetch failed: ${src.url}]\n`;
959
+ }
960
+ continue;
961
+ }
962
+ }
963
+ return {
964
+ text,
965
+ images
966
+ };
967
+ }
968
+ function extractSystemPrompt(system) {
969
+ if (typeof system === "string") return system;
970
+ if (!Array.isArray(system)) return "";
971
+ return system.filter((b) => b?.type === "text").map((b) => b.text ?? "").join("\n\n");
972
+ }
973
+ async function loadHistoryIntoSession$1({ sm, messages, upToIdxExclusive, modelName, log }) {
974
+ const toolCallNames = /* @__PURE__ */ new Map();
975
+ for (let i = 0; i < upToIdxExclusive; i++) {
976
+ const m = messages[i];
977
+ if (!m) continue;
978
+ if (m.role === "user") {
979
+ const { text, images } = await extractContent$1(m.content, log);
980
+ const content = images.length > 0 ? [...text ? [{
981
+ type: "text",
982
+ text
983
+ }] : [], ...images] : text;
984
+ if (typeof content === "string" && content.length > 0 || Array.isArray(content) && content.length > 0) sm.appendMessage({
985
+ role: "user",
986
+ content,
987
+ timestamp: Date.now()
988
+ });
989
+ continue;
990
+ }
991
+ if (m.role === "assistant") {
992
+ const contentArr = [];
993
+ const blocks = Array.isArray(m.content) ? m.content : [];
994
+ for (const b of blocks) if (b?.type === "text" && b.text) contentArr.push({
995
+ type: "text",
996
+ text: b.text
997
+ });
998
+ else if (b?.type === "tool_use") {
999
+ contentArr.push({
1000
+ type: "toolCall",
1001
+ id: b.id,
1002
+ name: b.name,
1003
+ arguments: b.input ?? {}
1004
+ });
1005
+ toolCallNames.set(b.id, b.name);
1006
+ }
1007
+ if (contentArr.length === 0) continue;
1008
+ sm.appendMessage({
1009
+ role: "assistant",
1010
+ content: contentArr,
1011
+ api: "anthropic-messages",
1012
+ provider: "anthropic",
1013
+ model: modelName,
1014
+ usage: {
1015
+ input: 0,
1016
+ output: 0,
1017
+ cacheRead: 0,
1018
+ cacheWrite: 0,
1019
+ totalTokens: 0,
1020
+ cost: {
1021
+ input: 0,
1022
+ output: 0,
1023
+ cacheRead: 0,
1024
+ cacheWrite: 0,
1025
+ total: 0
1026
+ }
1027
+ },
1028
+ stopReason: contentArr.some((c) => c.type === "toolCall") ? "toolUse" : "stop",
1029
+ timestamp: Date.now()
1030
+ });
1031
+ continue;
1032
+ }
1033
+ }
1034
+ }
1035
+ async function handleMessages(request, ctx, registry) {
1036
+ const { log, trace } = requestLogger(request);
1037
+ let parsed;
1038
+ try {
1039
+ parsed = JSON.parse(await request.text());
1040
+ } catch {
1041
+ return jsonError(400, "invalid json");
1042
+ }
1043
+ const { messages, stream = false, model: modelInput, system, x_model: modelSpecInput } = parsed ?? {};
1044
+ if (!Array.isArray(messages) || messages.length === 0) return jsonError(400, "messages array required");
1045
+ let lastUserIdx = -1;
1046
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "user") {
1047
+ lastUserIdx = i;
1048
+ break;
1049
+ }
1050
+ if (lastUserIdx < 0) return jsonError(400, "no user message");
1051
+ const { text: prompt, images } = await extractContent$1(messages[lastUserIdx].content, log);
1052
+ if (!prompt && images.length === 0) return jsonError(400, "user message has no content");
1053
+ const { model, modelSpec } = resolveRequestModel({
1054
+ modelInput,
1055
+ modelSpecInput,
1056
+ defaultModel: ctx.defaultModel,
1057
+ log
1058
+ });
1059
+ const modelName = model.id ?? "claude-opus-4-7";
1060
+ const baseSystemPrompt = extractSystemPrompt(system);
1061
+ const { sessionId } = parseSessionId(request);
1062
+ const shellEnv = parseShellEnv(request);
1063
+ const { session, sessionManager: sm } = await ctx.createSession({
1064
+ cwd: ctx.cwd,
1065
+ sessionId,
1066
+ perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
1067
+ systemPromptOverride: () => baseSystemPrompt.length > 0 ? baseSystemPrompt : void 0,
1068
+ shellEnv,
1069
+ model,
1070
+ thinkingLevel: null,
1071
+ log,
1072
+ onSessionSetup: (args) => ctx.onSessionSetup?.(args),
1073
+ prepare: ({ sessionManager }) => loadHistoryIntoSession$1({
1074
+ sm: sessionManager,
1075
+ messages,
1076
+ upToIdxExclusive: lastUserIdx,
1077
+ modelName,
1078
+ log
1079
+ })
1080
+ });
1081
+ registry.set(sessionId, session);
1082
+ const id = `msg_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
1083
+ if (stream) {
1084
+ const response = runStream$2({
1085
+ session,
1086
+ sm,
1087
+ prompt,
1088
+ images,
1089
+ id,
1090
+ sessionId,
1091
+ modelName,
1092
+ log,
1093
+ postPrompt: ctx.postPrompt,
1094
+ registry
1095
+ });
1096
+ const traceId = parseTraceId(trace.traceparent);
1097
+ if (traceId) response.headers.set("x-trace-id", traceId);
1098
+ return response;
1099
+ }
1100
+ const response = await runBlocking$2({
1101
+ session,
1102
+ sm,
1103
+ prompt,
1104
+ images,
1105
+ id,
1106
+ sessionId,
1107
+ modelName,
1108
+ log,
1109
+ postPrompt: ctx.postPrompt,
1110
+ registry
1111
+ });
1112
+ const traceId = parseTraceId(trace.traceparent);
1113
+ if (traceId) response.headers.set("x-trace-id", traceId);
1114
+ return response;
1115
+ }
1116
+ function runStream$2({ session, sm, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1117
+ return createSSEResponse(async (writer) => {
1118
+ let contentBlockIndex = 0;
1119
+ let textBlockOpen = false;
1120
+ const tcByContentIdx = /* @__PURE__ */ new Map();
1121
+ sseEvent(writer, "message_start", {
1122
+ type: "message_start",
1123
+ message: {
1124
+ id,
1125
+ type: "message",
1126
+ role: "assistant",
1127
+ model: modelName,
1128
+ content: [],
1129
+ stop_reason: null,
1130
+ stop_sequence: null,
1131
+ usage: {
1132
+ input_tokens: 0,
1133
+ cache_creation_input_tokens: 0,
1134
+ cache_read_input_tokens: 0,
1135
+ output_tokens: 0
1136
+ }
1137
+ }
1138
+ });
1139
+ sseEvent(writer, "ping", { type: "ping" });
1140
+ const ensureTextBlock = () => {
1141
+ if (textBlockOpen) return;
1142
+ sseEvent(writer, "content_block_start", {
1143
+ type: "content_block_start",
1144
+ index: contentBlockIndex,
1145
+ content_block: {
1146
+ type: "text",
1147
+ text: ""
1148
+ }
1149
+ });
1150
+ textBlockOpen = true;
1151
+ };
1152
+ const closeTextBlock = () => {
1153
+ if (!textBlockOpen) return;
1154
+ sseEvent(writer, "content_block_stop", {
1155
+ type: "content_block_stop",
1156
+ index: contentBlockIndex
1157
+ });
1158
+ contentBlockIndex++;
1159
+ textBlockOpen = false;
1160
+ };
1161
+ let capturedModelError = null;
1162
+ const emitProviderError = (providerError) => {
1163
+ log.warn({
1164
+ event: "model_provider_error",
1165
+ code: providerError.code,
1166
+ upstream_status: providerError.upstreamStatus,
1167
+ provider: providerError.provider
1168
+ }, "forwarding model-provider error to client");
1169
+ sseEvent(writer, "error", {
1170
+ type: "error",
1171
+ error: {
1172
+ type: providerError.type ?? "api_error",
1173
+ message: providerError.message,
1174
+ x_model_provider_error: {
1175
+ code: providerError.code,
1176
+ provider: providerError.provider,
1177
+ upstream_status: providerError.upstreamStatus
1178
+ }
1179
+ }
1180
+ });
1181
+ };
1182
+ session.subscribe((event) => {
1183
+ const ev = event;
1184
+ const endedMessage = ev.message ?? (Array.isArray(ev.messages) ? ev.messages[ev.messages.length - 1] : null);
1185
+ if (endedMessage?.stopReason === "error" && typeof endedMessage.errorMessage === "string" && endedMessage.errorMessage.length > 0) capturedModelError = endedMessage.errorMessage;
1186
+ if (ev.type !== "message_update") return;
1187
+ const inner = ev.assistantMessageEvent;
1188
+ if (!inner) return;
1189
+ if (inner.type === "text_delta") {
1190
+ ensureTextBlock();
1191
+ sseEvent(writer, "content_block_delta", {
1192
+ type: "content_block_delta",
1193
+ index: contentBlockIndex,
1194
+ delta: {
1195
+ type: "text_delta",
1196
+ text: inner.delta ?? ""
1197
+ }
1198
+ });
1199
+ return;
1200
+ }
1201
+ if (inner.type === "toolcall_start") {
1202
+ closeTextBlock();
1203
+ const idx = inner.contentIndex;
1204
+ const item = inner.partial?.content?.[idx];
1205
+ const tc = item?.type === "toolCall" ? item : void 0;
1206
+ const blockIdx = contentBlockIndex++;
1207
+ tcByContentIdx.set(idx, blockIdx);
1208
+ sseEvent(writer, "content_block_start", {
1209
+ type: "content_block_start",
1210
+ index: blockIdx,
1211
+ content_block: {
1212
+ type: "tool_use",
1213
+ id: tc?.id ?? `toolu_${blockIdx}`,
1214
+ name: tc?.name ?? ""
1215
+ }
1216
+ });
1217
+ return;
1218
+ }
1219
+ if (inner.type === "toolcall_delta") {
1220
+ const blockIdx = tcByContentIdx.get(inner.contentIndex);
1221
+ if (blockIdx === void 0) return;
1222
+ sseEvent(writer, "content_block_delta", {
1223
+ type: "content_block_delta",
1224
+ index: blockIdx,
1225
+ delta: {
1226
+ type: "input_json_delta",
1227
+ partial_json: inner.delta ?? ""
1228
+ }
1229
+ });
1230
+ }
1231
+ });
1232
+ let stopReason = "end_turn";
1233
+ const outputTokens = 0;
1234
+ try {
1235
+ await runConversation({
1236
+ session,
1237
+ prompt,
1238
+ images,
1239
+ log,
1240
+ postPrompt
1241
+ });
1242
+ if (capturedModelError !== null) emitProviderError(toModelProviderError(new Error(capturedModelError)));
1243
+ } catch (err) {
1244
+ log.error({
1245
+ err,
1246
+ event: "anthropic_stream_error"
1247
+ }, "stream error");
1248
+ stopReason = "end_turn";
1249
+ if (isModelProviderError(err)) emitProviderError(toModelProviderError(err));
1250
+ } finally {
1251
+ registry.delete(sessionId);
1252
+ }
1253
+ closeTextBlock();
1254
+ for (const blockIdx of tcByContentIdx.values()) sseEvent(writer, "content_block_stop", {
1255
+ type: "content_block_stop",
1256
+ index: blockIdx
1257
+ });
1258
+ if (tcByContentIdx.size > 0) stopReason = "tool_use";
1259
+ sseEvent(writer, "message_delta", {
1260
+ type: "message_delta",
1261
+ delta: {
1262
+ stop_reason: stopReason,
1263
+ stop_sequence: null
1264
+ },
1265
+ usage: { output_tokens: outputTokens }
1266
+ });
1267
+ sseEvent(writer, "message_stop", { type: "message_stop" });
1268
+ });
1269
+ }
1270
+ async function runBlocking$2({ session, sm, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1271
+ let text = "";
1272
+ const toolUseBlocks = [];
1273
+ const tcByContentIdx = /* @__PURE__ */ new Map();
1274
+ session.subscribe((event) => {
1275
+ if (event.type === "message_update") {
1276
+ const inner = event.assistantMessageEvent;
1277
+ if (inner?.type === "text_delta") {
1278
+ text += inner.delta ?? "";
1279
+ return;
1280
+ }
1281
+ if (inner?.type === "toolcall_start") {
1282
+ const idx = inner.contentIndex;
1283
+ const item = inner.partial?.content?.[idx];
1284
+ tcByContentIdx.set(idx, toolUseBlocks.length);
1285
+ toolUseBlocks.push({
1286
+ type: "tool_use",
1287
+ id: item?.id ?? `toolu_${toolUseBlocks.length}`,
1288
+ name: item?.name ?? "",
1289
+ input: {}
1290
+ });
1291
+ return;
1292
+ }
1293
+ if (inner?.type === "toolcall_delta") {
1294
+ const i = tcByContentIdx.get(inner.contentIndex);
1295
+ if (i !== void 0) {
1296
+ const block = toolUseBlocks[i];
1297
+ if (block && block.type === "tool_use") block.input = (typeof block.input === "string" ? block.input : "") + (inner.delta ?? "");
1298
+ }
1299
+ }
1300
+ }
1301
+ });
1302
+ try {
1303
+ await runConversation({
1304
+ session,
1305
+ prompt,
1306
+ images,
1307
+ log,
1308
+ postPrompt
1309
+ });
1310
+ } catch (err) {
1311
+ log.error({
1312
+ err,
1313
+ event: "anthropic_blocking_error"
1314
+ }, "blocking error");
1315
+ return jsonError(500, err?.message ?? String(err));
1316
+ } finally {
1317
+ registry.delete(sessionId);
1318
+ }
1319
+ for (const block of toolUseBlocks) if (block.type === "tool_use" && typeof block.input === "string") try {
1320
+ block.input = JSON.parse(block.input);
1321
+ } catch {
1322
+ block.input = {};
1323
+ }
1324
+ const content = [];
1325
+ if (text) content.push({
1326
+ type: "text",
1327
+ text
1328
+ });
1329
+ content.push(...toolUseBlocks);
1330
+ return jsonResponse({
1331
+ id,
1332
+ type: "message",
1333
+ role: "assistant",
1334
+ content,
1335
+ model: modelName,
1336
+ stop_reason: toolUseBlocks.length > 0 ? "tool_use" : "end_turn",
1337
+ stop_sequence: null,
1338
+ usage: {
1339
+ input_tokens: 0,
1340
+ output_tokens: 0
1341
+ }
1342
+ });
1343
+ }
1344
+ function create$2(options) {
1345
+ const cwd = options.cwd ?? process.cwd();
1346
+ const registry = options.sessionRegistry ?? createMapSessionRegistry();
1347
+ const ctx = {
1348
+ ...options,
1349
+ cwd
1350
+ };
1351
+ return async (request) => {
1352
+ const { pathname } = new URL(request.url);
1353
+ if (request.method !== "POST") return null;
1354
+ const abortSessionId = pathname.match(/^\/v1\/messages\/([^/]+)\/abort$/)?.[1];
1355
+ if (abortSessionId) try {
1356
+ return await runInTraceContext(() => handleAbort({
1357
+ request,
1358
+ sessionId: abortSessionId,
1359
+ registry
1360
+ }));
1361
+ } catch (err) {
1362
+ const { log } = requestLogger(request);
1363
+ log.error({
1364
+ err,
1365
+ event: "anthropic_messages_abort_unhandled_error"
1366
+ }, "unhandled error");
1367
+ return jsonError(500, "internal error");
1368
+ }
1369
+ if (pathname !== "/v1/messages") return null;
1370
+ try {
1371
+ return await runInTraceContext(() => handleMessages(request, ctx, registry));
1372
+ } catch (err) {
1373
+ const { log } = requestLogger(request);
1374
+ log.error({
1375
+ err,
1376
+ event: "anthropic_messages_unhandled_error"
1377
+ }, "unhandled error");
1378
+ return jsonError(500, "internal error");
1379
+ }
1380
+ };
1381
+ }
1382
+ //#endregion
1383
+ //#region src/billing-blocked-provider-signal.ts
1384
+ const BILLING_BLOCKED_PROVIDER_SIGNAL_PREFIX = "ANYONE_BILLING_BLOCKED_V1:";
1385
+ /**
1386
+ * Duplicated from the billing gate's `BlockReason` (apps/anyone/billing
1387
+ * gate/block-reason.ts): pi-server runs inside the sandbox and must not
1388
+ * depend on platform packages. A reason the proxy sends that predates this
1389
+ * build fails the enum and degrades to the untyped `billing_blocked`
1390
+ * handling — never a crash. Exported so agent-lifecycle's chat.test.ts can
1391
+ * pin this copy against its own (which billing's block-reason.test.ts in
1392
+ * turn pins against the gate), keeping every copy drift-checked.
1393
+ */
1394
+ const BILLING_BLOCK_REASONS = [
1395
+ "insufficient_balance",
1396
+ "payment_failed",
1397
+ "hard_spend_limit_reached"
1398
+ ];
1399
+ const payloadSchema = z.object({
1400
+ blockReason: z.enum(BILLING_BLOCK_REASONS),
1401
+ message: z.string()
1402
+ }).strict();
1403
+ const nestedProxyEnvelopeSchema = z.object({
1404
+ error: z.object({
1405
+ type: z.literal("billing_blocked"),
1406
+ code: z.literal("billing_blocked"),
1407
+ message: z.string()
1408
+ }).strict(),
1409
+ blockReason: z.enum(BILLING_BLOCK_REASONS),
1410
+ message: z.string()
1411
+ }).strict();
1412
+ function parsePrefixedSignal(message) {
1413
+ const normalized = message.startsWith("402 ") ? message.slice(4) : message;
1414
+ if (!normalized.startsWith(BILLING_BLOCKED_PROVIDER_SIGNAL_PREFIX)) return null;
1415
+ try {
1416
+ const parsed = payloadSchema.safeParse(JSON.parse(normalized.slice(26)));
1417
+ return parsed.success ? parsed.data : null;
1418
+ } catch {
1419
+ return null;
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Decode only the versioned platform envelope. Anthropic and OpenAI prefix the
1424
+ * nested error message with HTTP 402. Google's SDK instead preserves the full
1425
+ * proxy response as JSON, so that outer shape is validated separately.
1426
+ */
1427
+ function parseBillingBlockedProviderSignal(message) {
1428
+ const direct = parsePrefixedSignal(message);
1429
+ if (direct) return direct;
1430
+ try {
1431
+ const envelope = nestedProxyEnvelopeSchema.safeParse(JSON.parse(message));
1432
+ if (!envelope.success) return null;
1433
+ const nested = parsePrefixedSignal(envelope.data.error.message);
1434
+ const topLevel = parsePrefixedSignal(envelope.data.message);
1435
+ if (!nested || nested.blockReason !== envelope.data.blockReason || envelope.data.message !== nested.message && (!topLevel || topLevel.blockReason !== nested.blockReason || topLevel.message !== nested.message)) return null;
1436
+ return nested;
1437
+ } catch {
1438
+ return null;
1439
+ }
1440
+ }
1441
+ //#endregion
1442
+ //#region src/protocols/chat-completions.ts
1443
+ /**
1444
+ * OpenAI Chat Completions–compatible protocol handler.
1445
+ *
1446
+ * Catch-all: returns Response on match, null on no match.
1447
+ * Zero framework deps — just Web Standard Request/Response.
1448
+ *
1449
+ * Routes:
1450
+ * POST /v1/chat/completions
1451
+ * POST /v1/chat/completions/:id/steer
1452
+ * POST /v1/chat/completions/:id/abort
1453
+ *
1454
+ * @see https://platform.openai.com/docs/api-reference/chat/create
1455
+ * @see PROTOCOL.md for custom extensions
1456
+ */
1457
+ const ZERO_USAGE = {
1458
+ input: 0,
1459
+ output: 0,
1460
+ cacheRead: 0,
1461
+ cacheWrite: 0,
1462
+ totalTokens: 0,
1463
+ cost: {
1464
+ input: 0,
1465
+ output: 0,
1466
+ cacheRead: 0,
1467
+ cacheWrite: 0,
1468
+ total: 0
1469
+ }
1470
+ };
1471
+ function extractUsageFromAgentEnd(event) {
1472
+ const u = extractAgentEndUsage(event);
1473
+ if (!u) return null;
1474
+ return {
1475
+ prompt_tokens: u.input,
1476
+ completion_tokens: u.output,
1477
+ total_tokens: u.totalTokens,
1478
+ cache_read_tokens: u.cacheRead,
1479
+ cache_creation_tokens: u.cacheWrite
1480
+ };
1481
+ }
1482
+ function isDebug(request) {
1483
+ const h = request.headers.get("x-debug");
1484
+ return h === "1" || h === "true";
1485
+ }
1486
+ function parseRequestStartMs(request) {
1487
+ const h = request.headers.get("x-request-start-ms");
1488
+ if (h == null) return null;
1489
+ const n = Number(h);
1490
+ return Number.isFinite(n) && n > 0 ? n : null;
1491
+ }
1492
+ function withPassthroughHeaders({ model, request, prefixes }) {
1493
+ if (prefixes.length === 0) return model;
1494
+ const forwarded = {};
1495
+ request.headers.forEach((value, key) => {
1496
+ if (prefixes.some((p) => key.startsWith(p))) forwarded[key] = value;
1497
+ });
1498
+ if (Object.keys(forwarded).length === 0) return model;
1499
+ const existing = "headers" in model && model.headers != null && typeof model.headers === "object" && !Array.isArray(model.headers) ? model.headers : {};
1500
+ return {
1501
+ ...model,
1502
+ headers: {
1503
+ ...existing,
1504
+ ...forwarded
1505
+ }
1506
+ };
1507
+ }
1508
+ async function extractContent(content, { userAgent, log }) {
1509
+ if (typeof content === "string") return {
1510
+ text: content,
1511
+ images: []
1512
+ };
1513
+ if (!Array.isArray(content)) return {
1514
+ text: "",
1515
+ images: []
1516
+ };
1517
+ let text = "";
1518
+ const images = [];
1519
+ for (const part of content) {
1520
+ if (typeof part === "string") {
1521
+ text += part;
1522
+ continue;
1523
+ }
1524
+ if (part?.type === "text") {
1525
+ text += part.text ?? "";
1526
+ continue;
1527
+ }
1528
+ if (part?.type === "image_url") {
1529
+ const url = String(part.image_url?.url ?? "");
1530
+ const dataUrl = url.match(/^data:([^;]+);base64,(.+)$/);
1531
+ if (dataUrl) images.push({
1532
+ type: "image",
1533
+ mimeType: dataUrl[1],
1534
+ data: dataUrl[2]
1535
+ });
1536
+ else if (/^https?:\/\//i.test(url)) try {
1537
+ images.push(await fetchImageAsBase64({
1538
+ url,
1539
+ userAgent
1540
+ }));
1541
+ } catch (err) {
1542
+ log.warn({
1543
+ event: "image_fetch_failed",
1544
+ url,
1545
+ err
1546
+ }, "image fetch failed");
1547
+ text += `\n[image fetch failed: ${url}: ${err?.message ?? err}]\n`;
1548
+ }
1549
+ }
1550
+ }
1551
+ return {
1552
+ text,
1553
+ images
1554
+ };
1555
+ }
1556
+ function extractAssistantText(content) {
1557
+ if (typeof content === "string") return content;
1558
+ if (!Array.isArray(content)) return "";
1559
+ return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("");
1560
+ }
1561
+ function countMessageStats(messages) {
1562
+ let chars = 0;
1563
+ let images = 0;
1564
+ for (const m of messages || []) {
1565
+ const c = m?.content;
1566
+ if (typeof c === "string") chars += c.length;
1567
+ else if (Array.isArray(c)) {
1568
+ for (const p of c) if (typeof p === "string") chars += p.length;
1569
+ else if (p?.type === "text" && typeof p.text === "string") chars += p.text.length;
1570
+ else if (p?.type === "image_url") images += 1;
1571
+ }
1572
+ if (Array.isArray(m?.tool_calls)) for (const tc of m.tool_calls) {
1573
+ const args = tc?.function?.arguments;
1574
+ if (typeof args === "string") chars += args.length;
1575
+ }
1576
+ }
1577
+ return {
1578
+ chars,
1579
+ images
1580
+ };
1581
+ }
1582
+ async function loadHistoryIntoSession({ sm, messages, upToIdxExclusive, modelName, userAgent, log }) {
1583
+ const toolCallNames = /* @__PURE__ */ new Map();
1584
+ for (let i = 0; i < upToIdxExclusive; i++) {
1585
+ const m = messages[i];
1586
+ if (!m) continue;
1587
+ if (m.role === "user") {
1588
+ const { text, images } = await extractContent(m.content, {
1589
+ userAgent,
1590
+ log
1591
+ });
1592
+ const content = images.length > 0 ? [...text ? [{
1593
+ type: "text",
1594
+ text
1595
+ }] : [], ...images] : text;
1596
+ if (typeof content === "string" && content.length > 0 || Array.isArray(content) && content.length > 0) sm.appendMessage({
1597
+ role: "user",
1598
+ content,
1599
+ timestamp: Date.now()
1600
+ });
1601
+ continue;
1602
+ }
1603
+ if (m.role === "assistant") {
1604
+ const text = extractAssistantText(m.content);
1605
+ const contentArr = [];
1606
+ if (Array.isArray(m.x_thinking)) for (const t of m.x_thinking) {
1607
+ const tt = String(t?.text ?? "");
1608
+ if (!tt) continue;
1609
+ contentArr.push({
1610
+ type: "thinking",
1611
+ thinking: tt,
1612
+ ...t?.signature ? { thinkingSignature: t.signature } : {},
1613
+ ...t?.redacted ? { redacted: true } : {}
1614
+ });
1615
+ }
1616
+ if (text) contentArr.push({
1617
+ type: "text",
1618
+ text
1619
+ });
1620
+ if (Array.isArray(m.tool_calls)) for (const tc of m.tool_calls) {
1621
+ const name = tc?.function?.name;
1622
+ if (!tc?.id || !name) continue;
1623
+ let args = {};
1624
+ try {
1625
+ args = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
1626
+ } catch {
1627
+ args = { _raw: tc.function.arguments };
1628
+ }
1629
+ contentArr.push({
1630
+ type: "toolCall",
1631
+ id: tc.id,
1632
+ name,
1633
+ arguments: args
1634
+ });
1635
+ toolCallNames.set(tc.id, name);
1636
+ }
1637
+ if (contentArr.length === 0) continue;
1638
+ sm.appendMessage({
1639
+ role: "assistant",
1640
+ content: contentArr,
1641
+ api: "anthropic-messages",
1642
+ provider: "anthropic",
1643
+ model: modelName,
1644
+ usage: ZERO_USAGE,
1645
+ stopReason: contentArr.some((c) => c.type === "toolCall") ? "toolUse" : "stop",
1646
+ timestamp: Date.now()
1647
+ });
1648
+ continue;
1649
+ }
1650
+ if (m.role === "tool") {
1651
+ const text = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : "";
1652
+ const toolCallId = String(m.tool_call_id ?? "");
1653
+ sm.appendMessage({
1654
+ role: "toolResult",
1655
+ toolCallId,
1656
+ toolName: toolCallNames.get(toolCallId) ?? "",
1657
+ content: [{
1658
+ type: "text",
1659
+ text
1660
+ }],
1661
+ isError: Boolean(m.x_is_error),
1662
+ timestamp: Date.now()
1663
+ });
1664
+ continue;
1665
+ }
1666
+ }
1667
+ }
1668
+ function messageTimestampMs(m) {
1669
+ const ts = m?.timestamp;
1670
+ return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
1671
+ }
1672
+ function piMessagesToOpenAI(messages) {
1673
+ const out = [];
1674
+ for (const m of messages) {
1675
+ if (m?.role === "assistant") {
1676
+ let textContent = "";
1677
+ const toolCalls = [];
1678
+ const thinking = [];
1679
+ const content = Array.isArray(m.content) ? m.content : [];
1680
+ for (const c of content) if (c?.type === "text") textContent += c.text ?? "";
1681
+ else if (c?.type === "thinking") thinking.push({
1682
+ text: String(c.thinking ?? ""),
1683
+ ...c.thinkingSignature ? { signature: String(c.thinkingSignature) } : {},
1684
+ ...c.redacted ? { redacted: true } : {}
1685
+ });
1686
+ else if (c?.type === "toolCall") toolCalls.push({
1687
+ id: String(c.id),
1688
+ type: "function",
1689
+ function: {
1690
+ name: String(c.name ?? ""),
1691
+ arguments: JSON.stringify(c.arguments ?? {})
1692
+ }
1693
+ });
1694
+ const msg = {
1695
+ role: "assistant",
1696
+ content: textContent || null
1697
+ };
1698
+ if (toolCalls.length) msg.tool_calls = toolCalls;
1699
+ if (thinking.length) msg.x_thinking = thinking;
1700
+ const createdAtMs = messageTimestampMs(m);
1701
+ if (createdAtMs !== null) msg.x_created_at = createdAtMs;
1702
+ out.push(msg);
1703
+ continue;
1704
+ }
1705
+ if (m?.role === "toolResult") {
1706
+ const text = Array.isArray(m.content) ? m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : "";
1707
+ const tm = {
1708
+ role: "tool",
1709
+ tool_call_id: String(m.toolCallId ?? ""),
1710
+ content: text
1711
+ };
1712
+ if (m.isError) tm.x_is_error = true;
1713
+ const createdAtMs = messageTimestampMs(m);
1714
+ if (createdAtMs !== null) tm.x_created_at = createdAtMs;
1715
+ out.push(tm);
1716
+ continue;
1717
+ }
1718
+ }
1719
+ return out;
1720
+ }
1721
+ async function handleChatCompletions(request, ctx, registry, pendingSteerIds) {
1722
+ const reqStart = performance.now();
1723
+ const upstreamStartMs = parseRequestStartMs(request);
1724
+ const { log: baseLog, trace } = requestLogger(request);
1725
+ const log = isDebug(request) ? baseLog.child({}, { level: "debug" }) : baseLog;
1726
+ let parsed;
1727
+ try {
1728
+ parsed = JSON.parse(await request.text());
1729
+ } catch {
1730
+ return jsonError(400, "invalid json");
1731
+ }
1732
+ const { messages, stream = false, model: modelInput, thinkingLevel: thinkingInput, context_window: contextWindowInput, x_model: modelSpecInput } = parsed ?? {};
1733
+ if (!Array.isArray(messages) || messages.length === 0) return jsonError(400, "messages array required");
1734
+ let lastUserIdx = -1;
1735
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "user") {
1736
+ lastUserIdx = i;
1737
+ break;
1738
+ }
1739
+ if (lastUserIdx < 0) return jsonError(400, "no user message in history");
1740
+ const lastUser = messages[lastUserIdx];
1741
+ const reqUA = request.headers.get("user-agent") ?? void 0;
1742
+ const { text: prompt, images } = await extractContent(lastUser.content, {
1743
+ userAgent: reqUA,
1744
+ log
1745
+ });
1746
+ if (!prompt && images.length === 0) return jsonError(400, "user message has no content");
1747
+ const { model: requestModel, modelSpec } = resolveRequestModel({
1748
+ modelInput,
1749
+ modelSpecInput,
1750
+ defaultModel: ctx.defaultModel,
1751
+ log
1752
+ });
1753
+ const resolvedModel = applyContextWindowOverride(requestModel, contextWindowInput);
1754
+ const model = withPassthroughHeaders({
1755
+ model: resolvedModel,
1756
+ request,
1757
+ prefixes: ctx.passthroughHeaderPrefixes ?? []
1758
+ });
1759
+ const thinkingLevel = resolveThinkingLevel(thinkingInput);
1760
+ const modelName = model.id ?? "claude-opus-4-7";
1761
+ const requestSystemPrompts = [];
1762
+ for (const m of messages) if (m?.role === "system") {
1763
+ const text = extractAssistantText(m.content);
1764
+ if (text) requestSystemPrompts.push(text);
1765
+ }
1766
+ const baseSystemPrompt = requestSystemPrompts.filter((p) => p.length > 0).join("\n\n");
1767
+ const parsedSession = parseSessionId(request);
1768
+ if (parsedSession.invalid) return jsonError(400, "x-session-id must be a valid UUID");
1769
+ const sessionId = parsedSession.source === "header" ? parsedSession.sessionId : `chatcmpl-${parsedSession.sessionId}`;
1770
+ const shellEnv = parseShellEnv(request);
1771
+ const sessionSetupStart = performance.now();
1772
+ const [{ session, sessionManager: sm }, customTools] = await Promise.all([ctx.createSession({
1773
+ cwd: ctx.cwd,
1774
+ sessionId,
1775
+ perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
1776
+ systemPromptOverride: () => baseSystemPrompt.length > 0 ? baseSystemPrompt : void 0,
1777
+ shellEnv,
1778
+ model,
1779
+ thinkingLevel,
1780
+ log,
1781
+ onSessionSetup: (args) => ctx.onSessionSetup?.(args),
1782
+ prepare: ({ sessionManager }) => loadHistoryIntoSession({
1783
+ sm: sessionManager,
1784
+ messages,
1785
+ upToIdxExclusive: lastUserIdx,
1786
+ modelName,
1787
+ userAgent: reqUA,
1788
+ log
1789
+ })
1790
+ }), ctx.getTools ? ctx.getTools(request) : void 0]);
1791
+ const sessionSetupMs = Math.round(performance.now() - sessionSetupStart);
1792
+ const created = Math.floor(Date.now() / 1e3);
1793
+ registry.set(sessionId, session);
1794
+ const stats = countMessageStats(messages);
1795
+ log.info({
1796
+ event: "req_received",
1797
+ chatcmpl_id: sessionId,
1798
+ turns: messages.length,
1799
+ chars: stats.chars,
1800
+ images: stats.images,
1801
+ stream,
1802
+ model: modelName,
1803
+ x_model: modelSpec !== null,
1804
+ context_window: resolvedModel.contextWindow,
1805
+ setup_ms: Math.round(performance.now() - reqStart),
1806
+ session_setup_ms: sessionSetupMs,
1807
+ upstream_setup_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
1808
+ }, "chat req received");
1809
+ if (stream) {
1810
+ const response = runStream$1({
1811
+ session,
1812
+ sm,
1813
+ prompt,
1814
+ images,
1815
+ sessionId,
1816
+ created,
1817
+ reqStart,
1818
+ upstreamStartMs,
1819
+ log,
1820
+ trace,
1821
+ postPrompt: ctx.postPrompt,
1822
+ registry,
1823
+ pendingSteerIds
1824
+ });
1825
+ const traceId = parseTraceId(trace.traceparent);
1826
+ if (traceId) response.headers.set("x-trace-id", traceId);
1827
+ return response;
1828
+ }
1829
+ const response = await runBlocking$1({
1830
+ session,
1831
+ sm,
1832
+ prompt,
1833
+ images,
1834
+ sessionId,
1835
+ created,
1836
+ reqStart,
1837
+ upstreamStartMs,
1838
+ log,
1839
+ postPrompt: ctx.postPrompt,
1840
+ registry,
1841
+ pendingSteerIds
1842
+ });
1843
+ const traceId = parseTraceId(trace.traceparent);
1844
+ if (traceId) response.headers.set("x-trace-id", traceId);
1845
+ return response;
1846
+ }
1847
+ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart, upstreamStartMs, log, trace, postPrompt, registry, pendingSteerIds }) {
1848
+ return createSSEResponse(async (writer) => {
1849
+ writer.write(": ready\n\n");
1850
+ const baselineMessageCount = sm.buildSessionContext().messages.length;
1851
+ let roleEmitted = false;
1852
+ let firstChunkAt = null;
1853
+ const tcByContentIdx = /* @__PURE__ */ new Map();
1854
+ let nextTcIdx = 0;
1855
+ let lastSteeringCount = 0;
1856
+ let usage = null;
1857
+ let lastFollowUpCount = 0;
1858
+ let capturedModelError = null;
1859
+ const sendChunk = (delta, finishReason) => {
1860
+ if (firstChunkAt === null) {
1861
+ firstChunkAt = performance.now();
1862
+ log.info({
1863
+ event: "ttft",
1864
+ chatcmpl_id: sessionId,
1865
+ ttft_ms: Math.round(firstChunkAt - reqStart),
1866
+ upstream_ttft_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
1867
+ }, "time to first chunk");
1868
+ }
1869
+ const chunk = {
1870
+ id: sessionId,
1871
+ object: "chat.completion.chunk",
1872
+ created,
1873
+ choices: [{
1874
+ index: 0,
1875
+ delta,
1876
+ finish_reason: finishReason ?? null
1877
+ }]
1878
+ };
1879
+ writer.write(`data: ${JSON.stringify(chunk)}\n\n`);
1880
+ };
1881
+ const ensureRole = () => {
1882
+ if (roleEmitted) return;
1883
+ sendChunk({ role: "assistant" });
1884
+ roleEmitted = true;
1885
+ };
1886
+ let _evCount = 0;
1887
+ session.subscribe((event) => {
1888
+ const ev = event;
1889
+ const endedMessage = ev.message ?? (Array.isArray(ev.messages) ? ev.messages[ev.messages.length - 1] : null);
1890
+ if (endedMessage?.stopReason === "error" && typeof endedMessage.errorMessage === "string" && endedMessage.errorMessage.length > 0) capturedModelError = endedMessage.errorMessage;
1891
+ if (ev.type === "queue_update") {
1892
+ const steering = Array.isArray(ev.steering) ? ev.steering.length : 0;
1893
+ const followUp = Array.isArray(ev.followUp) ? ev.followUp.length : 0;
1894
+ const queues = pendingSteerIds.get(sessionId);
1895
+ const consumedIds = [];
1896
+ const consumedSteer = lastSteeringCount - steering;
1897
+ if (consumedSteer > 0 && queues && queues.steer.length > 0) consumedIds.push(...queues.steer.splice(0, consumedSteer));
1898
+ const consumedFollowUp = lastFollowUpCount - followUp;
1899
+ if (consumedFollowUp > 0 && queues && queues.followUp.length > 0) consumedIds.push(...queues.followUp.splice(0, consumedFollowUp));
1900
+ lastSteeringCount = steering;
1901
+ lastFollowUpCount = followUp;
1902
+ sendChunk({ x_queue_update: {
1903
+ steering,
1904
+ follow_up: followUp,
1905
+ ...consumedIds.length > 0 ? { consumed_steer_ids: consumedIds } : {}
1906
+ } });
1907
+ return;
1908
+ }
1909
+ _evCount++;
1910
+ const innerType = ev.type === "message_update" ? ev.assistantMessageEvent?.type ?? null : null;
1911
+ const messageContentSummary = ev.type === "message_update" || ev.type === "message_end" ? Array.isArray(ev.message?.content) ? ev.message.content.map((c) => ({
1912
+ type: c?.type,
1913
+ ...c?.type === "text" ? { textLen: (c.text ?? "").length } : {},
1914
+ ...c?.type === "thinking" ? { thinkingLen: (c.thinking ?? "").length } : {},
1915
+ ...c?.type === "toolCall" ? { name: c.name } : {}
1916
+ })) : null : null;
1917
+ log.info({
1918
+ chatcmpl_id: sessionId,
1919
+ evIdx: _evCount,
1920
+ eventType: ev.type,
1921
+ messageRole: ev.message?.role ?? null,
1922
+ stopReason: ev.message?.stopReason ?? null,
1923
+ innerType,
1924
+ ...innerType === "text_delta" || innerType === "thinking_delta" ? { delta: String(ev.assistantMessageEvent?.delta ?? "").slice(0, 200) } : {},
1925
+ ...messageContentSummary ? { messageContent: messageContentSummary } : {},
1926
+ ...ev.type === "message_start" || ev.type === "message_end" ? { messageJson: JSON.stringify(ev.message).slice(0, 500) } : {}
1927
+ }, "harness session event");
1928
+ if (ev.type === "tool_execution_start") {
1929
+ sendChunk({ x_tool_execution_start: { tool_call_id: String(ev.toolCallId ?? "") } });
1930
+ return;
1931
+ }
1932
+ if (ev.type === "tool_execution_end") {
1933
+ const text = Array.isArray(ev.result?.content) ? ev.result.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : typeof ev.result === "string" ? ev.result : "";
1934
+ sendChunk({ x_tool_call_result: {
1935
+ tool_call_id: String(ev.toolCallId ?? ""),
1936
+ content: text,
1937
+ ...ev.isError ? { is_error: true } : {}
1938
+ } });
1939
+ return;
1940
+ }
1941
+ if (ev.type === "agent_end") {
1942
+ usage = extractUsageFromAgentEnd(ev) ?? usage;
1943
+ return;
1944
+ }
1945
+ if (ev.type !== "message_update") return;
1946
+ const inner = ev.assistantMessageEvent;
1947
+ const t = inner?.type;
1948
+ if (t === "text_delta") {
1949
+ ensureRole();
1950
+ sendChunk({ content: inner.delta ?? "" });
1951
+ return;
1952
+ }
1953
+ if (t === "thinking_delta") {
1954
+ sendChunk({ x_thinking_delta: inner.delta ?? "" });
1955
+ return;
1956
+ }
1957
+ if (t === "toolcall_start") {
1958
+ const idx = inner.contentIndex;
1959
+ const item = inner.partial?.content?.[idx];
1960
+ const tc = item?.type === "toolCall" ? item : void 0;
1961
+ const openaiIdx = nextTcIdx++;
1962
+ tcByContentIdx.set(idx, openaiIdx);
1963
+ ensureRole();
1964
+ sendChunk({ tool_calls: [{
1965
+ index: openaiIdx,
1966
+ id: tc?.id ?? `call_${openaiIdx}`,
1967
+ type: "function",
1968
+ function: {
1969
+ name: tc?.name ?? "",
1970
+ arguments: ""
1971
+ }
1972
+ }] });
1973
+ return;
1974
+ }
1975
+ if (t === "toolcall_delta") {
1976
+ const openaiIdx = tcByContentIdx.get(inner.contentIndex);
1977
+ if (openaiIdx === void 0) return;
1978
+ sendChunk({ tool_calls: [{
1979
+ index: openaiIdx,
1980
+ function: { arguments: inner.delta ?? "" }
1981
+ }] });
1982
+ }
1983
+ });
1984
+ try {
1985
+ await runConversation({
1986
+ session,
1987
+ prompt,
1988
+ images,
1989
+ log,
1990
+ postPrompt
1991
+ });
1992
+ if (capturedModelError !== null) {
1993
+ const billingBlocked = parseBillingBlockedProviderSignal(capturedModelError);
1994
+ if (billingBlocked) {
1995
+ log.warn({
1996
+ event: "billing_blocked",
1997
+ chatcmpl_id: sessionId,
1998
+ source: "stop_reason_error"
1999
+ }, "forwarding platform billing block to client");
2000
+ emitBillingBlocked({
2001
+ writer,
2002
+ sessionId,
2003
+ created,
2004
+ billingBlocked
2005
+ });
2006
+ writer.write("data: [DONE]\n\n");
2007
+ return;
2008
+ }
2009
+ const providerError = toModelProviderError(new Error(capturedModelError));
2010
+ log.warn({
2011
+ event: "model_provider_error",
2012
+ chatcmpl_id: sessionId,
2013
+ code: providerError.code,
2014
+ upstream_status: providerError.upstreamStatus,
2015
+ provider: providerError.provider,
2016
+ source: "stop_reason_error"
2017
+ }, "forwarding model-provider error to client");
2018
+ emitModelProviderError({
2019
+ writer,
2020
+ sessionId,
2021
+ created,
2022
+ providerError
2023
+ });
2024
+ writer.write("data: [DONE]\n\n");
2025
+ } else {
2026
+ emitSessionMessagesTrailer({
2027
+ writer,
2028
+ sm,
2029
+ baselineMessageCount,
2030
+ sessionId,
2031
+ created,
2032
+ usage
2033
+ });
2034
+ sendChunk({}, "stop");
2035
+ writer.write("data: [DONE]\n\n");
2036
+ }
2037
+ } catch (err) {
2038
+ log.error({
2039
+ event: "chat_error",
2040
+ chatcmpl_id: sessionId,
2041
+ err
2042
+ }, "chat error");
2043
+ if (isModelProviderError(err)) {
2044
+ const billingBlocked = parseBillingBlockedProviderSignal(err instanceof Error ? err.message : String(err));
2045
+ if (billingBlocked) {
2046
+ emitBillingBlocked({
2047
+ writer,
2048
+ sessionId,
2049
+ created,
2050
+ billingBlocked
2051
+ });
2052
+ writer.write("data: [DONE]\n\n");
2053
+ return;
2054
+ }
2055
+ const providerError = toModelProviderError(err);
2056
+ log.warn({
2057
+ event: "model_provider_error",
2058
+ chatcmpl_id: sessionId,
2059
+ code: providerError.code,
2060
+ upstream_status: providerError.upstreamStatus,
2061
+ provider: providerError.provider
2062
+ }, "forwarding model-provider error to client");
2063
+ emitModelProviderError({
2064
+ writer,
2065
+ sessionId,
2066
+ created,
2067
+ providerError
2068
+ });
2069
+ } else sendChunk({ content: `\n[error: ${err?.message ?? err}]` }, "stop");
2070
+ writer.write("data: [DONE]\n\n");
2071
+ } finally {
2072
+ registry.delete(sessionId);
2073
+ pendingSteerIds.delete(sessionId);
2074
+ }
2075
+ log.info({
2076
+ event: "stream_done",
2077
+ chatcmpl_id: sessionId,
2078
+ duration_ms: Math.round(performance.now() - reqStart),
2079
+ upstream_duration_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
2080
+ }, "stream done");
2081
+ });
2082
+ }
2083
+ function emitSessionMessagesTrailer({ writer, sm, baselineMessageCount, sessionId, created, usage }) {
2084
+ const sessionMessages = piMessagesToOpenAI(sm.buildSessionContext().messages.slice(baselineMessageCount));
2085
+ if (sessionMessages.length === 0 && !usage) return;
2086
+ const chunk = {
2087
+ id: sessionId,
2088
+ object: "chat.completion.chunk",
2089
+ created,
2090
+ choices: [{
2091
+ index: 0,
2092
+ delta: {},
2093
+ finish_reason: null
2094
+ }],
2095
+ ...sessionMessages.length ? { x_session_messages: sessionMessages } : {},
2096
+ ...usage ? { usage } : {}
2097
+ };
2098
+ writer.write(`data: ${JSON.stringify(chunk)}\n\n`);
2099
+ }
2100
+ /**
2101
+ * Emit a terminal chunk carrying a structured model-provider error under the
2102
+ * `x_model_provider_error` extension field, so the worker can react on the
2103
+ * forwarded provider error (e.g. compact + retry on `context_length_exceeded`)
2104
+ * rather than seeing an ambiguous empty stream. `finish_reason: 'stop'`
2105
+ * cleanly closes the stream for any OpenAI-shaped reader that ignores the
2106
+ * extension field.
2107
+ */
2108
+ function emitModelProviderError({ writer, sessionId, created, providerError }) {
2109
+ const chunk = {
2110
+ id: sessionId,
2111
+ object: "chat.completion.chunk",
2112
+ created,
2113
+ choices: [{
2114
+ index: 0,
2115
+ delta: {},
2116
+ finish_reason: "stop"
2117
+ }],
2118
+ x_model_provider_error: {
2119
+ code: providerError.code,
2120
+ message: providerError.message,
2121
+ provider: providerError.provider,
2122
+ type: providerError.type,
2123
+ upstream_status: providerError.upstreamStatus
2124
+ }
2125
+ };
2126
+ writer.write(`data: ${JSON.stringify(chunk)}\n\n`);
2127
+ }
2128
+ function emitBillingBlocked({ writer, sessionId, created, billingBlocked }) {
2129
+ const chunk = {
2130
+ id: sessionId,
2131
+ object: "chat.completion.chunk",
2132
+ created,
2133
+ choices: [{
2134
+ index: 0,
2135
+ delta: {},
2136
+ finish_reason: "stop"
2137
+ }],
2138
+ x_billing_blocked: {
2139
+ block_reason: billingBlocked.blockReason,
2140
+ message: billingBlocked.message
2141
+ }
2142
+ };
2143
+ writer.write(`data: ${JSON.stringify(chunk)}\n\n`);
2144
+ }
2145
+ async function runBlocking$1({ session, sm, prompt, images, sessionId, created, reqStart, upstreamStartMs, log, postPrompt, registry, pendingSteerIds }) {
2146
+ const baselineMessageCount = sm.buildSessionContext().messages.length;
2147
+ let content = "";
2148
+ const toolCalls = [];
2149
+ const tcByContentIdx = /* @__PURE__ */ new Map();
2150
+ let usage = null;
2151
+ session.subscribe((event) => {
2152
+ if (event.type === "message_update") {
2153
+ const inner = event.assistantMessageEvent;
2154
+ const t = inner?.type;
2155
+ if (t === "text_delta") {
2156
+ content += inner.delta ?? "";
2157
+ return;
2158
+ }
2159
+ if (t === "toolcall_start") {
2160
+ const idx = inner.contentIndex;
2161
+ const item = inner.partial?.content?.[idx];
2162
+ tcByContentIdx.set(idx, toolCalls.length);
2163
+ toolCalls.push({
2164
+ id: item?.id ?? `call_${toolCalls.length}`,
2165
+ type: "function",
2166
+ function: {
2167
+ name: item?.name ?? "",
2168
+ arguments: ""
2169
+ }
2170
+ });
2171
+ return;
2172
+ }
2173
+ if (t === "toolcall_delta") {
2174
+ const i = tcByContentIdx.get(inner.contentIndex);
2175
+ if (i !== void 0 && toolCalls[i]) toolCalls[i].function.arguments += inner.delta ?? "";
2176
+ }
2177
+ return;
2178
+ }
2179
+ if (event.type === "agent_end") usage = extractUsageFromAgentEnd(event) ?? usage;
2180
+ });
2181
+ try {
2182
+ await runConversation({
2183
+ session,
2184
+ prompt,
2185
+ images,
2186
+ log,
2187
+ postPrompt
2188
+ });
2189
+ } catch (err) {
2190
+ log.error({
2191
+ event: "chat_error",
2192
+ chatcmpl_id: sessionId,
2193
+ err
2194
+ }, "chat error");
2195
+ return jsonError(500, err?.message ?? String(err));
2196
+ } finally {
2197
+ registry.delete(sessionId);
2198
+ pendingSteerIds.delete(sessionId);
2199
+ }
2200
+ const message = {
2201
+ role: "assistant",
2202
+ content: content || null
2203
+ };
2204
+ if (toolCalls.length) message.tool_calls = toolCalls;
2205
+ const all = sm.buildSessionContext().messages;
2206
+ const sessionMessages = piMessagesToOpenAI(all.slice(baselineMessageCount));
2207
+ const response = {
2208
+ id: sessionId,
2209
+ object: "chat.completion",
2210
+ created,
2211
+ choices: [{
2212
+ index: 0,
2213
+ message,
2214
+ finish_reason: "stop"
2215
+ }],
2216
+ ...usage ? { usage } : {},
2217
+ ...sessionMessages.length ? { x_session_messages: sessionMessages } : {}
2218
+ };
2219
+ log.info({
2220
+ event: "blocking_done",
2221
+ chatcmpl_id: sessionId,
2222
+ duration_ms: Math.round(performance.now() - reqStart),
2223
+ upstream_duration_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
2224
+ }, "blocking done");
2225
+ return new Response(JSON.stringify(response), {
2226
+ status: 200,
2227
+ headers: { "Content-Type": "application/json" }
2228
+ });
2229
+ }
2230
+ const steerContentPartSchema = z.union([z.object({
2231
+ type: z.literal("text"),
2232
+ text: z.string()
2233
+ }), z.object({
2234
+ type: z.literal("image_url"),
2235
+ image_url: z.object({ url: z.string() })
2236
+ })]);
2237
+ const steerRequestSchema = z.object({
2238
+ content: z.union([z.string().min(1), z.array(steerContentPartSchema).min(1)]).optional(),
2239
+ mode: z.enum(["steer", "follow-up"]).default("steer"),
2240
+ text: z.string().optional()
2241
+ }).refine((v) => v.content !== void 0 || v.text !== void 0, { message: "content or text is required" });
2242
+ z.string().uuid();
2243
+ async function handleSteer(request, sessionId, registry, pendingSteerIds) {
2244
+ const { log } = requestLogger(request);
2245
+ const session = registry.get(sessionId);
2246
+ if (!session) return jsonError(404, "session not found or already completed");
2247
+ let body;
2248
+ try {
2249
+ body = JSON.parse(await request.text());
2250
+ } catch {
2251
+ return jsonError(400, "invalid json");
2252
+ }
2253
+ const parsed = steerRequestSchema.safeParse(body);
2254
+ if (!parsed.success) return jsonError(400, parsed.error.message);
2255
+ const { mode } = parsed.data;
2256
+ const { text, images } = await extractContent(parsed.data.content ?? parsed.data.text ?? "", {
2257
+ userAgent: request.headers.get("user-agent") ?? void 0,
2258
+ log
2259
+ });
2260
+ if (!text && images.length === 0) return jsonError(400, "steer has no content");
2261
+ const steerId = randomUUID();
2262
+ let queues = pendingSteerIds.get(sessionId);
2263
+ if (!queues) {
2264
+ queues = {
2265
+ steer: [],
2266
+ followUp: []
2267
+ };
2268
+ pendingSteerIds.set(sessionId, queues);
2269
+ }
2270
+ if (mode === "follow-up") {
2271
+ queues.followUp.push(steerId);
2272
+ await session.followUp(text, images.length > 0 ? images : void 0);
2273
+ } else {
2274
+ queues.steer.push(steerId);
2275
+ await session.steer(text, images.length > 0 ? images : void 0);
2276
+ }
2277
+ log.info({
2278
+ event: "steer_accepted",
2279
+ session_id: sessionId,
2280
+ steer_id: steerId,
2281
+ mode
2282
+ }, "steering message queued");
2283
+ return jsonResponse({
2284
+ ok: true,
2285
+ id: steerId
2286
+ });
2287
+ }
2288
+ function create$1(options) {
2289
+ const cwd = options.cwd ?? process.cwd();
2290
+ const registry = options.sessionRegistry ?? createMapSessionRegistry();
2291
+ const pendingSteerIds = /* @__PURE__ */ new Map();
2292
+ const ctx = {
2293
+ ...options,
2294
+ cwd
2295
+ };
2296
+ return async (request) => {
2297
+ const { pathname } = new URL(request.url);
2298
+ if (request.method === "POST") {
2299
+ const steerSessionId = pathname.match(/^\/v1\/chat\/completions\/([^/]+)\/steer$/)?.[1];
2300
+ if (steerSessionId) try {
2301
+ return await runInTraceContext(() => handleSteer(request, steerSessionId, registry, pendingSteerIds));
2302
+ } catch (err) {
2303
+ const { log } = requestLogger(request);
2304
+ log.error({
2305
+ err,
2306
+ event: "steer_unhandled_error"
2307
+ }, "unhandled error");
2308
+ return jsonError(500, "internal error");
2309
+ }
2310
+ const abortSessionId = pathname.match(/^\/v1\/chat\/completions\/([^/]+)\/abort$/)?.[1];
2311
+ if (abortSessionId) try {
2312
+ return await runInTraceContext(() => handleAbort({
2313
+ request,
2314
+ sessionId: abortSessionId,
2315
+ registry
2316
+ }));
2317
+ } catch (err) {
2318
+ const { log } = requestLogger(request);
2319
+ log.error({
2320
+ err,
2321
+ event: "abort_unhandled_error"
2322
+ }, "unhandled error");
2323
+ return jsonError(500, "internal error");
2324
+ }
2325
+ if (pathname === "/v1/chat/completions") try {
2326
+ return await runInTraceContext(() => handleChatCompletions(request, ctx, registry, pendingSteerIds));
2327
+ } catch (err) {
2328
+ const { log } = requestLogger(request);
2329
+ log.error({
2330
+ err,
2331
+ event: "chat_completions_unhandled_error"
2332
+ }, "unhandled error");
2333
+ return jsonError(500, "internal error");
2334
+ }
2335
+ }
2336
+ return null;
2337
+ };
2338
+ }
2339
+ //#endregion
2340
+ //#region src/protocols/responses.ts
2341
+ /**
2342
+ * OpenAI Responses API–compatible protocol handler.
2343
+ *
2344
+ * Catch-all: returns Response on match, null on no match.
2345
+ * Zero server-framework deps — just Web Standard Request/Response.
2346
+ *
2347
+ * Routes:
2348
+ * POST /v1/responses
2349
+ * POST /v1/responses/:id/abort
2350
+ *
2351
+ * @see https://platform.openai.com/docs/api-reference/responses
2352
+ */
2353
+ async function extractInputContent(input, log) {
2354
+ let systemPrompt = "";
2355
+ const history = [];
2356
+ let lastUserText = "";
2357
+ const images = [];
2358
+ if (typeof input === "string") return {
2359
+ prompt: input,
2360
+ images: [],
2361
+ systemPrompt: "",
2362
+ history: []
2363
+ };
2364
+ if (!Array.isArray(input)) return {
2365
+ prompt: "",
2366
+ images: [],
2367
+ systemPrompt: "",
2368
+ history: []
2369
+ };
2370
+ for (const item of input) {
2371
+ if (item?.type === "message") {
2372
+ if (item.role === "developer" || item.role === "system") {
2373
+ const text = extractTextFromContent(item.content);
2374
+ if (text) systemPrompt += (systemPrompt ? "\n\n" : "") + text;
2375
+ continue;
2376
+ }
2377
+ if (item.role === "user") {
2378
+ const text = extractTextFromContent(item.content);
2379
+ const imgs = await extractImagesFromContent(item.content, log);
2380
+ lastUserText = text;
2381
+ images.push(...imgs);
2382
+ history.push(item);
2383
+ continue;
2384
+ }
2385
+ if (item.role === "assistant") {
2386
+ history.push(item);
2387
+ continue;
2388
+ }
2389
+ }
2390
+ if (item?.type === "function_call_output") history.push(item);
2391
+ }
2392
+ return {
2393
+ prompt: lastUserText,
2394
+ images,
2395
+ systemPrompt,
2396
+ history
2397
+ };
2398
+ }
2399
+ function extractTextFromContent(content) {
2400
+ if (typeof content === "string") return content;
2401
+ if (!Array.isArray(content)) return "";
2402
+ return content.filter((p) => p?.type === "input_text" || p?.type === "text").map((p) => p.text ?? "").join("");
2403
+ }
2404
+ async function extractImagesFromContent(content, log) {
2405
+ if (!Array.isArray(content)) return [];
2406
+ const images = [];
2407
+ for (const part of content) if (part?.type === "input_image") {
2408
+ const url = part.image_url ?? part.url;
2409
+ if (typeof url === "string" && /^https?:\/\//i.test(url)) try {
2410
+ images.push(await fetchImageAsBase64({ url }));
2411
+ } catch (err) {
2412
+ log.warn({
2413
+ event: "image_fetch_failed",
2414
+ url,
2415
+ err
2416
+ }, "image fetch failed");
2417
+ }
2418
+ else if (part.data && part.media_type) images.push({
2419
+ type: "image",
2420
+ mimeType: part.media_type,
2421
+ data: part.data
2422
+ });
2423
+ }
2424
+ return images;
2425
+ }
2426
+ async function handleResponses(request, ctx, registry) {
2427
+ const { log, trace } = requestLogger(request);
2428
+ let parsed;
2429
+ try {
2430
+ parsed = JSON.parse(await request.text());
2431
+ } catch {
2432
+ return jsonError(400, "invalid json");
2433
+ }
2434
+ const { input, stream = false, model: modelInput, instructions, x_model: modelSpecInput } = parsed ?? {};
2435
+ if (input == null) return jsonError(400, "input required");
2436
+ const extracted = await extractInputContent(input, log);
2437
+ const { prompt } = extracted;
2438
+ const { images, systemPrompt: inputSystemPrompt } = extracted;
2439
+ if (!prompt && images.length === 0) return jsonError(400, "no user content in input");
2440
+ const systemPrompt = [typeof instructions === "string" ? instructions : "", inputSystemPrompt].filter(Boolean).join("\n\n");
2441
+ const { model, modelSpec } = resolveRequestModel({
2442
+ modelInput,
2443
+ modelSpecInput,
2444
+ defaultModel: ctx.defaultModel,
2445
+ log
2446
+ });
2447
+ const modelName = model.id ?? "claude-opus-4-7";
2448
+ const { sessionId } = parseSessionId(request);
2449
+ const shellEnv = parseShellEnv(request);
2450
+ const { session } = await ctx.createSession({
2451
+ cwd: ctx.cwd,
2452
+ sessionId,
2453
+ perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
2454
+ systemPromptOverride: () => systemPrompt.length > 0 ? systemPrompt : void 0,
2455
+ shellEnv,
2456
+ model,
2457
+ thinkingLevel: null,
2458
+ log,
2459
+ onSessionSetup: (args) => ctx.onSessionSetup?.(args),
2460
+ prepare: null
2461
+ });
2462
+ registry.set(sessionId, session);
2463
+ const responseId = `resp_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
2464
+ if (stream) {
2465
+ const response = runStream({
2466
+ session,
2467
+ prompt,
2468
+ images,
2469
+ responseId,
2470
+ sessionId,
2471
+ modelName,
2472
+ log,
2473
+ postPrompt: ctx.postPrompt,
2474
+ registry
2475
+ });
2476
+ const traceId = parseTraceId(trace.traceparent);
2477
+ if (traceId) response.headers.set("x-trace-id", traceId);
2478
+ return response;
2479
+ }
2480
+ const response = await runBlocking({
2481
+ session,
2482
+ prompt,
2483
+ images,
2484
+ responseId,
2485
+ sessionId,
2486
+ modelName,
2487
+ log,
2488
+ postPrompt: ctx.postPrompt,
2489
+ registry
2490
+ });
2491
+ const traceId = parseTraceId(trace.traceparent);
2492
+ if (traceId) response.headers.set("x-trace-id", traceId);
2493
+ return response;
2494
+ }
2495
+ function runStream({ session, prompt, images, responseId, sessionId, modelName, log, postPrompt, registry }) {
2496
+ return createSSEResponse(async (writer) => {
2497
+ let seq = 0;
2498
+ let textContent = "";
2499
+ const outputItems = [];
2500
+ const tcByContentIdx = /* @__PURE__ */ new Map();
2501
+ let nextOutputIndex = 0;
2502
+ let capturedModelError = null;
2503
+ const send = (event) => {
2504
+ writer.write(`data: ${JSON.stringify(event)}\n\n`);
2505
+ };
2506
+ const failWithProviderError = (providerError) => {
2507
+ log.warn({
2508
+ event: "model_provider_error",
2509
+ code: providerError.code,
2510
+ upstream_status: providerError.upstreamStatus,
2511
+ provider: providerError.provider
2512
+ }, "forwarding model-provider error to client");
2513
+ send({
2514
+ type: "response.failed",
2515
+ sequence_number: seq++,
2516
+ response: {
2517
+ id: responseId,
2518
+ status: "failed",
2519
+ error: {
2520
+ code: providerError.code,
2521
+ message: providerError.message,
2522
+ x_model_provider_error: {
2523
+ provider: providerError.provider,
2524
+ type: providerError.type,
2525
+ upstream_status: providerError.upstreamStatus
2526
+ }
2527
+ }
2528
+ }
2529
+ });
2530
+ };
2531
+ send({
2532
+ type: "response.created",
2533
+ sequence_number: seq++,
2534
+ response: {
2535
+ id: responseId,
2536
+ object: "response",
2537
+ status: "in_progress",
2538
+ output: [],
2539
+ model: modelName,
2540
+ created_at: Math.floor(Date.now() / 1e3)
2541
+ }
2542
+ });
2543
+ let textItemEmitted = false;
2544
+ const ensureTextItem = () => {
2545
+ if (textItemEmitted) return;
2546
+ textItemEmitted = true;
2547
+ send({
2548
+ type: "response.output_item.added",
2549
+ sequence_number: seq++,
2550
+ output_index: nextOutputIndex,
2551
+ item: {
2552
+ type: "message",
2553
+ id: `item_${randomUUID().slice(0, 8)}`,
2554
+ role: "assistant",
2555
+ content: []
2556
+ }
2557
+ });
2558
+ };
2559
+ session.subscribe((event) => {
2560
+ const ev = event;
2561
+ const endedMessage = ev.message ?? (Array.isArray(ev.messages) ? ev.messages[ev.messages.length - 1] : null);
2562
+ if (endedMessage?.stopReason === "error" && typeof endedMessage.errorMessage === "string" && endedMessage.errorMessage.length > 0) capturedModelError = endedMessage.errorMessage;
2563
+ if (ev.type !== "message_update") return;
2564
+ const inner = ev.assistantMessageEvent;
2565
+ if (!inner) return;
2566
+ if (inner.type === "text_delta") {
2567
+ ensureTextItem();
2568
+ textContent += inner.delta ?? "";
2569
+ send({
2570
+ type: "response.output_text.delta",
2571
+ sequence_number: seq++,
2572
+ output_index: textItemEmitted ? 0 : nextOutputIndex,
2573
+ delta: inner.delta ?? ""
2574
+ });
2575
+ return;
2576
+ }
2577
+ if (inner.type === "toolcall_start") {
2578
+ const idx = inner.contentIndex;
2579
+ const item = inner.partial?.content?.[idx];
2580
+ const tc = item?.type === "toolCall" ? item : void 0;
2581
+ const itemId = `item_${randomUUID().slice(0, 8)}`;
2582
+ const callId = tc?.id ?? `call_${nextOutputIndex}`;
2583
+ const outputIndex = nextOutputIndex++;
2584
+ tcByContentIdx.set(idx, {
2585
+ itemId,
2586
+ callId,
2587
+ outputIndex
2588
+ });
2589
+ send({
2590
+ type: "response.output_item.added",
2591
+ sequence_number: seq++,
2592
+ output_index: outputIndex,
2593
+ item: {
2594
+ type: "function_call",
2595
+ id: itemId,
2596
+ call_id: callId,
2597
+ name: tc?.name ?? "",
2598
+ arguments: "",
2599
+ status: "in_progress"
2600
+ }
2601
+ });
2602
+ return;
2603
+ }
2604
+ if (inner.type === "toolcall_delta") {
2605
+ const info = tcByContentIdx.get(inner.contentIndex);
2606
+ if (!info) return;
2607
+ send({
2608
+ type: "response.function_call_arguments.delta",
2609
+ sequence_number: seq++,
2610
+ output_index: info.outputIndex,
2611
+ call_id: info.callId,
2612
+ delta: inner.delta ?? ""
2613
+ });
2614
+ }
2615
+ });
2616
+ try {
2617
+ await runConversation({
2618
+ session,
2619
+ prompt,
2620
+ images,
2621
+ log,
2622
+ postPrompt
2623
+ });
2624
+ if (capturedModelError !== null) failWithProviderError(toModelProviderError(new Error(capturedModelError)));
2625
+ } catch (err) {
2626
+ log.error({
2627
+ err,
2628
+ event: "responses_stream_error"
2629
+ }, "stream error");
2630
+ if (isModelProviderError(err)) failWithProviderError(toModelProviderError(err));
2631
+ } finally {
2632
+ registry.delete(sessionId);
2633
+ }
2634
+ if (textContent) {
2635
+ send({
2636
+ type: "response.output_text.done",
2637
+ sequence_number: seq++,
2638
+ output_index: 0,
2639
+ text: textContent
2640
+ });
2641
+ outputItems.push({
2642
+ type: "message",
2643
+ role: "assistant",
2644
+ content: [{
2645
+ type: "output_text",
2646
+ text: textContent
2647
+ }]
2648
+ });
2649
+ }
2650
+ for (const [, info] of tcByContentIdx) send({
2651
+ type: "response.output_item.done",
2652
+ sequence_number: seq++,
2653
+ output_index: info.outputIndex,
2654
+ item: {
2655
+ type: "function_call",
2656
+ id: info.itemId,
2657
+ call_id: info.callId,
2658
+ status: "completed"
2659
+ }
2660
+ });
2661
+ send({
2662
+ type: "response.completed",
2663
+ sequence_number: seq++,
2664
+ response: {
2665
+ id: responseId,
2666
+ object: "response",
2667
+ status: "completed",
2668
+ output: outputItems,
2669
+ model: modelName,
2670
+ output_text: textContent,
2671
+ created_at: Math.floor(Date.now() / 1e3),
2672
+ usage: {
2673
+ input_tokens: 0,
2674
+ output_tokens: 0,
2675
+ total_tokens: 0,
2676
+ input_tokens_details: { cached_tokens: 0 },
2677
+ output_tokens_details: { reasoning_tokens: 0 }
2678
+ }
2679
+ }
2680
+ });
2681
+ writer.write("data: [DONE]\n\n");
2682
+ });
2683
+ }
2684
+ async function runBlocking({ session, prompt, images, responseId, sessionId, modelName, log, postPrompt, registry }) {
2685
+ let textContent = "";
2686
+ const functionCalls = [];
2687
+ const tcByContentIdx = /* @__PURE__ */ new Map();
2688
+ session.subscribe((event) => {
2689
+ if (event.type === "message_update") {
2690
+ const inner = event.assistantMessageEvent;
2691
+ if (inner?.type === "text_delta") {
2692
+ textContent += inner.delta ?? "";
2693
+ return;
2694
+ }
2695
+ if (inner?.type === "toolcall_start") {
2696
+ const idx = inner.contentIndex;
2697
+ const item = inner.partial?.content?.[idx];
2698
+ tcByContentIdx.set(idx, functionCalls.length);
2699
+ functionCalls.push({
2700
+ type: "function_call",
2701
+ id: `item_${randomUUID().slice(0, 8)}`,
2702
+ call_id: item?.id ?? `call_${functionCalls.length}`,
2703
+ name: item?.name ?? "",
2704
+ arguments: "",
2705
+ status: "completed"
2706
+ });
2707
+ return;
2708
+ }
2709
+ if (inner?.type === "toolcall_delta") {
2710
+ const i = tcByContentIdx.get(inner.contentIndex);
2711
+ if (i !== void 0 && functionCalls[i]) functionCalls[i].arguments += inner.delta ?? "";
2712
+ }
2713
+ }
2714
+ });
2715
+ try {
2716
+ await runConversation({
2717
+ session,
2718
+ prompt,
2719
+ images,
2720
+ log,
2721
+ postPrompt
2722
+ });
2723
+ } catch (err) {
2724
+ log.error({
2725
+ err,
2726
+ event: "responses_blocking_error"
2727
+ }, "blocking error");
2728
+ return jsonError(500, err?.message ?? String(err));
2729
+ } finally {
2730
+ registry.delete(sessionId);
2731
+ }
2732
+ const output = [];
2733
+ if (textContent) output.push({
2734
+ type: "message",
2735
+ role: "assistant",
2736
+ content: [{
2737
+ type: "output_text",
2738
+ text: textContent
2739
+ }]
2740
+ });
2741
+ output.push(...functionCalls);
2742
+ return jsonResponse({
2743
+ id: responseId,
2744
+ object: "response",
2745
+ status: "completed",
2746
+ output,
2747
+ model: modelName,
2748
+ output_text: textContent,
2749
+ created_at: Math.floor(Date.now() / 1e3),
2750
+ usage: {
2751
+ input_tokens: 0,
2752
+ output_tokens: 0,
2753
+ total_tokens: 0,
2754
+ input_tokens_details: { cached_tokens: 0 },
2755
+ output_tokens_details: { reasoning_tokens: 0 }
2756
+ }
2757
+ });
2758
+ }
2759
+ function create(options) {
2760
+ const cwd = options.cwd ?? process.cwd();
2761
+ const registry = options.sessionRegistry ?? createMapSessionRegistry();
2762
+ const ctx = {
2763
+ ...options,
2764
+ cwd
2765
+ };
2766
+ return async (request) => {
2767
+ const { pathname } = new URL(request.url);
2768
+ if (request.method !== "POST") return null;
2769
+ const abortSessionId = pathname.match(/^\/v1\/responses\/([^/]+)\/abort$/)?.[1];
2770
+ if (abortSessionId) try {
2771
+ return await runInTraceContext(() => handleAbort({
2772
+ request,
2773
+ sessionId: abortSessionId,
2774
+ registry
2775
+ }));
2776
+ } catch (err) {
2777
+ const { log } = requestLogger(request);
2778
+ log.error({
2779
+ err,
2780
+ event: "responses_abort_unhandled_error"
2781
+ }, "unhandled error");
2782
+ return jsonError(500, "internal error");
2783
+ }
2784
+ if (pathname !== "/v1/responses") return null;
2785
+ try {
2786
+ return await runInTraceContext(() => handleResponses(request, ctx, registry));
2787
+ } catch (err) {
2788
+ const { log } = requestLogger(request);
2789
+ log.error({
2790
+ err,
2791
+ event: "responses_unhandled_error"
2792
+ }, "unhandled error");
2793
+ return jsonError(500, "internal error");
2794
+ }
2795
+ };
2796
+ }
2797
+ //#endregion
2798
+ //#region src/protocols/index.ts
2799
+ /**
2800
+ * Build the individual protocol handlers, each returning null for
2801
+ * requests it doesn't recognize. An in-memory session registry backs
2802
+ * chat-completions steering unless deliberately overridden.
2803
+ */
2804
+ function createProtocolHandlers(options) {
2805
+ const shared = {
2806
+ cwd: options.cwd,
2807
+ createSession: options.createSession,
2808
+ onSessionSetup: options.onSessionSetup,
2809
+ postPrompt: options.postPrompt,
2810
+ passthroughHeaderPrefixes: options.passthroughHeaderPrefixes
2811
+ };
2812
+ const chatCompletions = create$1({
2813
+ ...shared,
2814
+ sessionRegistry: createMapSessionRegistry(),
2815
+ ...options.chatCompletions
2816
+ });
2817
+ return {
2818
+ prewarm: createPrewarm({
2819
+ paths: options.prewarmPaths ?? [],
2820
+ chatCompletions
2821
+ }),
2822
+ chatCompletions,
2823
+ messages: create$2({
2824
+ ...shared,
2825
+ ...options.messages
2826
+ }),
2827
+ responses: create({
2828
+ ...shared,
2829
+ ...options.responses
2830
+ })
2831
+ };
2832
+ }
2833
+ /** Chain handlers; null when none matched. */
2834
+ function composeHandlers(handlers) {
2835
+ return async (request) => {
2836
+ for (const handler of handlers) {
2837
+ const response = await handler(request);
2838
+ if (response) return response;
2839
+ }
2840
+ return null;
2841
+ };
2842
+ }
2843
+ function createProtocols(options) {
2844
+ const handlers = createProtocolHandlers(options);
2845
+ const enabled = (name) => (!options.only || options.only.includes(name)) && (!options.exclude || !options.exclude.includes(name));
2846
+ const composed = composeHandlers([
2847
+ ...enabled("chat-completions") ? [handlers.prewarm, handlers.chatCompletions] : [],
2848
+ ...enabled("messages") ? [handlers.messages] : [],
2849
+ ...enabled("responses") ? [handlers.responses] : []
2850
+ ]);
2851
+ return async (request) => {
2852
+ const response = await composed(request);
2853
+ if (response) return response;
2854
+ return new Response(JSON.stringify({ error: { message: "not found" } }), {
2855
+ status: 404,
2856
+ headers: { "Content-Type": "application/json" }
2857
+ });
2858
+ };
2859
+ }
2860
+ //#endregion
2861
+ //#region src/express.ts
2862
+ function requestUrl(req) {
2863
+ return `${req.headers["x-forwarded-proto"] ?? "http"}://${req.headers["x-forwarded-host"] ?? req.headers.host ?? "localhost"}${req.originalUrl ?? req.url ?? "/"}`;
2864
+ }
2865
+ function requestHeaders(req) {
2866
+ const headers = new Headers();
2867
+ for (const [key, value] of Object.entries(req.headers)) {
2868
+ if (value == null) continue;
2869
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
2870
+ else headers.set(key, value);
2871
+ }
2872
+ return headers;
2873
+ }
2874
+ async function toWebRequest(req) {
2875
+ const init = {
2876
+ method: req.method,
2877
+ headers: requestHeaders(req)
2878
+ };
2879
+ if (req.method !== "GET" && req.method !== "HEAD") {
2880
+ const { Readable } = await import("node:stream");
2881
+ init.body = Readable.toWeb(req);
2882
+ init.duplex = "half";
2883
+ }
2884
+ return new Request(requestUrl(req), init);
2885
+ }
2886
+ async function sendWebResponse(res, webResponse) {
2887
+ const headers = {};
2888
+ webResponse.headers.forEach((v, k) => {
2889
+ headers[k] = v;
2890
+ });
2891
+ res.writeHead(webResponse.status, headers);
2892
+ if (webResponse.body) {
2893
+ const { Readable } = await import("node:stream");
2894
+ Readable.fromWeb(webResponse.body).pipe(res);
2895
+ } else res.end();
2896
+ }
2897
+ /**
2898
+ * Mountable middleware for a web-standard handler. A null result calls
2899
+ * next() so unmatched requests fall through to whatever the caller
2900
+ * mounts after it.
2901
+ */
2902
+ function webHandlerToMiddleware(handler) {
2903
+ return async (req, res, next) => {
2904
+ try {
2905
+ const webResponse = await handler(await toWebRequest(req));
2906
+ if (!webResponse) {
2907
+ next();
2908
+ return;
2909
+ }
2910
+ await sendWebResponse(res, webResponse);
2911
+ } catch (err) {
2912
+ logger.error({
2913
+ err,
2914
+ event: "server_error"
2915
+ }, "unhandled server error");
2916
+ if (!res.headersSent) {
2917
+ res.writeHead(500, { "content-type": "application/json" });
2918
+ res.end(JSON.stringify({ error: { message: "internal error" } }));
2919
+ }
2920
+ }
2921
+ };
2922
+ }
2923
+ /**
2924
+ * Path-scope a middleware the way express mounting does: skip (next())
2925
+ * unless the request path matches the prefix, and present a
2926
+ * mount-relative req.url to the inner middleware. Lets express-style
2927
+ * handlers that expect `app.use('/a2a', h)` participate in a composed
2928
+ * catch-all.
2929
+ */
2930
+ function mountAt(prefix, middleware) {
2931
+ return (req, res, next) => {
2932
+ const originalUrl = req.url ?? "/";
2933
+ const path = originalUrl.split("?")[0] ?? "";
2934
+ if (path !== prefix && !path.startsWith(`${prefix}/`)) {
2935
+ next();
2936
+ return;
2937
+ }
2938
+ const rest = originalUrl.slice(prefix.length);
2939
+ req.url = rest.startsWith("/") ? rest : `/${rest}`;
2940
+ middleware(req, res, (err) => {
2941
+ req.url = originalUrl;
2942
+ next(err);
2943
+ });
2944
+ };
2945
+ }
2946
+ /** Run middlewares in order; each next() advances to the following one. */
2947
+ function chainMiddleware(middlewares) {
2948
+ return (req, res, next) => {
2949
+ let i = 0;
2950
+ const run = (err) => {
2951
+ if (err) {
2952
+ next(err);
2953
+ return;
2954
+ }
2955
+ const middleware = middlewares[i++];
2956
+ if (!middleware) {
2957
+ next();
2958
+ return;
2959
+ }
2960
+ middleware(req, res, run);
2961
+ };
2962
+ run();
2963
+ };
2964
+ }
2965
+ //#endregion
2966
+ export { BILLING_BLOCK_REASONS, DEFAULT_MODEL, DEFAULT_PROVIDER, DEFAULT_THINKING_LEVEL, KNOWN_PI_PROVIDERS, PI_AGENT_DIR, VALID_THINKING_LEVELS, buildAgentCard, buildModelFromSpec, chainMiddleware, composeHandlers, createAgentExecutor, createMapSessionRegistry, createPrewarm, createProtocolHandlers, createProtocols, deriveTraceContext, getCurrentTraceparent, logger, modelSpecSchema, mountAt, newTraceContext, parseModelSpec, parseTraceId, requestHeaders, requestLogger, requestUrl, resolveRequestModel, runInTraceContext, setCurrentTraceparent, webHandlerToMiddleware, withSpecApiKey };