@janole/ai-sdk-provider-codex-asp 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@janole/ai-sdk-provider-codex-asp` is a [Vercel AI SDK](https://ai-sdk.dev/) v6 custom provider for the Codex App Server Protocol.
4
4
 
5
- Status: POC feature-complete for language model usage. Currently tested with [codex-cli](https://github.com/openai/codex/releases/tag/rust-v0.105.0) 0.105.0.
5
+ Status: POC feature-complete for language model usage. Currently tested with [codex-cli](https://github.com/openai/codex/releases/tag/rust-v0.106.0) 0.106.0.
6
6
 
7
7
  - `LanguageModelV3` provider implementation
8
8
  - Streaming (`streamText`) and non-streaming (`generateText`)
package/dist/index.cjs CHANGED
@@ -1,6 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var child_process = require('child_process');
4
+ var crypto = require('crypto');
5
+ var promises = require('fs/promises');
6
+ var os = require('os');
7
+ var path = require('path');
8
+ var url = require('url');
4
9
  var provider = require('@ai-sdk/provider');
5
10
 
6
11
  // src/utils/object.ts
@@ -274,6 +279,7 @@ var AppServerClient = class {
274
279
  // src/client/transport-persistent.ts
275
280
  var PersistentTransport = class {
276
281
  pool;
282
+ signal;
277
283
  worker = null;
278
284
  pendingInitializeId = null;
279
285
  initializeIntercepted = false;
@@ -282,9 +288,10 @@ var PersistentTransport = class {
282
288
  closeListeners = /* @__PURE__ */ new Set();
283
289
  constructor(settings) {
284
290
  this.pool = settings.pool;
291
+ this.signal = settings.signal;
285
292
  }
286
293
  async connect() {
287
- this.worker = this.pool.acquire();
294
+ this.worker = await this.pool.acquire(stripUndefined({ signal: this.signal }));
288
295
  await this.worker.ensureConnected();
289
296
  }
290
297
  disconnect() {
@@ -724,6 +731,7 @@ var CodexWorker = class {
724
731
  var CodexWorkerPool = class {
725
732
  workers;
726
733
  shutdownCalled = false;
734
+ waiters = [];
727
735
  constructor(settings) {
728
736
  const size = settings.poolSize ?? 1;
729
737
  const idleTimeoutMs = settings.idleTimeoutMs ?? 3e5;
@@ -735,7 +743,7 @@ var CodexWorkerPool = class {
735
743
  })
736
744
  );
737
745
  }
738
- acquire() {
746
+ async acquire(options) {
739
747
  if (this.shutdownCalled) {
740
748
  throw new CodexProviderError("Worker pool has been shut down.");
741
749
  }
@@ -743,20 +751,61 @@ var CodexWorkerPool = class {
743
751
  (w) => w.state === "idle" || w.state === "disconnected"
744
752
  );
745
753
  if (!worker) {
746
- throw new CodexProviderError(
747
- "All workers are busy. Try again later or increase poolSize."
748
- );
754
+ if (options?.signal?.aborted) {
755
+ throw new CodexProviderError("Worker acquisition aborted while waiting.");
756
+ }
757
+ return new Promise((resolve, reject) => {
758
+ const waiter = {
759
+ resolve,
760
+ reject,
761
+ signal: options?.signal,
762
+ abortHandler: void 0
763
+ };
764
+ if (waiter.signal) {
765
+ waiter.abortHandler = () => {
766
+ this.removeWaiter(waiter);
767
+ waiter.reject(new CodexProviderError("Worker acquisition aborted while waiting."));
768
+ };
769
+ waiter.signal.addEventListener("abort", waiter.abortHandler, { once: true });
770
+ }
771
+ this.waiters.push(waiter);
772
+ });
749
773
  }
750
774
  worker.acquire();
751
775
  return worker;
752
776
  }
753
777
  release(worker) {
754
- worker.release();
778
+ const waiter = this.waiters.shift();
779
+ if (waiter) {
780
+ this.clearWaiterAbortHandler(waiter);
781
+ waiter.resolve(worker);
782
+ } else {
783
+ worker.release();
784
+ }
755
785
  }
756
786
  async shutdown() {
757
787
  this.shutdownCalled = true;
788
+ while (this.waiters.length > 0) {
789
+ const waiter = this.waiters.shift();
790
+ this.clearWaiterAbortHandler(waiter);
791
+ waiter.reject(new CodexProviderError("Worker pool has been shut down."));
792
+ }
758
793
  await Promise.all(this.workers.map((w) => w.shutdown()));
759
794
  }
795
+ removeWaiter(target) {
796
+ const index = this.waiters.indexOf(target);
797
+ if (index >= 0) {
798
+ this.waiters.splice(index, 1);
799
+ }
800
+ }
801
+ /** Remove the abort listener so it doesn't fire after the waiter is already served. */
802
+ clearWaiterAbortHandler(waiter) {
803
+ if (!waiter.signal || !waiter.abortHandler) {
804
+ return;
805
+ }
806
+ waiter.signal.removeEventListener("abort", waiter.abortHandler);
807
+ waiter.abortHandler = void 0;
808
+ }
760
809
  };
761
810
 
762
811
  // src/dynamic-tools.ts
@@ -886,7 +935,7 @@ var DynamicToolsDispatcher = class {
886
935
  // package.json
887
936
  var package_default = {
888
937
  name: "@janole/ai-sdk-provider-codex-asp",
889
- version: "0.2.2"};
938
+ version: "0.2.4"};
890
939
 
891
940
  // src/package-info.ts
892
941
  var PACKAGE_NAME = package_default.name;
@@ -1273,8 +1322,6 @@ var CodexEventMapper = class {
1273
1322
  return parts;
1274
1323
  }
1275
1324
  };
1276
-
1277
- // src/protocol/prompt-mapper.ts
1278
1325
  function mapSystemPrompt(prompt) {
1279
1326
  const chunks = [];
1280
1327
  for (const message of prompt) {
@@ -1287,8 +1334,111 @@ function mapSystemPrompt(prompt) {
1287
1334
  }
1288
1335
  return chunks.length > 0 ? chunks.join("\n\n") : void 0;
1289
1336
  }
1290
- function mapPromptToTurnInput(prompt, isResume = false) {
1291
- if (isResume) {
1337
+ function textItem(text) {
1338
+ return { type: "text", text, text_elements: [] };
1339
+ }
1340
+ var MEDIA_TYPE_TO_EXT = {
1341
+ "image/png": ".png",
1342
+ "image/jpeg": ".jpg",
1343
+ "image/gif": ".gif",
1344
+ "image/webp": ".webp",
1345
+ "image/svg+xml": ".svg",
1346
+ "image/bmp": ".bmp",
1347
+ "image/tiff": ".tiff"
1348
+ };
1349
+ function extensionForMediaType(mediaType) {
1350
+ return MEDIA_TYPE_TO_EXT[mediaType] ?? ".bin";
1351
+ }
1352
+ var LocalFileWriter = class {
1353
+ async write(data, mediaType) {
1354
+ const ext = extensionForMediaType(mediaType);
1355
+ const filename = `codex-ai-sdk-${crypto.randomUUID()}${ext}`;
1356
+ const filepath = path.join(os.tmpdir(), filename);
1357
+ const buffer = typeof data === "string" ? Buffer.from(data, "base64") : data;
1358
+ await promises.writeFile(filepath, buffer);
1359
+ return url.pathToFileURL(filepath);
1360
+ }
1361
+ async cleanup(urls) {
1362
+ await Promise.allSettled(
1363
+ urls.filter((u) => u.protocol === "file:").map((u) => promises.unlink(u))
1364
+ );
1365
+ }
1366
+ };
1367
+ var PromptFileResolver = class {
1368
+ writer;
1369
+ written = [];
1370
+ constructor(writer) {
1371
+ this.writer = writer ?? new LocalFileWriter();
1372
+ }
1373
+ /**
1374
+ * Resolve inline file data and map user content to Codex input items.
1375
+ *
1376
+ * - Inline image data (base64 / Uint8Array) is written via the
1377
+ * {@link FileWriter} and converted to `localImage` or `image` items.
1378
+ * - URL-based image file parts are converted directly.
1379
+ * - Inline text file data is decoded and inlined as text.
1380
+ * - Unsupported media types are silently skipped.
1381
+ *
1382
+ * @param isResume - When true only the last user message is extracted.
1383
+ * When false (fresh thread) all user text is accumulated with images
1384
+ * flushing the text buffer to preserve ordering.
1385
+ */
1386
+ async resolve(prompt, isResume = false) {
1387
+ if (isResume) {
1388
+ return this.resolveResumed(prompt);
1389
+ }
1390
+ return this.resolveFresh(prompt);
1391
+ }
1392
+ /**
1393
+ * Remove all files created by previous {@link resolve} calls.
1394
+ * Best-effort — never throws.
1395
+ */
1396
+ async cleanup() {
1397
+ const urls = this.written.splice(0);
1398
+ if (urls.length > 0) {
1399
+ await this.writer.cleanup(urls);
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Convert a resolved image URL to a Codex input item.
1404
+ */
1405
+ mapImageUrl(mediaType, data) {
1406
+ if (!mediaType.startsWith("image/")) {
1407
+ return null;
1408
+ }
1409
+ if (data.protocol === "file:") {
1410
+ return { type: "localImage", path: url.fileURLToPath(data) };
1411
+ }
1412
+ return { type: "image", url: data.href };
1413
+ }
1414
+ /**
1415
+ * Resolve a single file part: write inline data via the writer, then
1416
+ * convert to a Codex input item. Text files are decoded and returned
1417
+ * as text items. Returns `null` for unsupported media types.
1418
+ */
1419
+ async resolveFilePart(part) {
1420
+ const { mediaType, data } = part;
1421
+ if (mediaType.startsWith("text/")) {
1422
+ if (data instanceof URL) {
1423
+ return textItem(data.href);
1424
+ }
1425
+ const text = typeof data === "string" ? Buffer.from(data, "base64").toString("utf-8") : new TextDecoder().decode(data);
1426
+ return textItem(text);
1427
+ }
1428
+ if (mediaType.startsWith("image/") && !(data instanceof URL)) {
1429
+ const url = await this.writer.write(data, mediaType);
1430
+ this.written.push(url);
1431
+ return this.mapImageUrl(mediaType, url);
1432
+ }
1433
+ if (data instanceof URL) {
1434
+ return this.mapImageUrl(mediaType, data);
1435
+ }
1436
+ return null;
1437
+ }
1438
+ /**
1439
+ * Resume path: extract parts from the last user message individually.
1440
+ */
1441
+ async resolveResumed(prompt) {
1292
1442
  for (let i = prompt.length - 1; i >= 0; i--) {
1293
1443
  const message = prompt[i];
1294
1444
  if (message?.role === "user") {
@@ -1297,7 +1447,12 @@ function mapPromptToTurnInput(prompt, isResume = false) {
1297
1447
  if (part.type === "text") {
1298
1448
  const text = part.text.trim();
1299
1449
  if (text.length > 0) {
1300
- items.push({ type: "text", text, text_elements: [] });
1450
+ items.push(textItem(text));
1451
+ }
1452
+ } else if (part.type === "file") {
1453
+ const mapped = await this.resolveFilePart(part);
1454
+ if (mapped) {
1455
+ items.push(mapped);
1301
1456
  }
1302
1457
  }
1303
1458
  }
@@ -1306,21 +1461,45 @@ function mapPromptToTurnInput(prompt, isResume = false) {
1306
1461
  }
1307
1462
  return [];
1308
1463
  }
1309
- const chunks = [];
1310
- for (const message of prompt) {
1311
- if (message.role === "user") {
1312
- for (const part of message.content) {
1313
- if (part.type === "text") {
1314
- const text = part.text.trim();
1315
- if (text.length > 0) {
1316
- chunks.push(text);
1464
+ /**
1465
+ * Fresh thread path: accumulate text chunks across all user messages,
1466
+ * flushing before each image to preserve ordering.
1467
+ */
1468
+ async resolveFresh(prompt) {
1469
+ const items = [];
1470
+ const textChunks = [];
1471
+ const flushText = () => {
1472
+ if (textChunks.length > 0) {
1473
+ items.push(textItem(textChunks.join("\n\n")));
1474
+ textChunks.length = 0;
1475
+ }
1476
+ };
1477
+ for (const message of prompt) {
1478
+ if (message.role === "user") {
1479
+ for (const part of message.content) {
1480
+ if (part.type === "text") {
1481
+ const text = part.text.trim();
1482
+ if (text.length > 0) {
1483
+ textChunks.push(text);
1484
+ }
1485
+ } else if (part.type === "file") {
1486
+ const mapped = await this.resolveFilePart(part);
1487
+ if (mapped) {
1488
+ if (mapped.type === "text") {
1489
+ textChunks.push(mapped.text);
1490
+ } else {
1491
+ flushText();
1492
+ items.push(mapped);
1493
+ }
1494
+ }
1317
1495
  }
1318
1496
  }
1319
1497
  }
1320
1498
  }
1499
+ flushText();
1500
+ return items;
1321
1501
  }
1322
- return [{ type: "text", text: chunks.join("\n\n"), text_elements: [] }];
1323
- }
1502
+ };
1324
1503
 
1325
1504
  // src/model.ts
1326
1505
  function createEmptyUsage() {
@@ -1555,7 +1734,7 @@ var CodexLanguageModel = class {
1555
1734
  });
1556
1735
  }
1557
1736
  doStream(options) {
1558
- const transport = this.config.providerSettings.transportFactory ? this.config.providerSettings.transportFactory() : this.config.providerSettings.transport?.type === "websocket" ? new WebSocketTransport(this.config.providerSettings.transport.websocket) : new StdioTransport(this.config.providerSettings.transport?.stdio);
1737
+ const transport = this.config.providerSettings.transportFactory ? this.config.providerSettings.transportFactory(options.abortSignal) : this.config.providerSettings.transport?.type === "websocket" ? new WebSocketTransport(this.config.providerSettings.transport.websocket) : new StdioTransport(this.config.providerSettings.transport?.stdio);
1559
1738
  const packetLogger = this.config.providerSettings.debug?.logPackets === true ? this.config.providerSettings.debug.logger ?? ((packet) => {
1560
1739
  if (packet.direction === "inbound") {
1561
1740
  console.debug("[codex packet]", packet.message);
@@ -1587,6 +1766,7 @@ var CodexLanguageModel = class {
1587
1766
  debugLog?.("outbound", "turn/interrupt", interruptParams);
1588
1767
  await client.request("turn/interrupt", interruptParams, interruptTimeoutMs);
1589
1768
  };
1769
+ const fileResolver = new PromptFileResolver();
1590
1770
  const stream = new ReadableStream({
1591
1771
  start: (controller) => {
1592
1772
  let closed = false;
@@ -1599,6 +1779,7 @@ var CodexLanguageModel = class {
1599
1779
  try {
1600
1780
  controller.close();
1601
1781
  } finally {
1782
+ await fileResolver.cleanup();
1602
1783
  await client.disconnect();
1603
1784
  }
1604
1785
  };
@@ -1610,6 +1791,7 @@ var CodexLanguageModel = class {
1610
1791
  try {
1611
1792
  controller.close();
1612
1793
  } finally {
1794
+ await fileResolver.cleanup();
1613
1795
  await client.disconnect();
1614
1796
  }
1615
1797
  };
@@ -1834,7 +2016,7 @@ var CodexLanguageModel = class {
1834
2016
  closeSuccessfully
1835
2017
  );
1836
2018
  }
1837
- const turnInput = mapPromptToTurnInput(options.prompt, !!resumeThreadId);
2019
+ const turnInput = await fileResolver.resolve(options.prompt, !!resumeThreadId);
1838
2020
  const turnStartParams = stripUndefined({
1839
2021
  threadId,
1840
2022
  input: turnInput,
@@ -1858,6 +2040,7 @@ var CodexLanguageModel = class {
1858
2040
  await interruptTurnIfPossible();
1859
2041
  } catch {
1860
2042
  }
2043
+ await fileResolver.cleanup();
1861
2044
  await client.disconnect();
1862
2045
  }
1863
2046
  });
@@ -1969,7 +2152,7 @@ function createCodexAppServer(settings = {}) {
1969
2152
  });
1970
2153
  }
1971
2154
  const persistentPool = persistentPoolHandle?.pool ?? null;
1972
- const effectiveTransportFactory = persistentPool ? () => new PersistentTransport({ pool: persistentPool }) : baseTransportFactory;
2155
+ const effectiveTransportFactory = persistentPool ? (signal) => new PersistentTransport(stripUndefined({ pool: persistentPool, signal })) : baseTransportFactory;
1973
2156
  const resolvedSettings = Object.freeze(stripUndefined({
1974
2157
  defaultModel: settings.defaultModel,
1975
2158
  experimentalApi: settings.experimentalApi,
@@ -2039,16 +2222,17 @@ exports.CodexWorker = CodexWorker;
2039
2222
  exports.CodexWorkerPool = CodexWorkerPool;
2040
2223
  exports.DynamicToolsDispatcher = DynamicToolsDispatcher;
2041
2224
  exports.JsonRpcError = JsonRpcError;
2225
+ exports.LocalFileWriter = LocalFileWriter;
2042
2226
  exports.PACKAGE_NAME = PACKAGE_NAME;
2043
2227
  exports.PACKAGE_VERSION = PACKAGE_VERSION;
2044
2228
  exports.PersistentTransport = PersistentTransport;
2229
+ exports.PromptFileResolver = PromptFileResolver;
2045
2230
  exports.StdioTransport = StdioTransport;
2046
2231
  exports.WebSocketTransport = WebSocketTransport;
2047
2232
  exports.codexAppServer = codexAppServer;
2048
2233
  exports.codexProviderMetadata = codexProviderMetadata;
2049
2234
  exports.createCodexAppServer = createCodexAppServer;
2050
2235
  exports.createCodexProvider = createCodexProvider;
2051
- exports.mapPromptToTurnInput = mapPromptToTurnInput;
2052
2236
  exports.mapSystemPrompt = mapSystemPrompt;
2053
2237
  exports.withProviderMetadata = withProviderMetadata;
2054
2238
  //# sourceMappingURL=index.cjs.map