@dexto/core 1.6.12 → 1.6.13

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 (36) hide show
  1. package/dist/events/index.cjs +1 -0
  2. package/dist/events/index.d.ts +16 -2
  3. package/dist/events/index.d.ts.map +1 -1
  4. package/dist/events/index.js +1 -0
  5. package/dist/llm/executor/turn-executor.cjs +11 -1
  6. package/dist/llm/executor/turn-executor.d.ts +3 -1
  7. package/dist/llm/executor/turn-executor.d.ts.map +1 -1
  8. package/dist/llm/executor/turn-executor.js +11 -1
  9. package/dist/llm/index.cjs +16 -0
  10. package/dist/llm/index.d.ts +2 -0
  11. package/dist/llm/index.d.ts.map +1 -1
  12. package/dist/llm/index.js +18 -0
  13. package/dist/llm/providers/codex-app-server.cjs +1391 -0
  14. package/dist/llm/providers/codex-app-server.d.ts +150 -0
  15. package/dist/llm/providers/codex-app-server.d.ts.map +1 -0
  16. package/dist/llm/providers/codex-app-server.js +1367 -0
  17. package/dist/llm/providers/codex-base-url.cjs +97 -0
  18. package/dist/llm/providers/codex-base-url.d.ts +9 -0
  19. package/dist/llm/providers/codex-base-url.d.ts.map +1 -0
  20. package/dist/llm/providers/codex-base-url.js +70 -0
  21. package/dist/llm/services/factory.cjs +19 -1
  22. package/dist/llm/services/factory.d.ts +3 -0
  23. package/dist/llm/services/factory.d.ts.map +1 -1
  24. package/dist/llm/services/factory.js +21 -1
  25. package/dist/session/chat-session.cjs +17 -0
  26. package/dist/session/chat-session.d.ts +1 -0
  27. package/dist/session/chat-session.d.ts.map +1 -1
  28. package/dist/session/chat-session.js +17 -0
  29. package/dist/session/session-manager.cjs +27 -1
  30. package/dist/session/session-manager.d.ts +6 -0
  31. package/dist/session/session-manager.d.ts.map +1 -1
  32. package/dist/session/session-manager.js +27 -1
  33. package/dist/utils/result.cjs +3 -2
  34. package/dist/utils/result.d.ts.map +1 -1
  35. package/dist/utils/result.js +3 -2
  36. package/package.json +1 -1
