@proposit/proposit-core 1.10.0 → 2.0.0

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 (116) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/expressions.d.ts +29 -0
  3. package/dist/cli/commands/expressions.d.ts.map +1 -1
  4. package/dist/cli/commands/expressions.js +118 -92
  5. package/dist/cli/commands/expressions.js.map +1 -1
  6. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.d.ts.map +1 -1
  7. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js +18 -2
  8. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js.map +1 -1
  9. package/dist/extensions/{ieee → citations/ieee}/citation-claim.d.ts +53 -59
  10. package/dist/extensions/{ieee → citations/ieee}/citation-claim.d.ts.map +1 -1
  11. package/dist/extensions/{ieee → citations/ieee}/citation-claim.js +2 -2
  12. package/dist/extensions/citations/ieee/citation-claim.js.map +1 -0
  13. package/dist/extensions/citations/ieee/formatting.d.ts.map +1 -0
  14. package/dist/extensions/{ieee → citations/ieee}/formatting.js +0 -1
  15. package/dist/extensions/citations/ieee/formatting.js.map +1 -0
  16. package/dist/extensions/citations/ieee/index.d.ts.map +1 -0
  17. package/dist/extensions/citations/ieee/index.js.map +1 -0
  18. package/dist/extensions/{ieee → citations/ieee}/references.d.ts +161 -181
  19. package/dist/extensions/citations/ieee/references.d.ts.map +1 -0
  20. package/dist/extensions/{ieee → citations/ieee}/references.js +48 -22
  21. package/dist/extensions/citations/ieee/references.js.map +1 -0
  22. package/dist/extensions/{ieee → citations/ieee}/relaxed.d.ts +159 -180
  23. package/dist/extensions/citations/ieee/relaxed.d.ts.map +1 -0
  24. package/dist/extensions/{ieee → citations/ieee}/relaxed.js +1 -4
  25. package/dist/extensions/{ieee → citations/ieee}/relaxed.js.map +1 -1
  26. package/dist/extensions/citations/ieee/segment-builder.d.ts.map +1 -0
  27. package/dist/extensions/citations/ieee/segment-builder.js.map +1 -0
  28. package/dist/extensions/{ieee → citations/ieee}/segment-templates.d.ts +0 -1
  29. package/dist/extensions/citations/ieee/segment-templates.d.ts.map +1 -0
  30. package/dist/extensions/{ieee → citations/ieee}/segment-templates.js +0 -27
  31. package/dist/extensions/citations/ieee/segment-templates.js.map +1 -0
  32. package/dist/extensions/citations/unparsed/index.d.ts +2 -0
  33. package/dist/extensions/citations/unparsed/index.d.ts.map +1 -0
  34. package/dist/extensions/citations/unparsed/index.js +2 -0
  35. package/dist/extensions/citations/unparsed/index.js.map +1 -0
  36. package/dist/extensions/citations/unparsed/unparsed-citation.d.ts +11 -0
  37. package/dist/extensions/citations/unparsed/unparsed-citation.d.ts.map +1 -0
  38. package/dist/extensions/citations/unparsed/unparsed-citation.js +22 -0
  39. package/dist/extensions/citations/unparsed/unparsed-citation.js.map +1 -0
  40. package/dist/extensions/openai/errors.d.ts +8 -0
  41. package/dist/extensions/openai/errors.d.ts.map +1 -1
  42. package/dist/extensions/openai/errors.js +58 -0
  43. package/dist/extensions/openai/errors.js.map +1 -1
  44. package/dist/extensions/openai/index.d.ts +4 -2
  45. package/dist/extensions/openai/index.d.ts.map +1 -1
  46. package/dist/extensions/openai/index.js +2 -1
  47. package/dist/extensions/openai/index.js.map +1 -1
  48. package/dist/extensions/openai/openai-http.d.ts +30 -0
  49. package/dist/extensions/openai/openai-http.d.ts.map +1 -0
  50. package/dist/extensions/openai/openai-http.js +310 -0
  51. package/dist/extensions/openai/openai-http.js.map +1 -0
  52. package/dist/extensions/openai/openai-parsing.d.ts +34 -0
  53. package/dist/extensions/openai/openai-parsing.d.ts.map +1 -0
  54. package/dist/extensions/openai/openai-parsing.js +226 -0
  55. package/dist/extensions/openai/openai-parsing.js.map +1 -0
  56. package/dist/extensions/openai/openai-retrieval.d.ts +150 -0
  57. package/dist/extensions/openai/openai-retrieval.d.ts.map +1 -0
  58. package/dist/extensions/openai/openai-retrieval.js +248 -0
  59. package/dist/extensions/openai/openai-retrieval.js.map +1 -0
  60. package/dist/extensions/openai/openai-tools.d.ts +9 -0
  61. package/dist/extensions/openai/openai-tools.d.ts.map +1 -0
  62. package/dist/extensions/openai/openai-tools.js +93 -0
  63. package/dist/extensions/openai/openai-tools.js.map +1 -0
  64. package/dist/extensions/openai/provider.d.ts +1 -100
  65. package/dist/extensions/openai/provider.d.ts.map +1 -1
  66. package/dist/extensions/openai/provider.js +5 -794
  67. package/dist/extensions/openai/provider.js.map +1 -1
  68. package/dist/extensions/openai/types.d.ts +1 -0
  69. package/dist/extensions/openai/types.d.ts.map +1 -1
  70. package/dist/extensions/openai/types.js +4 -1
  71. package/dist/extensions/openai/types.js.map +1 -1
  72. package/dist/lib/core/premise-engine.d.ts +15 -0
  73. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  74. package/dist/lib/core/premise-engine.js +62 -127
  75. package/dist/lib/core/premise-engine.js.map +1 -1
  76. package/dist/lib/index.d.ts +3 -3
  77. package/dist/lib/index.d.ts.map +1 -1
  78. package/dist/lib/index.js +1 -1
  79. package/dist/lib/index.js.map +1 -1
  80. package/dist/lib/llm/index.d.ts +1 -1
  81. package/dist/lib/llm/index.d.ts.map +1 -1
  82. package/dist/lib/llm/types.d.ts +40 -0
  83. package/dist/lib/llm/types.d.ts.map +1 -1
  84. package/dist/lib/parsing/prompt-builder.d.ts.map +1 -1
  85. package/dist/lib/parsing/prompt-builder.js +14 -3
  86. package/dist/lib/parsing/prompt-builder.js.map +1 -1
  87. package/dist/lib/pipelines/execute.d.ts +175 -3
  88. package/dist/lib/pipelines/execute.d.ts.map +1 -1
  89. package/dist/lib/pipelines/execute.js +574 -219
  90. package/dist/lib/pipelines/execute.js.map +1 -1
  91. package/dist/lib/pipelines/index.d.ts +3 -3
  92. package/dist/lib/pipelines/index.d.ts.map +1 -1
  93. package/dist/lib/pipelines/index.js +2 -2
  94. package/dist/lib/pipelines/index.js.map +1 -1
  95. package/dist/lib/pipelines/stage-helpers.d.ts +89 -1
  96. package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
  97. package/dist/lib/pipelines/stage-helpers.js +225 -31
  98. package/dist/lib/pipelines/stage-helpers.js.map +1 -1
  99. package/package.json +10 -5
  100. package/dist/extensions/ieee/citation-claim.js.map +0 -1
  101. package/dist/extensions/ieee/formatting.d.ts.map +0 -1
  102. package/dist/extensions/ieee/formatting.js.map +0 -1
  103. package/dist/extensions/ieee/index.d.ts.map +0 -1
  104. package/dist/extensions/ieee/index.js.map +0 -1
  105. package/dist/extensions/ieee/references.d.ts.map +0 -1
  106. package/dist/extensions/ieee/references.js.map +0 -1
  107. package/dist/extensions/ieee/relaxed.d.ts.map +0 -1
  108. package/dist/extensions/ieee/segment-builder.d.ts.map +0 -1
  109. package/dist/extensions/ieee/segment-builder.js.map +0 -1
  110. package/dist/extensions/ieee/segment-templates.d.ts.map +0 -1
  111. package/dist/extensions/ieee/segment-templates.js.map +0 -1
  112. /package/dist/extensions/{ieee → citations/ieee}/formatting.d.ts +0 -0
  113. /package/dist/extensions/{ieee → citations/ieee}/index.d.ts +0 -0
  114. /package/dist/extensions/{ieee → citations/ieee}/index.js +0 -0
  115. /package/dist/extensions/{ieee → citations/ieee}/segment-builder.d.ts +0 -0
  116. /package/dist/extensions/{ieee → citations/ieee}/segment-builder.js +0 -0
