roboport 0.0.1

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.
Files changed (51) hide show
  1. package/README.md +25 -0
  2. package/core/agent.d.ts +35 -0
  3. package/core/index.d.ts +9 -0
  4. package/core/mcp.d.ts +6 -0
  5. package/core/message.d.ts +36 -0
  6. package/core/model.d.ts +10 -0
  7. package/core/session.d.ts +75 -0
  8. package/core/skill.d.ts +11 -0
  9. package/core/stream.d.ts +33 -0
  10. package/core/tool.d.ts +77 -0
  11. package/env.d.ts +8 -0
  12. package/harness/claudeCode.d.ts +3 -0
  13. package/harness/codex.d.ts +3 -0
  14. package/harness/core.d.ts +7 -0
  15. package/harness/index.d.ts +5 -0
  16. package/harness/index.js +1512 -0
  17. package/harness/pi.d.ts +3 -0
  18. package/harness/shared.d.ts +33 -0
  19. package/harness/tools.d.ts +33 -0
  20. package/index.d.ts +2 -0
  21. package/index.js +537 -0
  22. package/mcp/auth.d.ts +40 -0
  23. package/mcp/clients/grafana.d.ts +17 -0
  24. package/mcp/clients/linear.d.ts +9 -0
  25. package/mcp/clients/tenderly.d.ts +11 -0
  26. package/mcp/core.d.ts +30 -0
  27. package/mcp/index.d.ts +4 -0
  28. package/mcp/index.js +1356 -0
  29. package/mcp/oauth.d.ts +36 -0
  30. package/mcp/servers/calculator.d.ts +1 -0
  31. package/mcp/storage.d.ts +29 -0
  32. package/models/anthropic.d.ts +15 -0
  33. package/models/google.d.ts +14 -0
  34. package/models/index.d.ts +6 -0
  35. package/models/index.js +2039 -0
  36. package/models/moonshot.d.ts +16 -0
  37. package/models/openai-codex-auth.d.ts +17 -0
  38. package/models/openai-compatible.d.ts +41 -0
  39. package/models/openai.d.ts +29 -0
  40. package/package.json +60 -0
  41. package/skills/index.d.ts +7 -0
  42. package/skills/index.js +1007 -0
  43. package/triggers/bus.d.ts +8 -0
  44. package/triggers/core.d.ts +14 -0
  45. package/triggers/index.d.ts +7 -0
  46. package/triggers/index.js +588 -0
  47. package/triggers/sources/cron.d.ts +29 -0
  48. package/triggers/sources/github.d.ts +148 -0
  49. package/triggers/sources/grafana.d.ts +37 -0
  50. package/triggers/sources/linear.d.ts +39 -0
  51. package/triggers/sources/telegram.d.ts +85 -0