@@ -0,0 +1,1367 @@
1
+ import "../../chunk-PTJYTZNU.js";
2
+ import { spawn } from "node:child_process";
3
+ import { createInterface } from "node:readline";
4
+ import { DextoRuntimeError } from "../../errors/DextoRuntimeError.js";
5
+ import { ErrorScope, ErrorType } from "../../errors/types.js";
6
+ import { safeStringify } from "../../utils/safe-stringify.js";
7
+ import { LLMErrorCode } from "../error-codes.js";
8
+ import { LLMError } from "../errors.js";
9
+ import { parseCodexBaseURL } from "./codex-base-url.js";
10
+ const CODEX_PROTOCOL_ERROR_CODE = "llm_codex_protocol_invalid";
11
+ const CODEX_CLIENT_RUNTIME_ERROR_CODE = "llm_codex_client_runtime";
12
+ function createCodexProtocolError(message, context) {
13
+ return new DextoRuntimeError(
14
+ CODEX_PROTOCOL_ERROR_CODE,
15
+ ErrorScope.LLM,
16
+ ErrorType.THIRD_PARTY,
17
+ message,
18
+ context
19
+ );
20
+ }
21
+ function createCodexClientRuntimeError(message, context, type = ErrorType.SYSTEM) {
22
+ return new DextoRuntimeError(
23
+ CODEX_CLIENT_RUNTIME_ERROR_CODE,
24
+ ErrorScope.LLM,
25
+ type,
26
+ message,
27
+ context
28
+ );
29
+ }
30
+ function createCodexClientExitedError(input) {
31
+ return createCodexClientRuntimeError(
32
+ `Codex app-server exited unexpectedly (${input.signal ?? `code ${input.code ?? "unknown"}`})`,
33
+ {
34
+ ...input.code !== void 0 ? { code: input.code } : {},
35
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
36
+ },
37
+ ErrorType.THIRD_PARTY
38
+ );
39
+ }
40
+ const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
41
+ const DEFAULT_CLIENT_INFO = {
42
+ name: "dexto",
43
+ title: "Dexto",
44
+ version: "1.0.0"
45
+ };
46
+ const CODEX_DEVELOPER_INSTRUCTIONS = [
47
+ "You are providing model responses for a host application.",
48
+ "Treat the provided input as the full conversation transcript.",
49
+ "Use the host-provided dynamic tools when tool use is needed.",
50
+ "Do not use Codex built-in tools, shell commands, file edits, approvals, MCP tools, or ask-user flows.",
51
+ "When you are not calling a tool, answer with the assistant response only."
52
+ ].join(" ");
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null;
55
+ }
56
+ function getString(value) {
57
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
58
+ }
59
+ function getBoolean(value) {
60
+ return typeof value === "boolean" ? value : null;
61
+ }
62
+ function getNumber(value) {
63
+ if (typeof value === "number" && Number.isFinite(value)) {
64
+ return value;
65
+ }
66
+ if (typeof value === "string") {
67
+ const parsed = Number(value);
68
+ return Number.isFinite(parsed) ? parsed : null;
69
+ }
70
+ return null;
71
+ }
72
+ function getArray(value) {
73
+ return Array.isArray(value) ? value : null;
74
+ }
75
+ function normalizeError(error) {
76
+ return error instanceof Error ? error : createCodexClientRuntimeError(String(error));
77
+ }
78
+ function parseCodexAccount(value) {
79
+ if (!isRecord(value)) {
80
+ return null;
81
+ }
82
+ const type = getString(value["type"]);
83
+ if (type === "apiKey") {
84
+ return { type: "apiKey" };
85
+ }
86
+ if (type !== "chatgpt") {
87
+ return null;
88
+ }
89
+ const email = getString(value["email"]);
90
+ const planType = getString(value["planType"]);
91
+ if (!email || !planType) {
92
+ return null;
93
+ }
94
+ return {
95
+ type: "chatgpt",
96
+ email,
97
+ planType
98
+ };
99
+ }
100
+ function parseReadAccountResponse(value) {
101
+ if (!isRecord(value)) {
102
+ throw createCodexProtocolError("Invalid account/read response from Codex", {
103
+ method: "account/read"
104
+ });
105
+ }
106
+ const requiresOpenaiAuth = getBoolean(value["requiresOpenaiAuth"]);
107
+ if (requiresOpenaiAuth === null) {
108
+ throw createCodexProtocolError(
109
+ "Codex account/read response is missing requiresOpenaiAuth",
110
+ {
111
+ method: "account/read"
112
+ }
113
+ );
114
+ }
115
+ return {
116
+ account: parseCodexAccount(value["account"]),
117
+ requiresOpenaiAuth
118
+ };
119
+ }
120
+ function parseLoginResponse(value) {
121
+ if (!isRecord(value)) {
122
+ throw createCodexProtocolError("Invalid account/login/start response from Codex", {
123
+ method: "account/login/start"
124
+ });
125
+ }
126
+ const type = getString(value["type"]);
127
+ if (!type) {
128
+ throw createCodexProtocolError("Codex login response is missing type", {
129
+ method: "account/login/start"
130
+ });
131
+ }
132
+ if (type === "apiKey") {
133
+ return { type };
134
+ }
135
+ if (type === "chatgptAuthTokens") {
136
+ return { type };
137
+ }
138
+ if (type !== "chatgpt") {
139
+ throw createCodexProtocolError(`Unsupported Codex login response type: ${type}`, {
140
+ method: "account/login/start",
141
+ type
142
+ });
143
+ }
144
+ const loginId = getString(value["loginId"]);
145
+ const authUrl = getString(value["authUrl"]);
146
+ if (!loginId || !authUrl) {
147
+ throw createCodexProtocolError("Codex ChatGPT login response is missing login details", {
148
+ method: "account/login/start"
149
+ });
150
+ }
151
+ return {
152
+ type,
153
+ loginId,
154
+ authUrl
155
+ };
156
+ }
157
+ function parseModelListResponse(value) {
158
+ if (!isRecord(value)) {
159
+ throw createCodexProtocolError("Invalid model/list response from Codex", {
160
+ method: "model/list"
161
+ });
162
+ }
163
+ const data = getArray(value["data"]);
164
+ if (!data) {
165
+ throw createCodexProtocolError("Codex model/list response is missing data", {
166
+ method: "model/list"
167
+ });
168
+ }
169
+ const models = [];
170
+ for (const entry of data) {
171
+ if (!isRecord(entry)) {
172
+ continue;
173
+ }
174
+ const id = getString(entry["id"]);
175
+ const model = getString(entry["model"]);
176
+ const displayName = getString(entry["displayName"]);
177
+ const description = getString(entry["description"]);
178
+ const hidden = getBoolean(entry["hidden"]);
179
+ const isDefault = getBoolean(entry["isDefault"]);
180
+ const defaultReasoningEffort = getString(entry["defaultReasoningEffort"]);
181
+ const supportedReasoningEffortsRaw = getArray(entry["supportedReasoningEfforts"]);
182
+ if (!id || !model || !displayName || description === null || hidden === null || isDefault === null || !defaultReasoningEffort || !supportedReasoningEffortsRaw) {
183
+ continue;
184
+ }
185
+ const supportedReasoningEfforts = supportedReasoningEffortsRaw.map((candidate) => getString(candidate)).filter((candidate) => candidate !== null);
186
+ models.push({
187
+ id,
188
+ model,
189
+ displayName,
190
+ description,
191
+ hidden,
192
+ isDefault,
193
+ supportedReasoningEfforts,
194
+ defaultReasoningEffort
195
+ });
196
+ }
197
+ return {
198
+ data: models,
199
+ nextCursor: getString(value["nextCursor"])
200
+ };
201
+ }
202
+ function parseThreadStartResponse(value) {
203
+ if (!isRecord(value) || !isRecord(value["thread"])) {
204
+ throw createCodexProtocolError("Invalid thread/start response from Codex", {
205
+ method: "thread/start"
206
+ });
207
+ }
208
+ const threadId = getString(value["thread"]["id"]);
209
+ if (!threadId) {
210
+ throw createCodexProtocolError("Codex thread/start response is missing a thread ID", {
211
+ method: "thread/start"
212
+ });
213
+ }
214
+ return {
215
+ thread: {
216
+ id: threadId
217
+ }
218
+ };
219
+ }
220
+ function parseTurnStartResponse(value) {
221
+ if (!isRecord(value) || !isRecord(value["turn"])) {
222
+ throw createCodexProtocolError("Invalid turn/start response from Codex", {
223
+ method: "turn/start"
224
+ });
225
+ }
226
+ const turnId = getString(value["turn"]["id"]);
227
+ if (!turnId) {
228
+ throw createCodexProtocolError("Codex turn/start response is missing a turn ID", {
229
+ method: "turn/start"
230
+ });
231
+ }
232
+ return {
233
+ turn: {
234
+ id: turnId
235
+ }
236
+ };
237
+ }
238
+ function parseRateLimitEntry(value) {
239
+ if (!isRecord(value)) {
240
+ return null;
241
+ }
242
+ const usedPercent = getNumber(value["usedPercent"]);
243
+ if (usedPercent === null) {
244
+ return null;
245
+ }
246
+ const normalizedUsedPercent = Math.max(0, Math.min(100, usedPercent));
247
+ const windowMinutes = getNumber(value["windowDurationMins"]) ?? getNumber(value["windowMinutes"]);
248
+ const limitId = getString(value["limitId"]);
249
+ const limitName = getString(value["limitName"]);
250
+ const resetsAt = getString(value["resetsAt"]);
251
+ return {
252
+ source: "chatgpt-login",
253
+ usedPercent: normalizedUsedPercent,
254
+ exceeded: normalizedUsedPercent >= 100,
255
+ ...limitId ? { limitId } : {},
256
+ ...limitName ? { limitName } : {},
257
+ ...resetsAt ? { resetsAt } : {},
258
+ ...windowMinutes !== null ? { windowMinutes } : {}
259
+ };
260
+ }
261
+ function collectRateLimitEntries(value) {
262
+ const entries = [];
263
+ const directEntry = parseRateLimitEntry(value);
264
+ if (directEntry) {
265
+ entries.push(directEntry);
266
+ }
267
+ if (!isRecord(value)) {
268
+ return entries;
269
+ }
270
+ const rateLimits = getArray(value["rateLimits"]);
271
+ if (rateLimits) {
272
+ for (const candidate of rateLimits) {
273
+ const entry = parseRateLimitEntry(candidate);
274
+ if (entry) {
275
+ entries.push(entry);
276
+ }
277
+ }
278
+ }
279
+ const rateLimitsByLimitId = isRecord(value["rateLimitsByLimitId"]) ? value["rateLimitsByLimitId"] : null;
280
+ if (rateLimitsByLimitId) {
281
+ for (const candidate of Object.values(rateLimitsByLimitId)) {
282
+ if (Array.isArray(candidate)) {
283
+ for (const item of candidate) {
284
+ const entry2 = parseRateLimitEntry(item);
285
+ if (entry2) {
286
+ entries.push(entry2);
287
+ }
288
+ }
289
+ continue;
290
+ }
291
+ const entry = parseRateLimitEntry(candidate);
292
+ if (entry) {
293
+ entries.push(entry);
294
+ }
295
+ }
296
+ }
297
+ return entries;
298
+ }
299
+ function pickPrimaryRateLimitSnapshot(value) {
300
+ const entries = collectRateLimitEntries(value);
301
+ if (entries.length === 0) {
302
+ return null;
303
+ }
304
+ entries.sort((left, right) => {
305
+ if (left.exceeded !== right.exceeded) {
306
+ return left.exceeded ? -1 : 1;
307
+ }
308
+ if (left.usedPercent !== right.usedPercent) {
309
+ return right.usedPercent - left.usedPercent;
310
+ }
311
+ const leftReset = left.resetsAt ? Date.parse(left.resetsAt) : Number.POSITIVE_INFINITY;
312
+ const rightReset = right.resetsAt ? Date.parse(right.resetsAt) : Number.POSITIVE_INFINITY;
313
+ return leftReset - rightReset;
314
+ });
315
+ return entries[0] ?? null;
316
+ }
317
+ function parseCodexErrorDetails(value) {
318
+ if (!isRecord(value)) {
319
+ return null;
320
+ }
321
+ const codexErrorInfo = isRecord(value["codexErrorInfo"]) ? value["codexErrorInfo"] : isRecord(value["codex_error_info"]) ? value["codex_error_info"] : null;
322
+ return {
323
+ message: getString(value["message"]),
324
+ additionalDetails: getString(value["additionalDetails"]) ?? getString(value["additional_details"]),
325
+ errorInfoKeys: codexErrorInfo ? Object.keys(codexErrorInfo) : []
326
+ };
327
+ }
328
+ function isUsageLimitError(details) {
329
+ if (!details) {
330
+ return false;
331
+ }
332
+ const normalizedInfoKeys = details.errorInfoKeys.map((key) => key.toLowerCase());
333
+ if (normalizedInfoKeys.includes("usagelimitexceeded")) {
334
+ return true;
335
+ }
336
+ const combinedText = `${details.message ?? ""} ${details.additionalDetails ?? ""}`.toLowerCase();
337
+ return combinedText.includes("usage limit") || combinedText.includes("rate limit") || combinedText.includes("quota exceeded") || combinedText.includes("purchase more credits");
338
+ }
339
+ function buildUsageLimitSnapshot(existing) {
340
+ if (!existing) {
341
+ return {
342
+ source: "chatgpt-login",
343
+ usedPercent: 100,
344
+ exceeded: true
345
+ };
346
+ }
347
+ return {
348
+ ...existing,
349
+ usedPercent: Math.max(100, existing.usedPercent),
350
+ exceeded: true
351
+ };
352
+ }
353
+ function toChatGPTUsageLimitError(details, modelId, snapshot) {
354
+ const message = details.message ?? "You have reached your ChatGPT usage limit.";
355
+ return new DextoRuntimeError(
356
+ LLMErrorCode.RATE_LIMIT_EXCEEDED,
357
+ ErrorScope.LLM,
358
+ ErrorType.RATE_LIMIT,
359
+ message,
360
+ {
361
+ provider: "openai-compatible",
362
+ model: modelId,
363
+ source: "chatgpt-login",
364
+ ...snapshot?.limitId ? { limitId: snapshot.limitId } : {},
365
+ ...snapshot?.limitName ? { limitName: snapshot.limitName } : {},
366
+ ...snapshot?.resetsAt ? { resetsAt: snapshot.resetsAt } : {},
367
+ ...snapshot?.windowMinutes !== void 0 ? { windowMinutes: snapshot.windowMinutes } : {},
368
+ usedPercent: snapshot?.usedPercent ?? 100,
369
+ additionalDetails: details.additionalDetails ?? void 0,
370
+ errorInfoKeys: details.errorInfoKeys
371
+ },
372
+ [
373
+ "Wait for your ChatGPT usage window to reset, or switch this session to an OpenAI API key.",
374
+ "Use `/model` to move this session onto your API key-backed OpenAI provider."
375
+ ]
376
+ );
377
+ }
378
+ function createUsage() {
379
+ return {
380
+ inputTokens: void 0,
381
+ outputTokens: void 0,
382
+ totalTokens: void 0
383
+ };
384
+ }
385
+ function promptMessageToTranscript(message) {
386
+ if (message.role === "system") {
387
+ return `System:
388
+ ${message.content}`;
389
+ }
390
+ const parts = message.role === "tool" ? message.content.map((part) => {
391
+ const output = safeStringify(part.output);
392
+ return `[Tool Result: ${part.toolName}#${part.toolCallId}]
393
+ ${output}`;
394
+ }) : message.content.map((part) => {
395
+ switch (part.type) {
396
+ case "text":
397
+ return part.text;
398
+ case "reasoning":
399
+ return `[Reasoning]
400
+ ${part.text}`;
401
+ case "tool-call":
402
+ return `[Tool Call: ${part.toolName}#${part.toolCallId}]
403
+ ${safeStringify(part.input)}`;
404
+ case "tool-result":
405
+ return `[Tool Result: ${part.toolName}#${part.toolCallId}]
406
+ ${safeStringify(part.output)}`;
407
+ case "file": {
408
+ const url = part.data instanceof URL ? part.data.toString() : typeof part.data === "string" && /^(https?:)?\/\//i.test(part.data) ? part.data : null;
409
+ const fileLabel = part.filename ?? part.mediaType;
410
+ if (url) {
411
+ return `[File: ${fileLabel}] ${url}`;
412
+ }
413
+ return `[File: ${fileLabel}] data omitted`;
414
+ }
415
+ default: {
416
+ const unknownType = isRecord(part) && typeof part["type"] === "string" ? part["type"] : "unknown";
417
+ return `[Unknown Prompt Part: ${unknownType}]`;
418
+ }
419
+ }
420
+ });
421
+ const roleLabel = message.role === "user" ? "User" : message.role === "assistant" ? "Assistant" : "Tool";
422
+ return `${roleLabel}:
423
+ ${parts.join("\n")}`;
424
+ }
425
+ function buildTranscript(prompt) {
426
+ return prompt.map((message) => promptMessageToTranscript(message)).join("\n\n");
427
+ }
428
+ function isFunctionTool(tool) {
429
+ return tool.type === "function";
430
+ }
431
+ function selectCodexFunctionTools(options) {
432
+ const functionTools = (options.tools ?? []).filter(isFunctionTool);
433
+ const toolChoice = options.toolChoice;
434
+ if (!toolChoice || toolChoice.type === "auto" || toolChoice.type === "required") {
435
+ return functionTools;
436
+ }
437
+ if (toolChoice.type === "none") {
438
+ return [];
439
+ }
440
+ return functionTools.filter((tool) => tool.name === toolChoice.toolName);
441
+ }
442
+ function buildDynamicTools(options) {
443
+ return selectCodexFunctionTools(options).map((tool) => ({
444
+ name: tool.name,
445
+ ...tool.description ? { description: tool.description } : {},
446
+ inputSchema: tool.inputSchema
447
+ }));
448
+ }
449
+ function buildWarnings(options) {
450
+ const warnings = [];
451
+ const unsupportedSettings = [
452
+ "maxOutputTokens",
453
+ "temperature",
454
+ "stopSequences",
455
+ "topP",
456
+ "topK",
457
+ "presencePenalty",
458
+ "frequencyPenalty",
459
+ "seed"
460
+ ];
461
+ for (const setting of unsupportedSettings) {
462
+ if (options[setting] !== void 0) {
463
+ warnings.push({
464
+ type: "unsupported-setting",
465
+ setting,
466
+ details: "Codex app-server manages this setting internally."
467
+ });
468
+ }
469
+ }
470
+ if (options.tools) {
471
+ for (const tool of options.tools) {
472
+ if (!isFunctionTool(tool)) {
473
+ warnings.push({
474
+ type: "unsupported-tool",
475
+ tool,
476
+ details: "Codex app-server integration only supports host function tools."
477
+ });
478
+ }
479
+ }
480
+ }
481
+ return warnings;
482
+ }
483
+ function getRequestedReasoningEffort(options) {
484
+ if (!isRecord(options.providerOptions)) {
485
+ return null;
486
+ }
487
+ const openAiCompatibleOptions = options.providerOptions["openaiCompatible"];
488
+ if (!isRecord(openAiCompatibleOptions)) {
489
+ return null;
490
+ }
491
+ const reasoningEffort = getString(openAiCompatibleOptions["reasoningEffort"]);
492
+ return reasoningEffort ?? null;
493
+ }
494
+ function buildDeveloperInstructions(options, dynamicTools) {
495
+ const instructions = [CODEX_DEVELOPER_INSTRUCTIONS];
496
+ if (dynamicTools.length > 0) {
497
+ instructions.push(
498
+ "Host-provided dynamic tools are available for this turn. Prefer them over guessing."
499
+ );
500
+ if (options.toolChoice?.type === "required") {
501
+ instructions.push(
502
+ "You must call at least one host-provided dynamic tool before your final answer."
503
+ );
504
+ }
505
+ } else {
506
+ instructions.push("No host-provided dynamic tools are available for this turn.");
507
+ }
508
+ if (options.toolChoice?.type === "tool") {
509
+ instructions.push(
510
+ `If you need a tool, use only the host tool named "${options.toolChoice.toolName}".`
511
+ );
512
+ }
513
+ if (options.responseFormat?.type === "json" && !options.responseFormat.schema) {
514
+ instructions.push("Return valid JSON only. Do not wrap it in Markdown fences.");
515
+ }
516
+ return instructions.join(" ");
517
+ }
518
+ function toCodexFailureMessage(error, modelId) {
519
+ if (error instanceof DextoRuntimeError) {
520
+ return error;
521
+ }
522
+ const normalized = normalizeError(error);
523
+ if (normalized.message.includes("spawn codex ENOENT")) {
524
+ return LLMError.missingConfig(
525
+ "openai-compatible",
526
+ "the Codex CLI on PATH (install Codex to use ChatGPT Login in Dexto)"
527
+ );
528
+ }
529
+ const fallbackDetails = {
530
+ message: normalized.message,
531
+ additionalDetails: null,
532
+ errorInfoKeys: []
533
+ };
534
+ if (isUsageLimitError(fallbackDetails)) {
535
+ return toChatGPTUsageLimitError(fallbackDetails, modelId, null);
536
+ }
537
+ return LLMError.generationFailed(normalized.message, "openai-compatible", modelId);
538
+ }
539
+ function enforceAuthMode(authMode, accountState) {
540
+ if (!accountState.account) {
541
+ if (accountState.requiresOpenaiAuth) {
542
+ throw LLMError.missingConfig(
543
+ "openai-compatible",
544
+ "Codex authentication (run `codex login` or re-run `dexto setup` and choose ChatGPT Login)"
545
+ );
546
+ }
547
+ return;
548
+ }
549
+ if (authMode === "auto") {
550
+ return;
551
+ }
552
+ if (authMode === "chatgpt" && accountState.account.type !== "chatgpt") {
553
+ throw LLMError.missingConfig(
554
+ "openai-compatible",
555
+ "a ChatGPT-backed Codex login (run `codex logout` and sign in with ChatGPT, or re-run `dexto setup`)"
556
+ );
557
+ }
558
+ if (authMode === "apikey" && accountState.account.type !== "apiKey") {
559
+ throw LLMError.missingConfig(
560
+ "openai-compatible",
561
+ "an API key-backed Codex login (run `codex login --with-api-key` or re-run `dexto setup`)"
562
+ );
563
+ }
564
+ }
565
+ function extractErrorMessage(params) {
566
+ if (!isRecord(params) || !isRecord(params["error"])) {
567
+ return null;
568
+ }
569
+ return getString(params["error"]["message"]);
570
+ }
571
+ function stringifyToolInput(input) {
572
+ try {
573
+ const serialized = JSON.stringify(input ?? {});
574
+ return serialized === void 0 ? "{}" : serialized;
575
+ } catch {
576
+ return "{}";
577
+ }
578
+ }
579
+ function parseDynamicToolCallRequest(params) {
580
+ if (!isRecord(params)) {
581
+ return null;
582
+ }
583
+ const threadId = getString(params["threadId"]);
584
+ const turnId = getString(params["turnId"]);
585
+ const callId = getString(params["callId"]);
586
+ const toolName = getString(params["tool"]);
587
+ if (!threadId || !turnId || !callId || !toolName) {
588
+ return null;
589
+ }
590
+ return {
591
+ threadId,
592
+ turnId,
593
+ callId,
594
+ toolName,
595
+ input: stringifyToolInput(params["arguments"])
596
+ };
597
+ }
598
+ class CodexAppServerClient {
599
+ command;
600
+ cwd;
601
+ requestTimeoutMs;
602
+ clientInfo;
603
+ child = null;
604
+ reader = null;
605
+ nextId = 1;
606
+ pending = /* @__PURE__ */ new Map();
607
+ listeners = /* @__PURE__ */ new Set();
608
+ requestListeners = /* @__PURE__ */ new Set();
609
+ started = false;
610
+ closed = false;
611
+ constructor(options = {}) {
612
+ this.command = options.command ?? "codex";
613
+ this.cwd = options.cwd;
614
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
615
+ this.clientInfo = options.clientInfo ?? DEFAULT_CLIENT_INFO;
616
+ }
617
+ static async create(options = {}) {
618
+ const client = new CodexAppServerClient(options);
619
+ try {
620
+ await client.start();
621
+ return client;
622
+ } catch (error) {
623
+ await client.close().catch(() => void 0);
624
+ throw error;
625
+ }
626
+ }
627
+ async close() {
628
+ if (this.closed) {
629
+ return;
630
+ }
631
+ this.closed = true;
632
+ this.started = false;
633
+ this.rejectPending(createCodexClientRuntimeError("Codex app-server client closed"));
634
+ this.listeners.clear();
635
+ this.requestListeners.clear();
636
+ if (this.reader) {
637
+ this.reader.close();
638
+ this.reader = null;
639
+ }
640
+ const child = this.child;
641
+ this.child = null;
642
+ if (!child || child.killed || child.exitCode !== null || child.signalCode !== null) {
643
+ return;
644
+ }
645
+ await new Promise((resolve) => {
646
+ child.once("exit", () => resolve());
647
+ child.kill();
648
+ });
649
+ }
650
+ onNotification(listener) {
651
+ this.listeners.add(listener);
652
+ return () => {
653
+ this.listeners.delete(listener);
654
+ };
655
+ }
656
+ onServerRequest(listener) {
657
+ this.requestListeners.add(listener);
658
+ return () => {
659
+ this.requestListeners.delete(listener);
660
+ };
661
+ }
662
+ async readAccount(refreshToken = false) {
663
+ const result = await this.request("account/read", { refreshToken });
664
+ return parseReadAccountResponse(result);
665
+ }
666
+ async logout() {
667
+ await this.request("account/logout", void 0);
668
+ }
669
+ async startLogin(params) {
670
+ const result = await this.request("account/login/start", params);
671
+ return parseLoginResponse(result);
672
+ }
673
+ async waitForLoginCompleted(loginId, options = {}) {
674
+ const params = await this.waitForNotification(
675
+ "account/login/completed",
676
+ (candidate) => {
677
+ if (!isRecord(candidate)) {
678
+ return false;
679
+ }
680
+ const candidateLoginId = getString(candidate["loginId"]);
681
+ return candidateLoginId === loginId;
682
+ },
683
+ options
684
+ );
685
+ if (!isRecord(params)) {
686
+ throw createCodexProtocolError(
687
+ "Invalid account/login/completed notification from Codex",
688
+ {
689
+ method: "account/login/completed"
690
+ }
691
+ );
692
+ }
693
+ const success = getBoolean(params["success"]);
694
+ if (success === null) {
695
+ throw createCodexProtocolError("Codex login completion is missing success", {
696
+ method: "account/login/completed"
697
+ });
698
+ }
699
+ return {
700
+ loginId: getString(params["loginId"]),
701
+ success,
702
+ error: getString(params["error"])
703
+ };
704
+ }
705
+ async listModels() {
706
+ const models = [];
707
+ let cursor = null;
708
+ while (true) {
709
+ const result = await this.request("model/list", {
710
+ includeHidden: false,
711
+ ...cursor ? { cursor } : {}
712
+ });
713
+ const parsed = parseModelListResponse(result);
714
+ models.push(...parsed.data);
715
+ if (!parsed.nextCursor) {
716
+ return models;
717
+ }
718
+ cursor = parsed.nextCursor;
719
+ }
720
+ }
721
+ async readRateLimits() {
722
+ const result = await this.request("account/rateLimits/read", void 0);
723
+ return pickPrimaryRateLimitSnapshot(result);
724
+ }
725
+ async startEphemeralThread(params) {
726
+ const result = await this.request("thread/start", {
727
+ model: params.model,
728
+ ...params.cwd ? { cwd: params.cwd } : {},
729
+ approvalPolicy: "untrusted",
730
+ sandbox: "read-only",
731
+ developerInstructions: params.developerInstructions ?? CODEX_DEVELOPER_INSTRUCTIONS,
732
+ ...params.dynamicTools && params.dynamicTools.length > 0 ? { dynamicTools: params.dynamicTools } : {},
733
+ ephemeral: true,
734
+ experimentalRawEvents: false,
735
+ persistExtendedHistory: false
736
+ });
737
+ return parseThreadStartResponse(result);
738
+ }
739
+ async startTurn(params) {
740
+ const result = await this.request("turn/start", {
741
+ threadId: params.threadId,
742
+ model: params.model,
743
+ input: [
744
+ {
745
+ type: "text",
746
+ text: params.transcript,
747
+ text_elements: []
748
+ }
749
+ ],
750
+ ...params.reasoningEffort ? { effort: params.reasoningEffort } : {},
751
+ ...params.outputSchema ? { outputSchema: params.outputSchema } : {}
752
+ });
753
+ return parseTurnStartResponse(result);
754
+ }
755
+ async waitForNotification(method, predicate, options = {}) {
756
+ if (this.closed) {
757
+ throw createCodexClientRuntimeError("Codex app-server client is closed");
758
+ }
759
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
760
+ return await new Promise((resolve, reject) => {
761
+ let timeout = null;
762
+ let offAbort = null;
763
+ const cleanup = (unsubscribe2) => {
764
+ unsubscribe2();
765
+ if (timeout) {
766
+ clearTimeout(timeout);
767
+ }
768
+ if (offAbort) {
769
+ offAbort();
770
+ }
771
+ };
772
+ const unsubscribe = this.onNotification((message) => {
773
+ if (message.method !== method) {
774
+ return;
775
+ }
776
+ if (predicate && !predicate(message.params)) {
777
+ return;
778
+ }
779
+ cleanup(unsubscribe);
780
+ resolve(message.params);
781
+ });
782
+ timeout = setTimeout(() => {
783
+ cleanup(unsubscribe);
784
+ reject(
785
+ createCodexClientRuntimeError(
786
+ `Timed out waiting for Codex notification: ${method}`,
787
+ { method },
788
+ ErrorType.TIMEOUT
789
+ )
790
+ );
791
+ }, timeoutMs);
792
+ if (options.signal) {
793
+ const onAbort = () => {
794
+ cleanup(unsubscribe);
795
+ reject(
796
+ options.signal?.reason instanceof Error ? options.signal.reason : createCodexClientRuntimeError(
797
+ "Codex operation aborted",
798
+ { method },
799
+ ErrorType.USER
800
+ )
801
+ );
802
+ };
803
+ if (options.signal.aborted) {
804
+ onAbort();
805
+ return;
806
+ }
807
+ options.signal.addEventListener("abort", onAbort, { once: true });
808
+ offAbort = () => options.signal?.removeEventListener("abort", onAbort);
809
+ }
810
+ });
811
+ }
812
+ async start() {
813
+ if (this.started) {
814
+ return;
815
+ }
816
+ const child = spawn(this.command, ["app-server"], {
817
+ cwd: this.cwd,
818
+ stdio: ["pipe", "pipe", "pipe"]
819
+ });
820
+ this.child = child;
821
+ this.reader = createInterface({
822
+ input: child.stdout,
823
+ crlfDelay: Infinity
824
+ });
825
+ const drainStderr = () => void 0;
826
+ child.stderr.on("data", drainStderr);
827
+ child.on("error", (error) => {
828
+ this.rejectPending(error);
829
+ });
830
+ child.on("exit", (code, signal) => {
831
+ const wasClosed = this.closed;
832
+ const error = createCodexClientExitedError({
833
+ ...code !== null ? { code } : {},
834
+ ...signal !== null ? { signal } : {}
835
+ });
836
+ child.stderr.off("data", drainStderr);
837
+ if (!wasClosed) {
838
+ this.publishNotification({
839
+ method: "codex/client-exited",
840
+ params: {
841
+ ...code !== null ? { code } : {},
842
+ ...signal !== null ? { signal } : {}
843
+ }
844
+ });
845
+ this.rejectPending(error);
846
+ }
847
+ });
848
+ this.reader.on("line", (line) => {
849
+ this.handleLine(line);
850
+ });
851
+ await this.request("initialize", {
852
+ clientInfo: this.clientInfo,
853
+ capabilities: {
854
+ experimentalApi: true
855
+ }
856
+ });
857
+ this.notify("initialized", {});
858
+ this.started = true;
859
+ }
860
+ handleLine(line) {
861
+ if (!line.trim()) {
862
+ return;
863
+ }
864
+ let payload;
865
+ try {
866
+ payload = JSON.parse(line);
867
+ } catch {
868
+ return;
869
+ }
870
+ if (!isRecord(payload)) {
871
+ return;
872
+ }
873
+ const id = payload["id"];
874
+ const method = getString(payload["method"]);
875
+ if ((typeof id === "number" || typeof id === "string") && method) {
876
+ const request = {
877
+ id,
878
+ method,
879
+ ...payload["params"] !== void 0 ? { params: payload["params"] } : {}
880
+ };
881
+ for (const listener of this.requestListeners) {
882
+ listener(request);
883
+ }
884
+ return;
885
+ }
886
+ if (typeof id === "number") {
887
+ const pending = this.pending.get(id);
888
+ if (!pending) {
889
+ return;
890
+ }
891
+ this.pending.delete(id);
892
+ clearTimeout(pending.timeout);
893
+ if (isRecord(payload["error"])) {
894
+ const message = getString(payload["error"]["message"]) ?? "Codex JSON-RPC request failed";
895
+ pending.reject(
896
+ createCodexClientRuntimeError(message, { id }, ErrorType.THIRD_PARTY)
897
+ );
898
+ return;
899
+ }
900
+ pending.resolve(payload["result"]);
901
+ return;
902
+ }
903
+ if (!method) {
904
+ return;
905
+ }
906
+ const notification = {
907
+ method,
908
+ ...payload["params"] !== void 0 ? { params: payload["params"] } : {}
909
+ };
910
+ this.publishNotification(notification);
911
+ }
912
+ publishNotification(notification) {
913
+ for (const listener of this.listeners) {
914
+ listener(notification);
915
+ }
916
+ }
917
+ notify(method, params) {
918
+ this.write({ method, params });
919
+ }
920
+ async request(method, params) {
921
+ if (this.closed) {
922
+ throw createCodexClientRuntimeError("Codex app-server client is closed");
923
+ }
924
+ const id = this.nextId++;
925
+ return await new Promise((resolve, reject) => {
926
+ const timeout = setTimeout(() => {
927
+ this.pending.delete(id);
928
+ reject(
929
+ createCodexClientRuntimeError(
930
+ `Codex request timed out: ${method}`,
931
+ { method, id },
932
+ ErrorType.TIMEOUT
933
+ )
934
+ );
935
+ }, this.requestTimeoutMs);
936
+ this.pending.set(id, {
937
+ resolve,
938
+ reject,
939
+ timeout
940
+ });
941
+ try {
942
+ this.write({
943
+ method,
944
+ id,
945
+ ...params !== void 0 ? { params } : {}
946
+ });
947
+ } catch (error) {
948
+ clearTimeout(timeout);
949
+ this.pending.delete(id);
950
+ reject(error);
951
+ }
952
+ });
953
+ }
954
+ respondToServerRequest(id, result) {
955
+ this.write({
956
+ id,
957
+ result
958
+ });
959
+ }
960
+ rejectServerRequest(id, message, code = -32601) {
961
+ this.write({
962
+ id,
963
+ error: {
964
+ code,
965
+ message
966
+ }
967
+ });
968
+ }
969
+ write(payload) {
970
+ if (!this.child?.stdin.writable) {
971
+ throw createCodexClientRuntimeError("Codex app-server stdin is not writable");
972
+ }
973
+ this.child.stdin.write(`${JSON.stringify(payload)}
974
+ `);
975
+ }
976
+ rejectPending(error) {
977
+ const normalized = normalizeError(error);
978
+ for (const [id, pending] of this.pending.entries()) {
979
+ clearTimeout(pending.timeout);
980
+ pending.reject(normalized);
981
+ this.pending.delete(id);
982
+ }
983
+ }
984
+ }
985
+ function createCodexLanguageModel(options) {
986
+ const parsedBaseURL = parseCodexBaseURL(options.baseURL);
987
+ const authMode = parsedBaseURL?.authMode ?? "auto";
988
+ async function executeTurn(callOptions) {
989
+ const warnings = buildWarnings(callOptions);
990
+ const transcript = buildTranscript(callOptions.prompt);
991
+ const reasoningEffort = getRequestedReasoningEffort(callOptions);
992
+ const dynamicTools = buildDynamicTools(callOptions);
993
+ const developerInstructions = buildDeveloperInstructions(callOptions, dynamicTools);
994
+ const outputSchema = callOptions.responseFormat?.type === "json" ? callOptions.responseFormat.schema : void 0;
995
+ let latestRateLimitSnapshot = null;
996
+ const emitRateLimitStatus = (snapshot) => {
997
+ if (!snapshot) {
998
+ return;
999
+ }
1000
+ latestRateLimitSnapshot = snapshot;
1001
+ options.onRateLimitStatus?.(snapshot);
1002
+ };
1003
+ let client = null;
1004
+ try {
1005
+ client = await CodexAppServerClient.create({
1006
+ ...options.cwd ? { cwd: options.cwd } : {}
1007
+ });
1008
+ const activeClient = client;
1009
+ const account = await activeClient.readAccount(false);
1010
+ enforceAuthMode(authMode, account);
1011
+ const shouldTrackRateLimits = account.account?.type === "chatgpt" || authMode === "chatgpt";
1012
+ if (shouldTrackRateLimits) {
1013
+ void activeClient.readRateLimits().then((snapshot) => emitRateLimitStatus(snapshot)).catch(() => void 0);
1014
+ }
1015
+ const thread = await activeClient.startEphemeralThread({
1016
+ model: options.modelId,
1017
+ ...options.cwd ? { cwd: options.cwd } : {},
1018
+ developerInstructions,
1019
+ dynamicTools
1020
+ });
1021
+ const turn = await activeClient.startTurn({
1022
+ threadId: thread.thread.id,
1023
+ model: options.modelId,
1024
+ transcript,
1025
+ ...reasoningEffort ? { reasoningEffort } : {},
1026
+ ...outputSchema ? { outputSchema } : {}
1027
+ });
1028
+ const stream = new ReadableStream({
1029
+ start(controller) {
1030
+ let closed = false;
1031
+ let streamStarted = false;
1032
+ let textStarted = false;
1033
+ let textEnded = false;
1034
+ let emittedText = "";
1035
+ let offAbort = null;
1036
+ const textPartId = `codex-text-${turn.turn.id}`;
1037
+ const cleanup = () => {
1038
+ if (closed) {
1039
+ return;
1040
+ }
1041
+ closed = true;
1042
+ unsubscribeNotifications();
1043
+ unsubscribeRequests();
1044
+ if (offAbort) {
1045
+ offAbort();
1046
+ offAbort = null;
1047
+ }
1048
+ void activeClient.close();
1049
+ };
1050
+ const ensureStreamStarted = () => {
1051
+ if (streamStarted) {
1052
+ return;
1053
+ }
1054
+ streamStarted = true;
1055
+ controller.enqueue({
1056
+ type: "stream-start",
1057
+ warnings
1058
+ });
1059
+ };
1060
+ const endText = () => {
1061
+ if (textStarted && !textEnded) {
1062
+ controller.enqueue({
1063
+ type: "text-end",
1064
+ id: textPartId
1065
+ });
1066
+ textEnded = true;
1067
+ }
1068
+ };
1069
+ const finishStream = (finishReason) => {
1070
+ ensureStreamStarted();
1071
+ endText();
1072
+ controller.enqueue({
1073
+ type: "finish",
1074
+ finishReason,
1075
+ usage: createUsage()
1076
+ });
1077
+ controller.close();
1078
+ cleanup();
1079
+ };
1080
+ const failStream = (error) => {
1081
+ if (closed) {
1082
+ return;
1083
+ }
1084
+ controller.enqueue({
1085
+ type: "error",
1086
+ error
1087
+ });
1088
+ cleanup();
1089
+ controller.close();
1090
+ };
1091
+ const failWithTurnError = (details, fallbackMessage) => {
1092
+ if (details && isUsageLimitError(details)) {
1093
+ const snapshot = buildUsageLimitSnapshot(latestRateLimitSnapshot);
1094
+ emitRateLimitStatus(snapshot);
1095
+ failStream(
1096
+ toChatGPTUsageLimitError(details, options.modelId, snapshot)
1097
+ );
1098
+ return;
1099
+ }
1100
+ failStream(
1101
+ LLMError.generationFailed(
1102
+ details?.message ?? fallbackMessage,
1103
+ "openai-compatible",
1104
+ options.modelId
1105
+ )
1106
+ );
1107
+ };
1108
+ const unsubscribeNotifications = activeClient.onNotification((message) => {
1109
+ if (message.method === "codex/client-exited") {
1110
+ const params = isRecord(message.params) ? message.params : {};
1111
+ failStream(
1112
+ createCodexClientExitedError({
1113
+ ...getNumber(params["code"]) !== null ? { code: getNumber(params["code"]) ?? void 0 } : {},
1114
+ ...getString(params["signal"]) !== null ? { signal: getString(params["signal"]) ?? void 0 } : {}
1115
+ })
1116
+ );
1117
+ return;
1118
+ }
1119
+ if (message.method === "account/rateLimits/updated") {
1120
+ emitRateLimitStatus(pickPrimaryRateLimitSnapshot(message.params));
1121
+ return;
1122
+ }
1123
+ if (message.method === "item/agentMessage/delta") {
1124
+ if (!isRecord(message.params)) {
1125
+ return;
1126
+ }
1127
+ const threadId = getString(message.params["threadId"]);
1128
+ const turnId = getString(message.params["turnId"]);
1129
+ const delta = getString(message.params["delta"]);
1130
+ if (threadId !== thread.thread.id || turnId !== turn.turn.id || delta === null) {
1131
+ return;
1132
+ }
1133
+ if (!textStarted) {
1134
+ ensureStreamStarted();
1135
+ controller.enqueue({
1136
+ type: "text-start",
1137
+ id: textPartId
1138
+ });
1139
+ textStarted = true;
1140
+ }
1141
+ emittedText += delta;
1142
+ controller.enqueue({
1143
+ type: "text-delta",
1144
+ id: textPartId,
1145
+ delta
1146
+ });
1147
+ return;
1148
+ }
1149
+ if (message.method === "item/completed") {
1150
+ if (!isRecord(message.params)) {
1151
+ return;
1152
+ }
1153
+ const threadId = getString(message.params["threadId"]);
1154
+ const turnId = getString(message.params["turnId"]);
1155
+ const item = isRecord(message.params["item"]) ? message.params["item"] : null;
1156
+ if (threadId !== thread.thread.id || turnId !== turn.turn.id || !item) {
1157
+ return;
1158
+ }
1159
+ if (item["type"] !== "agentMessage") {
1160
+ return;
1161
+ }
1162
+ const text = getString(item["text"]);
1163
+ if (!text) {
1164
+ return;
1165
+ }
1166
+ const missingText = text.startsWith(emittedText) ? text.slice(emittedText.length) : text;
1167
+ if (!textStarted) {
1168
+ ensureStreamStarted();
1169
+ controller.enqueue({
1170
+ type: "text-start",
1171
+ id: textPartId
1172
+ });
1173
+ textStarted = true;
1174
+ }
1175
+ if (missingText) {
1176
+ emittedText += missingText;
1177
+ controller.enqueue({
1178
+ type: "text-delta",
1179
+ id: textPartId,
1180
+ delta: missingText
1181
+ });
1182
+ }
1183
+ return;
1184
+ }
1185
+ if (message.method === "error") {
1186
+ if (!isRecord(message.params)) {
1187
+ return;
1188
+ }
1189
+ const willRetry = getBoolean(message.params["willRetry"]);
1190
+ if (willRetry === true) {
1191
+ return;
1192
+ }
1193
+ const threadId = getString(message.params["threadId"]);
1194
+ const turnId = getString(message.params["turnId"]);
1195
+ if (threadId !== thread.thread.id || turnId !== turn.turn.id) {
1196
+ return;
1197
+ }
1198
+ const details = parseCodexErrorDetails(
1199
+ isRecord(message.params["error"]) ? message.params["error"] : message.params
1200
+ );
1201
+ failWithTurnError(
1202
+ details,
1203
+ extractErrorMessage(message.params) ?? "Codex turn failed"
1204
+ );
1205
+ return;
1206
+ }
1207
+ if (message.method === "turn/completed") {
1208
+ if (!isRecord(message.params)) {
1209
+ return;
1210
+ }
1211
+ const threadId = getString(message.params["threadId"]);
1212
+ const turnInfo = isRecord(message.params["turn"]) ? message.params["turn"] : null;
1213
+ const turnId = turnInfo ? getString(turnInfo["id"]) : null;
1214
+ const status = turnInfo ? getString(turnInfo["status"]) : null;
1215
+ if (threadId !== thread.thread.id || turnId !== turn.turn.id || status === null) {
1216
+ return;
1217
+ }
1218
+ if (status === "failed") {
1219
+ const details = turnInfo && isRecord(turnInfo["error"]) ? parseCodexErrorDetails(turnInfo["error"]) : null;
1220
+ failWithTurnError(details, "Codex turn failed");
1221
+ return;
1222
+ }
1223
+ finishStream(status === "completed" ? "stop" : "other");
1224
+ }
1225
+ });
1226
+ const unsubscribeRequests = activeClient.onServerRequest((request) => {
1227
+ if (request.method !== "item/tool/call") {
1228
+ activeClient.rejectServerRequest(
1229
+ request.id,
1230
+ `Codex request "${request.method}" is not supported because Dexto executes tools and approvals itself.`
1231
+ );
1232
+ return;
1233
+ }
1234
+ const toolCall = parseDynamicToolCallRequest(request.params);
1235
+ if (!toolCall || toolCall.threadId !== thread.thread.id || toolCall.turnId !== turn.turn.id) {
1236
+ activeClient.rejectServerRequest(
1237
+ request.id,
1238
+ "Received an invalid Codex dynamic tool call payload."
1239
+ );
1240
+ return;
1241
+ }
1242
+ ensureStreamStarted();
1243
+ endText();
1244
+ controller.enqueue({
1245
+ type: "tool-input-start",
1246
+ id: toolCall.callId,
1247
+ toolName: toolCall.toolName
1248
+ });
1249
+ controller.enqueue({
1250
+ type: "tool-input-delta",
1251
+ id: toolCall.callId,
1252
+ delta: toolCall.input
1253
+ });
1254
+ controller.enqueue({
1255
+ type: "tool-input-end",
1256
+ id: toolCall.callId
1257
+ });
1258
+ controller.enqueue({
1259
+ type: "tool-call",
1260
+ toolCallId: toolCall.callId,
1261
+ toolName: toolCall.toolName,
1262
+ input: toolCall.input
1263
+ });
1264
+ finishStream("tool-calls");
1265
+ });
1266
+ if (callOptions.abortSignal) {
1267
+ const onAbort = () => {
1268
+ failStream(
1269
+ callOptions.abortSignal?.reason instanceof Error ? callOptions.abortSignal.reason : createCodexClientRuntimeError(
1270
+ "Codex generation aborted",
1271
+ { modelId: options.modelId },
1272
+ ErrorType.USER
1273
+ )
1274
+ );
1275
+ };
1276
+ if (callOptions.abortSignal.aborted) {
1277
+ onAbort();
1278
+ return;
1279
+ }
1280
+ callOptions.abortSignal.addEventListener("abort", onAbort, { once: true });
1281
+ offAbort = () => callOptions.abortSignal?.removeEventListener("abort", onAbort);
1282
+ }
1283
+ },
1284
+ cancel() {
1285
+ void activeClient.close();
1286
+ }
1287
+ });
1288
+ return {
1289
+ stream,
1290
+ request: {
1291
+ body: {
1292
+ provider: "codex-app-server",
1293
+ model: options.modelId,
1294
+ transcript,
1295
+ dynamicTools: dynamicTools.map((tool) => tool.name)
1296
+ }
1297
+ }
1298
+ };
1299
+ } catch (error) {
1300
+ await client?.close().catch(() => void 0);
1301
+ const mappedError = toCodexFailureMessage(error, options.modelId);
1302
+ if (mappedError instanceof DextoRuntimeError && mappedError.code === LLMErrorCode.RATE_LIMIT_EXCEEDED) {
1303
+ emitRateLimitStatus(buildUsageLimitSnapshot(latestRateLimitSnapshot));
1304
+ }
1305
+ throw mappedError;
1306
+ }
1307
+ }
1308
+ return {
1309
+ specificationVersion: "v2",
1310
+ provider: "codex-app-server",
1311
+ modelId: options.modelId,
1312
+ supportedUrls: {},
1313
+ async doGenerate(callOptions) {
1314
+ const execution = await executeTurn(callOptions);
1315
+ const reader = execution.stream.getReader();
1316
+ const content = [];
1317
+ let text = "";
1318
+ let finishReason = "other";
1319
+ const flushText = () => {
1320
+ if (!text) {
1321
+ return;
1322
+ }
1323
+ content.push({
1324
+ type: "text",
1325
+ text
1326
+ });
1327
+ text = "";
1328
+ };
1329
+ while (true) {
1330
+ const { done, value } = await reader.read();
1331
+ if (done) {
1332
+ break;
1333
+ }
1334
+ if (value.type === "text-delta") {
1335
+ text += value.delta;
1336
+ } else if (value.type === "tool-call") {
1337
+ flushText();
1338
+ content.push({
1339
+ type: "tool-call",
1340
+ toolCallId: value.toolCallId,
1341
+ toolName: value.toolName,
1342
+ input: value.input
1343
+ });
1344
+ } else if (value.type === "error") {
1345
+ throw toCodexFailureMessage(value.error, options.modelId);
1346
+ } else if (value.type === "finish") {
1347
+ finishReason = value.finishReason;
1348
+ }
1349
+ }
1350
+ flushText();
1351
+ return {
1352
+ content,
1353
+ finishReason,
1354
+ usage: createUsage(),
1355
+ warnings: buildWarnings(callOptions),
1356
+ request: execution.request
1357
+ };
1358
+ },
1359
+ async doStream(callOptions) {
1360
+ return await executeTurn(callOptions);
1361
+ }
1362
+ };
1363
+ }
1364
+ export {
1365
+ CodexAppServerClient,
1366
+ createCodexLanguageModel
1367
+ };