@proposit/proposit-core 1.11.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 (108) 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 +2 -149
  65. package/dist/extensions/openai/provider.d.ts.map +1 -1
  66. package/dist/extensions/openai/provider.js +5 -886
  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 +1 -1
  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/parsing/prompt-builder.d.ts.map +1 -1
  81. package/dist/lib/parsing/prompt-builder.js +14 -3
  82. package/dist/lib/parsing/prompt-builder.js.map +1 -1
  83. package/dist/lib/pipelines/index.d.ts +1 -1
  84. package/dist/lib/pipelines/index.d.ts.map +1 -1
  85. package/dist/lib/pipelines/index.js +1 -1
  86. package/dist/lib/pipelines/index.js.map +1 -1
  87. package/dist/lib/pipelines/stage-helpers.d.ts +9 -0
  88. package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
  89. package/dist/lib/pipelines/stage-helpers.js +11 -0
  90. package/dist/lib/pipelines/stage-helpers.js.map +1 -1
  91. package/package.json +10 -5
  92. package/dist/extensions/ieee/citation-claim.js.map +0 -1
  93. package/dist/extensions/ieee/formatting.d.ts.map +0 -1
  94. package/dist/extensions/ieee/formatting.js.map +0 -1
  95. package/dist/extensions/ieee/index.d.ts.map +0 -1
  96. package/dist/extensions/ieee/index.js.map +0 -1
  97. package/dist/extensions/ieee/references.d.ts.map +0 -1
  98. package/dist/extensions/ieee/references.js.map +0 -1
  99. package/dist/extensions/ieee/relaxed.d.ts.map +0 -1
  100. package/dist/extensions/ieee/segment-builder.d.ts.map +0 -1
  101. package/dist/extensions/ieee/segment-builder.js.map +0 -1
  102. package/dist/extensions/ieee/segment-templates.d.ts.map +0 -1
  103. package/dist/extensions/ieee/segment-templates.js.map +0 -1
  104. /package/dist/extensions/{ieee → citations/ieee}/formatting.d.ts +0 -0
  105. /package/dist/extensions/{ieee → citations/ieee}/index.d.ts +0 -0
  106. /package/dist/extensions/{ieee → citations/ieee}/index.js +0 -0
  107. /package/dist/extensions/{ieee → citations/ieee}/segment-builder.d.ts +0 -0
  108. /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,888 +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