@@ -30,9 +30,12 @@
30
30
  // readability.
31
31
  import { debugLlmFailure, debugLlmRequest, debugLlmResponse, } from "../../lib/pipelines/debug-log.js";
32
32
  import { typeboxToOpenAiSchema } from "./structured-output.js";
33
- import { NonRetryableLlmError, QuotaExhaustedLlmError, RateLimitLlmError, ResponseNotFoundError, SchemaValidationLlmError, ToolLoopExhaustedError, TransientLlmError, } from "./errors.js";
33
+ import { deriveSchemaName, findFunctionHandler, translateTools, } from "./openai-tools.js";
34
+ import { fetchResponseEnvelope } from "./openai-http.js";
35
+ import { formatIncompleteMessage, NonRetryableLlmError, ToolLoopExhaustedError, TransientLlmError, } from "./errors.js";
36
+ import { DEFAULT_BASE_URL, } from "./types.js";
37
+ import { extractAssistantText, extractUsage, mergeUsage, pickFunctionCalls, safeParseJson, } from "./openai-parsing.js";
34
38
  const STAGE_ID_MARKER = /<!--\s*stage-id:\s*([^\s>]+)\s*-->/;
35
- const DEFAULT_BASE_URL = "https://api.openai.com/v1/responses";
36
39
  const DEFAULT_MAX_TOOL_ROUNDS = 6;
37
40
  export function createOpenAiResponsesProvider(options) {
38
41
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
@@ -321,796 +324,4 @@ export function createOpenAiResponsesProvider(options) {
321
324
  };
322
325
  return { respond };
323
326
  }
