@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.
- package/README.md +1 -1
- package/dist/cli/commands/expressions.d.ts +29 -0
- package/dist/cli/commands/expressions.d.ts.map +1 -1
- package/dist/cli/commands/expressions.js +118 -92
- package/dist/cli/commands/expressions.js.map +1 -1
- package/dist/extensions/argument-ingestion/shared/finalize-response-v2.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js +18 -2
- package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js.map +1 -1
- package/dist/extensions/{ieee → citations/ieee}/citation-claim.d.ts +53 -59
- package/dist/extensions/{ieee → citations/ieee}/citation-claim.d.ts.map +1 -1
- package/dist/extensions/{ieee → citations/ieee}/citation-claim.js +2 -2
- package/dist/extensions/citations/ieee/citation-claim.js.map +1 -0
- package/dist/extensions/citations/ieee/formatting.d.ts.map +1 -0
- package/dist/extensions/{ieee → citations/ieee}/formatting.js +0 -1
- package/dist/extensions/citations/ieee/formatting.js.map +1 -0
- package/dist/extensions/citations/ieee/index.d.ts.map +1 -0
- package/dist/extensions/citations/ieee/index.js.map +1 -0
- package/dist/extensions/{ieee → citations/ieee}/references.d.ts +161 -181
- package/dist/extensions/citations/ieee/references.d.ts.map +1 -0
- package/dist/extensions/{ieee → citations/ieee}/references.js +48 -22
- package/dist/extensions/citations/ieee/references.js.map +1 -0
- package/dist/extensions/{ieee → citations/ieee}/relaxed.d.ts +159 -180
- package/dist/extensions/citations/ieee/relaxed.d.ts.map +1 -0
- package/dist/extensions/{ieee → citations/ieee}/relaxed.js +1 -4
- package/dist/extensions/{ieee → citations/ieee}/relaxed.js.map +1 -1
- package/dist/extensions/citations/ieee/segment-builder.d.ts.map +1 -0
- package/dist/extensions/citations/ieee/segment-builder.js.map +1 -0
- package/dist/extensions/{ieee → citations/ieee}/segment-templates.d.ts +0 -1
- package/dist/extensions/citations/ieee/segment-templates.d.ts.map +1 -0
- package/dist/extensions/{ieee → citations/ieee}/segment-templates.js +0 -27
- package/dist/extensions/citations/ieee/segment-templates.js.map +1 -0
- package/dist/extensions/citations/unparsed/index.d.ts +2 -0
- package/dist/extensions/citations/unparsed/index.d.ts.map +1 -0
- package/dist/extensions/citations/unparsed/index.js +2 -0
- package/dist/extensions/citations/unparsed/index.js.map +1 -0
- package/dist/extensions/citations/unparsed/unparsed-citation.d.ts +11 -0
- package/dist/extensions/citations/unparsed/unparsed-citation.d.ts.map +1 -0
- package/dist/extensions/citations/unparsed/unparsed-citation.js +22 -0
- package/dist/extensions/citations/unparsed/unparsed-citation.js.map +1 -0
- package/dist/extensions/openai/errors.d.ts +8 -0
- package/dist/extensions/openai/errors.d.ts.map +1 -1
- package/dist/extensions/openai/errors.js +58 -0
- package/dist/extensions/openai/errors.js.map +1 -1
- package/dist/extensions/openai/index.d.ts +4 -2
- package/dist/extensions/openai/index.d.ts.map +1 -1
- package/dist/extensions/openai/index.js +2 -1
- package/dist/extensions/openai/index.js.map +1 -1
- package/dist/extensions/openai/openai-http.d.ts +30 -0
- package/dist/extensions/openai/openai-http.d.ts.map +1 -0
- package/dist/extensions/openai/openai-http.js +310 -0
- package/dist/extensions/openai/openai-http.js.map +1 -0
- package/dist/extensions/openai/openai-parsing.d.ts +34 -0
- package/dist/extensions/openai/openai-parsing.d.ts.map +1 -0
- package/dist/extensions/openai/openai-parsing.js +226 -0
- package/dist/extensions/openai/openai-parsing.js.map +1 -0
- package/dist/extensions/openai/openai-retrieval.d.ts +150 -0
- package/dist/extensions/openai/openai-retrieval.d.ts.map +1 -0
- package/dist/extensions/openai/openai-retrieval.js +248 -0
- package/dist/extensions/openai/openai-retrieval.js.map +1 -0
- package/dist/extensions/openai/openai-tools.d.ts +9 -0
- package/dist/extensions/openai/openai-tools.d.ts.map +1 -0
- package/dist/extensions/openai/openai-tools.js +93 -0
- package/dist/extensions/openai/openai-tools.js.map +1 -0
- package/dist/extensions/openai/provider.d.ts +2 -149
- package/dist/extensions/openai/provider.d.ts.map +1 -1
- package/dist/extensions/openai/provider.js +5 -886
- package/dist/extensions/openai/provider.js.map +1 -1
- package/dist/extensions/openai/types.d.ts +1 -0
- package/dist/extensions/openai/types.d.ts.map +1 -1
- package/dist/extensions/openai/types.js +4 -1
- package/dist/extensions/openai/types.js.map +1 -1
- package/dist/lib/core/premise-engine.d.ts +15 -0
- package/dist/lib/core/premise-engine.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.js +62 -127
- package/dist/lib/core/premise-engine.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/parsing/prompt-builder.d.ts.map +1 -1
- package/dist/lib/parsing/prompt-builder.js +14 -3
- package/dist/lib/parsing/prompt-builder.js.map +1 -1
- package/dist/lib/pipelines/index.d.ts +1 -1
- package/dist/lib/pipelines/index.d.ts.map +1 -1
- package/dist/lib/pipelines/index.js +1 -1
- package/dist/lib/pipelines/index.js.map +1 -1
- package/dist/lib/pipelines/stage-helpers.d.ts +9 -0
- package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
- package/dist/lib/pipelines/stage-helpers.js +11 -0
- package/dist/lib/pipelines/stage-helpers.js.map +1 -1
- package/package.json +10 -5
- package/dist/extensions/ieee/citation-claim.js.map +0 -1
- package/dist/extensions/ieee/formatting.d.ts.map +0 -1
- package/dist/extensions/ieee/formatting.js.map +0 -1
- package/dist/extensions/ieee/index.d.ts.map +0 -1
- package/dist/extensions/ieee/index.js.map +0 -1
- package/dist/extensions/ieee/references.d.ts.map +0 -1
- package/dist/extensions/ieee/references.js.map +0 -1
- package/dist/extensions/ieee/relaxed.d.ts.map +0 -1
- package/dist/extensions/ieee/segment-builder.d.ts.map +0 -1
- package/dist/extensions/ieee/segment-builder.js.map +0 -1
- package/dist/extensions/ieee/segment-templates.d.ts.map +0 -1
- package/dist/extensions/ieee/segment-templates.js.map +0 -1
- /package/dist/extensions/{ieee → citations/ieee}/formatting.d.ts +0 -0
- /package/dist/extensions/{ieee → citations/ieee}/index.d.ts +0 -0
- /package/dist/extensions/{ieee → citations/ieee}/index.js +0 -0
- /package/dist/extensions/{ieee → citations/ieee}/segment-builder.d.ts +0 -0
- /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 {
|
|
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
|