@proposit/proposit-core 1.8.0 → 1.10.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/dist/cli/commands/parse.d.ts.map +1 -1
- package/dist/cli/commands/parse.js +10 -11
- package/dist/cli/commands/parse.js.map +1 -1
- package/dist/cli/commands/repair.js +1 -1
- package/dist/cli/commands/repair.js.map +1 -1
- package/dist/cli/commands/validate.js +1 -1
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/engine.js +4 -4
- package/dist/cli/engine.js.map +1 -1
- package/dist/cli/import.js +1 -1
- package/dist/cli/import.js.map +1 -1
- package/dist/cli/llm/index.js +2 -2
- package/dist/cli/llm/index.js.map +1 -1
- package/dist/extensions/argument-ingestion/shared/basics-extension.js +3 -3
- package/dist/extensions/argument-ingestion/shared/basics-extension.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 +1 -2
- package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js.map +1 -1
- package/dist/extensions/argument-ingestion/shared/types.d.ts +2 -2
- package/dist/extensions/argument-ingestion/stages/conclusion-selection.d.ts +12 -13
- package/dist/extensions/argument-ingestion/stages/conclusion-selection.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/conclusion-selection.js +110 -39
- package/dist/extensions/argument-ingestion/stages/conclusion-selection.js.map +1 -1
- package/dist/extensions/argument-ingestion/stages/schemas.d.ts +6 -0
- package/dist/extensions/argument-ingestion/stages/schemas.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/stages/schemas.js +14 -0
- package/dist/extensions/argument-ingestion/stages/schemas.js.map +1 -1
- package/dist/extensions/argument-ingestion/v1-single-shot.d.ts.map +1 -1
- package/dist/extensions/argument-ingestion/v1-single-shot.js +13 -13
- package/dist/extensions/argument-ingestion/v1-single-shot.js.map +1 -1
- package/dist/extensions/openai/errors.d.ts +23 -0
- package/dist/extensions/openai/errors.d.ts.map +1 -1
- package/dist/extensions/openai/errors.js +28 -0
- package/dist/extensions/openai/errors.js.map +1 -1
- package/dist/extensions/openai/index.d.ts +3 -3
- package/dist/extensions/openai/index.d.ts.map +1 -1
- package/dist/extensions/openai/index.js +4 -3
- package/dist/extensions/openai/index.js.map +1 -1
- package/dist/extensions/openai/provider.d.ts +118 -0
- package/dist/extensions/openai/provider.d.ts.map +1 -1
- package/dist/extensions/openai/provider.js +272 -23
- package/dist/extensions/openai/provider.js.map +1 -1
- package/dist/extensions/openai/structured-output.js +3 -3
- package/dist/extensions/openai/structured-output.js.map +1 -1
- package/dist/lib/core/argument-engine.d.ts +7 -8
- package/dist/lib/core/argument-engine.d.ts.map +1 -1
- package/dist/lib/core/argument-engine.js +36 -39
- package/dist/lib/core/argument-engine.js.map +1 -1
- package/dist/lib/core/expression-manager.d.ts +6 -6
- package/dist/lib/core/expression-manager.d.ts.map +1 -1
- package/dist/lib/core/expression-manager.js +35 -42
- package/dist/lib/core/expression-manager.js.map +1 -1
- package/dist/lib/core/fork-library.d.ts +1 -1
- package/dist/lib/core/fork-library.js +1 -1
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +5 -7
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +6 -6
- package/dist/lib/core/premise-engine.js +9 -9
- package/dist/lib/core/premise-engine.js.map +1 -1
- package/dist/lib/grammar/an-rules.d.ts +24 -33
- package/dist/lib/grammar/an-rules.d.ts.map +1 -1
- package/dist/lib/grammar/an-rules.js +83 -114
- package/dist/lib/grammar/an-rules.js.map +1 -1
- package/dist/lib/grammar/auto-normalize.d.ts.map +1 -1
- package/dist/lib/grammar/auto-normalize.js +5 -6
- package/dist/lib/grammar/auto-normalize.js.map +1 -1
- package/dist/lib/grammar/bounded-subtree.d.ts.map +1 -1
- package/dist/lib/grammar/bounded-subtree.js +2 -3
- package/dist/lib/grammar/bounded-subtree.js.map +1 -1
- package/dist/lib/grammar/naked-q.js +4 -4
- package/dist/lib/grammar/naked-q.js.map +1 -1
- package/dist/lib/grammar/normalize.js +4 -4
- package/dist/lib/grammar/normalize.js.map +1 -1
- package/dist/lib/grammar/populate-from.d.ts.map +1 -1
- package/dist/lib/grammar/populate-from.js +3 -4
- package/dist/lib/grammar/populate-from.js.map +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/llm/types.d.ts +15 -0
- package/dist/lib/llm/types.d.ts.map +1 -1
- package/dist/lib/llm/types.js +2 -2
- package/dist/lib/llm/types.js.map +1 -1
- package/dist/lib/parsing/argument-parser.js +1 -1
- package/dist/lib/parsing/argument-parser.js.map +1 -1
- package/dist/lib/pipelines/execute.js +1 -1
- package/dist/lib/pipelines/execute.js.map +1 -1
- package/dist/lib/pipelines/stage-helpers.d.ts +1 -1
- package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
- package/dist/lib/pipelines/stage-helpers.js +39 -2
- package/dist/lib/pipelines/stage-helpers.js.map +1 -1
- package/dist/lib/pipelines/types.d.ts +25 -0
- package/dist/lib/pipelines/types.d.ts.map +1 -1
- package/dist/lib/types/fork.d.ts +1 -1
- package/dist/lib/types/validation.js +9 -9
- package/dist/lib/types/validation.js.map +1 -1
- package/package.json +1 -1
|
@@ -38,6 +38,25 @@ export type TCreateOpenAiResponsesProviderOptions = {
|
|
|
38
38
|
* `NonRetryableLlmError`.
|
|
39
39
|
*/
|
|
40
40
|
backgroundMode?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Combine OpenAI **background mode** with **live SSE streaming**:
|
|
43
|
+
* the request is submitted with `{ background: true, stream: true,
|
|
44
|
+
* store: true }` so it keeps generating server-side even if the
|
|
45
|
+
* connection drops, and the SSE stream is consumed live while it
|
|
46
|
+
* runs. This is independent of the `stream` option (foreground-
|
|
47
|
+
* only streaming) and `backgroundMode` (poll-only).
|
|
48
|
+
*
|
|
49
|
+
* When `true`, `backgroundMode` is ignored for this provider
|
|
50
|
+
* instance. `stream` is also ignored (SSE is always used in this
|
|
51
|
+
* mode). V1 supports the **no-tools path only**: a request that
|
|
52
|
+
* sets `backgroundStreamMode` and carries `tools` throws
|
|
53
|
+
* `NonRetryableLlmError`.
|
|
54
|
+
*
|
|
55
|
+
* The response id is surfaced immediately from the submit POST
|
|
56
|
+
* body (before any SSE bytes), making it available for durable
|
|
57
|
+
* persistence by the caller. Defaults to **`false`**.
|
|
58
|
+
*/
|
|
59
|
+
backgroundStreamMode?: boolean;
|
|
41
60
|
/**
|
|
42
61
|
* Poll interval (ms) for the background submit-then-poll loop.
|
|
43
62
|
* Defaults to 2000. Ignored unless `backgroundMode` is `true`.
|
|
@@ -45,4 +64,103 @@ export type TCreateOpenAiResponsesProviderOptions = {
|
|
|
45
64
|
backgroundPollIntervalMs?: number;
|
|
46
65
|
};
|
|
47
66
|
export declare function createOpenAiResponsesProvider(options: TCreateOpenAiResponsesProviderOptions): TLlmProvider;
|
|
67
|
+
/**
|
|
68
|
+
* The status values the OpenAI Responses API reports for a stored
|
|
69
|
+
* response. `completed` / `failed` / `incomplete` / `cancelled` are
|
|
70
|
+
* terminal; `queued` / `in_progress` are transient.
|
|
71
|
+
*/
|
|
72
|
+
export type TResponseStatus = "queued" | "in_progress" | "completed" | "failed" | "incomplete" | "cancelled";
|
|
73
|
+
/**
|
|
74
|
+
* The structured result of a `retrieveResponse` call. All fields
|
|
75
|
+
* except `status` and `rawResponseId` are absent for non-terminal
|
|
76
|
+
* or failed responses.
|
|
77
|
+
*/
|
|
78
|
+
export type TRetrievedResponse = {
|
|
79
|
+
/** Current status of the stored response. */
|
|
80
|
+
status: TResponseStatus;
|
|
81
|
+
/**
|
|
82
|
+
* Parsed text output, present when `status === "completed"` and
|
|
83
|
+
* the response carried a `message` output item.
|
|
84
|
+
*/
|
|
85
|
+
output?: string;
|
|
86
|
+
/** Token usage reported by OpenAI, when available. */
|
|
87
|
+
tokenUsage?: import("../../lib/llm/types.js").TLlmTokenUsage;
|
|
88
|
+
/** The OpenAI response id that was retrieved. */
|
|
89
|
+
rawResponseId: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Retrieve a stored OpenAI response by id. Surfaces the current
|
|
93
|
+
* status, output text (when completed), and token usage.
|
|
94
|
+
*
|
|
95
|
+
* Throws {@link ResponseNotFoundError} when the response is not found
|
|
96
|
+
* (HTTP 404), which typically means the ~10-minute retention window
|
|
97
|
+
* has elapsed. Callers should clear the stored id, settle the
|
|
98
|
+
* associated stage as failed, and surface a retry prompt.
|
|
99
|
+
*
|
|
100
|
+
* @param id - The OpenAI response id to retrieve.
|
|
101
|
+
* @param options - Provider configuration (apiKey, optional baseUrl and fetch).
|
|
102
|
+
*/
|
|
103
|
+
export declare function retrieveResponse(id: string, options: {
|
|
104
|
+
apiKey: string;
|
|
105
|
+
baseUrl?: string;
|
|
106
|
+
fetch?: TOpenAiFetch;
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
}): Promise<TRetrievedResponse>;
|
|
109
|
+
/**
|
|
110
|
+
* Reconnect to a stored, still-generating background response and
|
|
111
|
+
* **stream it to completion**. This is what actually drives a dropped
|
|
112
|
+
* background response forward: a passive `retrieveResponse` GET only
|
|
113
|
+
* reads the current state and leaves a `queued` / `in_progress`
|
|
114
|
+
* response sitting where it is, whereas reconnecting with `stream=true`
|
|
115
|
+
* resumes consumption so the response reaches a terminal status.
|
|
116
|
+
*
|
|
117
|
+
* Issues `GET /responses/{id}?stream=true&starting_after=<cursor>` and
|
|
118
|
+
* consumes the SSE stream to its terminal event, returning the same
|
|
119
|
+
* {@link TRetrievedResponse} shape as {@link retrieveResponse}.
|
|
120
|
+
*
|
|
121
|
+
* Throws {@link ResponseNotFoundError} when the response is not found
|
|
122
|
+
* (HTTP 404 — typically the ~10-minute retention window elapsed).
|
|
123
|
+
* Honors `signal`: an abort propagates as an `AbortError` from the
|
|
124
|
+
* underlying stream read.
|
|
125
|
+
*
|
|
126
|
+
* @param id - The OpenAI response id to reconnect to.
|
|
127
|
+
* @param options - `apiKey`, optional `startingAfter` SSE cursor
|
|
128
|
+
* (defaults to 0 — replay from the start of the stored stream),
|
|
129
|
+
* optional `baseUrl`, `fetch`, and `signal`.
|
|
130
|
+
*/
|
|
131
|
+
export declare function reconnectStream(id: string, options: {
|
|
132
|
+
apiKey: string;
|
|
133
|
+
startingAfter?: number;
|
|
134
|
+
baseUrl?: string;
|
|
135
|
+
fetch?: TOpenAiFetch;
|
|
136
|
+
signal?: AbortSignal;
|
|
137
|
+
}): Promise<TRetrievedResponse>;
|
|
138
|
+
/**
|
|
139
|
+
* Cancel a stored, in-flight OpenAI response. Issues
|
|
140
|
+
* `POST /responses/{id}/cancel` and returns the resulting
|
|
141
|
+
* {@link TRetrievedResponse} (typically `status: "cancelled"`).
|
|
142
|
+
*
|
|
143
|
+
* Cancel is **idempotent** per the Responses API: cancelling twice, or
|
|
144
|
+
* cancelling an already-terminal response, simply returns the final
|
|
145
|
+
* `Response` object rather than erroring — so callers do not need to
|
|
146
|
+
* guard against double-cancel.
|
|
147
|
+
*
|
|
148
|
+
* Throws {@link ResponseNotFoundError} when the response is not found
|
|
149
|
+
* (HTTP 404 — typically the ~10-minute retention window elapsed).
|
|
150
|
+
* Honors `signal` (an abort propagates as an `AbortError`).
|
|
151
|
+
*
|
|
152
|
+
* Use this to stop an in-flight background response when a stage is
|
|
153
|
+
* abandoned (resync timeout) or an import is cancelled, so generation
|
|
154
|
+
* does not keep running (and billing) server-side after the consumer
|
|
155
|
+
* has given up on it.
|
|
156
|
+
*
|
|
157
|
+
* @param id - The OpenAI response id to cancel.
|
|
158
|
+
* @param options - `apiKey`, optional `baseUrl`, `fetch`, and `signal`.
|
|
159
|
+
*/
|
|
160
|
+
export declare function cancelResponse(id: string, options: {
|
|
161
|
+
apiKey: string;
|
|
162
|
+
baseUrl?: string;
|
|
163
|
+
fetch?: TOpenAiFetch;
|
|
164
|
+
signal?: AbortSignal;
|
|
165
|
+
}): Promise<TRetrievedResponse>;
|
|
48
166
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/extensions/openai/provider.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACR,YAAY,EAKf,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/extensions/openai/provider.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACR,YAAY,EAKf,MAAM,wBAAwB,CAAA;AAgB/B,OAAO,KAAK,EACR,YAAY,EAMf,MAAM,YAAY,CAAA;AAOnB,MAAM,MAAM,qCAAqC,GAAG;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;CACpC,CAAA;AAED,wBAAgB,6BAA6B,CACzC,OAAO,EAAE,qCAAqC,GAC/C,YAAY,CA8Td;AAID;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACrB,QAAQ,GACR,aAAa,GACb,WAAW,GACX,QAAQ,GACR,YAAY,GACZ,WAAW,CAAA;AAEjB;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC7B,6CAA6C;IAC7C,MAAM,EAAE,eAAe,CAAA;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,wBAAwB,EAAE,cAAc,CAAA;IAC5D,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAA;CACxB,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAClC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IACL,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB,GACF,OAAO,CAAC,kBAAkB,CAAC,CAU7B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,eAAe,CACjC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IACL,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB,GACF,OAAO,CAAC,kBAAkB,CAAC,CAmC7B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,cAAc,CAChC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IACL,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB,GACF,OAAO,CAAC,kBAAkB,CAAC,CAoC7B"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Concrete `TLlmProvider` backed by the OpenAI Responses API.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// `
|
|
3
|
+
// The V1 adapter: raw `fetch` to
|
|
4
|
+
// `https://api.openai.com/v1/responses` with strict-mode
|
|
5
5
|
// structured output via the inlined TypeBox → JSON Schema converter,
|
|
6
6
|
// translation of the framework's `TToolSpec` discriminated union
|
|
7
7
|
// into the Responses-API tool-shape, and a function-tool agent loop
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The provider deliberately uses raw `fetch` rather than the
|
|
13
13
|
// `openai` npm SDK. `openai` is declared as an optional peer in
|
|
14
|
-
// `package.json` for forward-looking insurance — future
|
|
14
|
+
// `package.json` for forward-looking insurance — a future version may
|
|
15
15
|
// adopt it; V1 keeps the dependency surface minimal.
|
|
16
16
|
//
|
|
17
17
|
// Error classification routes HTTP-status families into framework-
|
|
@@ -30,7 +30,7 @@
|
|
|
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, SchemaValidationLlmError, ToolLoopExhaustedError, TransientLlmError, } from "./errors.js";
|
|
33
|
+
import { NonRetryableLlmError, QuotaExhaustedLlmError, RateLimitLlmError, ResponseNotFoundError, SchemaValidationLlmError, ToolLoopExhaustedError, TransientLlmError, } from "./errors.js";
|
|
34
34
|
const STAGE_ID_MARKER = /<!--\s*stage-id:\s*([^\s>]+)\s*-->/;
|
|
35
35
|
const DEFAULT_BASE_URL = "https://api.openai.com/v1/responses";
|
|
36
36
|
const DEFAULT_MAX_TOOL_ROUNDS = 6;
|
|
@@ -42,12 +42,17 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
42
42
|
}
|
|
43
43
|
const maxToolRounds = options.maxToolCallRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
|
|
44
44
|
const useStream = options.stream ?? true;
|
|
45
|
-
const
|
|
45
|
+
const useBackgroundStream = options.backgroundStreamMode ?? false;
|
|
46
|
+
const useBackground = useBackgroundStream
|
|
47
|
+
? false
|
|
48
|
+
: (options.backgroundMode ?? false);
|
|
46
49
|
const backgroundPollIntervalMs = options.backgroundPollIntervalMs ?? 2000;
|
|
47
50
|
const respond = async (req) => {
|
|
48
|
-
if (useBackground
|
|
51
|
+
if ((useBackground || useBackgroundStream) &&
|
|
52
|
+
req.tools &&
|
|
53
|
+
req.tools.length > 0) {
|
|
49
54
|
throw new NonRetryableLlmError({
|
|
50
|
-
message: "OpenAI background mode does not support function tools in V1. Disable backgroundMode for tool-using requests, or run the tools synchronously.",
|
|
55
|
+
message: "OpenAI background mode does not support function tools in V1. Disable backgroundMode / backgroundStreamMode for tool-using requests, or run the tools synchronously.",
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
const schemaName = deriveSchemaName(req.outputSchema);
|
|
@@ -68,6 +73,17 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
68
73
|
];
|
|
69
74
|
let lastUsage = { input: 0, output: 0 };
|
|
70
75
|
let lastResponseId;
|
|
76
|
+
// Fire the mid-flight id callback at most once across the whole
|
|
77
|
+
// call (background-stream mode is no-tools-only, so the loop runs
|
|
78
|
+
// a single round, but guard regardless of future loop behavior).
|
|
79
|
+
let responseIdNotified = false;
|
|
80
|
+
const notifyResponseId = (responseId) => {
|
|
81
|
+
lastResponseId = responseId;
|
|
82
|
+
if (responseIdNotified)
|
|
83
|
+
return;
|
|
84
|
+
responseIdNotified = true;
|
|
85
|
+
req.onResponseCreated?.(responseId);
|
|
86
|
+
};
|
|
71
87
|
for (let round = 0; round < maxToolRounds; round += 1) {
|
|
72
88
|
const body = {
|
|
73
89
|
model: req.model,
|
|
@@ -108,8 +124,13 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
108
124
|
signal: req.signal,
|
|
109
125
|
stream: useStream,
|
|
110
126
|
background: useBackground,
|
|
127
|
+
backgroundStream: useBackgroundStream,
|
|
111
128
|
pollIntervalMs: backgroundPollIntervalMs,
|
|
129
|
+
onResponseId: notifyResponseId,
|
|
112
130
|
});
|
|
131
|
+
// The mid-flight callback (background-stream mode) already set
|
|
132
|
+
// `lastResponseId`; fall back to the terminal envelope id for
|
|
133
|
+
// the synchronous / poll paths where no mid-flight id fires.
|
|
113
134
|
lastResponseId = envelope.id ?? lastResponseId;
|
|
114
135
|
lastUsage = mergeUsage(lastUsage, extractUsage(envelope));
|
|
115
136
|
if (envelope.status === "failed") {
|
|
@@ -220,8 +241,7 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
220
241
|
// `function_call_output`. Omitting the
|
|
221
242
|
// `function_call` items returns a 400 with a
|
|
222
243
|
// conversation-state error on round 2+. Order is
|
|
223
|
-
// preserved across all calls in the round
|
|
224
|
-
// 1B.1 reviewer fold P1 #1).
|
|
244
|
+
// preserved across all calls in the round.
|
|
225
245
|
for (const call of functionCalls) {
|
|
226
246
|
const handler = findFunctionHandler(req.tools, call.name);
|
|
227
247
|
if (!handler) {
|
|
@@ -301,8 +321,165 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
301
321
|
};
|
|
302
322
|
return { respond };
|
|
303
323
|
}
|
|
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
|
+
}
|
|
304
471
|
// -- HTTP --
|
|
305
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
|
+
}
|
|
306
483
|
if (args.background) {
|
|
307
484
|
return runBackground({
|
|
308
485
|
url: args.url,
|
|
@@ -389,6 +566,12 @@ async function getResponseById(args) {
|
|
|
389
566
|
message: `Network error polling OpenAI background response: ${err instanceof Error ? err.message : String(err)}`,
|
|
390
567
|
});
|
|
391
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
|
+
}
|
|
392
575
|
if (!response.ok) {
|
|
393
576
|
const errorBody = await response.text().catch(() => "");
|
|
394
577
|
throw classifyHttpError(response.status, `OpenAI poll ${response.status.toString()}: ${errorBody || response.statusText}`);
|
|
@@ -417,6 +600,40 @@ function isTerminalBackgroundStatus(status) {
|
|
|
417
600
|
status === "incomplete" ||
|
|
418
601
|
status === "cancelled");
|
|
419
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
|
+
}
|
|
420
637
|
async function runBackground(args) {
|
|
421
638
|
if (args.signal?.aborted)
|
|
422
639
|
throw abortError();
|
|
@@ -555,7 +772,7 @@ function classifyHttpError(status, message, providerErrorCode) {
|
|
|
555
772
|
}
|
|
556
773
|
return new RateLimitLlmError({ message, status });
|
|
557
774
|
}
|
|
558
|
-
// 400 vs 422 split
|
|
775
|
+
// 400 vs 422 split:
|
|
559
776
|
//
|
|
560
777
|
// OpenAI returns 400 for malformed requests — typically a
|
|
561
778
|
// converter bug, an unsupported parameter, or a request shape
|
|
@@ -603,12 +820,23 @@ const SSE_TERMINAL_EVENTS = new Set([
|
|
|
603
820
|
"response.incomplete",
|
|
604
821
|
"response.failed",
|
|
605
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";
|
|
606
827
|
/**
|
|
607
|
-
* Parse one SSE event block. Returns
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
* `
|
|
611
|
-
*
|
|
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.
|
|
612
840
|
*/
|
|
613
841
|
function parseSseEvent(raw) {
|
|
614
842
|
let eventType;
|
|
@@ -639,7 +867,10 @@ function parseSseEvent(raw) {
|
|
|
639
867
|
// the Responses API does not emit.)
|
|
640
868
|
const type = parsed.type ?? eventType;
|
|
641
869
|
if (type && SSE_TERMINAL_EVENTS.has(type) && parsed.response) {
|
|
642
|
-
return 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 };
|
|
643
874
|
}
|
|
644
875
|
return undefined;
|
|
645
876
|
}
|
|
@@ -650,11 +881,18 @@ function parseSseEvent(raw) {
|
|
|
650
881
|
* `AbortError` from the underlying reader propagates verbatim so
|
|
651
882
|
* `llmStage` marks the stage `skipped`.
|
|
652
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
|
+
*
|
|
653
891
|
* Note: the event-separator scan assumes LF (`\n\n`) framing, which the
|
|
654
892
|
* OpenAI Responses API emits. Reuse against a strict CRLF-only SSE
|
|
655
893
|
* server would need the separator scan adjusted to `\r\n\r\n`.
|
|
656
894
|
*/
|
|
657
|
-
async function readSseEnvelope(response) {
|
|
895
|
+
async function readSseEnvelope(response, onResponseId) {
|
|
658
896
|
const body = response.body;
|
|
659
897
|
if (!body) {
|
|
660
898
|
throw new TransientLlmError({
|
|
@@ -665,6 +903,21 @@ async function readSseEnvelope(response) {
|
|
|
665
903
|
const decoder = new TextDecoder();
|
|
666
904
|
let buffer = "";
|
|
667
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
|
+
};
|
|
668
921
|
try {
|
|
669
922
|
for (;;) {
|
|
670
923
|
const chunk = await reader.read();
|
|
@@ -675,9 +928,7 @@ async function readSseEnvelope(response) {
|
|
|
675
928
|
while (sep !== -1) {
|
|
676
929
|
const rawEvent = buffer.slice(0, sep);
|
|
677
930
|
buffer = buffer.slice(sep + 2);
|
|
678
|
-
|
|
679
|
-
if (env)
|
|
680
|
-
terminal = env;
|
|
931
|
+
handleEvent(rawEvent);
|
|
681
932
|
sep = buffer.indexOf("\n\n");
|
|
682
933
|
}
|
|
683
934
|
}
|
|
@@ -698,9 +949,7 @@ async function readSseEnvelope(response) {
|
|
|
698
949
|
while (tailSep !== -1) {
|
|
699
950
|
const rawEvent = buffer.slice(0, tailSep);
|
|
700
951
|
buffer = buffer.slice(tailSep + 2);
|
|
701
|
-
|
|
702
|
-
if (env)
|
|
703
|
-
terminal = env;
|
|
952
|
+
handleEvent(rawEvent);
|
|
704
953
|
tailSep = buffer.indexOf("\n\n");
|
|
705
954
|
}
|
|
706
955
|
if (!terminal) {
|