324
- /**
325
- * Retrieve a stored OpenAI response by id. Surfaces the current
326
- * status, output text (when completed), and token usage.
327
- *
328
- * Throws {@link ResponseNotFoundError} when the response is not found
329
- * (HTTP 404), which typically means the ~10-minute retention window
330
- * has elapsed. Callers should clear the stored id, settle the
331
- * associated stage as failed, and surface a retry prompt.
332
- *
333
- * @param id - The OpenAI response id to retrieve.
334
- * @param options - Provider configuration (apiKey, optional baseUrl and fetch).
335
- */
336
- export async function retrieveResponse(id, options) {
337
- const fetchImpl = resolveFetch(options.fetch, "retrieveResponse");
338
- const envelope = await getResponseById({
339
- url: options.baseUrl ?? DEFAULT_BASE_URL,
340
- id,
341
- apiKey: options.apiKey,
342
- fetchImpl,
343
- signal: options.signal,
344
- });
345
- return envelopeToRetrievedResponse(envelope, id);
346
- }
347
- /**
348
- * Reconnect to a stored, still-generating background response and
349
- * **stream it to completion**. This is what actually drives a dropped
350
- * background response forward: a passive `retrieveResponse` GET only
351
- * reads the current state and leaves a `queued` / `in_progress`
352
- * response sitting where it is, whereas reconnecting with `stream=true`
353
- * resumes consumption so the response reaches a terminal status.
354
- *
355
- * Issues `GET /responses/{id}?stream=true&starting_after=<cursor>` and
356
- * consumes the SSE stream to its terminal event, returning the same
357
- * {@link TRetrievedResponse} shape as {@link retrieveResponse}.
358
- *
359
- * Throws {@link ResponseNotFoundError} when the response is not found
360
- * (HTTP 404 — typically the ~10-minute retention window elapsed).
361
- * Honors `signal`: an abort propagates as an `AbortError` from the
362
- * underlying stream read.
363
- *
364
- * @param id - The OpenAI response id to reconnect to.
365
- * @param options - `apiKey`, optional `startingAfter` SSE cursor
366
- * (defaults to 0 — replay from the start of the stored stream),
367
- * optional `baseUrl`, `fetch`, and `signal`.
368
- */
369
- export async function reconnectStream(id, options) {
370
- const fetchImpl = resolveFetch(options.fetch, "reconnectStream");
371
- const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
372
- const startingAfter = options.startingAfter ?? 0;
373
- const url = `${baseUrl}/${id}?stream=true&starting_after=${startingAfter.toString()}`;
374
- let response;
375
- try {
376
- response = await fetchImpl(url, {
377
- method: "GET",
378
- headers: { Authorization: `Bearer ${options.apiKey}` },
379
- signal: options.signal,
380
- });
381
- }
382
- catch (err) {
383
- if (isAbortError(err))
384
- throw err;
385
- throw new TransientLlmError({
386
- message: `Network error reconnecting to OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
387
- });
388
- }
389
- if (response.status === 404) {
390
- throw new ResponseNotFoundError({ responseId: id });
391
- }
392
- if (!response.ok) {
393
- const errorBody = await response.text().catch(() => "");
394
- throw classifyHttpError(response.status, `OpenAI reconnect ${response.status.toString()}: ${errorBody || response.statusText}`);
395
- }
396
- const envelope = await readSseEnvelope(response);
397
- return envelopeToRetrievedResponse(envelope, id);
398
- }
399
- /**
400
- * Cancel a stored, in-flight OpenAI response. Issues
401
- * `POST /responses/{id}/cancel` and returns the resulting
402
- * {@link TRetrievedResponse} (typically `status: "cancelled"`).
403
- *
404
- * Cancel is **idempotent** per the Responses API: cancelling twice, or
405
- * cancelling an already-terminal response, simply returns the final
406
- * `Response` object rather than erroring — so callers do not need to
407
- * guard against double-cancel.
408
- *
409
- * Throws {@link ResponseNotFoundError} when the response is not found
410
- * (HTTP 404 — typically the ~10-minute retention window elapsed).
411
- * Honors `signal` (an abort propagates as an `AbortError`).
412
- *
413
- * Use this to stop an in-flight background response when a stage is
414
- * abandoned (resync timeout) or an import is cancelled, so generation
415
- * does not keep running (and billing) server-side after the consumer
416
- * has given up on it.
417
- *
418
- * @param id - The OpenAI response id to cancel.
419
- * @param options - `apiKey`, optional `baseUrl`, `fetch`, and `signal`.
420
- */
421
- export async function cancelResponse(id, options) {
422
- const fetchImpl = resolveFetch(options.fetch, "cancelResponse");
423
- const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
424
- let response;
425
- try {
426
- response = await fetchImpl(`${baseUrl}/${id}/cancel`, {
427
- method: "POST",
428
- headers: { Authorization: `Bearer ${options.apiKey}` },
429
- signal: options.signal,
430
- });
431
- }
432
- catch (err) {
433
- if (isAbortError(err))
434
- throw err;
435
- throw new TransientLlmError({
436
- message: `Network error cancelling OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
437
- });
438
- }
439
- if (response.status === 404) {
440
- throw new ResponseNotFoundError({ responseId: id });
441
- }
442
- if (!response.ok) {
443
- const errorBody = await response.text().catch(() => "");
444
- throw classifyHttpError(response.status, `OpenAI cancel ${response.status.toString()}: ${errorBody || response.statusText}`);
445
- }
446
- const envelope = await parseJsonOrThrowTransient(response, "OpenAI cancel body was not valid JSON");
447
- return envelopeToRetrievedResponse(envelope, id);
448
- }
449
- function resolveFetch(injected, fnName) {
450
- const fetchImpl = injected ?? globalThis.fetch;
451
- if (!fetchImpl) {
452
- throw new Error(`${fnName}: no fetch implementation available. Pass \`fetch\` explicitly or run in an environment that provides \`globalThis.fetch\` (Node ≥18, modern browsers, Expo).`);
453
- }
454
- return fetchImpl;
455
- }
456
- function envelopeToRetrievedResponse(envelope, id) {
457
- const output = extractAssistantText(envelope.output);
458
- const usage = extractUsage(envelope);
459
- const result = {
460
- status: (envelope.status ?? "in_progress"),
461
- rawResponseId: envelope.id ?? id,
462
- };
463
- if (output !== undefined) {
464
- result.output = output;
465
- }
466
- if (usage.input > 0 || usage.output > 0) {
467
- result.tokenUsage = usage;
468
- }
469
- return result;
470
- }
471
- // -- HTTP --
472
- async function fetchResponseEnvelope(args) {
473
- if (args.backgroundStream) {
474
- return runBackgroundStream({
475
- url: args.url,
476
- apiKey: args.apiKey,
477
- body: args.body,
478
- fetchImpl: args.fetchImpl,
479
- signal: args.signal,
480
- onResponseId: args.onResponseId,
481
- });
482
- }
483
- if (args.background) {
484
- return runBackground({
485
- url: args.url,
486
- apiKey: args.apiKey,
487
- body: args.body,
488
- fetchImpl: args.fetchImpl,
489
- signal: args.signal,
490
- pollIntervalMs: args.pollIntervalMs,
491
- });
492
- }
493
- if (args.stream) {
494
- const response = await callOnce({
495
- url: args.url,
496
- apiKey: args.apiKey,
497
- body: { ...args.body, stream: true },
498
- fetchImpl: args.fetchImpl,
499
- signal: args.signal,
500
- });
501
- return readSseEnvelope(response);
502
- }
503
- const response = await callOnce({
504
- url: args.url,
505
- apiKey: args.apiKey,
506
- body: args.body,
507
- fetchImpl: args.fetchImpl,
508
- signal: args.signal,
509
- });
510
- return parseJsonOrThrowTransient(response, "OpenAI response body was not valid JSON");
511
- }
512
- async function parseJsonOrThrowTransient(response, context) {
513
- return response
514
- .json()
515
- .then((j) => j)
516
- .catch((err) => {
517
- throw new TransientLlmError({
518
- message: `${context}: ${err instanceof Error ? err.message : String(err)}`,
519
- });
520
- });
521
- }
522
- function abortError() {
523
- const e = new Error("The OpenAI background request was aborted.");
524
- e.name = "AbortError";
525
- return e;
526
- }
527
- // Resolves (never rejects) on abort by design: the poll loop owns the
528
- // abort→cancel→throw decision at exactly two checkpoints (top-of-loop and
529
- // the in-flight-GET catch), so this helper just needs to wake the loop
530
- // promptly instead of waiting out the full interval. Keeping it
531
- // non-throwing avoids a second, competing abort surface.
532
- function abortableDelay(ms, signal) {
533
- return new Promise((resolve) => {
534
- if (signal?.aborted) {
535
- resolve();
536
- return;
537
- }
538
- const onAbort = () => {
539
- cleanup();
540
- resolve();
541
- };
542
- const cleanup = () => {
543
- clearTimeout(timer);
544
- signal?.removeEventListener("abort", onAbort);
545
- };
546
- const timer = setTimeout(() => {
547
- cleanup();
548
- resolve();
549
- }, ms);
550
- signal?.addEventListener("abort", onAbort, { once: true });
551
- });
552
- }
553
- async function getResponseById(args) {
554
- let response;
555
- try {
556
- response = await args.fetchImpl(`${args.url}/${args.id}`, {
557
- method: "GET",
558
- headers: { Authorization: `Bearer ${args.apiKey}` },
559
- signal: args.signal,
560
- });
561
- }
562
- catch (err) {
563
- if (isAbortError(err))
564
- throw err;
565
- throw new TransientLlmError({
566
- message: `Network error polling OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
567
- });
568
- }
569
- if (response.status === 404) {
570
- // Response has aged out of the ~10-minute retention window or
571
- // was never stored. Surface as a typed error so callers can
572
- // clear the stored id and settle the stage as failed.
573
- throw new ResponseNotFoundError({ responseId: args.id });
574
- }
575
- if (!response.ok) {
576
- const errorBody = await response.text().catch(() => "");
577
- throw classifyHttpError(response.status, `OpenAI poll ${response.status.toString()}: ${errorBody || response.statusText}`);
578
- }
579
- return parseJsonOrThrowTransient(response, "OpenAI poll body was not valid JSON");
580
- }
581
- // Intentionally takes no AbortSignal — cancel must fire even though the
582
- // caller's signal has already aborted; passing the fired signal would
583
- // abort the cancel itself.
584
- async function cancelBackground(args) {
585
- // Best-effort + idempotent — swallow errors; the abort is surfaced
586
- // regardless of whether cancel succeeds.
587
- try {
588
- await args.fetchImpl(`${args.url}/${args.id}/cancel`, {
589
- method: "POST",
590
- headers: { Authorization: `Bearer ${args.apiKey}` },
591
- });
592
- }
593
- catch {
594
- // ignore
595
- }
596
- }
597
- function isTerminalBackgroundStatus(status) {
598
- return (status === "completed" ||
599
- status === "failed" ||
600
- status === "incomplete" ||
601
- status === "cancelled");
602
- }
603
- /**
604
- * Submit a single request with `{ background: true, stream: true,
605
- * store: true }` and consume the resulting SSE stream live, returning
606
- * the terminal envelope.
607
- *
608
- * A background response can only be streamed if it was *created* with
609
- * `stream: true` (a background-without-stream response is poll-only and
610
- * cannot later be streamed), so this mode uses one streaming create
611
- * call rather than a separate non-streaming submit POST. The response
612
- * id is therefore not in a JSON POST body — it arrives in the first
613
- * `response.created` SSE event. `onResponseId` fires the moment that
614
- * event is parsed (before the terminal event), so the caller can
615
- * persist the id while the call is still in flight.
616
- *
617
- * The response keeps generating server-side even if the connection
618
- * drops during stream consumption, and can be recovered via
619
- * `retrieveResponse` within the ~10-minute retention window. A
620
- * connection drop mid-stream (no terminal event before stream end) is
621
- * classified as a `TransientLlmError` so the framework's retry policy
622
- * applies — but because the id was already surfaced mid-flight, a
623
- * crashed in-flight call can be recovered rather than blindly re-run.
624
- */
625
- async function runBackgroundStream(args) {
626
- if (args.signal?.aborted)
627
- throw abortError();
628
- const httpResponse = await callOnce({
629
- url: args.url,
630
- apiKey: args.apiKey,
631
- body: { ...args.body, background: true, stream: true, store: true },
632
- fetchImpl: args.fetchImpl,
633
- signal: args.signal,
634
- });
635
- return readSseEnvelope(httpResponse, args.onResponseId);
636
- }
637
- async function runBackground(args) {
638
- if (args.signal?.aborted)
639
- throw abortError();
640
- const submit = await callOnce({
641
- url: args.url,
642
- apiKey: args.apiKey,
643
- body: { ...args.body, background: true, store: true },
644
- fetchImpl: args.fetchImpl,
645
- signal: args.signal,
646
- });
647
- const submitEnvelope = await parseJsonOrThrowTransient(submit, "OpenAI background submit body was not valid JSON");
648
- const id = submitEnvelope.id;
649
- if (!id) {
650
- throw new TransientLlmError({
651
- message: "OpenAI background submit returned no response id.",
652
- });
653
- }
654
- // Fast-path: a small/cached request can come back already terminal on
655
- // submit — return it directly rather than issuing a redundant poll GET
656
- // (and avoid a `store`-expiry window between submit and first poll).
657
- if (isTerminalBackgroundStatus(submitEnvelope.status)) {
658
- if (submitEnvelope.status === "cancelled")
659
- throw abortError();
660
- return submitEnvelope;
661
- }
662
- for (;;) {
663
- if (args.signal?.aborted) {
664
- await cancelBackground({
665
- url: args.url,
666
- id,
667
- apiKey: args.apiKey,
668
- fetchImpl: args.fetchImpl,
669
- });
670
- throw abortError();
671
- }
672
- let env;
673
- try {
674
- env = await getResponseById({
675
- url: args.url,
676
- id,
677
- apiKey: args.apiKey,
678
- fetchImpl: args.fetchImpl,
679
- signal: args.signal,
680
- });
681
- }
682
- catch (err) {
683
- // Abort landing DURING an in-flight poll GET surfaces as an
684
- // AbortError from getResponseById (real fetch rejects the
685
- // in-flight request). The top-of-loop check alone would miss
686
- // it, so cancel here before re-throwing — this is what makes
687
- // the "AbortSignal → cancel POST" guarantee hold mid-poll.
688
- if (isAbortError(err)) {
689
- await cancelBackground({
690
- url: args.url,
691
- id,
692
- apiKey: args.apiKey,
693
- fetchImpl: args.fetchImpl,
694
- });
695
- throw abortError();
696
- }
697
- throw err;
698
- }
699
- const status = env.status;
700
- // Deliberately inlined rather than calling
701
- // `isTerminalBackgroundStatus`: `cancelled` is terminal too but
702
- // needs the opposite disposition (throw `abortError()` vs. return
703
- // the envelope), so the two terminal cases are split here.
704
- if (status === "completed" ||
705
- status === "failed" ||
706
- status === "incomplete") {
707
- return env;
708
- }
709
- if (status === "cancelled") {
710
- throw abortError();
711
- }
712
- await abortableDelay(args.pollIntervalMs, args.signal);
713
- }
714
- }
715
- async function callOnce(args) {
716
- let response;
717
- try {
718
- response = await args.fetchImpl(args.url, {
719
- method: "POST",
720
- headers: {
721
- "Content-Type": "application/json",
722
- Authorization: `Bearer ${args.apiKey}`,
723
- },
724
- body: JSON.stringify(args.body),
725
- signal: args.signal,
726
- });
727
- }
728
- catch (err) {
729
- // Aborts come through as a thrown error with `name === "AbortError"`
730
- // (DOM spec). Surface as-is so `llmStage`'s mid-flight-abort
731
- // detector turns it into `StageAbortedError`. Other low-level
732
- // fetch failures are transient.
733
- if (isAbortError(err)) {
734
- throw err;
735
- }
736
- throw new TransientLlmError({
737
- message: `Network error calling OpenAI: ${err instanceof Error ? err.message : String(err)}`,
738
- });
739
- }
740
- if (response.ok) {
741
- return response;
742
- }
743
- const errorBody = await response.text().catch(() => "");
744
- const message = `OpenAI Responses API ${response.status.toString()}: ${errorBody || response.statusText}`;
745
- // Best-effort structured extraction of the provider error
746
- // code/type so a 429 can be split into persistent quota exhaustion
747
- // vs. transient throttling. Never throws on malformed bodies — an
748
- // unparseable body leaves `providerErrorCode` undefined, which
749
- // `classifyHttpError` treats as the safe transient default.
750
- let providerErrorCode;
751
- try {
752
- const parsed = JSON.parse(errorBody);
753
- providerErrorCode = parsed.error?.code ?? parsed.error?.type;
754
- }
755
- catch {
756
- providerErrorCode = undefined;
757
- }
758
- throw classifyHttpError(response.status, message, providerErrorCode);
759
- }
760
- function classifyHttpError(status, message, providerErrorCode) {
761
- if (status >= 500) {
762
- return new TransientLlmError({ message, status });
763
- }
764
- if (status === 429) {
765
- // 429 splits on the body's structured error code: persistent
766
- // budget exhaustion (`insufficient_quota`) is fail-fast, every
767
- // other (and every unparseable) 429 stays the transient
768
- // throttle. The safe default is always "transient + retryable"
769
- // — never a false quota trip.
770
- if (providerErrorCode === "insufficient_quota") {
771
- return new QuotaExhaustedLlmError({ message, status });
772
- }
773
- return new RateLimitLlmError({ message, status });
774
- }
775
- // 400 vs 422 split:
776
- //
777
- // OpenAI returns 400 for malformed requests — typically a
778
- // converter bug, an unsupported parameter, or a request shape
779
- // the API doesn't accept. Retrying a 400 just burns the second
780
- // attempt; classify as non-retryable so the framework surfaces
781
- // the error immediately.
782
- //
783
- // OpenAI returns 422 when the model's structured-output reply
784
- // failed server-side strict-mode validation. A re-roll can
785
- // sometimes succeed, so we route 422 through the
786
- // schema-validation class (which carries `retryReason:
787
- // "transient"` as the V1 retry workaround until the framework
788
- // grows a dedicated `schema_validation` retry tag).
789
- if (status === 400) {
790
- return new NonRetryableLlmError({ message, status });
791
- }
792
- if (status === 422) {
793
- return new SchemaValidationLlmError({ message, status });
794
- }
795
- return new NonRetryableLlmError({ message, status });
796
- }
797
- function isAbortError(err) {
798
- return (typeof err === "object" &&
799
- err !== null &&
800
- err.name === "AbortError");
801
- }
802
- // -- incomplete-reason → user-facing message ----------------------------
803
- //
804
- // Per-reason error messages for the `status: "incomplete"` branch.
805
- // The message is the dev's first read when a stage fails — keep it
806
- // actionable, name the cap reason verbatim, and (for the truncation
807
- // case specifically) point at the override knob that fixes it.
808
- function formatIncompleteMessage(reason) {
809
- if (reason === "max_output_tokens") {
810
- return `OpenAI Responses API returned status: "incomplete" (reason: max_output_tokens). The model's output exceeded the per-call \`max_output_tokens\` cap (either an explicit value on the request or the model's default). Pass a larger \`maxOutputTokens\` to TLlmRequest, or set the stage-level \`maxOutputTokens\` on the llmStage factory (e.g., the \`createIngestionV2Pipeline({ llm: { overrides: { ... } } })\` surface).`;
811
- }
812
- if (reason === "content_filter") {
813
- return `OpenAI Responses API returned status: "incomplete" (reason: content_filter). OpenAI's content policy refused to complete this output; the input or generated content was flagged. Retrying will not succeed — review the input text or the stage's prompt and re-request.`;
814
- }
815
- return `OpenAI Responses API returned status: "incomplete" (reason: ${reason}). The model stopped before completing the response. See OpenAI Responses API documentation for the complete \`incomplete_details.reason\` enumeration.`;
816
- }
817
- // -- response parsing --
818
- const SSE_TERMINAL_EVENTS = new Set([
819
- "response.completed",
820
- "response.incomplete",
821
- "response.failed",
822
- ]);
823
- // The lifecycle event the Responses API emits first, before any output
824
- // chunks. It carries the response object — including `.id` — so a
825
- // streaming consumer learns the id while the call is still in flight.
826
- const SSE_CREATED_EVENT = "response.created";
827
- /**
828
- * Parse one SSE event block. Returns:
829
- *
830
- * * `{ kind: "terminal", envelope }` for a terminal Responses-API
831
- * event (`response.completed` / `.incomplete` / `.failed`),
832
- * carrying the embedded full `response` envelope;
833
- * * `{ kind: "created", responseId }` for the lifecycle
834
- * `response.created` event, surfacing the response id the moment
835
- * it is known (before any output);
836
- * * `undefined` for every intermediate / unrecognized event.
837
- *
838
- * The events carry a `type` field inside the data JSON, so we key off
839
- * that and fall back to the SSE `event:` line.
840
- */
841
- function parseSseEvent(raw) {
842
- let eventType;
843
- const dataLines = [];
844
- for (const line of raw.split("\n")) {
845
- if (line.startsWith(":"))
846
- continue; // SSE comment line — ignore.
847
- if (line.startsWith("event:")) {
848
- eventType = line.slice(6).trim();
849
- }
850
- else if (line.startsWith("data:")) {
851
- dataLines.push(line.slice(5).replace(/^ /, ""));
852
- }
853
- }
854
- if (dataLines.length === 0)
855
- return undefined;
856
- let parsed;
857
- try {
858
- parsed = JSON.parse(dataLines.join("\n"));
859
- }
860
- catch {
861
- return undefined;
862
- }
863
- // Prefer the discriminator inside the data JSON (Responses-API
864
- // events carry `type` there); fall back to the SSE `event:` line so
865
- // the parser is robust if the API ever sends the type only there.
866
- // (No `[DONE]` sentinel handling — that is a Chat-Completions frame
867
- // the Responses API does not emit.)
868
- const type = parsed.type ?? eventType;
869
- if (type && SSE_TERMINAL_EVENTS.has(type) && parsed.response) {
870
- return { kind: "terminal", envelope: parsed.response };
871
- }
872
- if (type === SSE_CREATED_EVENT && parsed.response?.id) {
873
- return { kind: "created", responseId: parsed.response.id };
874
- }
875
- return undefined;
876
- }
877
- /**
878
- * Read an SSE `text/event-stream` body and return the envelope carried
879
- * by the terminal event. A stream that ends with no terminal event
880
- * (connection drop) throws `TransientLlmError` so the framework retries.
881
- * `AbortError` from the underlying reader propagates verbatim so
882
- * `llmStage` marks the stage `skipped`.
883
- *
884
- * `onResponseId`, when supplied, fires the moment the `response.created`
885
- * lifecycle event is parsed — i.e. while the call is still streaming,
886
- * before the terminal event arrives. This is the load-bearing seam for
887
- * background-stream mode: it lets a caller persist the response id
888
- * mid-flight so an in-flight call interrupted before completion can be
889
- * recovered from the upstream's stored copy. Invoked at most once.
890
- *
891
- * Note: the event-separator scan assumes LF (`\n\n`) framing, which the
892
- * OpenAI Responses API emits. Reuse against a strict CRLF-only SSE
893
- * server would need the separator scan adjusted to `\r\n\r\n`.
894
- */
895
- async function readSseEnvelope(response, onResponseId) {
896
- const body = response.body;
897
- if (!body) {
898
- throw new TransientLlmError({
899
- message: "OpenAI streaming response carried no body.",
900
- });
901
- }
902
- const reader = body.getReader();
903
- const decoder = new TextDecoder();
904
- let buffer = "";
905
- let terminal;
906
- let idSurfaced = false;
907
- const handleEvent = (rawEvent) => {
908
- const parsedEvent = parseSseEvent(rawEvent);
909
- if (!parsedEvent)
910
- return;
911
- if (parsedEvent.kind === "terminal") {
912
- terminal = parsedEvent.envelope;
913
- return;
914
- }
915
- // kind === "created": surface the id once, the moment it's known.
916
- if (!idSurfaced) {
917
- idSurfaced = true;
918
- onResponseId?.(parsedEvent.responseId);
919
- }
920
- };
921
- try {
922
- for (;;) {
923
- const chunk = await reader.read();
924
- if (chunk.done)
925
- break;
926
- buffer += decoder.decode(chunk.value, { stream: true });
927
- let sep = buffer.indexOf("\n\n");
928
- while (sep !== -1) {
929
- const rawEvent = buffer.slice(0, sep);
930
- buffer = buffer.slice(sep + 2);
931
- handleEvent(rawEvent);
932
- sep = buffer.indexOf("\n\n");
933
- }
934
- }
935
- }
936
- catch (err) {
937
- if (isAbortError(err)) {
938
- throw err;
939
- }
940
- throw new TransientLlmError({
941
- message: `OpenAI streaming read failed: ${err instanceof Error ? err.message : String(err)}`,
942
- });
943
- }
944
- // Flush any bytes the streaming decoder is still holding (an
945
- // incomplete multi-byte UTF-8 sequence at the final chunk boundary),
946
- // then scan for any remaining terminal frame.
947
- buffer += decoder.decode();
948
- let tailSep = buffer.indexOf("\n\n");
949
- while (tailSep !== -1) {
950
- const rawEvent = buffer.slice(0, tailSep);
951
- buffer = buffer.slice(tailSep + 2);
952
- handleEvent(rawEvent);
953
- tailSep = buffer.indexOf("\n\n");
954
- }
955
- if (!terminal) {
956
- throw new TransientLlmError({
957
- message: "OpenAI streaming ended without a terminal response event (connection drop?).",
958
- });
959
- }
960
- return terminal;
961
- }
962
- function pickFunctionCalls(output) {
963
- if (!output)
964
- return [];
965
- const calls = [];
966
- for (const item of output) {
967
- if (item.type === "function_call") {
968
- /* eslint-disable @typescript-eslint/naming-convention */
969
- const fc = item;
970
- /* eslint-enable @typescript-eslint/naming-convention */
971
- calls.push({
972
- callId: fc.call_id,
973
- name: fc.name,
974
- arguments: fc.arguments,
975
- });
976
- }
977
- }
978
- return calls;
979
- }
980
- function extractAssistantText(output) {
981
- if (!output)
982
- return undefined;
983
- for (const item of output) {
984
- if (item.type === "message") {
985
- const msg = item;
986
- const blocks = msg.content ?? [];
987
- const textBlock = blocks.find((b) => b.type === "output_text" || b.type === "text");
988
- if (textBlock?.text !== undefined) {
989
- return textBlock.text;
990
- }
991
- }
992
- }
993
- return undefined;
994
- }
995
- function safeParseJson(raw) {
996
- try {
997
- return JSON.parse(raw);
998
- }
999
- catch (err) {
1000
- throw new SchemaValidationLlmError({
1001
- message: `OpenAI returned malformed JSON in structured-output text: ${err instanceof Error ? err.message : String(err)}`,
1002
- });
1003
- }
1004
- }
1005
- function extractUsage(envelope) {
1006
- const usage = envelope.usage;
1007
- if (!usage)
1008
- return { input: 0, output: 0 };
1009
- const result = {
1010
- input: usage.input_tokens ?? 0,
1011
- output: usage.output_tokens ?? 0,
1012
- };
1013
- const reasoning = usage.output_tokens_details?.reasoning_tokens;
1014
- if (reasoning !== undefined) {
1015
- result.reasoning = reasoning;
1016
- }
1017
- return result;
1018
- }
1019
- function mergeUsage(accumulated, next) {
1020
- const merged = {
1021
- input: accumulated.input + next.input,
1022
- output: accumulated.output + next.output,
1023
- };
1024
- if (accumulated.reasoning !== undefined || next.reasoning !== undefined) {
1025
- merged.reasoning = (accumulated.reasoning ?? 0) + (next.reasoning ?? 0);
1026
- }
1027
- return merged;
1028
- }
1029
- // -- tool translation --
1030
- function translateTools(tools) {
1031
- return tools.map((tool) => {
1032
- switch (tool.kind) {
1033
- case "web_search":
1034
- return { type: "web_search" };
1035
- case "file_search":
1036
- return {
1037
- type: "file_search",
1038
- vector_store_ids: [tool.vectorStoreId],
1039
- };
1040
- case "mcp": {
1041
- const out = {
1042
- type: "mcp",
1043
- server_url: tool.serverUrl,
1044
- };
1045
- if (tool.toolName) {
1046
- return {
1047
- ...out,
1048
- allowed_tools: [tool.toolName],
1049
- };
1050
- }
1051
- return out;
1052
- }
1053
- case "function":
1054
- return {
1055
- type: "function",
1056
- name: tool.name,
1057
- description: tool.description,
1058
- parameters: typeboxToOpenAiSchema(tool.parameters),
1059
- strict: true,
1060
- };
1061
- }
1062
- });
1063
- }
1064
- function findFunctionHandler(tools, name) {
1065
- if (!tools)
1066
- return undefined;
1067
- for (const tool of tools) {
1068
- if (tool.kind === "function" && tool.name === name) {
1069
- return tool;
1070
- }
1071
- }
1072
- return undefined;
1073
- }
1074
- // -- schema name derivation --
1075
- function deriveSchemaName(schema) {
1076
- const id = schema.$id;
1077
- if (typeof id === "string" && id.length > 0) {
1078
- return sanitizeName(id);
1079
- }
1080
- const serialized = canonicalJson(schema);
1081
- return `schema_${shortHash(serialized)}`;
1082
- }
1083
- function sanitizeName(raw) {
1084
- // OpenAI requires schema names match `^[a-zA-Z0-9_-]{1,64}$`.
1085
- const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
1086
- return cleaned.length > 0 ? cleaned : "schema";
1087
- }
1088
- function canonicalJson(value) {
1089
- return JSON.stringify(value, (_key, v) => {
1090
- if (v !== null && typeof v === "object" && !Array.isArray(v)) {
1091
- const obj = v;
1092
- const sorted = {};
1093
- for (const key of Object.keys(obj).sort()) {
1094
- sorted[key] = obj[key];
1095
- }
1096
- return sorted;
1097
- }
1098
- return v;
1099
- });
1100
- }
1101
- function shortHash(input) {
1102
- // Stable 12-hex-char hash. We avoid pulling `crypto.subtle` because
1103
- // it's async and would force `deriveSchemaName` to be async too;
1104
- // FNV-1a is sufficient for naming uniqueness within a process.
1105
- let h1 = 0xcbf29ce4;
1106
- let h2 = 0x84222325;
1107
- for (let i = 0; i < input.length; i += 1) {
1108
- const c = input.charCodeAt(i);
1109
- h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
1110
- h2 = Math.imul(h2 ^ c, 0x01000193) >>> 0;
1111
- }
1112
- const hex1 = h1.toString(16).padStart(8, "0");
1113
- const hex2 = h2.toString(16).padStart(8, "0");
1114
- return (hex1 + hex2).slice(0, 12);
1115
- }
1116
327
  //# sourceMappingURL=provider.js.map