@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 +39 -0
- package/dist/core/browser-host.d.ts +29 -2
- package/dist/core/browser-host.d.ts.map +1 -1
- package/dist/core/browser-host.js +157 -0
- package/dist/core/engine.d.ts +7 -12
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +1 -0
- package/dist/core/generated/inderun-api.d.ts +5 -1
- package/dist/core/generated/inderun-api.d.ts.map +1 -1
- package/dist/core/host.d.ts +1 -1
- package/dist/core/host.d.ts.map +1 -1
- package/dist/core/provider.d.ts +21 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/sse.d.ts +40 -0
- package/dist/core/sse.d.ts.map +1 -0
- package/dist/core/sse.js +118 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/openai/provider.d.ts +44 -1
- package/dist/providers/openai/provider.d.ts.map +1 -1
- package/dist/providers/openai/provider.js +192 -30
- package/package.json +3 -3
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`
|
|
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,
|
|
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
|
}
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import { type
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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<
|
|
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,
|
|
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"}
|
package/dist/core/engine.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/core/host.d.ts
CHANGED
|
@@ -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
|
package/dist/core/host.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/core/provider.d.ts
CHANGED
|
@@ -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":"AAAA,OAAO,KAAK,
|
|
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"}
|
package/dist/core/sse.js
ADDED
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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;
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
59
|
-
"@independo/inderun-route-core-wasm": "^0.3.0-dev.
|
|
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"
|