@skydiveai/pi-server 0.1.0-beta.6

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,2854 @@
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
+ return "provider_error";
881
+ }
882
+ /**
883
+ * True when the thrown error looks like it originated from a model-provider
884
+ * call (it carries an HTTP status or a provider-shaped error body) rather than
885
+ * an arbitrary harness-internal throw. Conservative: when unsure, returns false
886
+ * so the caller keeps the existing generic handling.
887
+ */
888
+ function isModelProviderError(err) {
889
+ if (err === null || typeof err !== "object") return false;
890
+ const e = err;
891
+ if (numericStatus(e) !== null) return true;
892
+ if (e.error && typeof e.error === "object") return true;
893
+ const msg = messageText(err);
894
+ 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/i.test(msg);
895
+ }
896
+ /** Build the structured, forwardable error from a thrown model-provider error. */
897
+ function toModelProviderError(err) {
898
+ const e = err ?? {};
899
+ const status = numericStatus(e);
900
+ const message = messageText(err);
901
+ return {
902
+ code: classifyCode(status, message, machineCode(e)),
903
+ message,
904
+ provider: providerId(e, message),
905
+ type: providerErrorType(e),
906
+ upstreamStatus: status
907
+ };
908
+ }
909
+ //#endregion
910
+ //#region src/protocols/anthropic-messages.ts
911
+ /**
912
+ * Anthropic Messages API–compatible protocol handler.
913
+ *
914
+ * Catch-all: returns Response on match, null on no match.
915
+ * Zero server-framework deps — just Web Standard Request/Response.
916
+ *
917
+ * Routes:
918
+ * POST /v1/messages
919
+ * POST /v1/messages/:id/abort
920
+ *
921
+ * @see https://docs.anthropic.com/en/api/messages
922
+ */
923
+ function sseEvent(writer, event, data) {
924
+ writer.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
925
+ }
926
+ async function extractContent$1(content, log) {
927
+ if (typeof content === "string") return {
928
+ text: content,
929
+ images: []
930
+ };
931
+ if (!Array.isArray(content)) return {
932
+ text: "",
933
+ images: []
934
+ };
935
+ let text = "";
936
+ const images = [];
937
+ for (const block of content) {
938
+ if (block?.type === "text") {
939
+ text += block.text ?? "";
940
+ continue;
941
+ }
942
+ if (block?.type === "image") {
943
+ const src = block.source;
944
+ if (src?.type === "base64" && src.media_type && src.data) images.push({
945
+ type: "image",
946
+ mimeType: src.media_type,
947
+ data: src.data
948
+ });
949
+ else if (src?.type === "url" && src.url) try {
950
+ images.push(await fetchImageAsBase64({ url: src.url }));
951
+ } catch (err) {
952
+ log.warn({
953
+ event: "image_fetch_failed",
954
+ url: src.url,
955
+ err
956
+ }, "image fetch failed");
957
+ text += `\n[image fetch failed: ${src.url}]\n`;
958
+ }
959
+ continue;
960
+ }
961
+ }
962
+ return {
963
+ text,
964
+ images
965
+ };
966
+ }
967
+ function extractSystemPrompt(system) {
968
+ if (typeof system === "string") return system;
969
+ if (!Array.isArray(system)) return "";
970
+ return system.filter((b) => b?.type === "text").map((b) => b.text ?? "").join("\n\n");
971
+ }
972
+ async function loadHistoryIntoSession$1({ sm, messages, upToIdxExclusive, modelName, log }) {
973
+ const toolCallNames = /* @__PURE__ */ new Map();
974
+ for (let i = 0; i < upToIdxExclusive; i++) {
975
+ const m = messages[i];
976
+ if (!m) continue;
977
+ if (m.role === "user") {
978
+ const { text, images } = await extractContent$1(m.content, log);
979
+ const content = images.length > 0 ? [...text ? [{
980
+ type: "text",
981
+ text
982
+ }] : [], ...images] : text;
983
+ if (typeof content === "string" && content.length > 0 || Array.isArray(content) && content.length > 0) sm.appendMessage({
984
+ role: "user",
985
+ content,
986
+ timestamp: Date.now()
987
+ });
988
+ continue;
989
+ }
990
+ if (m.role === "assistant") {
991
+ const contentArr = [];
992
+ const blocks = Array.isArray(m.content) ? m.content : [];
993
+ for (const b of blocks) if (b?.type === "text" && b.text) contentArr.push({
994
+ type: "text",
995
+ text: b.text
996
+ });
997
+ else if (b?.type === "tool_use") {
998
+ contentArr.push({
999
+ type: "toolCall",
1000
+ id: b.id,
1001
+ name: b.name,
1002
+ arguments: b.input ?? {}
1003
+ });
1004
+ toolCallNames.set(b.id, b.name);
1005
+ }
1006
+ if (contentArr.length === 0) continue;
1007
+ sm.appendMessage({
1008
+ role: "assistant",
1009
+ content: contentArr,
1010
+ api: "anthropic-messages",
1011
+ provider: "anthropic",
1012
+ model: modelName,
1013
+ usage: {
1014
+ input: 0,
1015
+ output: 0,
1016
+ cacheRead: 0,
1017
+ cacheWrite: 0,
1018
+ totalTokens: 0,
1019
+ cost: {
1020
+ input: 0,
1021
+ output: 0,
1022
+ cacheRead: 0,
1023
+ cacheWrite: 0,
1024
+ total: 0
1025
+ }
1026
+ },
1027
+ stopReason: contentArr.some((c) => c.type === "toolCall") ? "toolUse" : "stop",
1028
+ timestamp: Date.now()
1029
+ });
1030
+ continue;
1031
+ }
1032
+ }
1033
+ }
1034
+ async function handleMessages(request, ctx, registry) {
1035
+ const { log, trace } = requestLogger(request);
1036
+ let parsed;
1037
+ try {
1038
+ parsed = JSON.parse(await request.text());
1039
+ } catch {
1040
+ return jsonError(400, "invalid json");
1041
+ }
1042
+ const { messages, stream = false, model: modelInput, system, x_model: modelSpecInput } = parsed ?? {};
1043
+ if (!Array.isArray(messages) || messages.length === 0) return jsonError(400, "messages array required");
1044
+ let lastUserIdx = -1;
1045
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "user") {
1046
+ lastUserIdx = i;
1047
+ break;
1048
+ }
1049
+ if (lastUserIdx < 0) return jsonError(400, "no user message");
1050
+ const { text: prompt, images } = await extractContent$1(messages[lastUserIdx].content, log);
1051
+ if (!prompt && images.length === 0) return jsonError(400, "user message has no content");
1052
+ const { model, modelSpec } = resolveRequestModel({
1053
+ modelInput,
1054
+ modelSpecInput,
1055
+ defaultModel: ctx.defaultModel,
1056
+ log
1057
+ });
1058
+ const modelName = model.id ?? "claude-opus-4-7";
1059
+ const baseSystemPrompt = extractSystemPrompt(system);
1060
+ const { sessionId } = parseSessionId(request);
1061
+ const shellEnv = parseShellEnv(request);
1062
+ const { session, sessionManager: sm } = await ctx.createSession({
1063
+ cwd: ctx.cwd,
1064
+ sessionId,
1065
+ perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
1066
+ systemPromptOverride: () => baseSystemPrompt.length > 0 ? baseSystemPrompt : void 0,
1067
+ shellEnv,
1068
+ model,
1069
+ thinkingLevel: null,
1070
+ log,
1071
+ onSessionSetup: (args) => ctx.onSessionSetup?.(args),
1072
+ prepare: ({ sessionManager }) => loadHistoryIntoSession$1({
1073
+ sm: sessionManager,
1074
+ messages,
1075
+ upToIdxExclusive: lastUserIdx,
1076
+ modelName,
1077
+ log
1078
+ })
1079
+ });
1080
+ registry.set(sessionId, session);
1081
+ const id = `msg_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
1082
+ if (stream) {
1083
+ const response = runStream$2({
1084
+ session,
1085
+ sm,
1086
+ prompt,
1087
+ images,
1088
+ id,
1089
+ sessionId,
1090
+ modelName,
1091
+ log,
1092
+ postPrompt: ctx.postPrompt,
1093
+ registry
1094
+ });
1095
+ const traceId = parseTraceId(trace.traceparent);
1096
+ if (traceId) response.headers.set("x-trace-id", traceId);
1097
+ return response;
1098
+ }
1099
+ const response = await runBlocking$2({
1100
+ session,
1101
+ sm,
1102
+ prompt,
1103
+ images,
1104
+ id,
1105
+ sessionId,
1106
+ modelName,
1107
+ log,
1108
+ postPrompt: ctx.postPrompt,
1109
+ registry
1110
+ });
1111
+ const traceId = parseTraceId(trace.traceparent);
1112
+ if (traceId) response.headers.set("x-trace-id", traceId);
1113
+ return response;
1114
+ }
1115
+ function runStream$2({ session, sm, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1116
+ return createSSEResponse(async (writer) => {
1117
+ let contentBlockIndex = 0;
1118
+ let textBlockOpen = false;
1119
+ const tcByContentIdx = /* @__PURE__ */ new Map();
1120
+ sseEvent(writer, "message_start", {
1121
+ type: "message_start",
1122
+ message: {
1123
+ id,
1124
+ type: "message",
1125
+ role: "assistant",
1126
+ model: modelName,
1127
+ content: [],
1128
+ stop_reason: null,
1129
+ stop_sequence: null,
1130
+ usage: {
1131
+ input_tokens: 0,
1132
+ cache_creation_input_tokens: 0,
1133
+ cache_read_input_tokens: 0,
1134
+ output_tokens: 0
1135
+ }
1136
+ }
1137
+ });
1138
+ sseEvent(writer, "ping", { type: "ping" });
1139
+ const ensureTextBlock = () => {
1140
+ if (textBlockOpen) return;
1141
+ sseEvent(writer, "content_block_start", {
1142
+ type: "content_block_start",
1143
+ index: contentBlockIndex,
1144
+ content_block: {
1145
+ type: "text",
1146
+ text: ""
1147
+ }
1148
+ });
1149
+ textBlockOpen = true;
1150
+ };
1151
+ const closeTextBlock = () => {
1152
+ if (!textBlockOpen) return;
1153
+ sseEvent(writer, "content_block_stop", {
1154
+ type: "content_block_stop",
1155
+ index: contentBlockIndex
1156
+ });
1157
+ contentBlockIndex++;
1158
+ textBlockOpen = false;
1159
+ };
1160
+ let capturedModelError = null;
1161
+ const emitProviderError = (providerError) => {
1162
+ log.warn({
1163
+ event: "model_provider_error",
1164
+ code: providerError.code,
1165
+ upstream_status: providerError.upstreamStatus,
1166
+ provider: providerError.provider
1167
+ }, "forwarding model-provider error to client");
1168
+ sseEvent(writer, "error", {
1169
+ type: "error",
1170
+ error: {
1171
+ type: providerError.type ?? "api_error",
1172
+ message: providerError.message,
1173
+ x_model_provider_error: {
1174
+ code: providerError.code,
1175
+ provider: providerError.provider,
1176
+ upstream_status: providerError.upstreamStatus
1177
+ }
1178
+ }
1179
+ });
1180
+ };
1181
+ session.subscribe((event) => {
1182
+ const ev = event;
1183
+ const endedMessage = ev.message ?? (Array.isArray(ev.messages) ? ev.messages[ev.messages.length - 1] : null);
1184
+ if (endedMessage?.stopReason === "error" && typeof endedMessage.errorMessage === "string" && endedMessage.errorMessage.length > 0) capturedModelError = endedMessage.errorMessage;
1185
+ if (ev.type !== "message_update") return;
1186
+ const inner = ev.assistantMessageEvent;
1187
+ if (!inner) return;
1188
+ if (inner.type === "text_delta") {
1189
+ ensureTextBlock();
1190
+ sseEvent(writer, "content_block_delta", {
1191
+ type: "content_block_delta",
1192
+ index: contentBlockIndex,
1193
+ delta: {
1194
+ type: "text_delta",
1195
+ text: inner.delta ?? ""
1196
+ }
1197
+ });
1198
+ return;
1199
+ }
1200
+ if (inner.type === "toolcall_start") {
1201
+ closeTextBlock();
1202
+ const idx = inner.contentIndex;
1203
+ const item = inner.partial?.content?.[idx];
1204
+ const tc = item?.type === "toolCall" ? item : void 0;
1205
+ const blockIdx = contentBlockIndex++;
1206
+ tcByContentIdx.set(idx, blockIdx);
1207
+ sseEvent(writer, "content_block_start", {
1208
+ type: "content_block_start",
1209
+ index: blockIdx,
1210
+ content_block: {
1211
+ type: "tool_use",
1212
+ id: tc?.id ?? `toolu_${blockIdx}`,
1213
+ name: tc?.name ?? ""
1214
+ }
1215
+ });
1216
+ return;
1217
+ }
1218
+ if (inner.type === "toolcall_delta") {
1219
+ const blockIdx = tcByContentIdx.get(inner.contentIndex);
1220
+ if (blockIdx === void 0) return;
1221
+ sseEvent(writer, "content_block_delta", {
1222
+ type: "content_block_delta",
1223
+ index: blockIdx,
1224
+ delta: {
1225
+ type: "input_json_delta",
1226
+ partial_json: inner.delta ?? ""
1227
+ }
1228
+ });
1229
+ }
1230
+ });
1231
+ let stopReason = "end_turn";
1232
+ const outputTokens = 0;
1233
+ try {
1234
+ await runConversation({
1235
+ session,
1236
+ prompt,
1237
+ images,
1238
+ log,
1239
+ postPrompt
1240
+ });
1241
+ if (capturedModelError !== null) emitProviderError(toModelProviderError(new Error(capturedModelError)));
1242
+ } catch (err) {
1243
+ log.error({
1244
+ err,
1245
+ event: "anthropic_stream_error"
1246
+ }, "stream error");
1247
+ stopReason = "end_turn";
1248
+ if (isModelProviderError(err)) emitProviderError(toModelProviderError(err));
1249
+ } finally {
1250
+ registry.delete(sessionId);
1251
+ }
1252
+ closeTextBlock();
1253
+ for (const blockIdx of tcByContentIdx.values()) sseEvent(writer, "content_block_stop", {
1254
+ type: "content_block_stop",
1255
+ index: blockIdx
1256
+ });
1257
+ if (tcByContentIdx.size > 0) stopReason = "tool_use";
1258
+ sseEvent(writer, "message_delta", {
1259
+ type: "message_delta",
1260
+ delta: {
1261
+ stop_reason: stopReason,
1262
+ stop_sequence: null
1263
+ },
1264
+ usage: { output_tokens: outputTokens }
1265
+ });
1266
+ sseEvent(writer, "message_stop", { type: "message_stop" });
1267
+ });
1268
+ }
1269
+ async function runBlocking$2({ session, sm, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1270
+ let text = "";
1271
+ const toolUseBlocks = [];
1272
+ const tcByContentIdx = /* @__PURE__ */ new Map();
1273
+ session.subscribe((event) => {
1274
+ if (event.type === "message_update") {
1275
+ const inner = event.assistantMessageEvent;
1276
+ if (inner?.type === "text_delta") {
1277
+ text += inner.delta ?? "";
1278
+ return;
1279
+ }
1280
+ if (inner?.type === "toolcall_start") {
1281
+ const idx = inner.contentIndex;
1282
+ const item = inner.partial?.content?.[idx];
1283
+ tcByContentIdx.set(idx, toolUseBlocks.length);
1284
+ toolUseBlocks.push({
1285
+ type: "tool_use",
1286
+ id: item?.id ?? `toolu_${toolUseBlocks.length}`,
1287
+ name: item?.name ?? "",
1288
+ input: {}
1289
+ });
1290
+ return;
1291
+ }
1292
+ if (inner?.type === "toolcall_delta") {
1293
+ const i = tcByContentIdx.get(inner.contentIndex);
1294
+ if (i !== void 0) {
1295
+ const block = toolUseBlocks[i];
1296
+ if (block && block.type === "tool_use") block.input = (typeof block.input === "string" ? block.input : "") + (inner.delta ?? "");
1297
+ }
1298
+ }
1299
+ }
1300
+ });
1301
+ try {
1302
+ await runConversation({
1303
+ session,
1304
+ prompt,
1305
+ images,
1306
+ log,
1307
+ postPrompt
1308
+ });
1309
+ } catch (err) {
1310
+ log.error({
1311
+ err,
1312
+ event: "anthropic_blocking_error"
1313
+ }, "blocking error");
1314
+ return jsonError(500, err?.message ?? String(err));
1315
+ } finally {
1316
+ registry.delete(sessionId);
1317
+ }
1318
+ for (const block of toolUseBlocks) if (block.type === "tool_use" && typeof block.input === "string") try {
1319
+ block.input = JSON.parse(block.input);
1320
+ } catch {
1321
+ block.input = {};
1322
+ }
1323
+ const content = [];
1324
+ if (text) content.push({
1325
+ type: "text",
1326
+ text
1327
+ });
1328
+ content.push(...toolUseBlocks);
1329
+ return jsonResponse({
1330
+ id,
1331
+ type: "message",
1332
+ role: "assistant",
1333
+ content,
1334
+ model: modelName,
1335
+ stop_reason: toolUseBlocks.length > 0 ? "tool_use" : "end_turn",
1336
+ stop_sequence: null,
1337
+ usage: {
1338
+ input_tokens: 0,
1339
+ output_tokens: 0
1340
+ }
1341
+ });
1342
+ }
1343
+ function create$2(options) {
1344
+ const cwd = options.cwd ?? process.cwd();
1345
+ const registry = options.sessionRegistry ?? createMapSessionRegistry();
1346
+ const ctx = {
1347
+ ...options,
1348
+ cwd
1349
+ };
1350
+ return async (request) => {
1351
+ const { pathname } = new URL(request.url);
1352
+ if (request.method !== "POST") return null;
1353
+ const abortSessionId = pathname.match(/^\/v1\/messages\/([^/]+)\/abort$/)?.[1];
1354
+ if (abortSessionId) try {
1355
+ return await runInTraceContext(() => handleAbort({
1356
+ request,
1357
+ sessionId: abortSessionId,
1358
+ registry
1359
+ }));
1360
+ } catch (err) {
1361
+ const { log } = requestLogger(request);
1362
+ log.error({
1363
+ err,
1364
+ event: "anthropic_messages_abort_unhandled_error"
1365
+ }, "unhandled error");
1366
+ return jsonError(500, "internal error");
1367
+ }
1368
+ if (pathname !== "/v1/messages") return null;
1369
+ try {
1370
+ return await runInTraceContext(() => handleMessages(request, ctx, registry));
1371
+ } catch (err) {
1372
+ const { log } = requestLogger(request);
1373
+ log.error({
1374
+ err,
1375
+ event: "anthropic_messages_unhandled_error"
1376
+ }, "unhandled error");
1377
+ return jsonError(500, "internal error");
1378
+ }
1379
+ };
1380
+ }
1381
+ //#endregion
1382
+ //#region src/protocols/chat-completions.ts
1383
+ /**
1384
+ * OpenAI Chat Completions–compatible protocol handler.
1385
+ *
1386
+ * Catch-all: returns Response on match, null on no match.
1387
+ * Zero framework deps — just Web Standard Request/Response.
1388
+ *
1389
+ * Routes:
1390
+ * POST /v1/chat/completions
1391
+ * POST /v1/chat/completions/:id/steer
1392
+ * POST /v1/chat/completions/:id/abort
1393
+ *
1394
+ * @see https://platform.openai.com/docs/api-reference/chat/create
1395
+ * @see PROTOCOL.md for custom extensions
1396
+ */
1397
+ const ZERO_USAGE = {
1398
+ input: 0,
1399
+ output: 0,
1400
+ cacheRead: 0,
1401
+ cacheWrite: 0,
1402
+ totalTokens: 0,
1403
+ cost: {
1404
+ input: 0,
1405
+ output: 0,
1406
+ cacheRead: 0,
1407
+ cacheWrite: 0,
1408
+ total: 0
1409
+ }
1410
+ };
1411
+ function extractUsageFromAgentEnd(event) {
1412
+ const u = extractAgentEndUsage(event);
1413
+ if (!u) return null;
1414
+ return {
1415
+ prompt_tokens: u.input,
1416
+ completion_tokens: u.output,
1417
+ total_tokens: u.totalTokens,
1418
+ cache_read_tokens: u.cacheRead,
1419
+ cache_creation_tokens: u.cacheWrite
1420
+ };
1421
+ }
1422
+ function isDebug(request) {
1423
+ const h = request.headers.get("x-debug");
1424
+ return h === "1" || h === "true";
1425
+ }
1426
+ function parseRequestStartMs(request) {
1427
+ const h = request.headers.get("x-request-start-ms");
1428
+ if (h == null) return null;
1429
+ const n = Number(h);
1430
+ return Number.isFinite(n) && n > 0 ? n : null;
1431
+ }
1432
+ function withPassthroughHeaders({ model, request, prefixes }) {
1433
+ if (prefixes.length === 0) return model;
1434
+ const forwarded = {};
1435
+ request.headers.forEach((value, key) => {
1436
+ if (prefixes.some((p) => key.startsWith(p))) forwarded[key] = value;
1437
+ });
1438
+ if (Object.keys(forwarded).length === 0) return model;
1439
+ const existing = "headers" in model && model.headers != null && typeof model.headers === "object" && !Array.isArray(model.headers) ? model.headers : {};
1440
+ return {
1441
+ ...model,
1442
+ headers: {
1443
+ ...existing,
1444
+ ...forwarded
1445
+ }
1446
+ };
1447
+ }
1448
+ async function extractContent(content, { userAgent, log }) {
1449
+ if (typeof content === "string") return {
1450
+ text: content,
1451
+ images: []
1452
+ };
1453
+ if (!Array.isArray(content)) return {
1454
+ text: "",
1455
+ images: []
1456
+ };
1457
+ let text = "";
1458
+ const images = [];
1459
+ for (const part of content) {
1460
+ if (typeof part === "string") {
1461
+ text += part;
1462
+ continue;
1463
+ }
1464
+ if (part?.type === "text") {
1465
+ text += part.text ?? "";
1466
+ continue;
1467
+ }
1468
+ if (part?.type === "image_url") {
1469
+ const url = String(part.image_url?.url ?? "");
1470
+ const dataUrl = url.match(/^data:([^;]+);base64,(.+)$/);
1471
+ if (dataUrl) images.push({
1472
+ type: "image",
1473
+ mimeType: dataUrl[1],
1474
+ data: dataUrl[2]
1475
+ });
1476
+ else if (/^https?:\/\//i.test(url)) try {
1477
+ images.push(await fetchImageAsBase64({
1478
+ url,
1479
+ userAgent
1480
+ }));
1481
+ } catch (err) {
1482
+ log.warn({
1483
+ event: "image_fetch_failed",
1484
+ url,
1485
+ err
1486
+ }, "image fetch failed");
1487
+ text += `\n[image fetch failed: ${url}: ${err?.message ?? err}]\n`;
1488
+ }
1489
+ }
1490
+ }
1491
+ return {
1492
+ text,
1493
+ images
1494
+ };
1495
+ }
1496
+ function extractAssistantText(content) {
1497
+ if (typeof content === "string") return content;
1498
+ if (!Array.isArray(content)) return "";
1499
+ return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("");
1500
+ }
1501
+ function countMessageStats(messages) {
1502
+ let chars = 0;
1503
+ let images = 0;
1504
+ for (const m of messages || []) {
1505
+ const c = m?.content;
1506
+ if (typeof c === "string") chars += c.length;
1507
+ else if (Array.isArray(c)) {
1508
+ for (const p of c) if (typeof p === "string") chars += p.length;
1509
+ else if (p?.type === "text" && typeof p.text === "string") chars += p.text.length;
1510
+ else if (p?.type === "image_url") images += 1;
1511
+ }
1512
+ if (Array.isArray(m?.tool_calls)) for (const tc of m.tool_calls) {
1513
+ const args = tc?.function?.arguments;
1514
+ if (typeof args === "string") chars += args.length;
1515
+ }
1516
+ }
1517
+ return {
1518
+ chars,
1519
+ images
1520
+ };
1521
+ }
1522
+ async function loadHistoryIntoSession({ sm, messages, upToIdxExclusive, modelName, userAgent, log }) {
1523
+ const toolCallNames = /* @__PURE__ */ new Map();
1524
+ for (let i = 0; i < upToIdxExclusive; i++) {
1525
+ const m = messages[i];
1526
+ if (!m) continue;
1527
+ if (m.role === "user") {
1528
+ const { text, images } = await extractContent(m.content, {
1529
+ userAgent,
1530
+ log
1531
+ });
1532
+ const content = images.length > 0 ? [...text ? [{
1533
+ type: "text",
1534
+ text
1535
+ }] : [], ...images] : text;
1536
+ if (typeof content === "string" && content.length > 0 || Array.isArray(content) && content.length > 0) sm.appendMessage({
1537
+ role: "user",
1538
+ content,
1539
+ timestamp: Date.now()
1540
+ });
1541
+ continue;
1542
+ }
1543
+ if (m.role === "assistant") {
1544
+ const text = extractAssistantText(m.content);
1545
+ const contentArr = [];
1546
+ if (Array.isArray(m.x_thinking)) for (const t of m.x_thinking) {
1547
+ const tt = String(t?.text ?? "");
1548
+ if (!tt) continue;
1549
+ contentArr.push({
1550
+ type: "thinking",
1551
+ thinking: tt,
1552
+ ...t?.signature ? { thinkingSignature: t.signature } : {},
1553
+ ...t?.redacted ? { redacted: true } : {}
1554
+ });
1555
+ }
1556
+ if (text) contentArr.push({
1557
+ type: "text",
1558
+ text
1559
+ });
1560
+ if (Array.isArray(m.tool_calls)) for (const tc of m.tool_calls) {
1561
+ const name = tc?.function?.name;
1562
+ if (!tc?.id || !name) continue;
1563
+ let args = {};
1564
+ try {
1565
+ args = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
1566
+ } catch {
1567
+ args = { _raw: tc.function.arguments };
1568
+ }
1569
+ contentArr.push({
1570
+ type: "toolCall",
1571
+ id: tc.id,
1572
+ name,
1573
+ arguments: args
1574
+ });
1575
+ toolCallNames.set(tc.id, name);
1576
+ }
1577
+ if (contentArr.length === 0) continue;
1578
+ sm.appendMessage({
1579
+ role: "assistant",
1580
+ content: contentArr,
1581
+ api: "anthropic-messages",
1582
+ provider: "anthropic",
1583
+ model: modelName,
1584
+ usage: ZERO_USAGE,
1585
+ stopReason: contentArr.some((c) => c.type === "toolCall") ? "toolUse" : "stop",
1586
+ timestamp: Date.now()
1587
+ });
1588
+ continue;
1589
+ }
1590
+ if (m.role === "tool") {
1591
+ const text = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : "";
1592
+ const toolCallId = String(m.tool_call_id ?? "");
1593
+ sm.appendMessage({
1594
+ role: "toolResult",
1595
+ toolCallId,
1596
+ toolName: toolCallNames.get(toolCallId) ?? "",
1597
+ content: [{
1598
+ type: "text",
1599
+ text
1600
+ }],
1601
+ isError: Boolean(m.x_is_error),
1602
+ timestamp: Date.now()
1603
+ });
1604
+ continue;
1605
+ }
1606
+ }
1607
+ }
1608
+ function piMessagesToOpenAI(messages) {
1609
+ const out = [];
1610
+ for (const m of messages) {
1611
+ if (m?.role === "assistant") {
1612
+ let textContent = "";
1613
+ const toolCalls = [];
1614
+ const thinking = [];
1615
+ const content = Array.isArray(m.content) ? m.content : [];
1616
+ for (const c of content) if (c?.type === "text") textContent += c.text ?? "";
1617
+ else if (c?.type === "thinking") thinking.push({
1618
+ text: String(c.thinking ?? ""),
1619
+ ...c.thinkingSignature ? { signature: String(c.thinkingSignature) } : {},
1620
+ ...c.redacted ? { redacted: true } : {}
1621
+ });
1622
+ else if (c?.type === "toolCall") toolCalls.push({
1623
+ id: String(c.id),
1624
+ type: "function",
1625
+ function: {
1626
+ name: String(c.name ?? ""),
1627
+ arguments: JSON.stringify(c.arguments ?? {})
1628
+ }
1629
+ });
1630
+ const msg = {
1631
+ role: "assistant",
1632
+ content: textContent || null
1633
+ };
1634
+ if (toolCalls.length) msg.tool_calls = toolCalls;
1635
+ if (thinking.length) msg.x_thinking = thinking;
1636
+ out.push(msg);
1637
+ continue;
1638
+ }
1639
+ if (m?.role === "toolResult") {
1640
+ const text = Array.isArray(m.content) ? m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : "";
1641
+ const tm = {
1642
+ role: "tool",
1643
+ tool_call_id: String(m.toolCallId ?? ""),
1644
+ content: text
1645
+ };
1646
+ if (m.isError) tm.x_is_error = true;
1647
+ out.push(tm);
1648
+ continue;
1649
+ }
1650
+ }
1651
+ return out;
1652
+ }
1653
+ async function handleChatCompletions(request, ctx, registry, pendingSteerIds) {
1654
+ const reqStart = performance.now();
1655
+ const upstreamStartMs = parseRequestStartMs(request);
1656
+ const { log: baseLog, trace } = requestLogger(request);
1657
+ const log = isDebug(request) ? baseLog.child({}, { level: "debug" }) : baseLog;
1658
+ let parsed;
1659
+ try {
1660
+ parsed = JSON.parse(await request.text());
1661
+ } catch {
1662
+ return jsonError(400, "invalid json");
1663
+ }
1664
+ const { messages, stream = false, model: modelInput, thinkingLevel: thinkingInput, context_window: contextWindowInput, x_model: modelSpecInput } = parsed ?? {};
1665
+ if (!Array.isArray(messages) || messages.length === 0) return jsonError(400, "messages array required");
1666
+ let lastUserIdx = -1;
1667
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "user") {
1668
+ lastUserIdx = i;
1669
+ break;
1670
+ }
1671
+ if (lastUserIdx < 0) return jsonError(400, "no user message in history");
1672
+ const lastUser = messages[lastUserIdx];
1673
+ const reqUA = request.headers.get("user-agent") ?? void 0;
1674
+ const { text: prompt, images } = await extractContent(lastUser.content, {
1675
+ userAgent: reqUA,
1676
+ log
1677
+ });
1678
+ if (!prompt && images.length === 0) return jsonError(400, "user message has no content");
1679
+ const { model: requestModel, modelSpec } = resolveRequestModel({
1680
+ modelInput,
1681
+ modelSpecInput,
1682
+ defaultModel: ctx.defaultModel,
1683
+ log
1684
+ });
1685
+ const resolvedModel = applyContextWindowOverride(requestModel, contextWindowInput);
1686
+ const model = withPassthroughHeaders({
1687
+ model: resolvedModel,
1688
+ request,
1689
+ prefixes: ctx.passthroughHeaderPrefixes ?? []
1690
+ });
1691
+ const thinkingLevel = resolveThinkingLevel(thinkingInput);
1692
+ const modelName = model.id ?? "claude-opus-4-7";
1693
+ const requestSystemPrompts = [];
1694
+ for (const m of messages) if (m?.role === "system") {
1695
+ const text = extractAssistantText(m.content);
1696
+ if (text) requestSystemPrompts.push(text);
1697
+ }
1698
+ const baseSystemPrompt = requestSystemPrompts.filter((p) => p.length > 0).join("\n\n");
1699
+ const parsedSession = parseSessionId(request);
1700
+ if (parsedSession.invalid) return jsonError(400, "x-session-id must be a valid UUID");
1701
+ const sessionId = parsedSession.source === "header" ? parsedSession.sessionId : `chatcmpl-${parsedSession.sessionId}`;
1702
+ const shellEnv = parseShellEnv(request);
1703
+ const sessionSetupStart = performance.now();
1704
+ const [{ session, sessionManager: sm }, customTools] = await Promise.all([ctx.createSession({
1705
+ cwd: ctx.cwd,
1706
+ sessionId,
1707
+ perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
1708
+ systemPromptOverride: () => baseSystemPrompt.length > 0 ? baseSystemPrompt : void 0,
1709
+ shellEnv,
1710
+ model,
1711
+ thinkingLevel,
1712
+ log,
1713
+ onSessionSetup: (args) => ctx.onSessionSetup?.(args),
1714
+ prepare: ({ sessionManager }) => loadHistoryIntoSession({
1715
+ sm: sessionManager,
1716
+ messages,
1717
+ upToIdxExclusive: lastUserIdx,
1718
+ modelName,
1719
+ userAgent: reqUA,
1720
+ log
1721
+ })
1722
+ }), ctx.getTools ? ctx.getTools(request) : void 0]);
1723
+ const sessionSetupMs = Math.round(performance.now() - sessionSetupStart);
1724
+ const created = Math.floor(Date.now() / 1e3);
1725
+ registry.set(sessionId, session);
1726
+ const stats = countMessageStats(messages);
1727
+ log.info({
1728
+ event: "req_received",
1729
+ chatcmpl_id: sessionId,
1730
+ turns: messages.length,
1731
+ chars: stats.chars,
1732
+ images: stats.images,
1733
+ stream,
1734
+ model: modelName,
1735
+ x_model: modelSpec !== null,
1736
+ context_window: resolvedModel.contextWindow,
1737
+ setup_ms: Math.round(performance.now() - reqStart),
1738
+ session_setup_ms: sessionSetupMs,
1739
+ upstream_setup_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
1740
+ }, "chat req received");
1741
+ if (stream) {
1742
+ const response = runStream$1({
1743
+ session,
1744
+ sm,
1745
+ prompt,
1746
+ images,
1747
+ sessionId,
1748
+ created,
1749
+ reqStart,
1750
+ upstreamStartMs,
1751
+ log,
1752
+ trace,
1753
+ postPrompt: ctx.postPrompt,
1754
+ registry,
1755
+ pendingSteerIds
1756
+ });
1757
+ const traceId = parseTraceId(trace.traceparent);
1758
+ if (traceId) response.headers.set("x-trace-id", traceId);
1759
+ return response;
1760
+ }
1761
+ const response = await runBlocking$1({
1762
+ session,
1763
+ sm,
1764
+ prompt,
1765
+ images,
1766
+ sessionId,
1767
+ created,
1768
+ reqStart,
1769
+ upstreamStartMs,
1770
+ log,
1771
+ postPrompt: ctx.postPrompt,
1772
+ registry,
1773
+ pendingSteerIds
1774
+ });
1775
+ const traceId = parseTraceId(trace.traceparent);
1776
+ if (traceId) response.headers.set("x-trace-id", traceId);
1777
+ return response;
1778
+ }
1779
+ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart, upstreamStartMs, log, trace, postPrompt, registry, pendingSteerIds }) {
1780
+ return createSSEResponse(async (writer) => {
1781
+ writer.write(": ready\n\n");
1782
+ const baselineMessageCount = sm.buildSessionContext().messages.length;
1783
+ let roleEmitted = false;
1784
+ let firstChunkAt = null;
1785
+ const tcByContentIdx = /* @__PURE__ */ new Map();
1786
+ let nextTcIdx = 0;
1787
+ let lastSteeringCount = 0;
1788
+ let usage = null;
1789
+ let lastFollowUpCount = 0;
1790
+ let capturedModelError = null;
1791
+ const sendChunk = (delta, finishReason) => {
1792
+ if (firstChunkAt === null) {
1793
+ firstChunkAt = performance.now();
1794
+ log.info({
1795
+ event: "ttft",
1796
+ chatcmpl_id: sessionId,
1797
+ ttft_ms: Math.round(firstChunkAt - reqStart),
1798
+ upstream_ttft_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
1799
+ }, "time to first chunk");
1800
+ }
1801
+ const chunk = {
1802
+ id: sessionId,
1803
+ object: "chat.completion.chunk",
1804
+ created,
1805
+ choices: [{
1806
+ index: 0,
1807
+ delta,
1808
+ finish_reason: finishReason ?? null
1809
+ }]
1810
+ };
1811
+ writer.write(`data: ${JSON.stringify(chunk)}\n\n`);
1812
+ };
1813
+ const ensureRole = () => {
1814
+ if (roleEmitted) return;
1815
+ sendChunk({ role: "assistant" });
1816
+ roleEmitted = true;
1817
+ };
1818
+ let _evCount = 0;
1819
+ session.subscribe((event) => {
1820
+ const ev = event;
1821
+ const endedMessage = ev.message ?? (Array.isArray(ev.messages) ? ev.messages[ev.messages.length - 1] : null);
1822
+ if (endedMessage?.stopReason === "error" && typeof endedMessage.errorMessage === "string" && endedMessage.errorMessage.length > 0) capturedModelError = endedMessage.errorMessage;
1823
+ if (ev.type === "queue_update") {
1824
+ const steering = Array.isArray(ev.steering) ? ev.steering.length : 0;
1825
+ const followUp = Array.isArray(ev.followUp) ? ev.followUp.length : 0;
1826
+ const queues = pendingSteerIds.get(sessionId);
1827
+ const consumedIds = [];
1828
+ const consumedSteer = lastSteeringCount - steering;
1829
+ if (consumedSteer > 0 && queues && queues.steer.length > 0) consumedIds.push(...queues.steer.splice(0, consumedSteer));
1830
+ const consumedFollowUp = lastFollowUpCount - followUp;
1831
+ if (consumedFollowUp > 0 && queues && queues.followUp.length > 0) consumedIds.push(...queues.followUp.splice(0, consumedFollowUp));
1832
+ lastSteeringCount = steering;
1833
+ lastFollowUpCount = followUp;
1834
+ sendChunk({ x_queue_update: {
1835
+ steering,
1836
+ follow_up: followUp,
1837
+ ...consumedIds.length > 0 ? { consumed_steer_ids: consumedIds } : {}
1838
+ } });
1839
+ return;
1840
+ }
1841
+ _evCount++;
1842
+ const innerType = ev.type === "message_update" ? ev.assistantMessageEvent?.type ?? null : null;
1843
+ const messageContentSummary = ev.type === "message_update" || ev.type === "message_end" ? Array.isArray(ev.message?.content) ? ev.message.content.map((c) => ({
1844
+ type: c?.type,
1845
+ ...c?.type === "text" ? { textLen: (c.text ?? "").length } : {},
1846
+ ...c?.type === "thinking" ? { thinkingLen: (c.thinking ?? "").length } : {},
1847
+ ...c?.type === "toolCall" ? { name: c.name } : {}
1848
+ })) : null : null;
1849
+ log.info({
1850
+ chatcmpl_id: sessionId,
1851
+ evIdx: _evCount,
1852
+ eventType: ev.type,
1853
+ messageRole: ev.message?.role ?? null,
1854
+ stopReason: ev.message?.stopReason ?? null,
1855
+ innerType,
1856
+ ...innerType === "text_delta" || innerType === "thinking_delta" ? { delta: String(ev.assistantMessageEvent?.delta ?? "").slice(0, 200) } : {},
1857
+ ...messageContentSummary ? { messageContent: messageContentSummary } : {},
1858
+ ...ev.type === "message_start" || ev.type === "message_end" ? { messageJson: JSON.stringify(ev.message).slice(0, 500) } : {}
1859
+ }, "harness session event");
1860
+ if (ev.type === "tool_execution_start") {
1861
+ sendChunk({ x_tool_execution_start: { tool_call_id: String(ev.toolCallId ?? "") } });
1862
+ return;
1863
+ }
1864
+ if (ev.type === "tool_execution_end") {
1865
+ 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 : "";
1866
+ sendChunk({ x_tool_call_result: {
1867
+ tool_call_id: String(ev.toolCallId ?? ""),
1868
+ content: text,
1869
+ ...ev.isError ? { is_error: true } : {}
1870
+ } });
1871
+ return;
1872
+ }
1873
+ if (ev.type === "agent_end") {
1874
+ usage = extractUsageFromAgentEnd(ev) ?? usage;
1875
+ return;
1876
+ }
1877
+ if (ev.type !== "message_update") return;
1878
+ const inner = ev.assistantMessageEvent;
1879
+ const t = inner?.type;
1880
+ if (t === "text_delta") {
1881
+ ensureRole();
1882
+ sendChunk({ content: inner.delta ?? "" });
1883
+ return;
1884
+ }
1885
+ if (t === "thinking_delta") {
1886
+ sendChunk({ x_thinking_delta: inner.delta ?? "" });
1887
+ return;
1888
+ }
1889
+ if (t === "toolcall_start") {
1890
+ const idx = inner.contentIndex;
1891
+ const item = inner.partial?.content?.[idx];
1892
+ const tc = item?.type === "toolCall" ? item : void 0;
1893
+ const openaiIdx = nextTcIdx++;
1894
+ tcByContentIdx.set(idx, openaiIdx);
1895
+ ensureRole();
1896
+ sendChunk({ tool_calls: [{
1897
+ index: openaiIdx,
1898
+ id: tc?.id ?? `call_${openaiIdx}`,
1899
+ type: "function",
1900
+ function: {
1901
+ name: tc?.name ?? "",
1902
+ arguments: ""
1903
+ }
1904
+ }] });
1905
+ return;
1906
+ }
1907
+ if (t === "toolcall_delta") {
1908
+ const openaiIdx = tcByContentIdx.get(inner.contentIndex);
1909
+ if (openaiIdx === void 0) return;
1910
+ sendChunk({ tool_calls: [{
1911
+ index: openaiIdx,
1912
+ function: { arguments: inner.delta ?? "" }
1913
+ }] });
1914
+ }
1915
+ });
1916
+ try {
1917
+ await runConversation({
1918
+ session,
1919
+ prompt,
1920
+ images,
1921
+ log,
1922
+ postPrompt
1923
+ });
1924
+ if (capturedModelError !== null) {
1925
+ const providerError = toModelProviderError(new Error(capturedModelError));
1926
+ log.warn({
1927
+ event: "model_provider_error",
1928
+ chatcmpl_id: sessionId,
1929
+ code: providerError.code,
1930
+ upstream_status: providerError.upstreamStatus,
1931
+ provider: providerError.provider,
1932
+ source: "stop_reason_error"
1933
+ }, "forwarding model-provider error to client");
1934
+ emitModelProviderError({
1935
+ writer,
1936
+ sessionId,
1937
+ created,
1938
+ providerError
1939
+ });
1940
+ writer.write("data: [DONE]\n\n");
1941
+ } else {
1942
+ emitSessionMessagesTrailer({
1943
+ writer,
1944
+ sm,
1945
+ baselineMessageCount,
1946
+ sessionId,
1947
+ created,
1948
+ usage
1949
+ });
1950
+ sendChunk({}, "stop");
1951
+ writer.write("data: [DONE]\n\n");
1952
+ }
1953
+ } catch (err) {
1954
+ log.error({
1955
+ event: "chat_error",
1956
+ chatcmpl_id: sessionId,
1957
+ err
1958
+ }, "chat error");
1959
+ if (isModelProviderError(err)) {
1960
+ const providerError = toModelProviderError(err);
1961
+ log.warn({
1962
+ event: "model_provider_error",
1963
+ chatcmpl_id: sessionId,
1964
+ code: providerError.code,
1965
+ upstream_status: providerError.upstreamStatus,
1966
+ provider: providerError.provider
1967
+ }, "forwarding model-provider error to client");
1968
+ emitModelProviderError({
1969
+ writer,
1970
+ sessionId,
1971
+ created,
1972
+ providerError
1973
+ });
1974
+ } else sendChunk({ content: `\n[error: ${err?.message ?? err}]` }, "stop");
1975
+ writer.write("data: [DONE]\n\n");
1976
+ } finally {
1977
+ registry.delete(sessionId);
1978
+ pendingSteerIds.delete(sessionId);
1979
+ }
1980
+ log.info({
1981
+ event: "stream_done",
1982
+ chatcmpl_id: sessionId,
1983
+ duration_ms: Math.round(performance.now() - reqStart),
1984
+ upstream_duration_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
1985
+ }, "stream done");
1986
+ });
1987
+ }
1988
+ function emitSessionMessagesTrailer({ writer, sm, baselineMessageCount, sessionId, created, usage }) {
1989
+ const sessionMessages = piMessagesToOpenAI(sm.buildSessionContext().messages.slice(baselineMessageCount));
1990
+ if (sessionMessages.length === 0 && !usage) return;
1991
+ const chunk = {
1992
+ id: sessionId,
1993
+ object: "chat.completion.chunk",
1994
+ created,
1995
+ choices: [{
1996
+ index: 0,
1997
+ delta: {},
1998
+ finish_reason: null
1999
+ }],
2000
+ ...sessionMessages.length ? { x_session_messages: sessionMessages } : {},
2001
+ ...usage ? { usage } : {}
2002
+ };
2003
+ writer.write(`data: ${JSON.stringify(chunk)}\n\n`);
2004
+ }
2005
+ /**
2006
+ * Emit a terminal chunk carrying a structured model-provider error under the
2007
+ * `x_model_provider_error` extension field, so the worker can react on the
2008
+ * forwarded provider error (e.g. compact + retry on `context_length_exceeded`)
2009
+ * rather than seeing an ambiguous empty stream. `finish_reason: 'stop'`
2010
+ * cleanly closes the stream for any OpenAI-shaped reader that ignores the
2011
+ * extension field.
2012
+ */
2013
+ function emitModelProviderError({ writer, sessionId, created, providerError }) {
2014
+ const chunk = {
2015
+ id: sessionId,
2016
+ object: "chat.completion.chunk",
2017
+ created,
2018
+ choices: [{
2019
+ index: 0,
2020
+ delta: {},
2021
+ finish_reason: "stop"
2022
+ }],
2023
+ x_model_provider_error: {
2024
+ code: providerError.code,
2025
+ message: providerError.message,
2026
+ provider: providerError.provider,
2027
+ type: providerError.type,
2028
+ upstream_status: providerError.upstreamStatus
2029
+ }
2030
+ };
2031
+ writer.write(`data: ${JSON.stringify(chunk)}\n\n`);
2032
+ }
2033
+ async function runBlocking$1({ session, sm, prompt, images, sessionId, created, reqStart, upstreamStartMs, log, postPrompt, registry, pendingSteerIds }) {
2034
+ const baselineMessageCount = sm.buildSessionContext().messages.length;
2035
+ let content = "";
2036
+ const toolCalls = [];
2037
+ const tcByContentIdx = /* @__PURE__ */ new Map();
2038
+ let usage = null;
2039
+ session.subscribe((event) => {
2040
+ if (event.type === "message_update") {
2041
+ const inner = event.assistantMessageEvent;
2042
+ const t = inner?.type;
2043
+ if (t === "text_delta") {
2044
+ content += inner.delta ?? "";
2045
+ return;
2046
+ }
2047
+ if (t === "toolcall_start") {
2048
+ const idx = inner.contentIndex;
2049
+ const item = inner.partial?.content?.[idx];
2050
+ tcByContentIdx.set(idx, toolCalls.length);
2051
+ toolCalls.push({
2052
+ id: item?.id ?? `call_${toolCalls.length}`,
2053
+ type: "function",
2054
+ function: {
2055
+ name: item?.name ?? "",
2056
+ arguments: ""
2057
+ }
2058
+ });
2059
+ return;
2060
+ }
2061
+ if (t === "toolcall_delta") {
2062
+ const i = tcByContentIdx.get(inner.contentIndex);
2063
+ if (i !== void 0 && toolCalls[i]) toolCalls[i].function.arguments += inner.delta ?? "";
2064
+ }
2065
+ return;
2066
+ }
2067
+ if (event.type === "agent_end") usage = extractUsageFromAgentEnd(event) ?? usage;
2068
+ });
2069
+ try {
2070
+ await runConversation({
2071
+ session,
2072
+ prompt,
2073
+ images,
2074
+ log,
2075
+ postPrompt
2076
+ });
2077
+ } catch (err) {
2078
+ log.error({
2079
+ event: "chat_error",
2080
+ chatcmpl_id: sessionId,
2081
+ err
2082
+ }, "chat error");
2083
+ return jsonError(500, err?.message ?? String(err));
2084
+ } finally {
2085
+ registry.delete(sessionId);
2086
+ pendingSteerIds.delete(sessionId);
2087
+ }
2088
+ const message = {
2089
+ role: "assistant",
2090
+ content: content || null
2091
+ };
2092
+ if (toolCalls.length) message.tool_calls = toolCalls;
2093
+ const all = sm.buildSessionContext().messages;
2094
+ const sessionMessages = piMessagesToOpenAI(all.slice(baselineMessageCount));
2095
+ const response = {
2096
+ id: sessionId,
2097
+ object: "chat.completion",
2098
+ created,
2099
+ choices: [{
2100
+ index: 0,
2101
+ message,
2102
+ finish_reason: "stop"
2103
+ }],
2104
+ ...usage ? { usage } : {},
2105
+ ...sessionMessages.length ? { x_session_messages: sessionMessages } : {}
2106
+ };
2107
+ log.info({
2108
+ event: "blocking_done",
2109
+ chatcmpl_id: sessionId,
2110
+ duration_ms: Math.round(performance.now() - reqStart),
2111
+ upstream_duration_ms: upstreamStartMs !== null ? Date.now() - upstreamStartMs : null
2112
+ }, "blocking done");
2113
+ return new Response(JSON.stringify(response), {
2114
+ status: 200,
2115
+ headers: { "Content-Type": "application/json" }
2116
+ });
2117
+ }
2118
+ const steerContentPartSchema = z.union([z.object({
2119
+ type: z.literal("text"),
2120
+ text: z.string()
2121
+ }), z.object({
2122
+ type: z.literal("image_url"),
2123
+ image_url: z.object({ url: z.string() })
2124
+ })]);
2125
+ const steerRequestSchema = z.object({
2126
+ content: z.union([z.string().min(1), z.array(steerContentPartSchema).min(1)]).optional(),
2127
+ mode: z.enum(["steer", "follow-up"]).default("steer"),
2128
+ text: z.string().optional()
2129
+ }).refine((v) => v.content !== void 0 || v.text !== void 0, { message: "content or text is required" });
2130
+ z.string().uuid();
2131
+ async function handleSteer(request, sessionId, registry, pendingSteerIds) {
2132
+ const { log } = requestLogger(request);
2133
+ const session = registry.get(sessionId);
2134
+ if (!session) return jsonError(404, "session not found or already completed");
2135
+ let body;
2136
+ try {
2137
+ body = JSON.parse(await request.text());
2138
+ } catch {
2139
+ return jsonError(400, "invalid json");
2140
+ }
2141
+ const parsed = steerRequestSchema.safeParse(body);
2142
+ if (!parsed.success) return jsonError(400, parsed.error.message);
2143
+ const { mode } = parsed.data;
2144
+ const { text, images } = await extractContent(parsed.data.content ?? parsed.data.text ?? "", {
2145
+ userAgent: request.headers.get("user-agent") ?? void 0,
2146
+ log
2147
+ });
2148
+ if (!text && images.length === 0) return jsonError(400, "steer has no content");
2149
+ const steerId = randomUUID();
2150
+ let queues = pendingSteerIds.get(sessionId);
2151
+ if (!queues) {
2152
+ queues = {
2153
+ steer: [],
2154
+ followUp: []
2155
+ };
2156
+ pendingSteerIds.set(sessionId, queues);
2157
+ }
2158
+ if (mode === "follow-up") {
2159
+ queues.followUp.push(steerId);
2160
+ await session.followUp(text, images.length > 0 ? images : void 0);
2161
+ } else {
2162
+ queues.steer.push(steerId);
2163
+ await session.steer(text, images.length > 0 ? images : void 0);
2164
+ }
2165
+ log.info({
2166
+ event: "steer_accepted",
2167
+ session_id: sessionId,
2168
+ steer_id: steerId,
2169
+ mode
2170
+ }, "steering message queued");
2171
+ return jsonResponse({
2172
+ ok: true,
2173
+ id: steerId
2174
+ });
2175
+ }
2176
+ function create$1(options) {
2177
+ const cwd = options.cwd ?? process.cwd();
2178
+ const registry = options.sessionRegistry ?? createMapSessionRegistry();
2179
+ const pendingSteerIds = /* @__PURE__ */ new Map();
2180
+ const ctx = {
2181
+ ...options,
2182
+ cwd
2183
+ };
2184
+ return async (request) => {
2185
+ const { pathname } = new URL(request.url);
2186
+ if (request.method === "POST") {
2187
+ const steerSessionId = pathname.match(/^\/v1\/chat\/completions\/([^/]+)\/steer$/)?.[1];
2188
+ if (steerSessionId) try {
2189
+ return await runInTraceContext(() => handleSteer(request, steerSessionId, registry, pendingSteerIds));
2190
+ } catch (err) {
2191
+ const { log } = requestLogger(request);
2192
+ log.error({
2193
+ err,
2194
+ event: "steer_unhandled_error"
2195
+ }, "unhandled error");
2196
+ return jsonError(500, "internal error");
2197
+ }
2198
+ const abortSessionId = pathname.match(/^\/v1\/chat\/completions\/([^/]+)\/abort$/)?.[1];
2199
+ if (abortSessionId) try {
2200
+ return await runInTraceContext(() => handleAbort({
2201
+ request,
2202
+ sessionId: abortSessionId,
2203
+ registry
2204
+ }));
2205
+ } catch (err) {
2206
+ const { log } = requestLogger(request);
2207
+ log.error({
2208
+ err,
2209
+ event: "abort_unhandled_error"
2210
+ }, "unhandled error");
2211
+ return jsonError(500, "internal error");
2212
+ }
2213
+ if (pathname === "/v1/chat/completions") try {
2214
+ return await runInTraceContext(() => handleChatCompletions(request, ctx, registry, pendingSteerIds));
2215
+ } catch (err) {
2216
+ const { log } = requestLogger(request);
2217
+ log.error({
2218
+ err,
2219
+ event: "chat_completions_unhandled_error"
2220
+ }, "unhandled error");
2221
+ return jsonError(500, "internal error");
2222
+ }
2223
+ }
2224
+ return null;
2225
+ };
2226
+ }
2227
+ //#endregion
2228
+ //#region src/protocols/responses.ts
2229
+ /**
2230
+ * OpenAI Responses API–compatible protocol handler.
2231
+ *
2232
+ * Catch-all: returns Response on match, null on no match.
2233
+ * Zero server-framework deps — just Web Standard Request/Response.
2234
+ *
2235
+ * Routes:
2236
+ * POST /v1/responses
2237
+ * POST /v1/responses/:id/abort
2238
+ *
2239
+ * @see https://platform.openai.com/docs/api-reference/responses
2240
+ */
2241
+ async function extractInputContent(input, log) {
2242
+ let systemPrompt = "";
2243
+ const history = [];
2244
+ let lastUserText = "";
2245
+ const images = [];
2246
+ if (typeof input === "string") return {
2247
+ prompt: input,
2248
+ images: [],
2249
+ systemPrompt: "",
2250
+ history: []
2251
+ };
2252
+ if (!Array.isArray(input)) return {
2253
+ prompt: "",
2254
+ images: [],
2255
+ systemPrompt: "",
2256
+ history: []
2257
+ };
2258
+ for (const item of input) {
2259
+ if (item?.type === "message") {
2260
+ if (item.role === "developer" || item.role === "system") {
2261
+ const text = extractTextFromContent(item.content);
2262
+ if (text) systemPrompt += (systemPrompt ? "\n\n" : "") + text;
2263
+ continue;
2264
+ }
2265
+ if (item.role === "user") {
2266
+ const text = extractTextFromContent(item.content);
2267
+ const imgs = await extractImagesFromContent(item.content, log);
2268
+ lastUserText = text;
2269
+ images.push(...imgs);
2270
+ history.push(item);
2271
+ continue;
2272
+ }
2273
+ if (item.role === "assistant") {
2274
+ history.push(item);
2275
+ continue;
2276
+ }
2277
+ }
2278
+ if (item?.type === "function_call_output") history.push(item);
2279
+ }
2280
+ return {
2281
+ prompt: lastUserText,
2282
+ images,
2283
+ systemPrompt,
2284
+ history
2285
+ };
2286
+ }
2287
+ function extractTextFromContent(content) {
2288
+ if (typeof content === "string") return content;
2289
+ if (!Array.isArray(content)) return "";
2290
+ return content.filter((p) => p?.type === "input_text" || p?.type === "text").map((p) => p.text ?? "").join("");
2291
+ }
2292
+ async function extractImagesFromContent(content, log) {
2293
+ if (!Array.isArray(content)) return [];
2294
+ const images = [];
2295
+ for (const part of content) if (part?.type === "input_image") {
2296
+ const url = part.image_url ?? part.url;
2297
+ if (typeof url === "string" && /^https?:\/\//i.test(url)) try {
2298
+ images.push(await fetchImageAsBase64({ url }));
2299
+ } catch (err) {
2300
+ log.warn({
2301
+ event: "image_fetch_failed",
2302
+ url,
2303
+ err
2304
+ }, "image fetch failed");
2305
+ }
2306
+ else if (part.data && part.media_type) images.push({
2307
+ type: "image",
2308
+ mimeType: part.media_type,
2309
+ data: part.data
2310
+ });
2311
+ }
2312
+ return images;
2313
+ }
2314
+ async function handleResponses(request, ctx, registry) {
2315
+ const { log, trace } = requestLogger(request);
2316
+ let parsed;
2317
+ try {
2318
+ parsed = JSON.parse(await request.text());
2319
+ } catch {
2320
+ return jsonError(400, "invalid json");
2321
+ }
2322
+ const { input, stream = false, model: modelInput, instructions, x_model: modelSpecInput } = parsed ?? {};
2323
+ if (input == null) return jsonError(400, "input required");
2324
+ const extracted = await extractInputContent(input, log);
2325
+ const { prompt } = extracted;
2326
+ const { images, systemPrompt: inputSystemPrompt } = extracted;
2327
+ if (!prompt && images.length === 0) return jsonError(400, "no user content in input");
2328
+ const systemPrompt = [typeof instructions === "string" ? instructions : "", inputSystemPrompt].filter(Boolean).join("\n\n");
2329
+ const { model, modelSpec } = resolveRequestModel({
2330
+ modelInput,
2331
+ modelSpecInput,
2332
+ defaultModel: ctx.defaultModel,
2333
+ log
2334
+ });
2335
+ const modelName = model.id ?? "claude-opus-4-7";
2336
+ const { sessionId } = parseSessionId(request);
2337
+ const shellEnv = parseShellEnv(request);
2338
+ const { session } = await ctx.createSession({
2339
+ cwd: ctx.cwd,
2340
+ sessionId,
2341
+ perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
2342
+ systemPromptOverride: () => systemPrompt.length > 0 ? systemPrompt : void 0,
2343
+ shellEnv,
2344
+ model,
2345
+ thinkingLevel: null,
2346
+ log,
2347
+ onSessionSetup: (args) => ctx.onSessionSetup?.(args),
2348
+ prepare: null
2349
+ });
2350
+ registry.set(sessionId, session);
2351
+ const responseId = `resp_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
2352
+ if (stream) {
2353
+ const response = runStream({
2354
+ session,
2355
+ prompt,
2356
+ images,
2357
+ responseId,
2358
+ sessionId,
2359
+ modelName,
2360
+ log,
2361
+ postPrompt: ctx.postPrompt,
2362
+ registry
2363
+ });
2364
+ const traceId = parseTraceId(trace.traceparent);
2365
+ if (traceId) response.headers.set("x-trace-id", traceId);
2366
+ return response;
2367
+ }
2368
+ const response = await runBlocking({
2369
+ session,
2370
+ prompt,
2371
+ images,
2372
+ responseId,
2373
+ sessionId,
2374
+ modelName,
2375
+ log,
2376
+ postPrompt: ctx.postPrompt,
2377
+ registry
2378
+ });
2379
+ const traceId = parseTraceId(trace.traceparent);
2380
+ if (traceId) response.headers.set("x-trace-id", traceId);
2381
+ return response;
2382
+ }
2383
+ function runStream({ session, prompt, images, responseId, sessionId, modelName, log, postPrompt, registry }) {
2384
+ return createSSEResponse(async (writer) => {
2385
+ let seq = 0;
2386
+ let textContent = "";
2387
+ const outputItems = [];
2388
+ const tcByContentIdx = /* @__PURE__ */ new Map();
2389
+ let nextOutputIndex = 0;
2390
+ let capturedModelError = null;
2391
+ const send = (event) => {
2392
+ writer.write(`data: ${JSON.stringify(event)}\n\n`);
2393
+ };
2394
+ const failWithProviderError = (providerError) => {
2395
+ log.warn({
2396
+ event: "model_provider_error",
2397
+ code: providerError.code,
2398
+ upstream_status: providerError.upstreamStatus,
2399
+ provider: providerError.provider
2400
+ }, "forwarding model-provider error to client");
2401
+ send({
2402
+ type: "response.failed",
2403
+ sequence_number: seq++,
2404
+ response: {
2405
+ id: responseId,
2406
+ status: "failed",
2407
+ error: {
2408
+ code: providerError.code,
2409
+ message: providerError.message,
2410
+ x_model_provider_error: {
2411
+ provider: providerError.provider,
2412
+ type: providerError.type,
2413
+ upstream_status: providerError.upstreamStatus
2414
+ }
2415
+ }
2416
+ }
2417
+ });
2418
+ };
2419
+ send({
2420
+ type: "response.created",
2421
+ sequence_number: seq++,
2422
+ response: {
2423
+ id: responseId,
2424
+ object: "response",
2425
+ status: "in_progress",
2426
+ output: [],
2427
+ model: modelName,
2428
+ created_at: Math.floor(Date.now() / 1e3)
2429
+ }
2430
+ });
2431
+ let textItemEmitted = false;
2432
+ const ensureTextItem = () => {
2433
+ if (textItemEmitted) return;
2434
+ textItemEmitted = true;
2435
+ send({
2436
+ type: "response.output_item.added",
2437
+ sequence_number: seq++,
2438
+ output_index: nextOutputIndex,
2439
+ item: {
2440
+ type: "message",
2441
+ id: `item_${randomUUID().slice(0, 8)}`,
2442
+ role: "assistant",
2443
+ content: []
2444
+ }
2445
+ });
2446
+ };
2447
+ session.subscribe((event) => {
2448
+ const ev = event;
2449
+ const endedMessage = ev.message ?? (Array.isArray(ev.messages) ? ev.messages[ev.messages.length - 1] : null);
2450
+ if (endedMessage?.stopReason === "error" && typeof endedMessage.errorMessage === "string" && endedMessage.errorMessage.length > 0) capturedModelError = endedMessage.errorMessage;
2451
+ if (ev.type !== "message_update") return;
2452
+ const inner = ev.assistantMessageEvent;
2453
+ if (!inner) return;
2454
+ if (inner.type === "text_delta") {
2455
+ ensureTextItem();
2456
+ textContent += inner.delta ?? "";
2457
+ send({
2458
+ type: "response.output_text.delta",
2459
+ sequence_number: seq++,
2460
+ output_index: textItemEmitted ? 0 : nextOutputIndex,
2461
+ delta: inner.delta ?? ""
2462
+ });
2463
+ return;
2464
+ }
2465
+ if (inner.type === "toolcall_start") {
2466
+ const idx = inner.contentIndex;
2467
+ const item = inner.partial?.content?.[idx];
2468
+ const tc = item?.type === "toolCall" ? item : void 0;
2469
+ const itemId = `item_${randomUUID().slice(0, 8)}`;
2470
+ const callId = tc?.id ?? `call_${nextOutputIndex}`;
2471
+ const outputIndex = nextOutputIndex++;
2472
+ tcByContentIdx.set(idx, {
2473
+ itemId,
2474
+ callId,
2475
+ outputIndex
2476
+ });
2477
+ send({
2478
+ type: "response.output_item.added",
2479
+ sequence_number: seq++,
2480
+ output_index: outputIndex,
2481
+ item: {
2482
+ type: "function_call",
2483
+ id: itemId,
2484
+ call_id: callId,
2485
+ name: tc?.name ?? "",
2486
+ arguments: "",
2487
+ status: "in_progress"
2488
+ }
2489
+ });
2490
+ return;
2491
+ }
2492
+ if (inner.type === "toolcall_delta") {
2493
+ const info = tcByContentIdx.get(inner.contentIndex);
2494
+ if (!info) return;
2495
+ send({
2496
+ type: "response.function_call_arguments.delta",
2497
+ sequence_number: seq++,
2498
+ output_index: info.outputIndex,
2499
+ call_id: info.callId,
2500
+ delta: inner.delta ?? ""
2501
+ });
2502
+ }
2503
+ });
2504
+ try {
2505
+ await runConversation({
2506
+ session,
2507
+ prompt,
2508
+ images,
2509
+ log,
2510
+ postPrompt
2511
+ });
2512
+ if (capturedModelError !== null) failWithProviderError(toModelProviderError(new Error(capturedModelError)));
2513
+ } catch (err) {
2514
+ log.error({
2515
+ err,
2516
+ event: "responses_stream_error"
2517
+ }, "stream error");
2518
+ if (isModelProviderError(err)) failWithProviderError(toModelProviderError(err));
2519
+ } finally {
2520
+ registry.delete(sessionId);
2521
+ }
2522
+ if (textContent) {
2523
+ send({
2524
+ type: "response.output_text.done",
2525
+ sequence_number: seq++,
2526
+ output_index: 0,
2527
+ text: textContent
2528
+ });
2529
+ outputItems.push({
2530
+ type: "message",
2531
+ role: "assistant",
2532
+ content: [{
2533
+ type: "output_text",
2534
+ text: textContent
2535
+ }]
2536
+ });
2537
+ }
2538
+ for (const [, info] of tcByContentIdx) send({
2539
+ type: "response.output_item.done",
2540
+ sequence_number: seq++,
2541
+ output_index: info.outputIndex,
2542
+ item: {
2543
+ type: "function_call",
2544
+ id: info.itemId,
2545
+ call_id: info.callId,
2546
+ status: "completed"
2547
+ }
2548
+ });
2549
+ send({
2550
+ type: "response.completed",
2551
+ sequence_number: seq++,
2552
+ response: {
2553
+ id: responseId,
2554
+ object: "response",
2555
+ status: "completed",
2556
+ output: outputItems,
2557
+ model: modelName,
2558
+ output_text: textContent,
2559
+ created_at: Math.floor(Date.now() / 1e3),
2560
+ usage: {
2561
+ input_tokens: 0,
2562
+ output_tokens: 0,
2563
+ total_tokens: 0,
2564
+ input_tokens_details: { cached_tokens: 0 },
2565
+ output_tokens_details: { reasoning_tokens: 0 }
2566
+ }
2567
+ }
2568
+ });
2569
+ writer.write("data: [DONE]\n\n");
2570
+ });
2571
+ }
2572
+ async function runBlocking({ session, prompt, images, responseId, sessionId, modelName, log, postPrompt, registry }) {
2573
+ let textContent = "";
2574
+ const functionCalls = [];
2575
+ const tcByContentIdx = /* @__PURE__ */ new Map();
2576
+ session.subscribe((event) => {
2577
+ if (event.type === "message_update") {
2578
+ const inner = event.assistantMessageEvent;
2579
+ if (inner?.type === "text_delta") {
2580
+ textContent += inner.delta ?? "";
2581
+ return;
2582
+ }
2583
+ if (inner?.type === "toolcall_start") {
2584
+ const idx = inner.contentIndex;
2585
+ const item = inner.partial?.content?.[idx];
2586
+ tcByContentIdx.set(idx, functionCalls.length);
2587
+ functionCalls.push({
2588
+ type: "function_call",
2589
+ id: `item_${randomUUID().slice(0, 8)}`,
2590
+ call_id: item?.id ?? `call_${functionCalls.length}`,
2591
+ name: item?.name ?? "",
2592
+ arguments: "",
2593
+ status: "completed"
2594
+ });
2595
+ return;
2596
+ }
2597
+ if (inner?.type === "toolcall_delta") {
2598
+ const i = tcByContentIdx.get(inner.contentIndex);
2599
+ if (i !== void 0 && functionCalls[i]) functionCalls[i].arguments += inner.delta ?? "";
2600
+ }
2601
+ }
2602
+ });
2603
+ try {
2604
+ await runConversation({
2605
+ session,
2606
+ prompt,
2607
+ images,
2608
+ log,
2609
+ postPrompt
2610
+ });
2611
+ } catch (err) {
2612
+ log.error({
2613
+ err,
2614
+ event: "responses_blocking_error"
2615
+ }, "blocking error");
2616
+ return jsonError(500, err?.message ?? String(err));
2617
+ } finally {
2618
+ registry.delete(sessionId);
2619
+ }
2620
+ const output = [];
2621
+ if (textContent) output.push({
2622
+ type: "message",
2623
+ role: "assistant",
2624
+ content: [{
2625
+ type: "output_text",
2626
+ text: textContent
2627
+ }]
2628
+ });
2629
+ output.push(...functionCalls);
2630
+ return jsonResponse({
2631
+ id: responseId,
2632
+ object: "response",
2633
+ status: "completed",
2634
+ output,
2635
+ model: modelName,
2636
+ output_text: textContent,
2637
+ created_at: Math.floor(Date.now() / 1e3),
2638
+ usage: {
2639
+ input_tokens: 0,
2640
+ output_tokens: 0,
2641
+ total_tokens: 0,
2642
+ input_tokens_details: { cached_tokens: 0 },
2643
+ output_tokens_details: { reasoning_tokens: 0 }
2644
+ }
2645
+ });
2646
+ }
2647
+ function create(options) {
2648
+ const cwd = options.cwd ?? process.cwd();
2649
+ const registry = options.sessionRegistry ?? createMapSessionRegistry();
2650
+ const ctx = {
2651
+ ...options,
2652
+ cwd
2653
+ };
2654
+ return async (request) => {
2655
+ const { pathname } = new URL(request.url);
2656
+ if (request.method !== "POST") return null;
2657
+ const abortSessionId = pathname.match(/^\/v1\/responses\/([^/]+)\/abort$/)?.[1];
2658
+ if (abortSessionId) try {
2659
+ return await runInTraceContext(() => handleAbort({
2660
+ request,
2661
+ sessionId: abortSessionId,
2662
+ registry
2663
+ }));
2664
+ } catch (err) {
2665
+ const { log } = requestLogger(request);
2666
+ log.error({
2667
+ err,
2668
+ event: "responses_abort_unhandled_error"
2669
+ }, "unhandled error");
2670
+ return jsonError(500, "internal error");
2671
+ }
2672
+ if (pathname !== "/v1/responses") return null;
2673
+ try {
2674
+ return await runInTraceContext(() => handleResponses(request, ctx, registry));
2675
+ } catch (err) {
2676
+ const { log } = requestLogger(request);
2677
+ log.error({
2678
+ err,
2679
+ event: "responses_unhandled_error"
2680
+ }, "unhandled error");
2681
+ return jsonError(500, "internal error");
2682
+ }
2683
+ };
2684
+ }
2685
+ //#endregion
2686
+ //#region src/protocols/index.ts
2687
+ /**
2688
+ * Build the individual protocol handlers, each returning null for
2689
+ * requests it doesn't recognize. An in-memory session registry backs
2690
+ * chat-completions steering unless deliberately overridden.
2691
+ */
2692
+ function createProtocolHandlers(options) {
2693
+ const shared = {
2694
+ cwd: options.cwd,
2695
+ createSession: options.createSession,
2696
+ onSessionSetup: options.onSessionSetup,
2697
+ postPrompt: options.postPrompt,
2698
+ passthroughHeaderPrefixes: options.passthroughHeaderPrefixes
2699
+ };
2700
+ const chatCompletions = create$1({
2701
+ ...shared,
2702
+ sessionRegistry: createMapSessionRegistry(),
2703
+ ...options.chatCompletions
2704
+ });
2705
+ return {
2706
+ prewarm: createPrewarm({
2707
+ paths: options.prewarmPaths ?? [],
2708
+ chatCompletions
2709
+ }),
2710
+ chatCompletions,
2711
+ messages: create$2({
2712
+ ...shared,
2713
+ ...options.messages
2714
+ }),
2715
+ responses: create({
2716
+ ...shared,
2717
+ ...options.responses
2718
+ })
2719
+ };
2720
+ }
2721
+ /** Chain handlers; null when none matched. */
2722
+ function composeHandlers(handlers) {
2723
+ return async (request) => {
2724
+ for (const handler of handlers) {
2725
+ const response = await handler(request);
2726
+ if (response) return response;
2727
+ }
2728
+ return null;
2729
+ };
2730
+ }
2731
+ function createProtocols(options) {
2732
+ const handlers = createProtocolHandlers(options);
2733
+ const enabled = (name) => (!options.only || options.only.includes(name)) && (!options.exclude || !options.exclude.includes(name));
2734
+ const composed = composeHandlers([
2735
+ ...enabled("chat-completions") ? [handlers.prewarm, handlers.chatCompletions] : [],
2736
+ ...enabled("messages") ? [handlers.messages] : [],
2737
+ ...enabled("responses") ? [handlers.responses] : []
2738
+ ]);
2739
+ return async (request) => {
2740
+ const response = await composed(request);
2741
+ if (response) return response;
2742
+ return new Response(JSON.stringify({ error: { message: "not found" } }), {
2743
+ status: 404,
2744
+ headers: { "Content-Type": "application/json" }
2745
+ });
2746
+ };
2747
+ }
2748
+ //#endregion
2749
+ //#region src/express.ts
2750
+ function requestUrl(req) {
2751
+ return `${req.headers["x-forwarded-proto"] ?? "http"}://${req.headers["x-forwarded-host"] ?? req.headers.host ?? "localhost"}${req.originalUrl ?? req.url ?? "/"}`;
2752
+ }
2753
+ function requestHeaders(req) {
2754
+ const headers = new Headers();
2755
+ for (const [key, value] of Object.entries(req.headers)) {
2756
+ if (value == null) continue;
2757
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
2758
+ else headers.set(key, value);
2759
+ }
2760
+ return headers;
2761
+ }
2762
+ async function toWebRequest(req) {
2763
+ const init = {
2764
+ method: req.method,
2765
+ headers: requestHeaders(req)
2766
+ };
2767
+ if (req.method !== "GET" && req.method !== "HEAD") {
2768
+ const { Readable } = await import("node:stream");
2769
+ init.body = Readable.toWeb(req);
2770
+ init.duplex = "half";
2771
+ }
2772
+ return new Request(requestUrl(req), init);
2773
+ }
2774
+ async function sendWebResponse(res, webResponse) {
2775
+ const headers = {};
2776
+ webResponse.headers.forEach((v, k) => {
2777
+ headers[k] = v;
2778
+ });
2779
+ res.writeHead(webResponse.status, headers);
2780
+ if (webResponse.body) {
2781
+ const { Readable } = await import("node:stream");
2782
+ Readable.fromWeb(webResponse.body).pipe(res);
2783
+ } else res.end();
2784
+ }
2785
+ /**
2786
+ * Mountable middleware for a web-standard handler. A null result calls
2787
+ * next() so unmatched requests fall through to whatever the caller
2788
+ * mounts after it.
2789
+ */
2790
+ function webHandlerToMiddleware(handler) {
2791
+ return async (req, res, next) => {
2792
+ try {
2793
+ const webResponse = await handler(await toWebRequest(req));
2794
+ if (!webResponse) {
2795
+ next();
2796
+ return;
2797
+ }
2798
+ await sendWebResponse(res, webResponse);
2799
+ } catch (err) {
2800
+ logger.error({
2801
+ err,
2802
+ event: "server_error"
2803
+ }, "unhandled server error");
2804
+ if (!res.headersSent) {
2805
+ res.writeHead(500, { "content-type": "application/json" });
2806
+ res.end(JSON.stringify({ error: { message: "internal error" } }));
2807
+ }
2808
+ }
2809
+ };
2810
+ }
2811
+ /**
2812
+ * Path-scope a middleware the way express mounting does: skip (next())
2813
+ * unless the request path matches the prefix, and present a
2814
+ * mount-relative req.url to the inner middleware. Lets express-style
2815
+ * handlers that expect `app.use('/a2a', h)` participate in a composed
2816
+ * catch-all.
2817
+ */
2818
+ function mountAt(prefix, middleware) {
2819
+ return (req, res, next) => {
2820
+ const originalUrl = req.url ?? "/";
2821
+ const path = originalUrl.split("?")[0] ?? "";
2822
+ if (path !== prefix && !path.startsWith(`${prefix}/`)) {
2823
+ next();
2824
+ return;
2825
+ }
2826
+ const rest = originalUrl.slice(prefix.length);
2827
+ req.url = rest.startsWith("/") ? rest : `/${rest}`;
2828
+ middleware(req, res, (err) => {
2829
+ req.url = originalUrl;
2830
+ next(err);
2831
+ });
2832
+ };
2833
+ }
2834
+ /** Run middlewares in order; each next() advances to the following one. */
2835
+ function chainMiddleware(middlewares) {
2836
+ return (req, res, next) => {
2837
+ let i = 0;
2838
+ const run = (err) => {
2839
+ if (err) {
2840
+ next(err);
2841
+ return;
2842
+ }
2843
+ const middleware = middlewares[i++];
2844
+ if (!middleware) {
2845
+ next();
2846
+ return;
2847
+ }
2848
+ middleware(req, res, run);
2849
+ };
2850
+ run();
2851
+ };
2852
+ }
2853
+ //#endregion
2854
+ export { 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 };