@@ -0,0 +1,2039 @@
1
+ // @bun
2
+ // src/core/agent.ts
3
+ import { z } from "zod";
4
+
5
+ // src/core/session.ts
6
+ class Turn {
7
+ queue = [];
8
+ waiters = [];
9
+ ended = false;
10
+ iterated = false;
11
+ resultPromise;
12
+ abortController = new AbortController;
13
+ constructor(runner) {
14
+ this.resultPromise = runner({
15
+ emit: (event) => this.emit(event),
16
+ signal: this.abortController.signal
17
+ }).then((messages) => {
18
+ this.close();
19
+ return messages;
20
+ }, (error) => {
21
+ this.close();
22
+ throw error;
23
+ });
24
+ this.resultPromise.catch(() => {});
25
+ }
26
+ emit(event) {
27
+ const waiter = this.waiters.shift();
28
+ if (waiter) {
29
+ waiter({ value: event, done: false });
30
+ } else {
31
+ this.queue.push(event);
32
+ }
33
+ }
34
+ close() {
35
+ this.ended = true;
36
+ while (this.waiters.length > 0) {
37
+ const waiter = this.waiters.shift();
38
+ waiter?.({ value: undefined, done: true });
39
+ }
40
+ }
41
+ abort(reason) {
42
+ this.abortController.abort(reason);
43
+ }
44
+ [Symbol.asyncIterator]() {
45
+ if (this.iterated) {
46
+ throw new Error("Turn can only be iterated once.");
47
+ }
48
+ this.iterated = true;
49
+ return {
50
+ next: () => {
51
+ if (this.queue.length > 0) {
52
+ const value = this.queue.shift();
53
+ return Promise.resolve({ value, done: false });
54
+ }
55
+ if (this.ended) {
56
+ return Promise.resolve({
57
+ value: undefined,
58
+ done: true
59
+ });
60
+ }
61
+ return new Promise((resolve) => this.waiters.push(resolve));
62
+ },
63
+ return: async () => {
64
+ this.abort("iteration ended");
65
+ await this.resultPromise.catch(() => {});
66
+ return {
67
+ value: undefined,
68
+ done: true
69
+ };
70
+ }
71
+ };
72
+ }
73
+ then(onfulfilled, onrejected) {
74
+ return this.resultPromise.then(onfulfilled, onrejected);
75
+ }
76
+ }
77
+
78
+ class Session {
79
+ internals;
80
+ state;
81
+ constructor(internals, state) {
82
+ this.internals = internals;
83
+ this.state = state;
84
+ }
85
+ get messages() {
86
+ return this.state.messages;
87
+ }
88
+ send(prompt) {
89
+ return this.internals.send(prompt);
90
+ }
91
+ async close() {
92
+ await this.internals.close();
93
+ }
94
+ async[Symbol.asyncDispose]() {
95
+ await this.close();
96
+ }
97
+ }
98
+
99
+ // src/core/tool.ts
100
+ import * as z4 from "zod/v4/core";
101
+ function hasParseMethod(schema) {
102
+ return typeof schema === "object" && schema !== null && "parse" in schema && typeof schema.parse === "function";
103
+ }
104
+
105
+ class Tool {
106
+ name;
107
+ description;
108
+ inputSchema;
109
+ jsonSchema;
110
+ execute;
111
+ deferred;
112
+ constructor(init) {
113
+ this.name = init.name;
114
+ this.description = init.description;
115
+ this.deferred = init.deferred ?? false;
116
+ if ("inputSchema" in init) {
117
+ this.inputSchema = init.inputSchema;
118
+ this.execute = init.execute;
119
+ } else {
120
+ this.jsonSchema = init.jsonSchema;
121
+ this.execute = init.execute;
122
+ }
123
+ }
124
+ toJsonSchema() {
125
+ if (this.jsonSchema !== undefined)
126
+ return this.jsonSchema;
127
+ if (this.inputSchema === undefined) {
128
+ throw new Error(`Tool "${this.name}" has neither inputSchema nor jsonSchema.`);
129
+ }
130
+ return z4.toJSONSchema(this.inputSchema);
131
+ }
132
+ parse(input) {
133
+ const schema = this.inputSchema;
134
+ if (!schema)
135
+ return input;
136
+ if (hasParseMethod(schema))
137
+ return schema.parse(input);
138
+ return z4.parse(schema, input);
139
+ }
140
+ }
141
+ function createRegistry(tools) {
142
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
143
+ const loadedNames = new Set(tools.filter((tool) => !tool.deferred).map((tool) => tool.name));
144
+ return {
145
+ loaded: () => tools.filter((tool) => loadedNames.has(tool.name)),
146
+ deferred: () => tools.filter((tool) => tool.deferred && !loadedNames.has(tool.name)),
147
+ load: (names) => {
148
+ const loaded = [];
149
+ const missing = [];
150
+ for (const name of names) {
151
+ const tool = byName.get(name);
152
+ if (!tool) {
153
+ missing.push(name);
154
+ continue;
155
+ }
156
+ loadedNames.add(name);
157
+ loaded.push(tool);
158
+ }
159
+ return { loaded, missing };
160
+ }
161
+ };
162
+ }
163
+
164
+ // src/core/agent.ts
165
+ class Agent {
166
+ model;
167
+ system;
168
+ tools;
169
+ skills;
170
+ mcp;
171
+ cwd;
172
+ registrations = [];
173
+ unsubs = [];
174
+ constructor({
175
+ model,
176
+ system,
177
+ tools,
178
+ skills,
179
+ mcp,
180
+ cwd
181
+ }) {
182
+ this.model = model;
183
+ this.system = system;
184
+ this.tools = tools;
185
+ this.skills = skills;
186
+ this.mcp = mcp ?? [];
187
+ this.cwd = cwd;
188
+ }
189
+ on(trigger, handler) {
190
+ this.registrations.push({ trigger, handler });
191
+ }
192
+ async start() {
193
+ for (const { trigger, handler } of this.registrations) {
194
+ const unsub = await trigger.start((event) => {
195
+ Promise.resolve().then(() => handler(event)).catch((error) => {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ console.error(`[roboport] trigger "${trigger.name}" handler failed: ${message}`);
198
+ });
199
+ });
200
+ this.unsubs.push(unsub);
201
+ }
202
+ }
203
+ async stop() {
204
+ const unsubs = this.unsubs;
205
+ this.unsubs = [];
206
+ await Promise.all(unsubs.map((u) => u()));
207
+ }
208
+ buildSystem(allTools) {
209
+ let system = this.system;
210
+ if (this.skills.length > 0) {
211
+ const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
212
+ `);
213
+ system = `${system}
214
+
215
+ # Skills
216
+ The following skills are available. When a task matches one, call the \`Skill\` tool with that skill's name to load its full content before proceeding.
217
+
218
+ ${skillsList}`;
219
+ }
220
+ const deferred = allTools.filter((tool) => tool.deferred);
221
+ if (deferred.length > 0) {
222
+ const list = deferred.map((tool) => `- ${tool.name}`).join(`
223
+ `);
224
+ system = `${system}
225
+
226
+ # Deferred tools
227
+ These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
228
+ ${list}`;
229
+ }
230
+ return system;
231
+ }
232
+ buildSkillTool() {
233
+ const byName = new Map(this.skills.map((skill) => [skill.name, skill]));
234
+ return new Tool({
235
+ name: "Skill",
236
+ description: 'Load the full content of a skill listed under "# Skills" in the system prompt. Call this when you decide a listed skill applies to the current task; the returned content extends your instructions for the rest of the session.',
237
+ inputSchema: z.object({
238
+ skill: z.string().describe("Name of the skill to load (must match a listed skill).")
239
+ }),
240
+ execute: ({ skill }) => {
241
+ const found = byName.get(skill);
242
+ if (!found) {
243
+ const available = [...byName.keys()].join(", ");
244
+ throw new Error(`Skill "${skill}" not found. Available: ${available}`);
245
+ }
246
+ return `<skill name="${found.name}">
247
+ ${found.content}
248
+ </skill>`;
249
+ }
250
+ });
251
+ }
252
+ session(init) {
253
+ const initialMessages = init?.messages ? [...init.messages] : [];
254
+ const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
255
+ const state = {
256
+ messages: initialMessages,
257
+ store: new Map
258
+ };
259
+ let activeTurn = null;
260
+ let mcpConnected = false;
261
+ let allTools = null;
262
+ let registry = null;
263
+ let ctx = null;
264
+ const ensureReady = async () => {
265
+ if (!allTools || !registry || !ctx) {
266
+ const mcpToolGroups = await Promise.all(this.mcp.map((mcp) => mcp.connect()));
267
+ mcpConnected = true;
268
+ allTools = [
269
+ ...this.tools,
270
+ ...mcpToolGroups.flat(),
271
+ ...this.skills.length > 0 ? [this.buildSkillTool()] : []
272
+ ];
273
+ registry = createRegistry(allTools);
274
+ ctx = {
275
+ complete: async (p) => {
276
+ const response = await this.model.createMessage({
277
+ messages: [{ role: "user", content: p }]
278
+ });
279
+ return response.content.filter((block) => block.type === "text").map((block) => block.text).join(`
280
+ `);
281
+ },
282
+ searchWeb: (query, opts) => this.model.searchWeb(query, opts),
283
+ session: state,
284
+ tools: registry,
285
+ cwd: sessionCwd
286
+ };
287
+ if (state.messages.length === 0 || state.messages[0]?.role !== "system") {
288
+ state.messages.unshift({
289
+ role: "system",
290
+ content: this.buildSystem(allTools)
291
+ });
292
+ }
293
+ }
294
+ return { tools: allTools, registry, ctx };
295
+ };
296
+ const internals = {
297
+ send: (prompt) => {
298
+ if (activeTurn !== null) {
299
+ throw new Error("Session.send() called while another turn is in flight.");
300
+ }
301
+ const turn = new Turn(async (turnCtx) => {
302
+ try {
303
+ const ready = await ensureReady();
304
+ state.messages.push(toUserMessage(prompt));
305
+ await runAgentLoop({
306
+ model: this.model,
307
+ state,
308
+ registry: ready.registry,
309
+ ctx: ready.ctx,
310
+ emit: turnCtx.emit,
311
+ signal: turnCtx.signal
312
+ });
313
+ return [...state.messages];
314
+ } finally {
315
+ activeTurn = null;
316
+ }
317
+ });
318
+ activeTurn = turn;
319
+ return turn;
320
+ },
321
+ close: async () => {
322
+ const pending = activeTurn;
323
+ if (pending) {
324
+ pending.abort("session closed");
325
+ await Promise.resolve(pending).catch(() => {});
326
+ }
327
+ if (mcpConnected) {
328
+ await Promise.all(this.mcp.map((mcp) => mcp.disconnect()));
329
+ mcpConnected = false;
330
+ }
331
+ }
332
+ };
333
+ return new Session(internals, state);
334
+ }
335
+ }
336
+ function toUserMessage(prompt) {
337
+ if (typeof prompt === "string")
338
+ return { role: "user", content: prompt };
339
+ return { role: "user", content: prompt };
340
+ }
341
+ async function runAgentLoop({
342
+ model,
343
+ state,
344
+ registry,
345
+ ctx,
346
+ emit,
347
+ signal
348
+ }) {
349
+ while (true) {
350
+ if (signal.aborted)
351
+ break;
352
+ const active = registry.loaded();
353
+ const toolByName = new Map(active.map((tool) => [tool.name, tool]));
354
+ emit({ type: "message-start" });
355
+ const assistantContent = [];
356
+ let stopReason = "end_turn";
357
+ let usage = { inputTokens: 0, outputTokens: 0 };
358
+ try {
359
+ for await (const event of model.streamMessage({
360
+ messages: state.messages,
361
+ tools: active,
362
+ signal
363
+ })) {
364
+ switch (event.type) {
365
+ case "text-delta":
366
+ emit({ type: "text-delta", text: event.text });
367
+ break;
368
+ case "text-end":
369
+ assistantContent.push({ type: "text", text: event.text });
370
+ emit({ type: "text", text: event.text });
371
+ break;
372
+ case "thinking-delta":
373
+ emit({ type: "thinking-delta", text: event.text });
374
+ break;
375
+ case "thinking-end":
376
+ assistantContent.push({
377
+ type: "thinking",
378
+ text: event.text,
379
+ ...event.signature !== undefined ? { signature: event.signature } : {},
380
+ ...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
381
+ });
382
+ emit({
383
+ type: "thinking",
384
+ text: event.text,
385
+ ...event.signature !== undefined ? { signature: event.signature } : {}
386
+ });
387
+ break;
388
+ case "tool-call":
389
+ assistantContent.push({
390
+ type: "tool-call",
391
+ toolCallId: event.toolCallId,
392
+ toolName: event.toolName,
393
+ input: event.input
394
+ });
395
+ emit({
396
+ type: "tool-call",
397
+ toolCallId: event.toolCallId,
398
+ toolName: event.toolName,
399
+ input: event.input
400
+ });
401
+ break;
402
+ case "message-end":
403
+ stopReason = event.stopReason;
404
+ usage = event.usage;
405
+ break;
406
+ default:
407
+ break;
408
+ }
409
+ }
410
+ } catch (error) {
411
+ if (signal.aborted) {
412
+ state.messages.push({ role: "assistant", content: assistantContent });
413
+ break;
414
+ }
415
+ const err = error instanceof Error ? error : new Error(String(error));
416
+ emit({ type: "error", error: err });
417
+ throw err;
418
+ }
419
+ state.messages.push({ role: "assistant", content: assistantContent });
420
+ emit({ type: "message-end", usage });
421
+ if (stopReason !== "tool_use") {
422
+ emit({ type: "turn-end" });
423
+ break;
424
+ }
425
+ const toolCalls = assistantContent.filter((block) => block.type === "tool-call");
426
+ const results = [];
427
+ for (const call of toolCalls) {
428
+ if (signal.aborted)
429
+ break;
430
+ const tool = toolByName.get(call.toolName);
431
+ const result = await runTool(tool, call, ctx);
432
+ results.push(result);
433
+ emit({
434
+ type: "tool-result",
435
+ toolCallId: result.toolCallId,
436
+ toolName: result.toolName,
437
+ output: result.output,
438
+ isError: typeof result.output === "string" ? result.output.startsWith("Error:") : false
439
+ });
440
+ }
441
+ state.messages.push({ role: "tool", content: results });
442
+ if (signal.aborted)
443
+ break;
444
+ }
445
+ }
446
+ async function runTool(tool, call, ctx) {
447
+ if (!tool) {
448
+ return {
449
+ type: "tool-result",
450
+ toolCallId: call.toolCallId,
451
+ toolName: call.toolName,
452
+ output: `Error: tool "${call.toolName}" not found`
453
+ };
454
+ }
455
+ try {
456
+ const parsed = tool.parse(call.input);
457
+ const output = await tool.execute(parsed, ctx);
458
+ return {
459
+ type: "tool-result",
460
+ toolCallId: call.toolCallId,
461
+ toolName: call.toolName,
462
+ output
463
+ };
464
+ } catch (error) {
465
+ return {
466
+ type: "tool-result",
467
+ toolCallId: call.toolCallId,
468
+ toolName: call.toolName,
469
+ output: `Error: ${error instanceof Error ? error.message : String(error)}`
470
+ };
471
+ }
472
+ }
473
+
474
+ // src/core/model.ts
475
+ class Model {
476
+ async createMessage(params) {
477
+ const content = [];
478
+ let id = "";
479
+ let stopReason = "end_turn";
480
+ let usage = { inputTokens: 0, outputTokens: 0 };
481
+ for await (const event of this.streamMessage(params)) {
482
+ switch (event.type) {
483
+ case "text-end":
484
+ content.push({ type: "text", text: event.text });
485
+ break;
486
+ case "thinking-end":
487
+ content.push({
488
+ type: "thinking",
489
+ text: event.text,
490
+ ...event.signature !== undefined ? { signature: event.signature } : {},
491
+ ...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
492
+ });
493
+ break;
494
+ case "tool-call":
495
+ content.push({
496
+ type: "tool-call",
497
+ toolCallId: event.toolCallId,
498
+ toolName: event.toolName,
499
+ input: event.input
500
+ });
501
+ break;
502
+ case "message-end":
503
+ id = event.id;
504
+ stopReason = event.stopReason;
505
+ usage = event.usage;
506
+ break;
507
+ default:
508
+ break;
509
+ }
510
+ }
511
+ return { id, content, stopReason, usage };
512
+ }
513
+ }
514
+
515
+ // src/core/skill.ts
516
+ class Skill {
517
+ name;
518
+ description;
519
+ content;
520
+ constructor({
521
+ name,
522
+ description,
523
+ content
524
+ }) {
525
+ this.name = name;
526
+ this.description = description;
527
+ this.content = content;
528
+ }
529
+ }
530
+
531
+ // src/core/stream.ts
532
+ async function* readSse(response) {
533
+ if (!response.body) {
534
+ throw new Error("Response body is empty; cannot stream.");
535
+ }
536
+ const reader = response.body.getReader();
537
+ const decoder = new TextDecoder;
538
+ let buffer = "";
539
+ function* drain() {
540
+ let sepIdx = findEventBoundary(buffer);
541
+ while (sepIdx !== -1) {
542
+ const rawEvent = buffer.slice(0, sepIdx.start);
543
+ buffer = buffer.slice(sepIdx.end);
544
+ const payload = extractDataPayload(rawEvent);
545
+ if (payload !== undefined) {
546
+ yield parsePayload(payload);
547
+ }
548
+ sepIdx = findEventBoundary(buffer);
549
+ }
550
+ }
551
+ try {
552
+ while (true) {
553
+ const { value, done } = await reader.read();
554
+ if (done)
555
+ break;
556
+ buffer += decoder.decode(value, { stream: true });
557
+ yield* drain();
558
+ }
559
+ buffer += decoder.decode();
560
+ yield* drain();
561
+ if (buffer.trim().length > 0) {
562
+ const payload = extractDataPayload(buffer);
563
+ if (payload !== undefined) {
564
+ yield parsePayload(payload);
565
+ } else {
566
+ throw new Error(`SSE stream ended with unterminated buffer: ${buffer.slice(0, 200)}`);
567
+ }
568
+ }
569
+ } finally {
570
+ reader.releaseLock();
571
+ }
572
+ }
573
+ function parsePayload(payload) {
574
+ try {
575
+ return JSON.parse(payload);
576
+ } catch (error) {
577
+ throw new Error(`Malformed SSE event payload: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
578
+ }
579
+ }
580
+ function findEventBoundary(buffer) {
581
+ const candidates = [`\r
582
+ \r
583
+ `, `
584
+
585
+ `, "\r\r"];
586
+ let best = -1;
587
+ let bestLen = 0;
588
+ for (const sep of candidates) {
589
+ const idx = buffer.indexOf(sep);
590
+ if (idx === -1)
591
+ continue;
592
+ if (best === -1 || idx < best) {
593
+ best = idx;
594
+ bestLen = sep.length;
595
+ }
596
+ }
597
+ if (best === -1)
598
+ return -1;
599
+ return { start: best, end: best + bestLen };
600
+ }
601
+ function extractDataPayload(rawEvent) {
602
+ const lines = rawEvent.split(/\r?\n/);
603
+ const dataLines = [];
604
+ for (const line of lines) {
605
+ if (!line.startsWith("data:"))
606
+ continue;
607
+ dataLines.push(line.slice("data:".length).replace(/^ /, ""));
608
+ }
609
+ if (dataLines.length === 0)
610
+ return;
611
+ const joined = dataLines.join(`
612
+ `);
613
+ if (joined === "[DONE]")
614
+ return;
615
+ return joined;
616
+ }
617
+
618
+ // src/env.ts
619
+ function read(name) {
620
+ const value = process.env[name];
621
+ return value && value.length > 0 ? value : undefined;
622
+ }
623
+ var env = {
624
+ get anthropicApiKey() {
625
+ return read("ANTHROPIC_API_KEY");
626
+ },
627
+ get geminiApiKey() {
628
+ return read("GEMINI_API_KEY");
629
+ },
630
+ get moonshotApiKey() {
631
+ return read("MOONSHOT_API_KEY");
632
+ },
633
+ get openaiApiKey() {
634
+ return read("OPENAI_API_KEY");
635
+ },
636
+ get openaiCodexAuthFile() {
637
+ return read("ROBOPORT_OPENAI_CODEX_AUTH_FILE");
638
+ },
639
+ get codexHome() {
640
+ return read("CODEX_HOME");
641
+ }
642
+ };
643
+
644
+ // src/models/anthropic.ts
645
+ var ANTHROPIC_MODELS = [
646
+ "claude-opus-4-8",
647
+ "claude-opus-4-7",
648
+ "claude-opus-4-6",
649
+ "claude-sonnet-4-6",
650
+ "claude-sonnet-4-5",
651
+ "claude-haiku-4-5"
652
+ ];
653
+ var ANTHROPIC_BUDGETS = {
654
+ minimal: 1024,
655
+ low: 4000,
656
+ medium: 1e4,
657
+ high: 31999,
658
+ xhigh: 50000
659
+ };
660
+ var ANTHROPIC_OUTPUT_BUFFER = 4096;
661
+ var ADAPTIVE_THINKING_MODELS = new Set([
662
+ "claude-opus-4-7",
663
+ "claude-opus-4-8"
664
+ ]);
665
+ var ADAPTIVE_EFFORTS = {
666
+ minimal: "minimal",
667
+ low: "low",
668
+ medium: "medium",
669
+ high: "high",
670
+ xhigh: "high"
671
+ };
672
+ function serializeToolOutput(output) {
673
+ if (output === undefined || output === null)
674
+ return "";
675
+ if (typeof output === "string")
676
+ return output;
677
+ return JSON.stringify(output);
678
+ }
679
+ function toWire(messages) {
680
+ let system;
681
+ const wireMessages = [];
682
+ for (const msg of messages) {
683
+ if (msg.role === "system") {
684
+ system = msg.content;
685
+ continue;
686
+ }
687
+ if (msg.role === "user") {
688
+ const content = typeof msg.content === "string" ? [{ type: "text", text: msg.content }] : msg.content.map((part) => ({ type: "text", text: part.text }));
689
+ wireMessages.push({ role: "user", content });
690
+ continue;
691
+ }
692
+ if (msg.role === "assistant") {
693
+ const content = [];
694
+ for (const part of msg.content) {
695
+ if (part.type === "text") {
696
+ content.push({ type: "text", text: part.text });
697
+ continue;
698
+ }
699
+ if (part.type === "thinking") {
700
+ if (part.redactedData !== undefined) {
701
+ content.push({
702
+ type: "redacted_thinking",
703
+ data: part.redactedData
704
+ });
705
+ } else if (part.signature !== undefined && part.signature !== "") {
706
+ content.push({
707
+ type: "thinking",
708
+ thinking: part.text,
709
+ signature: part.signature
710
+ });
711
+ }
712
+ continue;
713
+ }
714
+ content.push({
715
+ type: "tool_use",
716
+ id: part.toolCallId,
717
+ name: part.toolName,
718
+ input: part.input
719
+ });
720
+ }
721
+ wireMessages.push({ role: "assistant", content });
722
+ continue;
723
+ }
724
+ wireMessages.push({
725
+ role: "user",
726
+ content: msg.content.map((part) => ({
727
+ type: "tool_result",
728
+ tool_use_id: part.toolCallId,
729
+ content: serializeToolOutput(part.output)
730
+ }))
731
+ });
732
+ }
733
+ return { system, wireMessages };
734
+ }
735
+
736
+ class AnthropicModel extends Model {
737
+ modelName;
738
+ apiKey;
739
+ thinking;
740
+ constructor(modelName, options) {
741
+ super();
742
+ this.modelName = modelName;
743
+ const key = options?.apiKey ?? env.anthropicApiKey;
744
+ if (!key) {
745
+ throw new Error("No Anthropic API key found. Set ANTHROPIC_API_KEY or pass apiKey.");
746
+ }
747
+ this.apiKey = key;
748
+ this.thinking = options?.thinking ?? "off";
749
+ }
750
+ async* streamMessage(params) {
751
+ const { messages, tools, maxTokens = 8192, signal } = params;
752
+ const { system, wireMessages } = toWire(messages);
753
+ const wireTools = tools?.map((tool) => ({
754
+ name: tool.name,
755
+ description: tool.description,
756
+ input_schema: tool.toJsonSchema()
757
+ }));
758
+ const useAdaptive = ADAPTIVE_THINKING_MODELS.has(this.modelName);
759
+ const budget = this.thinking === "off" || useAdaptive ? undefined : ANTHROPIC_BUDGETS[this.thinking];
760
+ const effectiveMaxTokens = budget === undefined ? maxTokens : Math.max(maxTokens, budget + ANTHROPIC_OUTPUT_BUFFER);
761
+ const body = {
762
+ model: this.modelName,
763
+ max_tokens: effectiveMaxTokens,
764
+ messages: wireMessages,
765
+ stream: true
766
+ };
767
+ if (system !== undefined)
768
+ body.system = system;
769
+ if (wireTools !== undefined && wireTools.length > 0)
770
+ body.tools = wireTools;
771
+ if (budget !== undefined) {
772
+ body.thinking = { type: "enabled", budget_tokens: budget };
773
+ } else if (useAdaptive && this.thinking !== "off") {
774
+ body.thinking = { type: "adaptive" };
775
+ body.output_config = { effort: ADAPTIVE_EFFORTS[this.thinking] };
776
+ }
777
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
778
+ method: "POST",
779
+ headers: {
780
+ "content-type": "application/json",
781
+ "x-api-key": this.apiKey,
782
+ "anthropic-version": "2023-06-01",
783
+ accept: "text/event-stream"
784
+ },
785
+ body: JSON.stringify(body),
786
+ signal
787
+ });
788
+ if (!response.ok) {
789
+ throw new Error(`Anthropic API error ${response.status}: ${await response.text()}`);
790
+ }
791
+ const blocks = new Map;
792
+ let id = "";
793
+ let stopReason = "end_turn";
794
+ let inputTokens = 0;
795
+ let outputTokens = 0;
796
+ let sawMessageStop = false;
797
+ for await (const raw of readSse(response)) {
798
+ const event = raw;
799
+ if (event.type === "message_start") {
800
+ id = event.message.id;
801
+ if (event.message.usage?.input_tokens !== undefined) {
802
+ inputTokens = event.message.usage.input_tokens;
803
+ }
804
+ if (event.message.usage?.output_tokens !== undefined) {
805
+ outputTokens = event.message.usage.output_tokens;
806
+ }
807
+ continue;
808
+ }
809
+ if (event.type === "content_block_start") {
810
+ const block = event.content_block;
811
+ if (block.type === "text") {
812
+ blocks.set(event.index, { kind: "text", text: block.text ?? "" });
813
+ } else if (block.type === "thinking") {
814
+ blocks.set(event.index, {
815
+ kind: "thinking",
816
+ text: block.thinking ?? ""
817
+ });
818
+ } else if (block.type === "redacted_thinking") {
819
+ blocks.set(event.index, {
820
+ kind: "redacted_thinking",
821
+ text: "",
822
+ redactedData: block.data
823
+ });
824
+ } else if (block.type === "tool_use") {
825
+ blocks.set(event.index, {
826
+ kind: "tool_use",
827
+ text: "",
828
+ toolCallId: block.id,
829
+ toolName: block.name,
830
+ argsBuffer: ""
831
+ });
832
+ }
833
+ continue;
834
+ }
835
+ if (event.type === "content_block_delta") {
836
+ const builder = blocks.get(event.index);
837
+ if (!builder)
838
+ continue;
839
+ const delta = event.delta;
840
+ if (delta.type === "text_delta") {
841
+ builder.text += delta.text;
842
+ yield { type: "text-delta", text: delta.text };
843
+ } else if (delta.type === "thinking_delta") {
844
+ builder.text += delta.thinking;
845
+ yield { type: "thinking-delta", text: delta.thinking };
846
+ } else if (delta.type === "signature_delta") {
847
+ builder.signature = (builder.signature ?? "") + delta.signature;
848
+ } else if (delta.type === "input_json_delta") {
849
+ builder.argsBuffer = (builder.argsBuffer ?? "") + delta.partial_json;
850
+ }
851
+ continue;
852
+ }
853
+ if (event.type === "content_block_stop") {
854
+ const builder = blocks.get(event.index);
855
+ if (!builder)
856
+ continue;
857
+ if (builder.kind === "text") {
858
+ yield { type: "text-end", text: builder.text };
859
+ } else if (builder.kind === "thinking") {
860
+ yield {
861
+ type: "thinking-end",
862
+ text: builder.text,
863
+ ...builder.signature !== undefined ? { signature: builder.signature } : {}
864
+ };
865
+ } else if (builder.kind === "redacted_thinking") {
866
+ yield {
867
+ type: "thinking-end",
868
+ text: "",
869
+ redactedData: builder.redactedData
870
+ };
871
+ } else if (builder.kind === "tool_use") {
872
+ const input = parseToolInput(builder.argsBuffer ?? "");
873
+ yield {
874
+ type: "tool-call",
875
+ toolCallId: builder.toolCallId ?? "",
876
+ toolName: builder.toolName ?? "",
877
+ input
878
+ };
879
+ }
880
+ blocks.delete(event.index);
881
+ continue;
882
+ }
883
+ if (event.type === "message_delta") {
884
+ if (event.delta.stop_reason)
885
+ stopReason = event.delta.stop_reason;
886
+ if (event.usage?.input_tokens !== undefined) {
887
+ inputTokens = event.usage.input_tokens;
888
+ }
889
+ if (event.usage?.output_tokens !== undefined) {
890
+ outputTokens = event.usage.output_tokens;
891
+ }
892
+ continue;
893
+ }
894
+ if (event.type === "message_stop") {
895
+ sawMessageStop = true;
896
+ continue;
897
+ }
898
+ if (event.type === "error") {
899
+ throw new Error(event.error?.message ?? "Anthropic stream returned an error.");
900
+ }
901
+ }
902
+ if (!sawMessageStop) {
903
+ throw new Error("Anthropic stream ended before message_stop; response is truncated.");
904
+ }
905
+ yield {
906
+ type: "message-end",
907
+ id,
908
+ stopReason,
909
+ usage: { inputTokens, outputTokens }
910
+ };
911
+ }
912
+ async searchWeb(query, opts) {
913
+ const tool = {
914
+ type: "web_search_20250305",
915
+ name: "web_search"
916
+ };
917
+ if (opts?.allowedDomains)
918
+ tool.allowed_domains = opts.allowedDomains;
919
+ if (opts?.blockedDomains)
920
+ tool.blocked_domains = opts.blockedDomains;
921
+ if (opts?.maxUses !== undefined)
922
+ tool.max_uses = opts.maxUses;
923
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
924
+ method: "POST",
925
+ headers: {
926
+ "content-type": "application/json",
927
+ "x-api-key": this.apiKey,
928
+ "anthropic-version": "2023-06-01"
929
+ },
930
+ body: JSON.stringify({
931
+ model: this.modelName,
932
+ max_tokens: 1024,
933
+ tools: [tool],
934
+ messages: [{ role: "user", content: `Search the web for: ${query}` }]
935
+ })
936
+ });
937
+ if (!response.ok) {
938
+ throw new Error(`Anthropic web_search failed (${response.status}): ${await response.text()}`);
939
+ }
940
+ const json = await response.json();
941
+ const hits = [];
942
+ for (const block of json.content) {
943
+ if (block.type !== "web_search_tool_result")
944
+ continue;
945
+ const content = block.content;
946
+ if (!Array.isArray(content))
947
+ continue;
948
+ for (const item of content) {
949
+ if (item.type !== "web_search_result")
950
+ continue;
951
+ hits.push({
952
+ title: item.title,
953
+ url: item.url,
954
+ pageAge: item.page_age ?? undefined
955
+ });
956
+ }
957
+ }
958
+ return hits;
959
+ }
960
+ }
961
+ function parseToolInput(raw) {
962
+ if (!raw)
963
+ return {};
964
+ try {
965
+ return JSON.parse(raw);
966
+ } catch {
967
+ return raw;
968
+ }
969
+ }
970
+
971
+ // src/models/openai-compatible.ts
972
+ function serializeToolOutput2(output) {
973
+ if (output === undefined || output === null)
974
+ return "";
975
+ if (typeof output === "string")
976
+ return output;
977
+ return JSON.stringify(output);
978
+ }
979
+ function mapFinishReason(reason) {
980
+ switch (reason) {
981
+ case "stop":
982
+ return "end_turn";
983
+ case "tool_calls":
984
+ return "tool_use";
985
+ case "length":
986
+ return "max_tokens";
987
+ case "content_filter":
988
+ return "refusal";
989
+ default:
990
+ return "end_turn";
991
+ }
992
+ }
993
+ function parseToolArguments(raw) {
994
+ if (!raw)
995
+ return {};
996
+ try {
997
+ return JSON.parse(raw);
998
+ } catch {
999
+ return raw;
1000
+ }
1001
+ }
1002
+
1003
+ class OpenAICompatibleModel extends Model {
1004
+ modelName;
1005
+ apiKey;
1006
+ baseUrl;
1007
+ thinking;
1008
+ constructor(modelName, options) {
1009
+ super();
1010
+ this.modelName = modelName;
1011
+ this.apiKey = options.apiKey;
1012
+ this.baseUrl = options.baseUrl;
1013
+ this.thinking = options.thinking ?? "off";
1014
+ }
1015
+ async* streamMessage(params) {
1016
+ const { messages, tools, maxTokens = 8192, signal } = params;
1017
+ const wireMessages = this.serializeMessages(messages);
1018
+ const wireTools = tools?.map((tool) => ({
1019
+ type: "function",
1020
+ function: {
1021
+ name: tool.name,
1022
+ description: tool.description,
1023
+ parameters: tool.toJsonSchema()
1024
+ }
1025
+ }));
1026
+ const body = {
1027
+ model: this.modelName,
1028
+ messages: wireMessages,
1029
+ max_completion_tokens: maxTokens,
1030
+ stream: true,
1031
+ stream_options: { include_usage: true }
1032
+ };
1033
+ if (wireTools !== undefined && wireTools.length > 0)
1034
+ body.tools = wireTools;
1035
+ this.applyThinking(body);
1036
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
1037
+ method: "POST",
1038
+ headers: {
1039
+ "content-type": "application/json",
1040
+ authorization: `Bearer ${this.apiKey}`,
1041
+ accept: "text/event-stream"
1042
+ },
1043
+ body: JSON.stringify(body),
1044
+ signal
1045
+ });
1046
+ if (!response.ok) {
1047
+ throw new Error(`Chat completions error ${response.status}: ${await response.text()}`);
1048
+ }
1049
+ let id = "";
1050
+ let stopReason = "end_turn";
1051
+ let inputTokens = 0;
1052
+ let outputTokens = 0;
1053
+ let textOpen = false;
1054
+ let textBuffer = "";
1055
+ let thinkingOpen = false;
1056
+ let thinkingBuffer = "";
1057
+ let sawFinishReason = false;
1058
+ const toolBuilders = new Map;
1059
+ const toolOrder = [];
1060
+ for await (const raw of readSse(response)) {
1061
+ const chunk = raw;
1062
+ if (chunk.id && !id)
1063
+ id = chunk.id;
1064
+ if (chunk.usage) {
1065
+ if (chunk.usage.prompt_tokens !== undefined) {
1066
+ inputTokens = chunk.usage.prompt_tokens;
1067
+ }
1068
+ if (chunk.usage.completion_tokens !== undefined) {
1069
+ outputTokens = chunk.usage.completion_tokens;
1070
+ }
1071
+ }
1072
+ const choice = chunk.choices?.[0];
1073
+ if (!choice)
1074
+ continue;
1075
+ const delta = choice.delta;
1076
+ if (delta) {
1077
+ const reasoning = delta.reasoning_content;
1078
+ if (typeof reasoning === "string" && reasoning.length > 0) {
1079
+ if (!thinkingOpen)
1080
+ thinkingOpen = true;
1081
+ thinkingBuffer += reasoning;
1082
+ yield { type: "thinking-delta", text: reasoning };
1083
+ }
1084
+ const content = delta.content;
1085
+ if (typeof content === "string" && content.length > 0) {
1086
+ if (thinkingOpen) {
1087
+ yield { type: "thinking-end", text: thinkingBuffer };
1088
+ thinkingOpen = false;
1089
+ thinkingBuffer = "";
1090
+ }
1091
+ if (!textOpen)
1092
+ textOpen = true;
1093
+ textBuffer += content;
1094
+ yield { type: "text-delta", text: content };
1095
+ }
1096
+ if (delta.tool_calls) {
1097
+ for (const tc of delta.tool_calls) {
1098
+ const idx = tc.index;
1099
+ let builder = toolBuilders.get(idx);
1100
+ if (!builder) {
1101
+ builder = {
1102
+ id: tc.id ?? "",
1103
+ name: tc.function?.name ?? "",
1104
+ argsBuffer: ""
1105
+ };
1106
+ toolBuilders.set(idx, builder);
1107
+ toolOrder.push(idx);
1108
+ }
1109
+ if (tc.id)
1110
+ builder.id = tc.id;
1111
+ if (tc.function?.name)
1112
+ builder.name = tc.function.name;
1113
+ if (tc.function?.arguments) {
1114
+ builder.argsBuffer += tc.function.arguments;
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+ if (choice.finish_reason) {
1120
+ stopReason = mapFinishReason(choice.finish_reason);
1121
+ sawFinishReason = true;
1122
+ }
1123
+ }
1124
+ if (!sawFinishReason) {
1125
+ throw new Error("Chat completions stream ended without finish_reason; response is truncated.");
1126
+ }
1127
+ if (textOpen) {
1128
+ yield { type: "text-end", text: textBuffer };
1129
+ }
1130
+ if (thinkingOpen) {
1131
+ yield { type: "thinking-end", text: thinkingBuffer };
1132
+ }
1133
+ for (const idx of toolOrder) {
1134
+ const builder = toolBuilders.get(idx);
1135
+ if (!builder)
1136
+ continue;
1137
+ yield {
1138
+ type: "tool-call",
1139
+ toolCallId: builder.id,
1140
+ toolName: builder.name,
1141
+ input: parseToolArguments(builder.argsBuffer)
1142
+ };
1143
+ }
1144
+ yield {
1145
+ type: "message-end",
1146
+ id,
1147
+ stopReason,
1148
+ usage: { inputTokens, outputTokens }
1149
+ };
1150
+ }
1151
+ serializeMessages(messages) {
1152
+ const wire = [];
1153
+ for (const msg of messages) {
1154
+ if (msg.role === "system") {
1155
+ wire.push({ role: "system", content: msg.content });
1156
+ continue;
1157
+ }
1158
+ if (msg.role === "user") {
1159
+ const content = typeof msg.content === "string" ? msg.content : msg.content.map((part) => part.text).join(`
1160
+ `);
1161
+ wire.push({ role: "user", content });
1162
+ continue;
1163
+ }
1164
+ if (msg.role === "assistant") {
1165
+ const texts = [];
1166
+ const toolCalls = [];
1167
+ for (const part of msg.content) {
1168
+ if (part.type === "text") {
1169
+ texts.push(part.text);
1170
+ } else if (part.type === "tool-call") {
1171
+ toolCalls.push({
1172
+ id: part.toolCallId,
1173
+ type: "function",
1174
+ function: {
1175
+ name: part.toolName,
1176
+ arguments: JSON.stringify(part.input ?? {})
1177
+ }
1178
+ });
1179
+ }
1180
+ }
1181
+ const assistantMsg = {
1182
+ role: "assistant",
1183
+ content: texts.length > 0 ? texts.join(`
1184
+ `) : null,
1185
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
1186
+ };
1187
+ wire.push(this.adaptAssistantWire(assistantMsg));
1188
+ continue;
1189
+ }
1190
+ for (const part of msg.content) {
1191
+ wire.push({
1192
+ role: "tool",
1193
+ tool_call_id: part.toolCallId,
1194
+ content: serializeToolOutput2(part.output)
1195
+ });
1196
+ }
1197
+ }
1198
+ return wire;
1199
+ }
1200
+ adaptAssistantWire(msg) {
1201
+ return msg;
1202
+ }
1203
+ applyThinking(body) {}
1204
+ }
1205
+
1206
+ // src/models/google.ts
1207
+ var GEMINI_MODELS = [
1208
+ "gemini-3.5-flash",
1209
+ "gemini-3.1-pro",
1210
+ "gemini-3.1-flash-lite",
1211
+ "gemini-3-flash"
1212
+ ];
1213
+
1214
+ class GeminiModel extends OpenAICompatibleModel {
1215
+ constructor(modelName, options) {
1216
+ const key = options?.apiKey ?? env.geminiApiKey;
1217
+ if (!key) {
1218
+ throw new Error("No Gemini API key found. Set GEMINI_API_KEY or pass apiKey.");
1219
+ }
1220
+ super(modelName, {
1221
+ apiKey: key,
1222
+ baseUrl: options?.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta/openai",
1223
+ thinking: options?.thinking
1224
+ });
1225
+ }
1226
+ applyThinking(body) {
1227
+ if (this.thinking === "off")
1228
+ return;
1229
+ const effort = this.thinking === "minimal" || this.thinking === "low" ? "low" : this.thinking === "medium" ? "medium" : "high";
1230
+ body.reasoning_effort = effort;
1231
+ }
1232
+ async searchWeb(query, opts) {
1233
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.modelName}:generateContent`;
1234
+ const response = await fetch(url, {
1235
+ method: "POST",
1236
+ headers: {
1237
+ "content-type": "application/json",
1238
+ "x-goog-api-key": this.apiKey
1239
+ },
1240
+ body: JSON.stringify({
1241
+ contents: [{ role: "user", parts: [{ text: query }] }],
1242
+ tools: [{ google_search: {} }]
1243
+ })
1244
+ });
1245
+ if (!response.ok) {
1246
+ throw new Error(`Gemini web_search failed (${response.status}): ${await response.text()}`);
1247
+ }
1248
+ const json = await response.json();
1249
+ const chunks = json.candidates?.[0]?.groundingMetadata?.groundingChunks;
1250
+ if (!Array.isArray(chunks))
1251
+ return [];
1252
+ const hits = [];
1253
+ const seen = new Set;
1254
+ for (const chunk of chunks) {
1255
+ const uri = chunk.web?.uri;
1256
+ if (!uri || seen.has(uri))
1257
+ continue;
1258
+ seen.add(uri);
1259
+ hits.push({ title: chunk.web?.title ?? uri, url: uri });
1260
+ }
1261
+ if (opts?.maxUses !== undefined)
1262
+ return hits.slice(0, opts.maxUses);
1263
+ return hits;
1264
+ }
1265
+ }
1266
+
1267
+ // src/models/moonshot.ts
1268
+ var MOONSHOT_MODELS = ["kimi-k2.6", "kimi-k2.5"];
1269
+
1270
+ class MoonshotModel extends OpenAICompatibleModel {
1271
+ constructor(modelName, options) {
1272
+ const key = options?.apiKey ?? env.moonshotApiKey;
1273
+ if (!key) {
1274
+ throw new Error("No Moonshot API key found. Set MOONSHOT_API_KEY or pass apiKey.");
1275
+ }
1276
+ super(modelName, {
1277
+ apiKey: key,
1278
+ baseUrl: options?.baseUrl ?? "https://api.moonshot.ai/v1",
1279
+ thinking: options?.thinking
1280
+ });
1281
+ }
1282
+ adaptAssistantWire(msg) {
1283
+ if (!msg.tool_calls)
1284
+ return msg;
1285
+ return { ...msg, reasoning_content: "" };
1286
+ }
1287
+ applyThinking(body) {
1288
+ if (this.thinking === "off")
1289
+ return;
1290
+ if (this.modelName.includes("k2.6")) {
1291
+ body.chat_template_kwargs = { thinking: { type: "enabled" } };
1292
+ return;
1293
+ }
1294
+ body.enable_thinking = true;
1295
+ }
1296
+ async searchWeb(query, opts) {
1297
+ const messages = [
1298
+ {
1299
+ role: "user",
1300
+ content: `Search the web for: ${query}. List the most relevant results with their URLs.`
1301
+ }
1302
+ ];
1303
+ const tools = [
1304
+ {
1305
+ type: "builtin_function",
1306
+ function: { name: "$web_search" }
1307
+ }
1308
+ ];
1309
+ let response = await this.chat(messages, tools);
1310
+ let choice = response.choices[0];
1311
+ for (let i = 0;i < 4; i++) {
1312
+ if (!choice)
1313
+ break;
1314
+ const toolCalls = choice.message.tool_calls;
1315
+ if (!toolCalls || toolCalls.length === 0)
1316
+ break;
1317
+ messages.push({
1318
+ role: "assistant",
1319
+ content: choice.message.content ?? "",
1320
+ reasoning_content: choice.message.reasoning_content ?? "",
1321
+ tool_calls: toolCalls
1322
+ });
1323
+ for (const call of toolCalls) {
1324
+ messages.push({
1325
+ role: "tool",
1326
+ tool_call_id: call.id,
1327
+ name: call.function.name,
1328
+ content: call.function.arguments
1329
+ });
1330
+ }
1331
+ response = await this.chat(messages, tools);
1332
+ choice = response.choices[0];
1333
+ }
1334
+ const text = choice?.message.content ?? "";
1335
+ const hits = [];
1336
+ const seen = new Set;
1337
+ const urlRegex = /https?:\/\/[^\s)\]>"']+/g;
1338
+ for (const raw of text.match(urlRegex) ?? []) {
1339
+ const url = raw.replace(/[.,;:!?]+$/, "");
1340
+ if (seen.has(url))
1341
+ continue;
1342
+ seen.add(url);
1343
+ hits.push({ title: url, url });
1344
+ }
1345
+ if (opts?.maxUses !== undefined)
1346
+ return hits.slice(0, opts.maxUses);
1347
+ return hits;
1348
+ }
1349
+ async chat(messages, tools) {
1350
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
1351
+ method: "POST",
1352
+ headers: {
1353
+ "content-type": "application/json",
1354
+ authorization: `Bearer ${this.apiKey}`
1355
+ },
1356
+ body: JSON.stringify({
1357
+ model: this.modelName,
1358
+ messages,
1359
+ tools
1360
+ })
1361
+ });
1362
+ if (!response.ok) {
1363
+ throw new Error(`Moonshot web_search failed (${response.status}): ${await response.text()}`);
1364
+ }
1365
+ return await response.json();
1366
+ }
1367
+ }
1368
+
1369
+ // src/models/openai-codex-auth.ts
1370
+ import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
1371
+ import { homedir } from "os";
1372
+ import { dirname, join, resolve } from "path";
1373
+ var CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1374
+ var CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
1375
+ var CHATGPT_AUTH_CLAIM = "https://api.openai.com/auth";
1376
+ var DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
1377
+ function expandPath(path) {
1378
+ if (path === "~")
1379
+ return homedir();
1380
+ if (path.startsWith("~/"))
1381
+ return join(homedir(), path.slice(2));
1382
+ return path;
1383
+ }
1384
+ function defaultAuthFiles() {
1385
+ const files = [];
1386
+ const explicit = env.openaiCodexAuthFile;
1387
+ if (explicit)
1388
+ files.push(expandPath(explicit));
1389
+ const codexHome = env.codexHome;
1390
+ if (codexHome)
1391
+ files.push(join(expandPath(codexHome), "auth.json"));
1392
+ files.push(join(homedir(), ".codex", "auth.json"));
1393
+ files.push(join(homedir(), ".roboport", "openai-codex-auth.json"));
1394
+ return files;
1395
+ }
1396
+ function parseJwtPayload(token) {
1397
+ const [, payload] = token.split(".");
1398
+ if (!payload)
1399
+ return;
1400
+ try {
1401
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
1402
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
1403
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
1404
+ } catch {
1405
+ return;
1406
+ }
1407
+ }
1408
+ function jwtExpiresSoon(token) {
1409
+ const payload = parseJwtPayload(token);
1410
+ const exp = typeof payload?.exp === "number" ? payload.exp : undefined;
1411
+ if (exp === undefined)
1412
+ return false;
1413
+ return exp * 1000 <= Date.now() + 60000;
1414
+ }
1415
+ function extractRawIdToken(tokens) {
1416
+ if (typeof tokens.id_token === "string")
1417
+ return tokens.id_token;
1418
+ return tokens.id_token?.raw_jwt;
1419
+ }
1420
+ function extractAccountId(tokens) {
1421
+ if (tokens.account_id)
1422
+ return tokens.account_id;
1423
+ if (!tokens.access_token)
1424
+ return;
1425
+ const payload = parseJwtPayload(tokens.access_token);
1426
+ const auth = payload?.[CHATGPT_AUTH_CLAIM];
1427
+ if (typeof auth !== "object" || auth === null)
1428
+ return;
1429
+ const accountId = auth.chatgpt_account_id;
1430
+ return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
1431
+ }
1432
+ function extractFedrampFlag(tokens) {
1433
+ const token = tokens.access_token ?? extractRawIdToken(tokens);
1434
+ if (!token)
1435
+ return false;
1436
+ const payload = parseJwtPayload(token);
1437
+ const auth = payload?.[CHATGPT_AUTH_CLAIM];
1438
+ if (typeof auth !== "object" || auth === null)
1439
+ return false;
1440
+ return auth.chatgpt_account_is_fedramp === true;
1441
+ }
1442
+ async function readJson(path) {
1443
+ const raw = await readFile(path, "utf8");
1444
+ return JSON.parse(raw);
1445
+ }
1446
+ async function writeJson(path, data) {
1447
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
1448
+ await writeFile(path, `${JSON.stringify(data, null, 2)}
1449
+ `, "utf8");
1450
+ await chmod(path, 384);
1451
+ }
1452
+ function sleep(ms) {
1453
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1454
+ }
1455
+ async function withAuthFileLock(authFile, fn) {
1456
+ const lockDir = `${authFile}.lock`;
1457
+ const deadline = Date.now() + 1e4;
1458
+ while (true) {
1459
+ try {
1460
+ await mkdir(lockDir, { mode: 448 });
1461
+ break;
1462
+ } catch (error) {
1463
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
1464
+ if (code !== "EEXIST" || Date.now() >= deadline)
1465
+ throw error;
1466
+ await sleep(100);
1467
+ }
1468
+ }
1469
+ try {
1470
+ return await fn();
1471
+ } finally {
1472
+ await rm(lockDir, { recursive: true, force: true });
1473
+ }
1474
+ }
1475
+ async function findAuthFile(authFile) {
1476
+ if (authFile)
1477
+ return resolve(expandPath(authFile));
1478
+ for (const candidate of defaultAuthFiles()) {
1479
+ try {
1480
+ const data = await readJson(candidate);
1481
+ if (data.tokens?.access_token && data.tokens.refresh_token) {
1482
+ return candidate;
1483
+ }
1484
+ } catch {}
1485
+ }
1486
+ throw new Error('No OpenAI Codex auth found. Run `codex login` locally or pass auth: { type: "codex", authFile }.');
1487
+ }
1488
+ async function refreshCodexTokens(refreshToken) {
1489
+ const response = await fetch(CODEX_TOKEN_URL, {
1490
+ method: "POST",
1491
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1492
+ body: new URLSearchParams({
1493
+ grant_type: "refresh_token",
1494
+ refresh_token: refreshToken,
1495
+ client_id: CODEX_CLIENT_ID
1496
+ })
1497
+ });
1498
+ if (!response.ok) {
1499
+ throw new Error(`OpenAI Codex token refresh failed (${response.status}): ${await response.text()}`);
1500
+ }
1501
+ const json = await response.json();
1502
+ if (!json.access_token || !json.refresh_token) {
1503
+ throw new Error("OpenAI Codex token refresh response was missing tokens.");
1504
+ }
1505
+ return {
1506
+ id_token: json.id_token,
1507
+ access_token: json.access_token,
1508
+ refresh_token: json.refresh_token,
1509
+ account_id: extractAccountId({ access_token: json.access_token })
1510
+ };
1511
+ }
1512
+
1513
+ class OpenAICodexAuth {
1514
+ authFile;
1515
+ baseUrl;
1516
+ constructor(options) {
1517
+ this.authFile = options.authFile;
1518
+ this.baseUrl = (options.baseUrl ?? DEFAULT_CODEX_BASE_URL).replace(/\/+$/, "");
1519
+ }
1520
+ async getHeaders() {
1521
+ const authFile = await findAuthFile(this.authFile);
1522
+ this.authFile = authFile;
1523
+ let auth = await readJson(authFile);
1524
+ let tokens = auth.tokens;
1525
+ if (!tokens?.access_token || !tokens.refresh_token) {
1526
+ throw new Error(`OpenAI Codex auth file at ${authFile} does not contain ChatGPT OAuth tokens.`);
1527
+ }
1528
+ if (jwtExpiresSoon(tokens.access_token)) {
1529
+ auth = await withAuthFileLock(authFile, async () => {
1530
+ const latest = await readJson(authFile);
1531
+ const latestTokens = latest.tokens;
1532
+ if (!latestTokens?.access_token || !latestTokens.refresh_token) {
1533
+ throw new Error(`OpenAI Codex auth file at ${authFile} does not contain ChatGPT OAuth tokens.`);
1534
+ }
1535
+ if (!jwtExpiresSoon(latestTokens.access_token))
1536
+ return latest;
1537
+ const refreshedTokens = await refreshCodexTokens(latestTokens.refresh_token);
1538
+ latest.tokens = refreshedTokens;
1539
+ latest.auth_mode = "chatgpt";
1540
+ latest.last_refresh = new Date().toISOString();
1541
+ await writeJson(authFile, latest);
1542
+ return latest;
1543
+ });
1544
+ tokens = auth.tokens;
1545
+ if (!tokens?.access_token || !tokens.refresh_token) {
1546
+ throw new Error(`OpenAI Codex auth file at ${authFile} does not contain ChatGPT OAuth tokens after refresh.`);
1547
+ }
1548
+ }
1549
+ const accountId = extractAccountId(tokens);
1550
+ if (!accountId) {
1551
+ throw new Error("OpenAI Codex auth token does not contain account id.");
1552
+ }
1553
+ return {
1554
+ authorization: `Bearer ${tokens.access_token}`,
1555
+ accountId,
1556
+ isFedrampAccount: extractFedrampFlag(tokens)
1557
+ };
1558
+ }
1559
+ }
1560
+
1561
+ // src/models/openai.ts
1562
+ var OPENAI_MODELS = [
1563
+ "gpt-5.5",
1564
+ "gpt-5.4",
1565
+ "gpt-5.4-mini",
1566
+ "gpt-5.4-nano",
1567
+ "gpt-5.3-codex"
1568
+ ];
1569
+ function serializeToolOutput3(output) {
1570
+ if (output === undefined || output === null)
1571
+ return "";
1572
+ if (typeof output === "string")
1573
+ return output;
1574
+ return JSON.stringify(output);
1575
+ }
1576
+ function parseToolArguments2(raw) {
1577
+ if (!raw)
1578
+ return {};
1579
+ try {
1580
+ return JSON.parse(raw);
1581
+ } catch {
1582
+ return raw;
1583
+ }
1584
+ }
1585
+ function sanitizeResponsesId(id) {
1586
+ const normalized = id.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+$/, "");
1587
+ return normalized.slice(0, 64) || "item";
1588
+ }
1589
+ function responsesMessageInput(messages) {
1590
+ const instructions = [];
1591
+ const input = [];
1592
+ let messageIndex = 0;
1593
+ for (const msg of messages) {
1594
+ if (msg.role === "system") {
1595
+ instructions.push(msg.content);
1596
+ continue;
1597
+ }
1598
+ if (msg.role === "user") {
1599
+ const text = typeof msg.content === "string" ? msg.content : msg.content.map((part) => part.text).join(`
1600
+ `);
1601
+ input.push({ role: "user", content: [{ type: "input_text", text }] });
1602
+ continue;
1603
+ }
1604
+ if (msg.role === "assistant") {
1605
+ for (const part of msg.content) {
1606
+ if (part.type === "text") {
1607
+ input.push({
1608
+ type: "message",
1609
+ role: "assistant",
1610
+ content: [
1611
+ { type: "output_text", text: part.text, annotations: [] }
1612
+ ],
1613
+ status: "completed",
1614
+ id: `msg_${messageIndex}`
1615
+ });
1616
+ } else if (part.type === "tool-call") {
1617
+ input.push({
1618
+ type: "function_call",
1619
+ id: `fc_${sanitizeResponsesId(part.toolCallId)}`,
1620
+ call_id: part.toolCallId,
1621
+ name: part.toolName,
1622
+ arguments: JSON.stringify(part.input ?? {})
1623
+ });
1624
+ }
1625
+ }
1626
+ messageIndex++;
1627
+ continue;
1628
+ }
1629
+ for (const part of msg.content) {
1630
+ input.push({
1631
+ type: "function_call_output",
1632
+ call_id: part.toolCallId,
1633
+ output: serializeToolOutput3(part.output)
1634
+ });
1635
+ }
1636
+ }
1637
+ return {
1638
+ instructions: instructions.length > 0 ? instructions.join(`
1639
+
1640
+ `) : undefined,
1641
+ input
1642
+ };
1643
+ }
1644
+ function responsesTools(tools) {
1645
+ if (!tools || tools.length === 0)
1646
+ return;
1647
+ return tools.map((tool) => ({
1648
+ type: "function",
1649
+ name: tool.name,
1650
+ description: tool.description,
1651
+ parameters: tool.toJsonSchema()
1652
+ }));
1653
+ }
1654
+ function mapResponsesStatus(status) {
1655
+ switch (status) {
1656
+ case "incomplete":
1657
+ return "max_tokens";
1658
+ case "failed":
1659
+ case "cancelled":
1660
+ return "refusal";
1661
+ default:
1662
+ return "end_turn";
1663
+ }
1664
+ }
1665
+
1666
+ class OpenAIModel extends OpenAICompatibleModel {
1667
+ codexAuth;
1668
+ constructor(modelName, options) {
1669
+ const auth = options?.auth ?? { type: "apiKey" };
1670
+ const key = auth.type === "apiKey" ? auth.apiKey ?? env.openaiApiKey ?? "" : "";
1671
+ if (auth.type === "apiKey" && !key) {
1672
+ throw new Error('No OpenAI API key found. Set OPENAI_API_KEY or pass auth: { type: "apiKey", apiKey }.');
1673
+ }
1674
+ super(modelName, {
1675
+ apiKey: key,
1676
+ baseUrl: options?.baseUrl ?? "https://api.openai.com/v1",
1677
+ thinking: options?.thinking
1678
+ });
1679
+ if (auth.type === "codex") {
1680
+ this.codexAuth = new OpenAICodexAuth({
1681
+ type: "codex",
1682
+ authFile: auth.authFile,
1683
+ baseUrl: options?.baseUrl
1684
+ });
1685
+ this.baseUrl = this.codexAuth.baseUrl;
1686
+ }
1687
+ }
1688
+ async* streamMessage(params) {
1689
+ if (this.codexAuth || isResponsesOnlyModel(this.modelName)) {
1690
+ yield* this.streamResponses(params);
1691
+ return;
1692
+ }
1693
+ yield* super.streamMessage(params);
1694
+ }
1695
+ applyThinking(body) {
1696
+ if (this.thinking === "off")
1697
+ return;
1698
+ body.reasoning_effort = this.thinking === "xhigh" ? "high" : this.thinking;
1699
+ }
1700
+ async searchWeb(query, opts) {
1701
+ if (this.codexAuth) {
1702
+ const json2 = await this.fetchResponsesBuffered({
1703
+ model: this.modelName,
1704
+ stream: true,
1705
+ store: false,
1706
+ tools: [{ type: "web_search" }],
1707
+ input: [
1708
+ {
1709
+ role: "user",
1710
+ content: [
1711
+ { type: "input_text", text: `Search the web for: ${query}` }
1712
+ ]
1713
+ }
1714
+ ],
1715
+ instructions: "You are a helpful assistant."
1716
+ });
1717
+ const hits2 = extractSearchHits(json2);
1718
+ if (hits2.length > 0) {
1719
+ return opts?.maxUses !== undefined ? hits2.slice(0, opts.maxUses) : hits2;
1720
+ }
1721
+ const answer = extractAnswerText(json2);
1722
+ if (!answer)
1723
+ return [];
1724
+ const result = [
1725
+ { title: "Web search answer", text: answer }
1726
+ ];
1727
+ return opts?.maxUses !== undefined ? result.slice(0, opts.maxUses) : result;
1728
+ }
1729
+ const response = await fetch(`${this.baseUrl}/responses`, {
1730
+ method: "POST",
1731
+ headers: {
1732
+ "content-type": "application/json",
1733
+ authorization: `Bearer ${this.apiKey}`
1734
+ },
1735
+ body: JSON.stringify({
1736
+ model: this.modelName,
1737
+ tools: [{ type: "web_search_preview" }],
1738
+ input: `Search the web for: ${query}`
1739
+ })
1740
+ });
1741
+ if (!response.ok) {
1742
+ throw new Error(`OpenAI web_search failed (${response.status}): ${await response.text()}`);
1743
+ }
1744
+ const json = await response.json();
1745
+ const hits = [];
1746
+ const seen = new Set;
1747
+ for (const item of json.output) {
1748
+ if (item.type !== "message")
1749
+ continue;
1750
+ const parts = item.content;
1751
+ if (!Array.isArray(parts))
1752
+ continue;
1753
+ for (const part of parts) {
1754
+ const annotations = part.annotations;
1755
+ if (!Array.isArray(annotations))
1756
+ continue;
1757
+ for (const ann of annotations) {
1758
+ if (ann.type !== "url_citation")
1759
+ continue;
1760
+ if (!ann.url || seen.has(ann.url))
1761
+ continue;
1762
+ seen.add(ann.url);
1763
+ hits.push({ title: ann.title ?? ann.url, url: ann.url });
1764
+ }
1765
+ }
1766
+ }
1767
+ if (opts?.maxUses !== undefined)
1768
+ return hits.slice(0, opts.maxUses);
1769
+ return hits;
1770
+ }
1771
+ async* streamResponses(params) {
1772
+ if (this.codexAuth && params.maxTokens !== undefined) {
1773
+ throw new Error("OpenAI Codex auth does not support maxTokens.");
1774
+ }
1775
+ const { messages, tools, maxTokens, signal } = params;
1776
+ const { instructions, input } = responsesMessageInput(messages);
1777
+ const wireTools = responsesTools(tools);
1778
+ const body = {
1779
+ model: this.modelName,
1780
+ input,
1781
+ store: false,
1782
+ stream: true
1783
+ };
1784
+ body.instructions = instructions ?? "You are a helpful assistant.";
1785
+ if (wireTools)
1786
+ body.tools = wireTools;
1787
+ if (!this.codexAuth && maxTokens !== undefined) {
1788
+ body.max_output_tokens = maxTokens;
1789
+ }
1790
+ if (this.thinking !== "off") {
1791
+ body.reasoning = { effort: this.thinking };
1792
+ }
1793
+ const { url, headers } = await this.responsesEndpoint();
1794
+ const response = await fetch(url, {
1795
+ method: "POST",
1796
+ headers: { ...headers, accept: "text/event-stream" },
1797
+ body: JSON.stringify(body),
1798
+ signal
1799
+ });
1800
+ if (!response.ok) {
1801
+ throw new Error(`OpenAI Responses error ${response.status}: ${await response.text()}`);
1802
+ }
1803
+ let id = "";
1804
+ let stopReason = "end_turn";
1805
+ let inputTokens = 0;
1806
+ let outputTokens = 0;
1807
+ let sawToolCall = false;
1808
+ let sawCompleted = false;
1809
+ let textOpen = false;
1810
+ let thinkingOpen = false;
1811
+ const toolByItemId = new Map;
1812
+ const itemOrder = [];
1813
+ for await (const raw of readSse(response)) {
1814
+ const event = raw;
1815
+ if (event.type === "response.created") {
1816
+ if (event.response.id)
1817
+ id = event.response.id;
1818
+ continue;
1819
+ }
1820
+ if (event.type === "response.output_item.added") {
1821
+ const item = event.item;
1822
+ if (item.type === "function_call" && item.id) {
1823
+ toolByItemId.set(item.id, {
1824
+ callId: item.call_id ?? item.id,
1825
+ name: item.name ?? "",
1826
+ argsBuffer: ""
1827
+ });
1828
+ itemOrder.push(item.id);
1829
+ }
1830
+ continue;
1831
+ }
1832
+ if (event.type === "response.output_text.delta") {
1833
+ if (!textOpen)
1834
+ textOpen = true;
1835
+ yield { type: "text-delta", text: event.delta };
1836
+ continue;
1837
+ }
1838
+ if (event.type === "response.output_text.done") {
1839
+ if (textOpen) {
1840
+ yield { type: "text-end", text: event.text };
1841
+ textOpen = false;
1842
+ }
1843
+ continue;
1844
+ }
1845
+ if (event.type === "response.reasoning_summary_text.delta") {
1846
+ if (!thinkingOpen)
1847
+ thinkingOpen = true;
1848
+ yield { type: "thinking-delta", text: event.delta };
1849
+ continue;
1850
+ }
1851
+ if (event.type === "response.reasoning_summary_text.done") {
1852
+ if (thinkingOpen) {
1853
+ yield { type: "thinking-end", text: event.text };
1854
+ thinkingOpen = false;
1855
+ }
1856
+ continue;
1857
+ }
1858
+ if (event.type === "response.function_call_arguments.delta") {
1859
+ if (event.item_id) {
1860
+ const builder = toolByItemId.get(event.item_id);
1861
+ if (builder)
1862
+ builder.argsBuffer += event.delta;
1863
+ }
1864
+ continue;
1865
+ }
1866
+ if (event.type === "response.function_call_arguments.done") {
1867
+ if (event.item_id) {
1868
+ const builder = toolByItemId.get(event.item_id);
1869
+ if (builder)
1870
+ builder.argsBuffer = event.arguments;
1871
+ }
1872
+ continue;
1873
+ }
1874
+ if (event.type === "response.output_item.done") {
1875
+ const item = event.item;
1876
+ if (item.type === "function_call") {
1877
+ sawToolCall = true;
1878
+ const callId = item.call_id ?? "";
1879
+ const matched = [...toolByItemId.values()].find((b) => b.callId === callId);
1880
+ const args = matched?.argsBuffer ?? item.arguments ?? "";
1881
+ yield {
1882
+ type: "tool-call",
1883
+ toolCallId: callId,
1884
+ toolName: item.name,
1885
+ input: parseToolArguments2(args)
1886
+ };
1887
+ }
1888
+ continue;
1889
+ }
1890
+ if (event.type === "response.completed") {
1891
+ sawCompleted = true;
1892
+ if (!id && event.response.id)
1893
+ id = event.response.id;
1894
+ stopReason = sawToolCall ? "tool_use" : mapResponsesStatus(event.response.status);
1895
+ inputTokens = event.response.usage?.input_tokens ?? inputTokens;
1896
+ outputTokens = event.response.usage?.output_tokens ?? outputTokens;
1897
+ continue;
1898
+ }
1899
+ if (event.type === "response.failed") {
1900
+ throw new Error(event.response?.error?.message ?? "OpenAI Responses stream failed.");
1901
+ }
1902
+ }
1903
+ if (!sawCompleted) {
1904
+ throw new Error("OpenAI Responses stream ended before response.completed; response is truncated.");
1905
+ }
1906
+ yield {
1907
+ type: "message-end",
1908
+ id,
1909
+ stopReason,
1910
+ usage: { inputTokens, outputTokens }
1911
+ };
1912
+ }
1913
+ async fetchResponsesBuffered(body) {
1914
+ const { url, headers } = await this.responsesEndpoint();
1915
+ const response = await fetch(url, {
1916
+ method: "POST",
1917
+ headers,
1918
+ body: JSON.stringify(body)
1919
+ });
1920
+ if (!response.ok) {
1921
+ throw new Error(`OpenAI Responses error ${response.status}: ${await response.text()}`);
1922
+ }
1923
+ if (body.stream === true)
1924
+ return parseResponsesStream(await response.text());
1925
+ return await response.json();
1926
+ }
1927
+ async responsesEndpoint() {
1928
+ if (this.codexAuth) {
1929
+ const auth = await this.codexAuth.getHeaders();
1930
+ const headers = {
1931
+ authorization: auth.authorization,
1932
+ "chatgpt-account-id": auth.accountId,
1933
+ originator: "roboport",
1934
+ "openai-beta": "responses=experimental",
1935
+ accept: "application/json",
1936
+ "content-type": "application/json"
1937
+ };
1938
+ if (auth.isFedrampAccount)
1939
+ headers["x-openai-fedramp"] = "true";
1940
+ return { url: `${this.codexAuth.baseUrl}/responses`, headers };
1941
+ }
1942
+ return {
1943
+ url: `${this.baseUrl}/responses`,
1944
+ headers: {
1945
+ authorization: `Bearer ${this.apiKey}`,
1946
+ accept: "application/json",
1947
+ "content-type": "application/json"
1948
+ }
1949
+ };
1950
+ }
1951
+ }
1952
+ function isResponsesOnlyModel(modelName) {
1953
+ return modelName.includes("codex");
1954
+ }
1955
+ function parseResponsesStream(raw) {
1956
+ const output = [];
1957
+ let completed;
1958
+ for (const event of parseSseEvents(raw)) {
1959
+ if (event.type === "response.output_item.done") {
1960
+ output.push(event.item);
1961
+ continue;
1962
+ }
1963
+ if (event.type === "response.completed") {
1964
+ completed = event.response;
1965
+ continue;
1966
+ }
1967
+ if (event.type === "response.failed") {
1968
+ throw new Error(event.response?.error?.message ?? "OpenAI Codex response failed.");
1969
+ }
1970
+ }
1971
+ if (!completed) {
1972
+ throw new Error("OpenAI Codex stream ended before response.completed.");
1973
+ }
1974
+ return {
1975
+ ...completed,
1976
+ output: completed.output && completed.output.length > 0 ? completed.output : output
1977
+ };
1978
+ }
1979
+ function parseSseEvents(raw) {
1980
+ const events = [];
1981
+ for (const block of raw.split(/\r?\n\r?\n/)) {
1982
+ const dataLines = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).trimStart());
1983
+ if (dataLines.length === 0)
1984
+ continue;
1985
+ const data = dataLines.join(`
1986
+ `);
1987
+ if (data === "[DONE]")
1988
+ continue;
1989
+ try {
1990
+ events.push(JSON.parse(data));
1991
+ } catch {}
1992
+ }
1993
+ return events;
1994
+ }
1995
+ function extractAnswerText(json) {
1996
+ const parts = [];
1997
+ for (const item of json.output ?? []) {
1998
+ if (item.type !== "message")
1999
+ continue;
2000
+ for (const part of item.content ?? []) {
2001
+ if (typeof part.text === "string")
2002
+ parts.push(part.text);
2003
+ }
2004
+ }
2005
+ return parts.join("").trim();
2006
+ }
2007
+ function extractSearchHits(json) {
2008
+ const hits = [];
2009
+ const seen = new Set;
2010
+ for (const item of json.output ?? []) {
2011
+ if (item.type !== "message")
2012
+ continue;
2013
+ for (const part of item.content ?? []) {
2014
+ const annotations = part.annotations;
2015
+ if (!Array.isArray(annotations))
2016
+ continue;
2017
+ for (const ann of annotations) {
2018
+ if (ann.type !== "url_citation")
2019
+ continue;
2020
+ if (!ann.url || seen.has(ann.url))
2021
+ continue;
2022
+ seen.add(ann.url);
2023
+ hits.push({ title: ann.title ?? ann.url, url: ann.url });
2024
+ }
2025
+ }
2026
+ }
2027
+ return hits;
2028
+ }
2029
+ export {
2030
+ OpenAIModel,
2031
+ OpenAICompatibleModel,
2032
+ OPENAI_MODELS,
2033
+ MoonshotModel,
2034
+ MOONSHOT_MODELS,
2035
+ GeminiModel,
2036
+ GEMINI_MODELS,
2037
+ AnthropicModel,
2038
+ ANTHROPIC_MODELS
2039
+ };