@independo/inderun-web 0.3.0-dev.5 → 0.3.0-dev.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,10 +30,49 @@ const result = await inderun.run({
30
30
  });
31
31
  ```
32
32
 
33
+ ## Streaming
34
+
35
+ `stream()` returns the run handle, its canonical `StreamEvent` sequence, and a `cancel()` hook:
36
+
37
+ ```ts
38
+ const { handle, events, cancel } = await inderun.stream({
39
+ schemaVersion: "1.0",
40
+ task: { kind: "text_to_text" },
41
+ prompt: "Write a one-sentence summary of IndeRun."
42
+ });
43
+
44
+ for await (const event of events) {
45
+ if (event.type === "content_delta") process.stdout.write(event.payload.text);
46
+ if (event.type === "terminal") console.log(event.payload.outcome);
47
+ }
48
+ ```
49
+
50
+ Order by `event.sequence`, not by arrival: it is the ordering authority for a run. Treat an
51
+ unrecognized `event.type` as ignore-or-pass-through — the set is open and additive. Exactly one
52
+ `terminal` event is produced per run, and `cancel()` is idempotent.
53
+
54
+ Streaming needs a host that can deliver a response body incrementally.
55
+ `createBrowserHostServices` provides one; a custom host without a
56
+ `streamingHttpClient` still runs Mode 1, and a stream request is refused at routing
57
+ time with a `streaming_unavailable` reason rather than failing later.
58
+
59
+ ### Custom OpenAI-compatible endpoints
60
+
61
+ The adapter speaks the OpenAI **Responses** API, not chat completions. A custom endpoint must
62
+ accept `"stream": true` and emit `text/event-stream` with the Responses event types —
63
+ `response.output_text.delta`, and `response.completed` / `response.incomplete` carrying the
64
+ response object. Unknown event types are ignored, so an endpoint that emits extra events is
65
+ fine; one that emits a different vocabulary entirely is not.
66
+
33
67
  ## Security Model
34
68
 
35
69
  Browser apps should use a proxy endpoint and keep provider credentials server-side. `createIndeRunWeb` rejects direct calls to the public OpenAI Responses endpoint unless `allowDirectOpenAIEndpoint: true` is set for a controlled environment.
36
70
 
71
+ This applies to streaming too, and is the reason `@independo/inderun-demo-proxy` relays event
72
+ streams rather than buffering them: a browser cannot open its own authenticated connection to
73
+ OpenAI without shipping the key in client code. Never embed a developer-owned API key in
74
+ distributed client code — put it behind a trusted backend proxy and point `endpointUrl` at that.
75
+
37
76
  ## Advanced: registering the OpenAI provider directly
38
77
 
39
78
  `createIndeRunWeb` wires the OpenAI Responses provider for you. To register it
@@ -1,4 +1,4 @@
1
- import type { ClockService, ConnectivityService, HostServices, HttpClientService, HttpRequest, HttpResponse } from "./host.js";
1
+ import type { ClockService, ConnectivityService, HostServices, HttpClientService, HttpRequest, HttpResponse, HttpStreamResponse, HttpStreamingClientService } from "./host.js";
2
2
  /**
3
3
  * Configuration for the fetch-backed HTTP host service.
4
4
  */
@@ -49,12 +49,39 @@ export declare class FetchHttpClient implements HttpClientService {
49
49
  */
50
50
  send(request: HttpRequest): Promise<HttpResponse>;
51
51
  }
52
+ /**
53
+ * Streaming HTTP client service that adapts `fetch` to IndeRun's incremental
54
+ * HTTP contract by handing back `Response.body` rather than buffering it.
55
+ *
56
+ * Two abort sources are merged onto one controller: `timeoutMs`, which bounds
57
+ * only the wait for the response head, and the caller's `signal`, which stays
58
+ * live for the whole body. The timeout timer is therefore cleared as soon as
59
+ * the head resolves — a stream that legitimately stays open for minutes must
60
+ * not be killed by a time-to-first-byte budget.
61
+ */
62
+ export declare class FetchStreamingHttpClient implements HttpStreamingClientService {
63
+ private readonly fetchFn;
64
+ /**
65
+ * Creates a fetch-backed streaming HTTP client.
66
+ * @param options - Optional fetch implementation override.
67
+ * @throws Error when no fetch implementation is available.
68
+ */
69
+ constructor(options?: FetchHttpClientOptions);
70
+ /**
71
+ * Sends a normalized HTTP request and resolves once the response head is available.
72
+ * @param request - Normalized HTTP request payload.
73
+ * @param signal - Optional caller-driven cancellation covering the whole response.
74
+ * @returns Response head plus a lazily-consumed body chunk sequence.
75
+ */
76
+ stream(request: HttpRequest, signal?: AbortSignal): Promise<HttpStreamResponse>;
77
+ }
52
78
  /**
53
79
  * Options for constructing default browser host services.
54
80
  */
55
81
  export interface CreateBrowserHostServicesOptions extends Partial<HostServices> {
56
82
  /**
57
- * Optional fetch implementation used when `httpClient` is not supplied.
83
+ * Optional fetch implementation used when `httpClient`/`streamingHttpClient`
84
+ * are not supplied.
58
85
  */
59
86
  fetchFn?: typeof fetch;
60
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"browser-host.d.ts","sourceRoot":"","sources":["../../src/core/browser-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,YAAY,EACb,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,0BAA2B,YAAW,mBAAmB;IACpE;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;CAOnC;AAED;;GAEG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD;;OAEG;IACH,GAAG,IAAI,MAAM;IAIb;;OAEG;IACH,YAAY,IAAI,MAAM;CAOvB;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC;;;;OAIG;gBACS,OAAO,GAAE,sBAA2B;IAShD;;;;OAIG;IACG,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;CA0CxD;AAED;;GAEG;AACH,MAAM,WAAW,gCAAiC,SAAQ,OAAO,CAAC,YAAY,CAAC;IAC7E;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,YAAY,CA2Bd"}
1
+ {"version":3,"file":"browser-host.d.ts","sourceRoot":"","sources":["../../src/core/browser-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,0BAA0B,EAC3B,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,0BAA2B,YAAW,mBAAmB;IACpE;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;CAOnC;AAED;;GAEG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD;;OAEG;IACH,GAAG,IAAI,MAAM;IAIb;;OAEG;IACH,YAAY,IAAI,MAAM;CAOvB;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC;;;;OAIG;gBACS,OAAO,GAAE,sBAA2B;IAShD;;;;OAIG;IACG,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;CA0CxD;AAED;;;;;;;;;GASG;AACH,qBAAa,wBAAyB,YAAW,0BAA0B;IACzE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC;;;;OAIG;gBACS,OAAO,GAAE,sBAA2B;IAShD;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAwHtF;AAED;;GAEG;AACH,MAAM,WAAW,gCAAiC,SAAQ,OAAO,CAAC,YAAY,CAAC;IAC7E;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,YAAY,CAmCd"}
@@ -93,6 +93,154 @@ export class FetchHttpClient {
93
93
  }
94
94
  }
95
95
  }
96
+ /**
97
+ * Streaming HTTP client service that adapts `fetch` to IndeRun's incremental
98
+ * HTTP contract by handing back `Response.body` rather than buffering it.
99
+ *
100
+ * Two abort sources are merged onto one controller: `timeoutMs`, which bounds
101
+ * only the wait for the response head, and the caller's `signal`, which stays
102
+ * live for the whole body. The timeout timer is therefore cleared as soon as
103
+ * the head resolves — a stream that legitimately stays open for minutes must
104
+ * not be killed by a time-to-first-byte budget.
105
+ */
106
+ export class FetchStreamingHttpClient {
107
+ fetchFn;
108
+ /**
109
+ * Creates a fetch-backed streaming HTTP client.
110
+ * @param options - Optional fetch implementation override.
111
+ * @throws Error when no fetch implementation is available.
112
+ */
113
+ constructor(options = {}) {
114
+ const fetchFn = options.fetchFn ?? globalThis.fetch?.bind(globalThis);
115
+ if (!fetchFn) {
116
+ throw new Error("FetchStreamingHttpClient requires a fetch implementation.");
117
+ }
118
+ this.fetchFn = fetchFn;
119
+ }
120
+ /**
121
+ * Sends a normalized HTTP request and resolves once the response head is available.
122
+ * @param request - Normalized HTTP request payload.
123
+ * @param signal - Optional caller-driven cancellation covering the whole response.
124
+ * @returns Response head plus a lazily-consumed body chunk sequence.
125
+ */
126
+ async stream(request, signal) {
127
+ const controller = typeof AbortController === "undefined" ? undefined : new AbortController();
128
+ let timeoutId;
129
+ let onAbort;
130
+ if (controller) {
131
+ if (request.timeoutMs !== undefined) {
132
+ timeoutId = setTimeout(() => controller.abort(), request.timeoutMs);
133
+ }
134
+ if (signal) {
135
+ if (signal.aborted) {
136
+ controller.abort();
137
+ }
138
+ else {
139
+ onAbort = () => controller.abort();
140
+ signal.addEventListener("abort", onAbort, { once: true });
141
+ }
142
+ }
143
+ }
144
+ const clearTimeoutTimer = () => {
145
+ if (timeoutId !== undefined) {
146
+ clearTimeout(timeoutId);
147
+ timeoutId = undefined;
148
+ }
149
+ };
150
+ const releaseAbortListener = () => {
151
+ if (signal && onAbort) {
152
+ signal.removeEventListener("abort", onAbort);
153
+ onAbort = undefined;
154
+ }
155
+ };
156
+ let response;
157
+ try {
158
+ const init = { method: request.method };
159
+ if (request.headers) {
160
+ init.headers = request.headers;
161
+ }
162
+ if (request.body !== undefined) {
163
+ init.body = request.body;
164
+ }
165
+ if (controller) {
166
+ init.signal = controller.signal;
167
+ }
168
+ response = await this.fetchFn(request.url, init);
169
+ }
170
+ catch (error) {
171
+ clearTimeoutTimer();
172
+ releaseAbortListener();
173
+ throw error;
174
+ }
175
+ clearTimeoutTimer();
176
+ const headers = {};
177
+ response.headers.forEach((value, key) => {
178
+ headers[key.toLowerCase()] = value;
179
+ });
180
+ const stream = response.body;
181
+ async function* iterate() {
182
+ try {
183
+ if (!stream) {
184
+ // A bodyless response (204, HEAD) or a fetch polyfill without stream
185
+ // support: fall back to the buffered text so callers still terminate.
186
+ const text = await response.text();
187
+ if (text.length > 0) {
188
+ yield new TextEncoder().encode(text);
189
+ }
190
+ return;
191
+ }
192
+ const reader = stream.getReader();
193
+ // The body is also raced against `signal` directly, not just against the
194
+ // fetch-level abort: a fetch implementation is free not to error an
195
+ // in-flight body when its request signal fires, and the caller's
196
+ // cancellation guarantee must not depend on that.
197
+ let onBodyAbort;
198
+ const aborted = new Promise((_resolve, reject) => {
199
+ if (!signal)
200
+ return;
201
+ onBodyAbort = () => reject(Object.assign(new Error("The stream was aborted."), { name: "AbortError" }));
202
+ if (signal.aborted) {
203
+ onBodyAbort();
204
+ }
205
+ else {
206
+ signal.addEventListener("abort", onBodyAbort, { once: true });
207
+ }
208
+ });
209
+ // Keeps an abort that arrives after the body finished from surfacing as
210
+ // an unhandled rejection; the race below still observes it.
211
+ void aborted.catch(() => undefined);
212
+ try {
213
+ for (;;) {
214
+ const { done, value } = await Promise.race([reader.read(), aborted]);
215
+ if (done)
216
+ return;
217
+ if (value)
218
+ yield value;
219
+ }
220
+ }
221
+ catch (error) {
222
+ await reader.cancel().catch(() => undefined);
223
+ throw error;
224
+ }
225
+ finally {
226
+ if (signal && onBodyAbort) {
227
+ signal.removeEventListener("abort", onBodyAbort);
228
+ }
229
+ reader.releaseLock();
230
+ }
231
+ }
232
+ finally {
233
+ releaseAbortListener();
234
+ }
235
+ }
236
+ return {
237
+ status: response.status,
238
+ statusText: response.statusText,
239
+ headers,
240
+ body: iterate()
241
+ };
242
+ }
243
+ }
96
244
  /**
97
245
  * Creates a browser-oriented HostServices bundle for the Web SDK.
98
246
  * Supplied host services override the defaults; missing connectivity, clock, and HTTP services are created.
@@ -112,6 +260,15 @@ export function createBrowserHostServices(options = {}) {
112
260
  else {
113
261
  hostServices.httpClient = new FetchHttpClient();
114
262
  }
263
+ if (options.streamingHttpClient) {
264
+ hostServices.streamingHttpClient = options.streamingHttpClient;
265
+ }
266
+ else if (options.fetchFn) {
267
+ hostServices.streamingHttpClient = new FetchStreamingHttpClient({ fetchFn: options.fetchFn });
268
+ }
269
+ else {
270
+ hostServices.streamingHttpClient = new FetchStreamingHttpClient();
271
+ }
115
272
  if (options.deviceConstraints) {
116
273
  hostServices.deviceConstraints = options.deviceConstraints;
117
274
  }
@@ -1,20 +1,15 @@
1
- import { type StreamEvent, type StreamRunHandle, type TaskRequest, type TaskResult } from "@independo/inderun-contracts";
1
+ import { type TaskRequest, type TaskResult } from "@independo/inderun-contracts";
2
2
  import type { HostServices } from "./host.js";
3
- import type { ProviderCapabilitySnapshot } from "./provider.js";
3
+ import type { ProviderCapabilitySnapshot, StreamRun } from "./provider.js";
4
4
  import type { IndeRunApi } from "./generated/inderun-api.js";
5
5
  import { ProviderRegistry } from "./registry.js";
6
6
  import { type TelemetryService } from "./telemetry.js";
7
7
  /**
8
- * Handle returned by IndeRun.stream(). `events` is the canonical StreamEvent
9
- * sequence for this run, terminating in exactly one terminal StreamEvent per the
10
- * Event Gate's guarantee. `cancel()` is idempotent: repeated or concurrent calls
11
- * produce exactly one `cancelled` terminal outcome.
8
+ * @deprecated Renamed to `StreamRun`, which is the name the generated
9
+ * `IndeRunApi` surface and the Swift/Kotlin SDKs use. Kept as an alias so the
10
+ * rename is not a breaking change.
12
11
  */
13
- export interface StreamHandleResult {
14
- handle: StreamRunHandle;
15
- events: AsyncIterable<StreamEvent>;
16
- cancel(reason?: string): void;
17
- }
12
+ export type StreamHandleResult = StreamRun;
18
13
  /**
19
14
  * Main orchestrator SDK entrypoint class.
20
15
  * Validates tasks, executes routing rules based on request constraints and connectivity,
@@ -69,7 +64,7 @@ export declare class IndeRun implements IndeRunApi {
69
64
  * @param request - The canonical task request containing prompts, tasks, and constraints.
70
65
  * @throws {IndeRunException} If validation fails or no eligible streaming provider exists.
71
66
  */
72
- stream(request: TaskRequest): Promise<StreamHandleResult>;
67
+ stream(request: TaskRequest): Promise<StreamRun>;
73
68
  /**
74
69
  * Reports each registered provider's static descriptor and current dynamic capability check,
75
70
  * without executing a task. Useful for UI that shows live provider availability before a run.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EAEpB,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAmB,0BAA0B,EAAE,MAAM,eAAe,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAGlG;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IACnC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAWtC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAXtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAE3C;;;;;OAKG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,gBAAgB,CAAC,EAAE,gBAAgB;IAOrC;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAuIpD;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA8Q/D;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAmB,0BAA0B,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAGlG;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAWtC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAXtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAE3C;;;;;OAKG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,gBAAgB,CAAC,EAAE,gBAAgB;IAOrC;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAuIpD;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC;IA+QtD;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}
@@ -335,6 +335,7 @@ export class IndeRun {
335
335
  schemaVersion: "1.0",
336
336
  finalText: ev.finalText,
337
337
  telemetry: { providerUsed: providerId, totalMs: now() - startTime },
338
+ ...(ev.finishReason !== undefined ? { finishReason: ev.finishReason } : {}),
338
339
  ...(ev.usage !== undefined ? { usage: ev.usage } : {})
339
340
  };
340
341
  const terminal = gate.terminate(outcome, now());
@@ -1,10 +1,14 @@
1
1
  import type { TaskRequest, TaskResult } from "@independo/inderun-contracts";
2
- import type { ProviderCapabilitySnapshot } from "../provider.js";
2
+ import type { StreamRun, ProviderCapabilitySnapshot } from "../provider.js";
3
3
  export interface IndeRunApi {
4
4
  /**
5
5
  * Executes a Mode 1 (request/response) text-to-text task against a routed provider and returns the completed TaskResult. Routing (local vs. cloud, which provider) is decided internally from TaskRequest.constraints/preferences — the caller does not select a provider directly. Rejects the request up front on validation failure; otherwise throws an IndeRunException (carrying an IndeRunError) if no eligible provider is found or every attempted provider fails. There is no partial or streaming result in this mode: run() either resolves with a complete TaskResult or throws.
6
6
  */
7
7
  run(request: TaskRequest): Promise<TaskResult>;
8
+ /**
9
+ * Executes a Mode 2 (streaming) text-to-text task and returns a handle to the run, its canonical StreamEvent sequence, and a cancel hook. The requested interaction mode is a routing input rather than a filter applied after routing: providers that cannot stream are rejected during planning with a normalized reason, so a stream and a run over the same registry may legitimately resolve to different provider chains. Two failure surfaces exist deliberately — validation and route-selection failures reject this call, since there is no handle to correlate them with yet, while provider failure, cancellation, and completion all arrive as the single terminal StreamEvent in the returned sequence. Fallback is narrower than run()'s: a provider failure is only fallback-eligible before the first content event has been delivered; once one has, the run is committed to that provider and a later failure becomes a terminal error rather than a silent provider swap. Cancellation forecloses both at any commit state and always produces exactly one cancelled outcome.
10
+ */
11
+ stream(request: TaskRequest): Promise<StreamRun>;
8
12
  /**
9
13
  * Returns a live snapshot of every registered provider's static descriptor and current dynamic availability, without executing a task or producing any side effects. Call this before run() when you need to show which providers/models are currently available — for example a settings screen or provider picker. Availability can change between calls (a local model can unload, cloud credentials can expire), so don't cache the result across a run() call.
10
14
  */
@@ -1 +1 @@
1
- {"version":3,"file":"inderun-api.d.ts","sourceRoot":"","sources":["../../../src/core/generated/inderun-api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAEjE,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C;;OAEG;IACH,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;CAC5D"}
1
+ {"version":3,"file":"inderun-api.d.ts","sourceRoot":"","sources":["../../../src/core/generated/inderun-api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAE5E,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACjD;;OAEG;IACH,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;CAC5D"}
@@ -1,2 +1,2 @@
1
- export type { ClockService, ConnectivityService, DeviceConstraintsService, HostServices, HttpClientService, HttpRequest, HttpResponse, SecureStorageService, TelemetryService, ThermalState } from "@independo/inderun-contracts";
1
+ export type { AbortSignalLike, ClockService, ConnectivityService, DeviceConstraintsService, HostServices, HttpClientService, HttpRequest, HttpResponse, HttpStreamResponse, HttpStreamingClientService, SecureStorageService, TelemetryService, ThermalState } from "@independo/inderun-contracts";
2
2
  //# sourceMappingURL=host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/core/host.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACb,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/core/host.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,YAAY,EACZ,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,0BAA0B,EAC1B,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACb,MAAM,8BAA8B,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { TaskRequest, TaskResult } from "@independo/inderun-contracts";
1
+ import type { StreamEvent, StreamRunHandle, TaskRequest, TaskResult } from "@independo/inderun-contracts";
2
2
  import type { HostServices } from "./host.js";
3
3
  /**
4
4
  * Static metadata descriptor defining the capabilities, requirements, and constraints
@@ -168,6 +168,12 @@ export type ProviderStreamEvent = {
168
168
  } | {
169
169
  kind: "done";
170
170
  finalText: string;
171
+ /**
172
+ * How generation ended, when the provider reports it. Mirrors
173
+ * `TaskResult.finishReason` for Mode 1 and surfaces on the `completed`
174
+ * terminal outcome; omit when the provider gives no signal.
175
+ */
176
+ finishReason?: TaskResult["finishReason"];
171
177
  usage?: TaskResult["usage"];
172
178
  } | {
173
179
  kind: "error";
@@ -196,4 +202,18 @@ export interface ProviderAdapter {
196
202
  */
197
203
  stream?(req: TaskRequest, ctx: ProviderStreamContext): AsyncIterable<ProviderStreamEvent>;
198
204
  }
205
+ /**
206
+ * What `IndeRun.stream()` returns: the run's identity, its canonical event
207
+ * sequence, and a cancel hook.
208
+ *
209
+ * `events` terminates in exactly one terminal `StreamEvent`, per the Event
210
+ * Gate's guarantee, and is single-use — the run is driven once, not restarted
211
+ * per consumer. `cancel()` is idempotent: repeated or concurrent calls produce
212
+ * exactly one `cancelled` terminal outcome.
213
+ */
214
+ export interface StreamRun {
215
+ handle: StreamRunHandle;
216
+ events: AsyncIterable<StreamEvent>;
217
+ cancel(reason?: string): void;
218
+ }
199
219
  //# sourceMappingURL=provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/core/provider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/core/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A single dispatched server-sent event.
3
+ *
4
+ * `event` and `id` are `undefined` when the stream did not carry those fields.
5
+ * `data` is the accumulated data payload with its trailing newline removed, and
6
+ * is passed through verbatim — sentinels such as OpenAI's `[DONE]` are the
7
+ * caller's concern, not the framing layer's.
8
+ */
9
+ export interface SseEvent {
10
+ event?: string;
11
+ data: string;
12
+ id?: string;
13
+ }
14
+ /**
15
+ * Frames a raw byte stream into server-sent events.
16
+ *
17
+ * Follows the WHATWG server-sent events stream parsing rules, minus
18
+ * reconnection: `retry` is parsed as a field and ignored, and there is no
19
+ * Last-Event-ID reconnect logic. Concretely that means:
20
+ *
21
+ * - lines end with LF, CRLF, or bare CR;
22
+ * - a line beginning with `:` is a comment (servers send these as keep-alives);
23
+ * - `field: value` strips exactly one leading space from the value, and a line
24
+ * with no colon is that field with an empty value;
25
+ * - `data` fields accumulate, joined by newlines;
26
+ * - a blank line dispatches the accumulated event, or resets without
27
+ * dispatching if no data was accumulated;
28
+ * - a stream that ends mid-event does **not** dispatch the partial event, since
29
+ * a truncated event must never be reported as a complete one.
30
+ *
31
+ * Chunk boundaries are meaningless: this decodes incrementally (`stream: true`)
32
+ * so a boundary may fall inside a multi-byte UTF-8 sequence or mid-line.
33
+ *
34
+ * Held to `contracts/fixtures/streaming/sse-framing.json` together with the
35
+ * Swift and Kotlin implementations of the same protocol.
36
+ *
37
+ * @param chunks - Raw response body chunks, in arrival order.
38
+ */
39
+ export declare function parseSseStream(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<SseEvent>;
40
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/core/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAuB,cAAc,CAAC,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,QAAQ,CAAC,CAsFjG"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Frames a raw byte stream into server-sent events.
3
+ *
4
+ * Follows the WHATWG server-sent events stream parsing rules, minus
5
+ * reconnection: `retry` is parsed as a field and ignored, and there is no
6
+ * Last-Event-ID reconnect logic. Concretely that means:
7
+ *
8
+ * - lines end with LF, CRLF, or bare CR;
9
+ * - a line beginning with `:` is a comment (servers send these as keep-alives);
10
+ * - `field: value` strips exactly one leading space from the value, and a line
11
+ * with no colon is that field with an empty value;
12
+ * - `data` fields accumulate, joined by newlines;
13
+ * - a blank line dispatches the accumulated event, or resets without
14
+ * dispatching if no data was accumulated;
15
+ * - a stream that ends mid-event does **not** dispatch the partial event, since
16
+ * a truncated event must never be reported as a complete one.
17
+ *
18
+ * Chunk boundaries are meaningless: this decodes incrementally (`stream: true`)
19
+ * so a boundary may fall inside a multi-byte UTF-8 sequence or mid-line.
20
+ *
21
+ * Held to `contracts/fixtures/streaming/sse-framing.json` together with the
22
+ * Swift and Kotlin implementations of the same protocol.
23
+ *
24
+ * @param chunks - Raw response body chunks, in arrival order.
25
+ */
26
+ export async function* parseSseStream(chunks) {
27
+ const decoder = new TextDecoder("utf-8");
28
+ let buffer = "";
29
+ let eventType;
30
+ let data;
31
+ let id;
32
+ const takeEvent = () => {
33
+ const dispatched = data === undefined ? undefined : buildEvent(eventType, data, id);
34
+ eventType = undefined;
35
+ data = undefined;
36
+ id = undefined;
37
+ return dispatched;
38
+ };
39
+ const handleLine = (line) => {
40
+ if (line.length === 0)
41
+ return takeEvent();
42
+ if (line.startsWith(":"))
43
+ return undefined;
44
+ const colon = line.indexOf(":");
45
+ const field = colon === -1 ? line : line.slice(0, colon);
46
+ let value = colon === -1 ? "" : line.slice(colon + 1);
47
+ if (value.startsWith(" "))
48
+ value = value.slice(1);
49
+ switch (field) {
50
+ case "event":
51
+ eventType = value;
52
+ break;
53
+ case "data":
54
+ data = data === undefined ? value : `${data}\n${value}`;
55
+ break;
56
+ case "id":
57
+ // Per the SSE spec an id containing a NUL is ignored outright.
58
+ if (!value.includes("\0"))
59
+ id = value;
60
+ break;
61
+ default:
62
+ // `retry` and anything unrecognized are dropped rather than erroring.
63
+ break;
64
+ }
65
+ return undefined;
66
+ };
67
+ for await (const chunk of chunks) {
68
+ buffer += decoder.decode(chunk, { stream: true });
69
+ for (;;) {
70
+ const line = takeLine();
71
+ if (line === undefined)
72
+ break;
73
+ const dispatched = handleLine(line);
74
+ if (dispatched)
75
+ yield dispatched;
76
+ }
77
+ }
78
+ // A trailing bare CR is ambiguous until the stream ends: it could still have
79
+ // been the first half of a CRLF.
80
+ buffer += decoder.decode();
81
+ for (;;) {
82
+ const line = takeLine(true);
83
+ if (line === undefined)
84
+ break;
85
+ const dispatched = handleLine(line);
86
+ if (dispatched)
87
+ yield dispatched;
88
+ }
89
+ function takeLine(atEnd = false) {
90
+ for (let index = 0; index < buffer.length; index += 1) {
91
+ const char = buffer[index];
92
+ if (char === "\n") {
93
+ const line = buffer.slice(0, index);
94
+ buffer = buffer.slice(index + 1);
95
+ return line;
96
+ }
97
+ if (char === "\r") {
98
+ const isLast = index === buffer.length - 1;
99
+ // Withhold a trailing CR until we know whether an LF follows, unless
100
+ // the stream is over and no LF can arrive.
101
+ if (isLast && !atEnd)
102
+ return undefined;
103
+ const skip = buffer[index + 1] === "\n" ? 2 : 1;
104
+ const line = buffer.slice(0, index);
105
+ buffer = buffer.slice(index + skip);
106
+ return line;
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ }
112
+ function buildEvent(eventType, data, id) {
113
+ return {
114
+ ...(eventType !== undefined ? { event: eventType } : {}),
115
+ data,
116
+ ...(id !== undefined ? { id } : {})
117
+ };
118
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { type ConnectivityService, type DeviceConstraintsService, type SecureStorageService, type ClockService, type HttpClientService, type HttpRequest, type HttpResponse, type HostServices, type ThermalState } from "./core/host.js";
2
- export { type ProviderDescriptor, type ProviderDynamicCapabilities, type ProviderCapabilitySnapshot, type RunContext, type ProviderAdapter, type ProviderStreamContext, type ProviderStreamEvent } from "./core/provider.js";
1
+ export { type AbortSignalLike, type ConnectivityService, type DeviceConstraintsService, type SecureStorageService, type ClockService, type HttpClientService, type HttpRequest, type HttpResponse, type HttpStreamResponse, type HttpStreamingClientService, type HostServices, type ThermalState } from "./core/host.js";
2
+ export { type ProviderDescriptor, type ProviderDynamicCapabilities, type ProviderCapabilitySnapshot, type RunContext, type ProviderAdapter, type ProviderStreamContext, type ProviderStreamEvent, type StreamRun } from "./core/provider.js";
3
3
  export { EventGate } from "./core/event-gate.js";
4
4
  export { ProviderRegistry } from "./core/registry.js";
5
5
  export { type RouteSelection, Router } from "./core/router.js";
@@ -8,6 +8,6 @@ export { type IndeRunExceptionParams, IndeRunException, createCapabilityMismatch
8
8
  export { IndeRun, type StreamHandleResult } from "./core/engine.js";
9
9
  export type { IndeRunApi } from "./core/generated/inderun-api.js";
10
10
  export { type TelemetryEventType, type TelemetryEvent, type TelemetryService, NoOpTelemetryService } from "./core/telemetry.js";
11
- export { BrowserConnectivityService, FetchHttpClient, SystemClockService, createBrowserHostServices, type CreateBrowserHostServicesOptions, type FetchHttpClientOptions } from "./core/browser-host.js";
11
+ export { BrowserConnectivityService, FetchHttpClient, FetchStreamingHttpClient, SystemClockService, createBrowserHostServices, type CreateBrowserHostServicesOptions, type FetchHttpClientOptions } from "./core/browser-host.js";
12
12
  export { createIndeRunWeb, type CreateIndeRunWebOptions } from "./web-sdk.js";
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,KAAK,gCAAgC,EACrC,KAAK,sBAAsB,EAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAC/B,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,KAAK,gCAAgC,EACrC,KAAK,sBAAsB,EAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { WasmRoutePlanner } from "./core/route-planner.js";
7
7
  export { IndeRunException, createCapabilityMismatch, createOffline, createAuthError, createRateLimited, createTimeout, createUnavailable, createInternal, toIndeRunException } from "./core/errors.js";
8
8
  export { IndeRun } from "./core/engine.js";
9
9
  export { NoOpTelemetryService } from "./core/telemetry.js";
10
- export { BrowserConnectivityService, FetchHttpClient, SystemClockService, createBrowserHostServices } from "./core/browser-host.js";
10
+ export { BrowserConnectivityService, FetchHttpClient, FetchStreamingHttpClient, SystemClockService, createBrowserHostServices } from "./core/browser-host.js";
11
11
  export { createIndeRunWeb } from "./web-sdk.js";
12
12
  // The OpenAI Responses adapter is provider-specific and intentionally excluded
13
13
  // from this unified index. Import it from "@independo/inderun-web/openai".
@@ -1,6 +1,6 @@
1
1
  import type { TaskRequest, TaskResult } from "@independo/inderun-contracts";
2
2
  import type { HostServices } from "../../core/host.js";
3
- import type { ProviderAdapter, ProviderDescriptor, ProviderDynamicCapabilities, RunContext } from "../../core/provider.js";
3
+ import type { ProviderAdapter, ProviderDescriptor, ProviderDynamicCapabilities, ProviderStreamContext, ProviderStreamEvent, RunContext } from "../../core/provider.js";
4
4
  export declare const DEFAULT_OPENAI_RESPONSES_ENDPOINT = "https://api.openai.com/v1/responses";
5
5
  /**
6
6
  * Configuration for the OpenAI Responses provider.
@@ -84,6 +84,49 @@ export declare class OpenAIResponsesProvider implements ProviderAdapter {
84
84
  * @throws IndeRunException mapped to the standard error taxonomy.
85
85
  */
86
86
  run(request: TaskRequest, context: RunContext): Promise<TaskResult>;
87
+ /**
88
+ * Executes a normalized text-to-text task in Mode 2 against the OpenAI
89
+ * Responses API's server-sent event stream.
90
+ *
91
+ * The request is the Mode 1 body plus `"stream": true`, so both modes stay a
92
+ * single code path up to the transport. The response head is classified
93
+ * before the body is read: a non-2xx is drained to text and run through the
94
+ * same `mapHttpError` as Mode 1, because a 429 must surface as RateLimited
95
+ * rather than as a malformed event stream.
96
+ *
97
+ * Event mapping, from the Responses API's typed SSE events to the canonical
98
+ * provider vocabulary:
99
+ *
100
+ * - `response.output_text.delta` becomes a delta;
101
+ * - `response.completed` and `response.incomplete` become the terminal done,
102
+ * with `finalText`, `usage`, and `finishReason` read off the embedded
103
+ * response object by the same helpers Mode 1 uses — it has the same shape;
104
+ * - `response.failed` and `error` become a terminal error;
105
+ * - every other event type is ignored, since the set is open and additive.
106
+ *
107
+ * @param request - Canonical IndeRun task request.
108
+ * @param context - Engine stream context: run id, host services, abort signal.
109
+ */
110
+ stream(request: TaskRequest, context: ProviderStreamContext): AsyncGenerator<ProviderStreamEvent>;
111
+ /**
112
+ * Maps a `response.failed` / `error` stream event onto the error taxonomy.
113
+ * These arrive over an HTTP 200 body, so there is no status code to classify
114
+ * from; OpenAI reports rate limiting and auth failures here with the same
115
+ * `code` values it uses in unary error bodies.
116
+ *
117
+ * Two payload shapes exist and both are accepted: the standalone `error`
118
+ * event carries `code`/`message` at its root, while `response.failed` nests
119
+ * them under `response.error`. The root `type` is the event name rather than
120
+ * an error type, so it is never read as one.
121
+ */
122
+ private mapStreamError;
123
+ /**
124
+ * Resolves the request headers, including the bearer token behind
125
+ * `authContextRef`. Shared by `run` and `stream`: the credential never travels
126
+ * in the request payload, only the slot id does, so both modes must resolve it
127
+ * the same way.
128
+ */
129
+ private resolveHeaders;
87
130
  private createRequestBody;
88
131
  /**
89
132
  * Maps an OpenAI HTTP error response onto the IndeRun error taxonomy:
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAQ5E,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,2BAA2B,EAC3B,UAAU,EACX,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,iCAAiC,wCAAwC,CAAC;AAKvF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IACjC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAwBD;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAUjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,YAAY,CAAC,CAA6D;IAElF;;;OAGG;gBAC0B,OAAO,EAAE,qBAAqB;IAM3D;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAsB9B;;;;;;;;OAQG;IACG,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,2BAA2B,CAAC;YAkB9D,sBAAsB;IAuCpC;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAkHzE,OAAO,CAAC,iBAAiB;IAuBzB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;CAmErB"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAQ5E,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACX,MAAM,wBAAwB,CAAC;AA4BhC,eAAO,MAAM,iCAAiC,wCAAwC,CAAC;AAKvF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IACjC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA6BD;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAUjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,YAAY,CAAC,CAA6D;IAElF;;;OAGG;gBAC0B,OAAO,EAAE,qBAAqB;IAM3D;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAuB9B;;;;;;;;OAQG;IACG,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,2BAA2B,CAAC;YA+B9D,sBAAsB;IAuCpC;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAiFzE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACI,MAAM,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,mBAAmB,CAAC;IAiGtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;OAKG;YACW,cAAc;IA2C5B,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;CAmErB"}
@@ -1,4 +1,14 @@
1
1
  import { createAuthError, createInternal, createRateLimited, createTimeout, createUnavailable } from "../../core/errors.js";
2
+ import { parseSseStream } from "../../core/sse.js";
3
+ /** Buffers a body stream to text, for error responses that are not event streams. */
4
+ async function drainToText(body) {
5
+ const decoder = new TextDecoder();
6
+ let text = "";
7
+ for await (const chunk of body) {
8
+ text += decoder.decode(chunk, { stream: true });
9
+ }
10
+ return text + decoder.decode();
11
+ }
2
12
  export const DEFAULT_OPENAI_RESPONSES_ENDPOINT = "https://api.openai.com/v1/responses";
3
13
  const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 3000;
4
14
  const DEFAULT_HEALTH_CHECK_CACHE_MS = 5000;
@@ -33,9 +43,10 @@ export class OpenAIResponsesProvider {
33
43
  id: this.id,
34
44
  type: "cloud",
35
45
  transport: "http",
46
+ streamingStyle: "tokens",
36
47
  supports: {
37
48
  run: true,
38
- streaming: false,
49
+ streaming: true,
39
50
  realtime: false,
40
51
  tools: false,
41
52
  reasoningEvents: false,
@@ -71,7 +82,18 @@ export class OpenAIResponsesProvider {
71
82
  reason: "OpenAI Responses provider requires a SecureStorageService when auth is enabled."
72
83
  };
73
84
  }
74
- return this.checkEndpointReachable(host.httpClient, host.clock);
85
+ const reachability = await this.checkEndpointReachable(host.httpClient, host.clock);
86
+ if (host.streamingHttpClient) {
87
+ return reachability;
88
+ }
89
+ // Mode 1 still works without it; only streaming is taken away, and the
90
+ // planner turns this into an inspectable `streaming_unavailable` rejection
91
+ // rather than an unexplained routing failure.
92
+ return {
93
+ ...reachability,
94
+ streamingAvailable: false,
95
+ streamingUnavailableReason: "Host does not provide an HttpStreamingClientService, which OpenAI streaming requires."
96
+ };
75
97
  }
76
98
  async checkEndpointReachable(httpClient, clock) {
77
99
  const now = clock ? clock.now() : Date.now();
@@ -120,33 +142,7 @@ export class OpenAIResponsesProvider {
120
142
  runId: context.runId
121
143
  });
122
144
  }
123
- const headers = {
124
- "Content-Type": "application/json"
125
- };
126
- if (this.auth === "authContextRef") {
127
- const slotId = request.authContextRef ?? this.options.authContextRef;
128
- if (!slotId) {
129
- throw createAuthError("OpenAI Responses provider requires authContextRef.", {
130
- providerId: this.id,
131
- runId: context.runId
132
- });
133
- }
134
- const secureStorage = context.hostServices.secureStorage;
135
- if (!secureStorage) {
136
- throw createAuthError("OpenAI Responses provider requires a SecureStorageService when auth is enabled.", {
137
- providerId: this.id,
138
- runId: context.runId
139
- });
140
- }
141
- const secret = await secureStorage.getSecret(slotId);
142
- if (!secret) {
143
- throw createAuthError(`No OpenAI credential found for authContextRef '${slotId}'.`, {
144
- providerId: this.id,
145
- runId: context.runId
146
- });
147
- }
148
- headers.Authorization = `Bearer ${secret}`;
149
- }
145
+ const headers = await this.resolveHeaders(request, context);
150
146
  let response;
151
147
  try {
152
148
  const httpRequest = {
@@ -203,11 +199,177 @@ export class OpenAIResponsesProvider {
203
199
  }
204
200
  return result;
205
201
  }
206
- createRequestBody(request) {
202
+ /**
203
+ * Executes a normalized text-to-text task in Mode 2 against the OpenAI
204
+ * Responses API's server-sent event stream.
205
+ *
206
+ * The request is the Mode 1 body plus `"stream": true`, so both modes stay a
207
+ * single code path up to the transport. The response head is classified
208
+ * before the body is read: a non-2xx is drained to text and run through the
209
+ * same `mapHttpError` as Mode 1, because a 429 must surface as RateLimited
210
+ * rather than as a malformed event stream.
211
+ *
212
+ * Event mapping, from the Responses API's typed SSE events to the canonical
213
+ * provider vocabulary:
214
+ *
215
+ * - `response.output_text.delta` becomes a delta;
216
+ * - `response.completed` and `response.incomplete` become the terminal done,
217
+ * with `finalText`, `usage`, and `finishReason` read off the embedded
218
+ * response object by the same helpers Mode 1 uses — it has the same shape;
219
+ * - `response.failed` and `error` become a terminal error;
220
+ * - every other event type is ignored, since the set is open and additive.
221
+ *
222
+ * @param request - Canonical IndeRun task request.
223
+ * @param context - Engine stream context: run id, host services, abort signal.
224
+ */
225
+ async *stream(request, context) {
226
+ const streamingClient = context.hostServices.streamingHttpClient;
227
+ if (!streamingClient) {
228
+ throw createUnavailable("OpenAI Responses streaming requires an HttpStreamingClientService.", {
229
+ providerId: this.id,
230
+ runId: context.runId
231
+ });
232
+ }
233
+ const headers = await this.resolveHeaders(request, context);
234
+ headers.Accept = "text/event-stream";
235
+ let response;
236
+ try {
237
+ const httpRequest = {
238
+ method: "POST",
239
+ url: this.endpointUrl,
240
+ headers,
241
+ body: JSON.stringify(this.createRequestBody(request, true))
242
+ };
243
+ if (this.options.timeoutMs !== undefined) {
244
+ httpRequest.timeoutMs = this.options.timeoutMs;
245
+ }
246
+ response = await streamingClient.stream(httpRequest, context.signal);
247
+ }
248
+ catch (err) {
249
+ if (isAbortError(err)) {
250
+ throw createTimeout("OpenAI Responses stream timed out before the first byte.", {
251
+ providerId: this.id,
252
+ runId: context.runId
253
+ });
254
+ }
255
+ throw createUnavailable("OpenAI Responses stream failed before a response was received.", {
256
+ providerId: this.id,
257
+ runId: context.runId,
258
+ details: { originalError: getErrorSummary(err) }
259
+ });
260
+ }
261
+ if (response.status < 200 || response.status >= 300) {
262
+ throw this.mapHttpError(response.status, response.statusText, response.headers, await drainToText(response.body), context);
263
+ }
264
+ for await (const sseEvent of parseSseStream(response.body)) {
265
+ if (sseEvent.data === "[DONE]") {
266
+ continue;
267
+ }
268
+ const parsed = parseJson(sseEvent.data);
269
+ // The `event:` line and the payload's own `type` carry the same value;
270
+ // prefer the payload so a proxy that drops event names still works.
271
+ const type = parsed.type ?? sseEvent.event;
272
+ if (type === "response.output_text.delta") {
273
+ if (parsed.delta) {
274
+ yield { kind: "delta", text: parsed.delta };
275
+ }
276
+ continue;
277
+ }
278
+ if (type === "response.completed" || type === "response.incomplete") {
279
+ const body = parsed.response ?? {};
280
+ const done = {
281
+ kind: "done",
282
+ finalText: extractOutputText(body) ?? "",
283
+ finishReason: getFinishReason(body)
284
+ };
285
+ const usage = getUsage(body);
286
+ if (usage) {
287
+ done.usage = usage;
288
+ }
289
+ yield done;
290
+ return;
291
+ }
292
+ if (type === "response.failed" || type === "error") {
293
+ yield { kind: "error", error: this.mapStreamError(parsed, context) };
294
+ return;
295
+ }
296
+ }
297
+ // Falling out of the loop means the stream ended without a terminal event.
298
+ // The engine reports that as a provider fault; inventing a completion from
299
+ // whatever deltas happened to arrive would hide a truncated response.
300
+ }
301
+ /**
302
+ * Maps a `response.failed` / `error` stream event onto the error taxonomy.
303
+ * These arrive over an HTTP 200 body, so there is no status code to classify
304
+ * from; OpenAI reports rate limiting and auth failures here with the same
305
+ * `code` values it uses in unary error bodies.
306
+ *
307
+ * Two payload shapes exist and both are accepted: the standalone `error`
308
+ * event carries `code`/`message` at its root, while `response.failed` nests
309
+ * them under `response.error`. The root `type` is the event name rather than
310
+ * an error type, so it is never read as one.
311
+ */
312
+ mapStreamError(event, context) {
313
+ const nested = event.error ?? event.response?.error;
314
+ const message = nested?.message ?? event.message ?? "OpenAI Responses stream reported a failure.";
315
+ const code = nested?.code ?? event.code;
316
+ const details = { errorType: nested?.type, errorCode: code };
317
+ const params = { providerId: this.id, runId: context.runId, details };
318
+ switch (code) {
319
+ case "rate_limit_exceeded":
320
+ return createRateLimited(message, { ...params, retryable: true });
321
+ case "invalid_api_key":
322
+ case "authentication_error":
323
+ return createAuthError(message, params);
324
+ default:
325
+ return createUnavailable(message, { ...params, retryable: true });
326
+ }
327
+ }
328
+ /**
329
+ * Resolves the request headers, including the bearer token behind
330
+ * `authContextRef`. Shared by `run` and `stream`: the credential never travels
331
+ * in the request payload, only the slot id does, so both modes must resolve it
332
+ * the same way.
333
+ */
334
+ async resolveHeaders(request, context) {
335
+ const headers = {
336
+ "Content-Type": "application/json"
337
+ };
338
+ if (this.auth !== "authContextRef") {
339
+ return headers;
340
+ }
341
+ const slotId = request.authContextRef ?? this.options.authContextRef;
342
+ if (!slotId) {
343
+ throw createAuthError("OpenAI Responses provider requires authContextRef.", {
344
+ providerId: this.id,
345
+ runId: context.runId
346
+ });
347
+ }
348
+ const secureStorage = context.hostServices.secureStorage;
349
+ if (!secureStorage) {
350
+ throw createAuthError("OpenAI Responses provider requires a SecureStorageService when auth is enabled.", {
351
+ providerId: this.id,
352
+ runId: context.runId
353
+ });
354
+ }
355
+ const secret = await secureStorage.getSecret(slotId);
356
+ if (!secret) {
357
+ throw createAuthError(`No OpenAI credential found for authContextRef '${slotId}'.`, {
358
+ providerId: this.id,
359
+ runId: context.runId
360
+ });
361
+ }
362
+ headers.Authorization = `Bearer ${secret}`;
363
+ return headers;
364
+ }
365
+ createRequestBody(request, stream = false) {
207
366
  const body = {
208
367
  model: this.options.model,
209
368
  input: createInput(request)
210
369
  };
370
+ if (stream) {
371
+ body.stream = true;
372
+ }
211
373
  const generation = request.generation;
212
374
  if (generation?.maxOutputTokens !== undefined) {
213
375
  body.max_output_tokens = generation.maxOutputTokens;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@independo/inderun-web",
3
- "version": "0.3.0-dev.5",
3
+ "version": "0.3.0-dev.6",
4
4
  "description": "TypeScript/Web SDK for IndeRun.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -55,8 +55,8 @@
55
55
  "README.md"
56
56
  ],
57
57
  "dependencies": {
58
- "@independo/inderun-contracts": "^0.3.0-dev.5",
59
- "@independo/inderun-route-core-wasm": "^0.3.0-dev.5"
58
+ "@independo/inderun-contracts": "^0.3.0-dev.6",
59
+ "@independo/inderun-route-core-wasm": "^0.3.0-dev.6"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@huggingface/transformers": "^4.0.0"