- /**
450
- * Submit a background OpenAI response and return its `responseId` +
451
- * submit-time `status` **without polling or streaming to completion**.
452
- *
453
- * This is the submit-only half of the existing `backgroundMode`
454
- * (`runBackground`): it POSTs `{ background: true, store: true }`, parses
455
- * the submit envelope, and returns immediately — the caller drives the
456
- * response to completion later via {@link retrieveResponse} (typically
457
- * after a durable suspend keyed on the returned `responseId`). It is the
458
- * provider capability the pipeline's `launchStage` needs.
459
- *
460
- * **Terminal-on-submit fast-path:** a small/cached request can come back
461
- * already terminal (`completed`/`failed`/`incomplete`/`cancelled`) on the
462
- * submit POST. This function still returns `{ responseId, status }` for
463
- * that case (no throw, no poll); the caller proceeds to
464
- * `retrieveResponse(responseId)`, which sees the terminal state
465
- * immediately.
466
- *
467
- * **No-tools precondition:** background mode does not support function
468
- * tools in V1 — a tool-bearing request throws {@link NonRetryableLlmError},
469
- * matching `respond`'s background guard.
470
- *
471
- * @param req - The structured-output request (system/user prompts +
472
- * `outputSchema` + model knobs). Tools are rejected.
473
- * @param options - `apiKey`, optional `baseUrl`, `fetch`, and `signal`.
474
- */
475
- export async function submitBackgroundResponse(req, options) {
476
- if (req.tools && req.tools.length > 0) {
477
- throw new NonRetryableLlmError({
478
- message: "OpenAI background mode does not support function tools in V1. Disable backgroundMode / backgroundStreamMode for tool-using requests, or run the tools synchronously.",
479
- });
480
- }
481
- const fetchImpl = resolveFetch(options.fetch, "submitBackgroundResponse");
482
- const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
483
- const schemaName = deriveSchemaName(req.outputSchema);
484
- const convertedSchema = typeboxToOpenAiSchema(req.outputSchema);
485
- const body = {
486
- model: req.model,
487
- input: [
488
- { role: "system", content: req.systemPrompt },
489
- { role: "user", content: req.userMessage },
490
- ],
491
- text: {
492
- format: {
493
- type: "json_schema",
494
- name: schemaName,
495
- strict: true,
496
- schema: convertedSchema,
497
- },
498
- },
499
- background: true,
500
- store: true,
501
- };
502
- if (req.maxOutputTokens !== undefined) {
503
- body.max_output_tokens = req.maxOutputTokens;
504
- }
505
- if (req.reasoningEffort) {
506
- body.reasoning = { effort: req.reasoningEffort };
507
- }
508
- const submit = await callOnce({
509
- url: baseUrl,
510
- apiKey: options.apiKey,
511
- body,
512
- fetchImpl,
513
- signal: req.signal ?? options.signal,
514
- });
515
- const submitEnvelope = await parseJsonOrThrowTransient(submit, "OpenAI background submit body was not valid JSON");
516
- const id = submitEnvelope.id;
517
- if (!id) {
518
- throw new TransientLlmError({
519
- message: "OpenAI background submit returned no response id.",
520
- });
521
- }
522
- // Return at submit — including the terminal-on-submit fast-path. Unlike
523
- // `runBackground`, a `cancelled` submit envelope is NOT thrown here: the
524
- // submit-only contract returns every terminal status, and the caller's
525
- // completion step classifies it (a cancelled response settles as a skip).
526
- return {
527
- responseId: id,
528
- status: (submitEnvelope.status ?? "queued"),
529
- };
530
- }
531
- function resolveFetch(injected, fnName) {
532
- const fetchImpl = injected ?? globalThis.fetch;
533
- if (!fetchImpl) {
534
- 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).`);
535
- }
536
- return fetchImpl;
537
- }
538
- function envelopeToRetrievedResponse(envelope, id) {
539
- const output = extractAssistantText(envelope.output);
540
- const usage = extractUsage(envelope);
541
- const result = {
542
- status: (envelope.status ?? "in_progress"),
543
- rawResponseId: envelope.id ?? id,
544
- };
545
- if (output !== undefined) {
546
- result.output = output;
547
- }
548
- if (usage.input > 0 || usage.output > 0) {
549
- result.tokenUsage = usage;
550
- }
551
- // Surface the incomplete reason / error message so a completion-side
552
- // consumer (e.g. `completeStage`) can classify the outcome.
553
- const incompleteReason = envelope.incomplete_details?.reason;
554
- if (incompleteReason !== undefined) {
555
- result.incompleteReason = incompleteReason;
556
- }
557
- const errorMessage = envelope.error?.message;
558
- if (errorMessage !== undefined) {
559
- result.errorMessage = errorMessage;
560
- }
561
- return result;
562
- }
563
- // -- HTTP --
564
- async function fetchResponseEnvelope(args) {
565
- if (args.backgroundStream) {
566
- return runBackgroundStream({
567
- url: args.url,
568
- apiKey: args.apiKey,
569
- body: args.body,
570
- fetchImpl: args.fetchImpl,
571
- signal: args.signal,
572
- onResponseId: args.onResponseId,
573
- });
574
- }
575
- if (args.background) {
576
- return runBackground({
577
- url: args.url,
578
- apiKey: args.apiKey,
579
- body: args.body,
580
- fetchImpl: args.fetchImpl,
581
- signal: args.signal,
582
- pollIntervalMs: args.pollIntervalMs,
583
- });
584
- }
585
- if (args.stream) {
586
- const response = await callOnce({
587
- url: args.url,
588
- apiKey: args.apiKey,
589
- body: { ...args.body, stream: true },
590
- fetchImpl: args.fetchImpl,
591
- signal: args.signal,
592
- });
593
- return readSseEnvelope(response);
594
- }
595
- const response = await callOnce({
596
- url: args.url,
597
- apiKey: args.apiKey,
598
- body: args.body,
599
- fetchImpl: args.fetchImpl,
600
- signal: args.signal,
601
- });
602
- return parseJsonOrThrowTransient(response, "OpenAI response body was not valid JSON");
603
- }
604
- async function parseJsonOrThrowTransient(response, context) {
605
- return response
606
- .json()
607
- .then((j) => j)
608
- .catch((err) => {
609
- throw new TransientLlmError({
610
- message: `${context}: ${err instanceof Error ? err.message : String(err)}`,
611
- });
612
- });
613
- }
614
- function abortError() {
615
- const e = new Error("The OpenAI background request was aborted.");
616
- e.name = "AbortError";
617
- return e;
618
- }
619
- // Resolves (never rejects) on abort by design: the poll loop owns the
620
- // abort→cancel→throw decision at exactly two checkpoints (top-of-loop and
621
- // the in-flight-GET catch), so this helper just needs to wake the loop
622
- // promptly instead of waiting out the full interval. Keeping it
623
- // non-throwing avoids a second, competing abort surface.
624
- function abortableDelay(ms, signal) {
625
- return new Promise((resolve) => {
626
- if (signal?.aborted) {
627
- resolve();
628
- return;
629
- }
630
- const onAbort = () => {
631
- cleanup();
632
- resolve();
633
- };
634
- const cleanup = () => {
635
- clearTimeout(timer);
636
- signal?.removeEventListener("abort", onAbort);
637
- };
638
- const timer = setTimeout(() => {
639
- cleanup();
640
- resolve();
641
- }, ms);
642
- signal?.addEventListener("abort", onAbort, { once: true });
643
- });
644
- }
645
- async function getResponseById(args) {
646
- let response;
647
- try {
648
- response = await args.fetchImpl(`${args.url}/${args.id}`, {
649
- method: "GET",
650
- headers: { Authorization: `Bearer ${args.apiKey}` },
651
- signal: args.signal,
652
- });
653
- }
654
- catch (err) {
655
- if (isAbortError(err))
656
- throw err;
657
- throw new TransientLlmError({
658
- message: `Network error polling OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
659
- });
660
- }
661
- if (response.status === 404) {
662
- // Response has aged out of the ~10-minute retention window or
663
- // was never stored. Surface as a typed error so callers can
664
- // clear the stored id and settle the stage as failed.
665
- throw new ResponseNotFoundError({ responseId: args.id });
666
- }
667
- if (!response.ok) {
668
- const errorBody = await response.text().catch(() => "");
669
- throw classifyHttpError(response.status, `OpenAI poll ${response.status.toString()}: ${errorBody || response.statusText}`);
670
- }
671
- return parseJsonOrThrowTransient(response, "OpenAI poll body was not valid JSON");
672
- }
673
- // Intentionally takes no AbortSignal — cancel must fire even though the
674
- // caller's signal has already aborted; passing the fired signal would
675
- // abort the cancel itself.
676
- async function cancelBackground(args) {
677
- // Best-effort + idempotent — swallow errors; the abort is surfaced
678
- // regardless of whether cancel succeeds.
679
- try {
680
- await args.fetchImpl(`${args.url}/${args.id}/cancel`, {
681
- method: "POST",
682
- headers: { Authorization: `Bearer ${args.apiKey}` },
683
- });
684
- }
685
- catch {
686
- // ignore
687
- }
688
- }
689
- function isTerminalBackgroundStatus(status) {
690
- return (status === "completed" ||
691
- status === "failed" ||
692
- status === "incomplete" ||
693
- status === "cancelled");
694
- }
695
- /**
696
- * Submit a single request with `{ background: true, stream: true,
697
- * store: true }` and consume the resulting SSE stream live, returning
698
- * the terminal envelope.
699
- *
700
- * A background response can only be streamed if it was *created* with
701
- * `stream: true` (a background-without-stream response is poll-only and
702
- * cannot later be streamed), so this mode uses one streaming create
703
- * call rather than a separate non-streaming submit POST. The response
704
- * id is therefore not in a JSON POST body — it arrives in the first
705
- * `response.created` SSE event. `onResponseId` fires the moment that
706
- * event is parsed (before the terminal event), so the caller can
707
- * persist the id while the call is still in flight.
708
- *
709
- * The response keeps generating server-side even if the connection
710
- * drops during stream consumption, and can be recovered via
711
- * `retrieveResponse` within the ~10-minute retention window. A
712
- * connection drop mid-stream (no terminal event before stream end) is
713
- * classified as a `TransientLlmError` so the framework's retry policy
714
- * applies — but because the id was already surfaced mid-flight, a
715
- * crashed in-flight call can be recovered rather than blindly re-run.
716
- */
717
- async function runBackgroundStream(args) {
718
- if (args.signal?.aborted)
719
- throw abortError();
720
- const httpResponse = await callOnce({
721
- url: args.url,
722
- apiKey: args.apiKey,
723
- body: { ...args.body, background: true, stream: true, store: true },
724
- fetchImpl: args.fetchImpl,
725
- signal: args.signal,
726
- });
727
- return readSseEnvelope(httpResponse, args.onResponseId);
728
- }
729
- async function runBackground(args) {
730
- if (args.signal?.aborted)
731
- throw abortError();
732
- const submit = await callOnce({
733
- url: args.url,
734
- apiKey: args.apiKey,
735
- body: { ...args.body, background: true, store: true },
736
- fetchImpl: args.fetchImpl,
737
- signal: args.signal,
738
- });
739
- const submitEnvelope = await parseJsonOrThrowTransient(submit, "OpenAI background submit body was not valid JSON");
740
- const id = submitEnvelope.id;
741
- if (!id) {
742
- throw new TransientLlmError({
743
- message: "OpenAI background submit returned no response id.",
744
- });
745
- }
746
- // Fast-path: a small/cached request can come back already terminal on
747
- // submit — return it directly rather than issuing a redundant poll GET
748
- // (and avoid a `store`-expiry window between submit and first poll).
749
- if (isTerminalBackgroundStatus(submitEnvelope.status)) {
750
- if (submitEnvelope.status === "cancelled")
751
- throw abortError();
752
- return submitEnvelope;
753
- }
754
- for (;;) {
755
- if (args.signal?.aborted) {
756
- await cancelBackground({
757
- url: args.url,
758
- id,
759
- apiKey: args.apiKey,
760
- fetchImpl: args.fetchImpl,
761
- });
762
- throw abortError();
763
- }
764
- let env;
765
- try {
766
- env = await getResponseById({
767
- url: args.url,
768
- id,
769
- apiKey: args.apiKey,
770
- fetchImpl: args.fetchImpl,
771
- signal: args.signal,
772
- });
773
- }
774
- catch (err) {
775
- // Abort landing DURING an in-flight poll GET surfaces as an
776
- // AbortError from getResponseById (real fetch rejects the
777
- // in-flight request). The top-of-loop check alone would miss
778
- // it, so cancel here before re-throwing — this is what makes
779
- // the "AbortSignal → cancel POST" guarantee hold mid-poll.
780
- if (isAbortError(err)) {
781
- await cancelBackground({
782
- url: args.url,
783
- id,
784
- apiKey: args.apiKey,
785
- fetchImpl: args.fetchImpl,
786
- });
787
- throw abortError();
788
- }
789
- throw err;
790
- }
791
- const status = env.status;
792
- // Deliberately inlined rather than calling
793
- // `isTerminalBackgroundStatus`: `cancelled` is terminal too but
794
- // needs the opposite disposition (throw `abortError()` vs. return
795
- // the envelope), so the two terminal cases are split here.
796
- if (status === "completed" ||
797
- status === "failed" ||
798
- status === "incomplete") {
799
- return env;
800
- }
801
- if (status === "cancelled") {
802
- throw abortError();
803
- }
804
- await abortableDelay(args.pollIntervalMs, args.signal);
805
- }
806
- }
807
- async function callOnce(args) {
808
- let response;
809
- try {
810
- response = await args.fetchImpl(args.url, {
811
- method: "POST",
812
- headers: {
813
- "Content-Type": "application/json",
814
- Authorization: `Bearer ${args.apiKey}`,
815
- },
816
- body: JSON.stringify(args.body),
817
- signal: args.signal,
818
- });
819
- }
820
- catch (err) {
821
- // Aborts come through as a thrown error with `name === "AbortError"`
822
- // (DOM spec). Surface as-is so `llmStage`'s mid-flight-abort
823
- // detector turns it into `StageAbortedError`. Other low-level
824
- // fetch failures are transient.
825
- if (isAbortError(err)) {
826
- throw err;
827
- }
828
- throw new TransientLlmError({
829
- message: `Network error calling OpenAI: ${err instanceof Error ? err.message : String(err)}`,
830
- });
831
- }
832
- if (response.ok) {
833
- return response;
834
- }
835
- const errorBody = await response.text().catch(() => "");
836
- const message = `OpenAI Responses API ${response.status.toString()}: ${errorBody || response.statusText}`;
837
- // Best-effort structured extraction of the provider error
838
- // code/type so a 429 can be split into persistent quota exhaustion
839
- // vs. transient throttling. Never throws on malformed bodies — an
840
- // unparseable body leaves `providerErrorCode` undefined, which
841
- // `classifyHttpError` treats as the safe transient default.
842
- let providerErrorCode;
843
- try {
844
- const parsed = JSON.parse(errorBody);
845
- providerErrorCode = parsed.error?.code ?? parsed.error?.type;
846
- }
847
- catch {
848
- providerErrorCode = undefined;
849
- }
850
- throw classifyHttpError(response.status, message, providerErrorCode);
851
- }
852
- function classifyHttpError(status, message, providerErrorCode) {
853
- if (status >= 500) {
854
- return new TransientLlmError({ message, status });
855
- }
856
- if (status === 429) {
857
- // 429 splits on the body's structured error code: persistent
858
- // budget exhaustion (`insufficient_quota`) is fail-fast, every
859
- // other (and every unparseable) 429 stays the transient
860
- // throttle. The safe default is always "transient + retryable"
861
- // — never a false quota trip.
862
- if (providerErrorCode === "insufficient_quota") {
863
- return new QuotaExhaustedLlmError({ message, status });
864
- }
865
- return new RateLimitLlmError({ message, status });
866
- }
867
- // 400 vs 422 split:
868
- //
869
- // OpenAI returns 400 for malformed requests — typically a
870
- // converter bug, an unsupported parameter, or a request shape
871
- // the API doesn't accept. Retrying a 400 just burns the second
872
- // attempt; classify as non-retryable so the framework surfaces
873
- // the error immediately.
874
- //
875
- // OpenAI returns 422 when the model's structured-output reply
876
- // failed server-side strict-mode validation. A re-roll can
877
- // sometimes succeed, so we route 422 through the
878
- // schema-validation class (which carries `retryReason:
879
- // "transient"` as the V1 retry workaround until the framework
880
- // grows a dedicated `schema_validation` retry tag).
881
- if (status === 400) {
882
- return new NonRetryableLlmError({ message, status });
883
- }
884
- if (status === 422) {
885
- return new SchemaValidationLlmError({ message, status });
886
- }
887
- return new NonRetryableLlmError({ message, status });
888
- }
889
- function isAbortError(err) {
890
- return (typeof err === "object" &&
891
- err !== null &&
892
- err.name === "AbortError");
893
- }
894
- // -- incomplete-reason → user-facing message ----------------------------
895
- //
896
- // Per-reason error messages for the `status: "incomplete"` branch.
897
- // The message is the dev's first read when a stage fails — keep it
898
- // actionable, name the cap reason verbatim, and (for the truncation
899
- // case specifically) point at the override knob that fixes it.
900
- function formatIncompleteMessage(reason) {
901
- if (reason === "max_output_tokens") {
902
- 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).`;
903
- }
904
- if (reason === "content_filter") {
905
- 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.`;
906
- }
907
- 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.`;
908
- }
909
- // -- response parsing --
910
- const SSE_TERMINAL_EVENTS = new Set([
911
- "response.completed",
912
- "response.incomplete",
913
- "response.failed",
914
- ]);
915
- // The lifecycle event the Responses API emits first, before any output
916
- // chunks. It carries the response object — including `.id` — so a
917
- // streaming consumer learns the id while the call is still in flight.
918
- const SSE_CREATED_EVENT = "response.created";
919
- /**
920
- * Parse one SSE event block. Returns:
921
- *
922
- * * `{ kind: "terminal", envelope }` for a terminal Responses-API
923
- * event (`response.completed` / `.incomplete` / `.failed`),
924
- * carrying the embedded full `response` envelope;
925
- * * `{ kind: "created", responseId }` for the lifecycle
926
- * `response.created` event, surfacing the response id the moment
927
- * it is known (before any output);
928
- * * `undefined` for every intermediate / unrecognized event.
929
- *
930
- * The events carry a `type` field inside the data JSON, so we key off
931
- * that and fall back to the SSE `event:` line.
932
- */
933
- function parseSseEvent(raw) {
934
- let eventType;
935
- const dataLines = [];
936
- for (const line of raw.split("\n")) {
937
- if (line.startsWith(":"))
938
- continue; // SSE comment line — ignore.
939
- if (line.startsWith("event:")) {
940
- eventType = line.slice(6).trim();
941
- }
942
- else if (line.startsWith("data:")) {
943
- dataLines.push(line.slice(5).replace(/^ /, ""));
944
- }
945
- }
946
- if (dataLines.length === 0)
947
- return undefined;
948
- let parsed;
949
- try {
950
- parsed = JSON.parse(dataLines.join("\n"));
951
- }
952
- catch {
953
- return undefined;
954
- }
955
- // Prefer the discriminator inside the data JSON (Responses-API
956
- // events carry `type` there); fall back to the SSE `event:` line so
957
- // the parser is robust if the API ever sends the type only there.
958
- // (No `[DONE]` sentinel handling — that is a Chat-Completions frame
959
- // the Responses API does not emit.)
960
- const type = parsed.type ?? eventType;
961
- if (type && SSE_TERMINAL_EVENTS.has(type) && parsed.response) {
962
- return { kind: "terminal", envelope: parsed.response };
963
- }
964
- if (type === SSE_CREATED_EVENT && parsed.response?.id) {
965
- return { kind: "created", responseId: parsed.response.id };
966
- }
967
- return undefined;
968
- }
969
- /**
970
- * Read an SSE `text/event-stream` body and return the envelope carried
971
- * by the terminal event. A stream that ends with no terminal event
972
- * (connection drop) throws `TransientLlmError` so the framework retries.
973
- * `AbortError` from the underlying reader propagates verbatim so
974
- * `llmStage` marks the stage `skipped`.
975
- *
976
- * `onResponseId`, when supplied, fires the moment the `response.created`
977
- * lifecycle event is parsed — i.e. while the call is still streaming,
978
- * before the terminal event arrives. This is the load-bearing seam for
979
- * background-stream mode: it lets a caller persist the response id
980
- * mid-flight so an in-flight call interrupted before completion can be
981
- * recovered from the upstream's stored copy. Invoked at most once.
982
- *
983
- * Note: the event-separator scan assumes LF (`\n\n`) framing, which the
984
- * OpenAI Responses API emits. Reuse against a strict CRLF-only SSE
985
- * server would need the separator scan adjusted to `\r\n\r\n`.
986
- */
987
- async function readSseEnvelope(response, onResponseId) {
988
- const body = response.body;
989
- if (!body) {
990
- throw new TransientLlmError({
991
- message: "OpenAI streaming response carried no body.",
992
- });
993
- }
994
- const reader = body.getReader();
995
- const decoder = new TextDecoder();
996
- let buffer = "";
997
- let terminal;
998
- let idSurfaced = false;
999
- const handleEvent = (rawEvent) => {
1000
- const parsedEvent = parseSseEvent(rawEvent);
1001
- if (!parsedEvent)
1002
- return;
1003
- if (parsedEvent.kind === "terminal") {
1004
- terminal = parsedEvent.envelope;
1005
- return;
1006
- }
1007
- // kind === "created": surface the id once, the moment it's known.
1008
- if (!idSurfaced) {
1009
- idSurfaced = true;
1010
- onResponseId?.(parsedEvent.responseId);
1011
- }
1012
- };
1013
- try {
1014
- for (;;) {
1015
- const chunk = await reader.read();
1016
- if (chunk.done)
1017
- break;
1018
- buffer += decoder.decode(chunk.value, { stream: true });
1019
- let sep = buffer.indexOf("\n\n");
1020
- while (sep !== -1) {
1021
- const rawEvent = buffer.slice(0, sep);
1022
- buffer = buffer.slice(sep + 2);
1023
- handleEvent(rawEvent);
1024
- sep = buffer.indexOf("\n\n");
1025
- }
1026
- }
1027
- }
1028
- catch (err) {
1029
- if (isAbortError(err)) {
1030
- throw err;
1031
- }
1032
- throw new TransientLlmError({
1033
- message: `OpenAI streaming read failed: ${err instanceof Error ? err.message : String(err)}`,
1034
- });
1035
- }
1036
- // Flush any bytes the streaming decoder is still holding (an
1037
- // incomplete multi-byte UTF-8 sequence at the final chunk boundary),
1038
- // then scan for any remaining terminal frame.
1039
- buffer += decoder.decode();
1040
- let tailSep = buffer.indexOf("\n\n");
1041
- while (tailSep !== -1) {
1042
- const rawEvent = buffer.slice(0, tailSep);
1043
- buffer = buffer.slice(tailSep + 2);
1044
- handleEvent(rawEvent);
1045
- tailSep = buffer.indexOf("\n\n");
1046
- }
1047
- if (!terminal) {
1048
- throw new TransientLlmError({
1049
- message: "OpenAI streaming ended without a terminal response event (connection drop?).",
1050
- });
1051
- }
1052
- return terminal;
1053
- }
1054
- function pickFunctionCalls(output) {
1055
- if (!output)
1056
- return [];
1057
- const calls = [];
1058
- for (const item of output) {
1059
- if (item.type === "function_call") {
1060
- /* eslint-disable @typescript-eslint/naming-convention */
1061
- const fc = item;
1062
- /* eslint-enable @typescript-eslint/naming-convention */
1063
- calls.push({
1064
- callId: fc.call_id,
1065
- name: fc.name,
1066
- arguments: fc.arguments,
1067
- });
1068
- }
1069
- }
1070
- return calls;
1071
- }
1072
- function extractAssistantText(output) {
1073
- if (!output)
1074
- return undefined;
1075
- for (const item of output) {
1076
- if (item.type === "message") {
1077
- const msg = item;
1078
- const blocks = msg.content ?? [];
1079
- const textBlock = blocks.find((b) => b.type === "output_text" || b.type === "text");
1080
- if (textBlock?.text !== undefined) {
1081
- return textBlock.text;
1082
- }
1083
- }
1084
- }
1085
- return undefined;
1086
- }
1087
- function safeParseJson(raw) {
1088
- try {
1089
- return JSON.parse(raw);
1090
- }
1091
- catch (err) {
1092
- throw new SchemaValidationLlmError({
1093
- message: `OpenAI returned malformed JSON in structured-output text: ${err instanceof Error ? err.message : String(err)}`,
1094
- });
1095
- }
1096
- }
1097
- function extractUsage(envelope) {
1098
- const usage = envelope.usage;
1099
- if (!usage)
1100
- return { input: 0, output: 0 };
1101
- const result = {
1102
- input: usage.input_tokens ?? 0,
1103
- output: usage.output_tokens ?? 0,
1104
- };
1105
- const reasoning = usage.output_tokens_details?.reasoning_tokens;
1106
- if (reasoning !== undefined) {
1107
- result.reasoning = reasoning;
1108
- }
1109
- return result;
1110
- }
1111
- function mergeUsage(accumulated, next) {
1112
- const merged = {
1113
- input: accumulated.input + next.input,
1114
- output: accumulated.output + next.output,
1115
- };
1116
- if (accumulated.reasoning !== undefined || next.reasoning !== undefined) {
1117
- merged.reasoning = (accumulated.reasoning ?? 0) + (next.reasoning ?? 0);
1118
- }
1119
- return merged;
1120
- }
1121
- // -- tool translation --
1122
- function translateTools(tools) {
1123
- return tools.map((tool) => {
1124
- switch (tool.kind) {
1125
- case "web_search":
1126
- return { type: "web_search" };
1127
- case "file_search":
1128
- return {
1129
- type: "file_search",
1130
- vector_store_ids: [tool.vectorStoreId],
1131
- };
1132
- case "mcp": {
1133
- const out = {
1134
- type: "mcp",
1135
- server_url: tool.serverUrl,
1136
- };
1137
- if (tool.toolName) {
1138
- return {
1139
- ...out,
1140
- allowed_tools: [tool.toolName],
1141
- };
1142
- }
1143
- return out;
1144
- }
1145
- case "function":
1146
- return {
1147
- type: "function",
1148
- name: tool.name,
1149
- description: tool.description,
1150
- parameters: typeboxToOpenAiSchema(tool.parameters),
1151
- strict: true,
1152
- };
1153
- }
1154
- });
1155
- }
1156
- function findFunctionHandler(tools, name) {
1157
- if (!tools)
1158
- return undefined;
1159
- for (const tool of tools) {
1160
- if (tool.kind === "function" && tool.name === name) {
1161
- return tool;
1162
- }
1163
- }
1164
- return undefined;
1165
- }
1166
- // -- schema name derivation --
1167
- function deriveSchemaName(schema) {
1168
- const id = schema.$id;
1169
- if (typeof id === "string" && id.length > 0) {
1170
- return sanitizeName(id);
1171
- }
1172
- const serialized = canonicalJson(schema);
1173
- return `schema_${shortHash(serialized)}`;
1174
- }
1175
- function sanitizeName(raw) {
1176
- // OpenAI requires schema names match `^[a-zA-Z0-9_-]{1,64}$`.
1177
- const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
1178
- return cleaned.length > 0 ? cleaned : "schema";
1179
- }
1180
- function canonicalJson(value) {
1181
- return JSON.stringify(value, (_key, v) => {
1182
- if (v !== null && typeof v === "object" && !Array.isArray(v)) {
1183
- const obj = v;
1184
- const sorted = {};
1185
- for (const key of Object.keys(obj).sort()) {
1186
- sorted[key] = obj[key];
1187
- }
1188
- return sorted;
1189
- }
1190
- return v;
1191
- });
1192
- }
1193
- function shortHash(input) {
1194
- // Stable 12-hex-char hash. We avoid pulling `crypto.subtle` because
1195
- // it's async and would force `deriveSchemaName` to be async too;
1196
- // FNV-1a is sufficient for naming uniqueness within a process.
1197
- let h1 = 0xcbf29ce4;
1198
- let h2 = 0x84222325;
1199
- for (let i = 0; i < input.length; i += 1) {
1200
- const c = input.charCodeAt(i);
1201
- h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
1202
- h2 = Math.imul(h2 ^ c, 0x01000193) >>> 0;
1203
- }
1204
- const hex1 = h1.toString(16).padStart(8, "0");
1205
- const hex2 = h2.toString(16).padStart(8, "0");
1206
- return (hex1 + hex2).slice(0, 12);
1207
- }
1208
327
  //# sourceMappingURL=provider.js.map