@inferencesh/sdk 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/dist/agent/actions.js +1 -4
- package/dist/agent/api.d.ts +37 -6
- package/dist/agent/api.js +37 -3
- package/dist/agent/reducer.js +23 -7
- package/dist/agent/types.d.ts +30 -5
- package/dist/api/agents.d.ts +9 -3
- package/dist/api/agents.js +87 -65
- package/dist/api/api-keys.d.ts +2 -5
- package/dist/api/apps.d.ts +9 -2
- package/dist/api/apps.js +9 -1
- package/dist/api/chats.d.ts +13 -3
- package/dist/api/chats.js +13 -2
- package/dist/api/engines.d.ts +4 -2
- package/dist/api/engines.js +3 -2
- package/dist/api/flow-runs.d.ts +7 -2
- package/dist/api/flow-runs.js +7 -2
- package/dist/api/flows.d.ts +13 -2
- package/dist/api/flows.js +15 -1
- package/dist/api/knowledge.d.ts +7 -4
- package/dist/api/knowledge.js +6 -4
- package/dist/api/mcp-servers.d.ts +2 -1
- package/dist/api/projects.d.ts +5 -3
- package/dist/api/projects.js +3 -2
- package/dist/api/secrets.d.ts +2 -2
- package/dist/api/tasks.js +39 -60
- package/dist/api/teams.d.ts +4 -2
- package/dist/api/teams.js +3 -2
- package/dist/api/write-bodies.d.ts +110 -0
- package/dist/api/write-bodies.js +1 -0
- package/dist/http/client.d.ts +68 -7
- package/dist/http/client.js +138 -97
- package/dist/http/index.d.ts +2 -2
- package/dist/http/loopback.d.ts +8 -0
- package/dist/http/loopback.js +18 -0
- package/dist/http/stream-until-terminal.d.ts +55 -0
- package/dist/http/stream-until-terminal.js +88 -0
- package/dist/http/stream.d.ts +1 -0
- package/dist/http/stream.js +7 -3
- package/dist/http/streamable.d.ts +25 -5
- package/dist/http/streamable.js +28 -40
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/proxy/endpoints.json +156 -0
- package/dist/proxy/express.d.ts +5 -6
- package/dist/proxy/express.js +18 -2
- package/dist/proxy/hono.d.ts +5 -3
- package/dist/proxy/hono.js +4 -3
- package/dist/proxy/index.d.ts +165 -6
- package/dist/proxy/index.js +409 -25
- package/dist/proxy/nextjs.d.ts +29 -17
- package/dist/proxy/nextjs.js +72 -62
- package/dist/proxy/remix.d.ts +5 -3
- package/dist/proxy/remix.js +4 -3
- package/dist/proxy/svelte.d.ts +5 -4
- package/dist/proxy/svelte.js +4 -4
- package/dist/types.d.ts +591 -271
- package/dist/types.js +117 -252
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/package.json +2 -1
package/dist/http/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InferenceError, RequirementsNotMetException } from './errors';
|
|
2
2
|
import { EventSource } from 'eventsource';
|
|
3
|
+
import { SDK_VERSION } from '../version';
|
|
3
4
|
/**
|
|
4
5
|
* Low-level HTTP client for inference.sh API
|
|
5
6
|
*
|
|
@@ -16,7 +17,7 @@ export class HttpClient {
|
|
|
16
17
|
this.baseUrl = config.baseUrl || 'https://api.inference.sh';
|
|
17
18
|
this.proxyUrl = config.proxyUrl;
|
|
18
19
|
this.getToken = config.getToken;
|
|
19
|
-
this.customHeaders = { 'X-Client-Source':
|
|
20
|
+
this.customHeaders = { 'X-Client-Source': `inference-sdk-js/${SDK_VERSION}`, ...config.headers };
|
|
20
21
|
// Bearer auth needs no cookies, and 'include' makes browsers reject the
|
|
21
22
|
// response unless the API answers with Allow-Credentials — which it does not
|
|
22
23
|
// for third-party origins (WebKit reports that as "Load failed"). Cookies
|
|
@@ -54,32 +55,139 @@ export class HttpClient {
|
|
|
54
55
|
}
|
|
55
56
|
return resolved;
|
|
56
57
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
if (this.
|
|
60
|
-
return
|
|
58
|
+
/** The bearer token a request sent now carries; none through a proxy, which adds its own. */
|
|
59
|
+
async bearerToken() {
|
|
60
|
+
if (this.proxyUrl)
|
|
61
|
+
return undefined;
|
|
62
|
+
return (this.getToken ? await this.getToken() : this.apiKey) || undefined;
|
|
63
|
+
}
|
|
64
|
+
/** The configured headers plus what identifies the request: its bearer, or through a proxy its target. */
|
|
65
|
+
requestHeaders(targetUrl, token) {
|
|
66
|
+
const headers = this.resolveHeaders();
|
|
67
|
+
if (this.proxyUrl) {
|
|
68
|
+
headers['x-inf-target-url'] = targetUrl.toString();
|
|
69
|
+
}
|
|
70
|
+
else if (token) {
|
|
71
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
61
72
|
}
|
|
62
|
-
return
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Where a request whose caller cannot set headers (EventSource) or reads
|
|
77
|
+
* the URL (streams) goes: the API, or the proxy with the target as a query
|
|
78
|
+
* parameter.
|
|
79
|
+
*/
|
|
80
|
+
streamUrl(targetUrl) {
|
|
81
|
+
if (!this.proxyUrl)
|
|
82
|
+
return targetUrl.toString();
|
|
83
|
+
const proxyUrlWithQuery = new URL(this.proxyUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
84
|
+
proxyUrlWithQuery.searchParams.set('__inf_target', targetUrl.toString());
|
|
85
|
+
return proxyUrlWithQuery.toString();
|
|
63
86
|
}
|
|
64
87
|
/**
|
|
65
88
|
* Make an HTTP request to the API
|
|
66
89
|
*/
|
|
67
|
-
async request(method, endpoint, options = {}) {
|
|
68
|
-
const
|
|
90
|
+
async request(method, endpoint, { handleErrors = true, ...options } = {}) {
|
|
91
|
+
const send = (token) => this.executeRequest(method, endpoint, options, token);
|
|
92
|
+
const token = await this.bearerToken();
|
|
69
93
|
try {
|
|
70
|
-
return await
|
|
94
|
+
return await send(token);
|
|
71
95
|
}
|
|
72
96
|
catch (error) {
|
|
73
|
-
if (this.onError) {
|
|
74
|
-
return await this.onError(error,
|
|
97
|
+
if (this.onError && handleErrors) {
|
|
98
|
+
return await this.onError(error, async () => send(await this.bearerToken()), { token });
|
|
75
99
|
}
|
|
76
100
|
throw error;
|
|
77
101
|
}
|
|
78
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Fetch an API endpoint with the client's auth, headers and credentials
|
|
105
|
+
* mode and resolve with the raw response, for bodies request() does not
|
|
106
|
+
* parse (streams, text). A failure goes through onError like one of
|
|
107
|
+
* request() does: a refused response, or a request that got no response.
|
|
108
|
+
* It resolves with the response onError's retry got; when onError resolves
|
|
109
|
+
* with anything else the failure stands and is thrown, the error request()
|
|
110
|
+
* would throw. A request aborted through `init.signal` rejects with the
|
|
111
|
+
* abort and is not an error for onError.
|
|
112
|
+
*/
|
|
113
|
+
async fetch(endpoint, init = {}) {
|
|
114
|
+
const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
|
|
115
|
+
const response = await this.fetchHandled(this.streamUrl(targetUrl), targetUrl, init, 'throw');
|
|
116
|
+
// What onError's retry got is handed over unread, and can be a refusal too.
|
|
117
|
+
if (!response.ok)
|
|
118
|
+
throw await this.readError(response);
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* One fetch of `url` on behalf of `targetUrl`. A refused response, or a
|
|
123
|
+
* request that got none, goes to onError; the result is the response its
|
|
124
|
+
* retry got. Without one the failure stands: the error of a request that
|
|
125
|
+
* got no response is thrown, and a refusal is thrown as the error its body
|
|
126
|
+
* was read into or, for a caller that passes the response on
|
|
127
|
+
* (`refused: 'return'`), handed back with its body unread.
|
|
128
|
+
*/
|
|
129
|
+
async fetchHandled(url, targetUrl, init, refused) {
|
|
130
|
+
const send = (token) => fetch(url, {
|
|
131
|
+
...init,
|
|
132
|
+
headers: { ...init.headers, ...this.requestHeaders(targetUrl, token) },
|
|
133
|
+
credentials: this.credentials,
|
|
134
|
+
});
|
|
135
|
+
const retry = async () => send(await this.bearerToken());
|
|
136
|
+
const token = await this.bearerToken();
|
|
137
|
+
let response;
|
|
138
|
+
try {
|
|
139
|
+
response = await send(token);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// The caller aborted the request; nothing failed.
|
|
143
|
+
if (!this.onError || init.signal?.aborted)
|
|
144
|
+
throw error;
|
|
145
|
+
const handled = await this.onError(error, retry, { token });
|
|
146
|
+
if (handled instanceof globalThis.Response)
|
|
147
|
+
return handled;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
if (response.ok || (refused === 'return' && !this.onError))
|
|
151
|
+
return response;
|
|
152
|
+
// The body is read once: from a clone only when the response is handed back.
|
|
153
|
+
const error = await this.readError(refused === 'return' ? response.clone() : response);
|
|
154
|
+
const handled = await this.onError?.(error, retry, { token });
|
|
155
|
+
if (handled instanceof globalThis.Response)
|
|
156
|
+
return handled;
|
|
157
|
+
if (refused === 'return')
|
|
158
|
+
return response;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
/** The error a refused response stands for, read from its body. */
|
|
162
|
+
async readError(response) {
|
|
163
|
+
const responseText = await response.text().catch(() => '');
|
|
164
|
+
let data = null;
|
|
165
|
+
try {
|
|
166
|
+
data = JSON.parse(responseText);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Not JSON
|
|
170
|
+
}
|
|
171
|
+
return this.responseError(response.status, responseText, data);
|
|
172
|
+
}
|
|
173
|
+
/** HTTP errors → RFC 9457 problem+json */
|
|
174
|
+
responseError(status, responseText, data) {
|
|
175
|
+
if (status === 412 && data && typeof data === 'object' && 'errors' in data && Array.isArray(data.errors)) {
|
|
176
|
+
return RequirementsNotMetException.fromResponse(data, status);
|
|
177
|
+
}
|
|
178
|
+
let errorDetail;
|
|
179
|
+
if (data && typeof data === 'object') {
|
|
180
|
+
errorDetail = this.extractErrorDetail(data) ?? JSON.stringify(data);
|
|
181
|
+
}
|
|
182
|
+
else if (responseText) {
|
|
183
|
+
errorDetail = responseText.slice(0, 500);
|
|
184
|
+
}
|
|
185
|
+
return new InferenceError(status, errorDetail || 'Request failed', responseText);
|
|
186
|
+
}
|
|
79
187
|
/**
|
|
80
188
|
* Execute the actual HTTP request (internal)
|
|
81
189
|
*/
|
|
82
|
-
async executeRequest(method, endpoint, options
|
|
190
|
+
async executeRequest(method, endpoint, options, token) {
|
|
83
191
|
// Build the target URL (always points to the API)
|
|
84
192
|
const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
|
|
85
193
|
if (options.params) {
|
|
@@ -92,23 +200,11 @@ export class HttpClient {
|
|
|
92
200
|
});
|
|
93
201
|
}
|
|
94
202
|
// In proxy mode, requests go to the proxy with target URL in a header
|
|
95
|
-
const
|
|
96
|
-
const fetchUrl = isProxyMode ? this.proxyUrl : targetUrl.toString();
|
|
203
|
+
const fetchUrl = this.proxyUrl ?? targetUrl.toString();
|
|
97
204
|
const headers = {
|
|
98
205
|
'Content-Type': 'application/json',
|
|
99
|
-
...this.
|
|
206
|
+
...this.requestHeaders(targetUrl, token),
|
|
100
207
|
};
|
|
101
|
-
if (isProxyMode) {
|
|
102
|
-
// Proxy mode: send target URL as header, no auth (proxy handles it)
|
|
103
|
-
headers['x-inf-target-url'] = targetUrl.toString();
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
// Direct mode: include authorization header
|
|
107
|
-
const token = this.getAuthToken();
|
|
108
|
-
if (token) {
|
|
109
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
208
|
const fetchOptions = {
|
|
113
209
|
method: method.toUpperCase(),
|
|
114
210
|
headers,
|
|
@@ -126,19 +222,8 @@ export class HttpClient {
|
|
|
126
222
|
catch {
|
|
127
223
|
// Not JSON
|
|
128
224
|
}
|
|
129
|
-
// HTTP errors → RFC 9457 problem+json
|
|
130
225
|
if (!response.ok) {
|
|
131
|
-
|
|
132
|
-
throw RequirementsNotMetException.fromResponse(data, response.status);
|
|
133
|
-
}
|
|
134
|
-
let errorDetail;
|
|
135
|
-
if (data && typeof data === 'object') {
|
|
136
|
-
errorDetail = this.extractErrorDetail(data) ?? JSON.stringify(data);
|
|
137
|
-
}
|
|
138
|
-
else if (responseText) {
|
|
139
|
-
errorDetail = responseText.slice(0, 500);
|
|
140
|
-
}
|
|
141
|
-
throw new InferenceError(response.status, errorDetail || 'Request failed', responseText);
|
|
226
|
+
throw this.responseError(response.status, responseText, data);
|
|
142
227
|
}
|
|
143
228
|
if (response.status === 204 || !responseText) {
|
|
144
229
|
return { data: undefined, messages: [] };
|
|
@@ -156,26 +241,22 @@ export class HttpClient {
|
|
|
156
241
|
/**
|
|
157
242
|
* Get URL and headers for NDJSON streaming.
|
|
158
243
|
* Returns the full URL and auth headers needed for streamable requests.
|
|
244
|
+
* It answers synchronously, so it throws when getToken is async.
|
|
245
|
+
* @deprecated Use fetch(), or a StreamRequest built on it
|
|
246
|
+
* (`(init) => http.fetch(endpoint, init)`): a request made from this config
|
|
247
|
+
* bypasses onError and carries the token of the moment the config was read.
|
|
159
248
|
*/
|
|
160
249
|
getStreamableConfig(endpoint) {
|
|
161
250
|
const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (isProxyMode) {
|
|
166
|
-
const proxyUrlWithQuery = new URL(this.proxyUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
167
|
-
proxyUrlWithQuery.searchParams.set('__inf_target', targetUrl.toString());
|
|
168
|
-
url = proxyUrlWithQuery.toString();
|
|
169
|
-
headers['x-inf-target-url'] = targetUrl.toString();
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
url = targetUrl.toString();
|
|
173
|
-
const token = this.getAuthToken();
|
|
174
|
-
if (token) {
|
|
175
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
176
|
-
}
|
|
251
|
+
const token = this.proxyUrl ? undefined : this.getToken ? this.getToken() : this.apiKey;
|
|
252
|
+
if (token instanceof Promise) {
|
|
253
|
+
throw new Error('getStreamableConfig() cannot wait for an async getToken; use fetch() instead');
|
|
177
254
|
}
|
|
178
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
url: this.streamUrl(targetUrl),
|
|
257
|
+
headers: this.requestHeaders(targetUrl, token || undefined),
|
|
258
|
+
credentials: this.credentials,
|
|
259
|
+
};
|
|
179
260
|
}
|
|
180
261
|
/**
|
|
181
262
|
* Pull a human-readable detail string from a parsed problem+json / API error
|
|
@@ -189,56 +270,16 @@ export class HttpClient {
|
|
|
189
270
|
}
|
|
190
271
|
/**
|
|
191
272
|
* Create an EventSource for SSE streaming
|
|
192
|
-
* @deprecated Use
|
|
273
|
+
* @deprecated Use StreamableManager with a StreamRequest instead
|
|
193
274
|
*/
|
|
194
275
|
createEventSource(endpoint) {
|
|
195
276
|
const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
|
|
196
|
-
const isProxyMode = !!this.proxyUrl;
|
|
197
|
-
// For proxy mode: Browser EventSource can't send custom headers,
|
|
198
|
-
// so append target URL as query param instead
|
|
199
|
-
let fetchUrl;
|
|
200
|
-
if (isProxyMode) {
|
|
201
|
-
const proxyUrlWithQuery = new URL(this.proxyUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
202
|
-
proxyUrlWithQuery.searchParams.set('__inf_target', targetUrl.toString());
|
|
203
|
-
fetchUrl = proxyUrlWithQuery.toString();
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
fetchUrl = targetUrl.toString();
|
|
207
|
-
}
|
|
208
|
-
const resolvedHeaders = this.resolveHeaders();
|
|
209
277
|
// The EventSource error event omits the response body, so a failed initial
|
|
210
278
|
// connection (e.g. 403 otp_required) is handled here in the fetch wrapper:
|
|
211
279
|
// route it through onError like request() does, and hand the retried
|
|
212
280
|
// response (e.g. after OTP verification) back so the stream connects.
|
|
213
|
-
const doFetch =
|
|
214
|
-
|
|
215
|
-
...init?.headers,
|
|
216
|
-
...resolvedHeaders,
|
|
217
|
-
};
|
|
218
|
-
if (isProxyMode) {
|
|
219
|
-
headers['x-inf-target-url'] = targetUrl.toString();
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
const token = this.getAuthToken();
|
|
223
|
-
if (token) {
|
|
224
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const response = await fetch(input, { ...init, headers, credentials: this.credentials });
|
|
228
|
-
if (!response.ok && this.onError) {
|
|
229
|
-
const body = await response.clone().text().catch(() => '');
|
|
230
|
-
let data;
|
|
231
|
-
try {
|
|
232
|
-
data = JSON.parse(body);
|
|
233
|
-
}
|
|
234
|
-
catch { /* non-JSON body */ }
|
|
235
|
-
const error = new InferenceError(response.status, this.extractErrorDetail(data) ?? 'Request failed', body);
|
|
236
|
-
const handled = await this.onError(error, () => doFetch(input, init));
|
|
237
|
-
return handled ?? response;
|
|
238
|
-
}
|
|
239
|
-
return response;
|
|
240
|
-
};
|
|
241
|
-
return Promise.resolve(new EventSource(fetchUrl, { fetch: doFetch }));
|
|
281
|
+
const doFetch = (input, init) => this.fetchHandled(input, targetUrl, init ?? {}, 'return');
|
|
282
|
+
return Promise.resolve(new EventSource(this.streamUrl(targetUrl), { fetch: doFetch }));
|
|
242
283
|
}
|
|
243
284
|
}
|
|
244
285
|
/**
|
package/dist/http/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HttpClient, type HttpClientConfig, type ErrorHandler, createHttpClient } from './client';
|
|
1
|
+
export { HttpClient, type HttpClientConfig, type ErrorHandler, type FailedRequest, type HttpFetchInit, createHttpClient } from './client';
|
|
2
2
|
export { StreamManager, type StreamManagerOptions, type PartialDataWrapper } from './stream';
|
|
3
|
-
export { streamable, streamableRaw, StreamableManager, type StreamableOptions, type StreamableMessage, type StreamableManagerOptions } from './streamable';
|
|
3
|
+
export { streamable, streamableRaw, StreamableManager, type StreamableOptions, type StreamableMessage, type StreamableManagerOptions, type StreamableSource, type StreamRequest, type StreamRequestInit } from './streamable';
|
|
4
4
|
export { InferenceError, RequirementsNotMetException } from './errors';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback hosts, where plain http never leaves the machine (local dev):
|
|
3
|
+
* `localhost`, any `*.localhost`, `[::1]` and 127.0.0.0/8. This is the one
|
|
4
|
+
* copy of the rule; the proxy, the app and common-js all ask it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isLoopbackHost(hostname: string): boolean;
|
|
7
|
+
/** Whether `url` is https, or plain http to a loopback host. */
|
|
8
|
+
export declare function isHttpsOrLoopback(url: URL): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback hosts, where plain http never leaves the machine (local dev):
|
|
3
|
+
* `localhost`, any `*.localhost`, `[::1]` and 127.0.0.0/8. This is the one
|
|
4
|
+
* copy of the rule; the proxy, the app and common-js all ask it.
|
|
5
|
+
*/
|
|
6
|
+
export function isLoopbackHost(hostname) {
|
|
7
|
+
const h = hostname.toLowerCase();
|
|
8
|
+
return h === 'localhost'
|
|
9
|
+
|| h.endsWith('.localhost')
|
|
10
|
+
|| h === '[::1]'
|
|
11
|
+
|| /^127(\.\d{1,3}){3}$/.test(h);
|
|
12
|
+
}
|
|
13
|
+
/** Whether `url` is https, or plain http to a loopback host. */
|
|
14
|
+
export function isHttpsOrLoopback(url) {
|
|
15
|
+
if (url.protocol === 'https:')
|
|
16
|
+
return true;
|
|
17
|
+
return url.protocol === 'http:' && isLoopbackHost(url.hostname);
|
|
18
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow a resource's NDJSON stream until the resource reaches a terminal
|
|
3
|
+
* state.
|
|
4
|
+
*
|
|
5
|
+
* StreamableManager never reconnects, and a stream can close cleanly before
|
|
6
|
+
* the resource is done (load balancer idle timeout, API deploy). A wait built
|
|
7
|
+
* on it must not hang then. This helper owns that contract for every
|
|
8
|
+
* "stream until done" wait: when the stream ends early it fetches the
|
|
9
|
+
* resource once (`reconcile`) and settles with what that shows, or rejects
|
|
10
|
+
* when the resource is still not terminal. sdk-py's `wait_for_completion`
|
|
11
|
+
* does the same final GET.
|
|
12
|
+
*/
|
|
13
|
+
import { type StreamableManagerOptions, type StreamRequest } from './streamable';
|
|
14
|
+
/** Settles a stream-until-terminal wait. Calls after the first are ignored. */
|
|
15
|
+
export interface TerminalSettler<R> {
|
|
16
|
+
resolve(value: R): void;
|
|
17
|
+
reject(error: Error): void;
|
|
18
|
+
}
|
|
19
|
+
/** The stream to follow and how to tell when its resource is done. */
|
|
20
|
+
export type StreamUntilTerminalSpec<T> = Pick<StreamableManagerOptions<T>, 'body' | 'onData' | 'onPartialData' | 'onStart' | 'onDelta'> & {
|
|
21
|
+
/** Sends the stream request, e.g. `(init) => http.fetch(endpoint, init)`. */
|
|
22
|
+
request: StreamRequest;
|
|
23
|
+
/** Typed event listeners ({"event": name, "data": ...} messages). */
|
|
24
|
+
events?: Record<string, (data: any, fields?: string[]) => void>;
|
|
25
|
+
/**
|
|
26
|
+
* Called once when the stream ends cleanly before the wait settled. Fetch
|
|
27
|
+
* the resource and settle if it is terminal; returning without settling
|
|
28
|
+
* rejects the wait with `closedMessage`.
|
|
29
|
+
*/
|
|
30
|
+
reconcile: () => Promise<void>;
|
|
31
|
+
/** Rejection message when the stream closed and the resource is still not terminal. */
|
|
32
|
+
closedMessage: string;
|
|
33
|
+
/**
|
|
34
|
+
* Called when the wait ends because `stop()` was called. Without it `done`
|
|
35
|
+
* stays pending.
|
|
36
|
+
*/
|
|
37
|
+
onStopped?: () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Keep the stream open after the wait settled so its listeners go on
|
|
40
|
+
* seeing updates (default false: settling stops the stream).
|
|
41
|
+
*/
|
|
42
|
+
keepOpen?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export interface StreamUntilTerminal<R> {
|
|
45
|
+
/** Settles when the resource is terminal, the stream fails, or it closed early and the resource is not terminal. */
|
|
46
|
+
done: Promise<R>;
|
|
47
|
+
/** Ends the stream on purpose; see `onStopped`. */
|
|
48
|
+
stop(): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `build` receives the settler and returns the stream spec; its handlers call
|
|
52
|
+
* `settle.resolve`/`settle.reject` once the resource is terminal. Settling
|
|
53
|
+
* stops the stream unless the spec says `keepOpen`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function streamUntilTerminal<T, R>(build: (settle: TerminalSettler<R>) => StreamUntilTerminalSpec<T>): StreamUntilTerminal<R>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow a resource's NDJSON stream until the resource reaches a terminal
|
|
3
|
+
* state.
|
|
4
|
+
*
|
|
5
|
+
* StreamableManager never reconnects, and a stream can close cleanly before
|
|
6
|
+
* the resource is done (load balancer idle timeout, API deploy). A wait built
|
|
7
|
+
* on it must not hang then. This helper owns that contract for every
|
|
8
|
+
* "stream until done" wait: when the stream ends early it fetches the
|
|
9
|
+
* resource once (`reconcile`) and settles with what that shows, or rejects
|
|
10
|
+
* when the resource is still not terminal. sdk-py's `wait_for_completion`
|
|
11
|
+
* does the same final GET.
|
|
12
|
+
*/
|
|
13
|
+
import { StreamableManager } from './streamable';
|
|
14
|
+
/**
|
|
15
|
+
* `build` receives the settler and returns the stream spec; its handlers call
|
|
16
|
+
* `settle.resolve`/`settle.reject` once the resource is terminal. Settling
|
|
17
|
+
* stops the stream unless the spec says `keepOpen`.
|
|
18
|
+
*/
|
|
19
|
+
export function streamUntilTerminal(build) {
|
|
20
|
+
let settled = false;
|
|
21
|
+
let stopped = false;
|
|
22
|
+
let manager;
|
|
23
|
+
const done = new Promise((resolve, reject) => {
|
|
24
|
+
const settle = {
|
|
25
|
+
resolve: (value) => {
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
settled = true;
|
|
29
|
+
if (!keepOpen)
|
|
30
|
+
manager?.stop();
|
|
31
|
+
resolve(value);
|
|
32
|
+
},
|
|
33
|
+
reject: (error) => {
|
|
34
|
+
if (settled)
|
|
35
|
+
return;
|
|
36
|
+
settled = true;
|
|
37
|
+
if (!keepOpen)
|
|
38
|
+
manager?.stop();
|
|
39
|
+
reject(error);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const { events, reconcile, closedMessage, onStopped, keepOpen, ...managerOptions } = build(settle);
|
|
43
|
+
let streamError = null;
|
|
44
|
+
manager = new StreamableManager({
|
|
45
|
+
...managerOptions,
|
|
46
|
+
onError: (error) => {
|
|
47
|
+
streamError = error;
|
|
48
|
+
},
|
|
49
|
+
onEnd: () => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
if (stopped) {
|
|
53
|
+
onStopped?.();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (streamError) {
|
|
57
|
+
settle.reject(streamError);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Closed cleanly before the resource was terminal: ask the API once.
|
|
61
|
+
const closed = (cause) => {
|
|
62
|
+
if (stopped && !settled) {
|
|
63
|
+
onStopped?.();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const error = new Error(closedMessage);
|
|
67
|
+
if (cause !== undefined)
|
|
68
|
+
error.cause = cause;
|
|
69
|
+
settle.reject(error);
|
|
70
|
+
};
|
|
71
|
+
reconcile().then(() => closed(), closed);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
for (const [name, listener] of Object.entries(events ?? {})) {
|
|
75
|
+
manager.addEventListener(name, listener);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
manager.start();
|
|
79
|
+
return {
|
|
80
|
+
done,
|
|
81
|
+
stop: () => {
|
|
82
|
+
if (stopped)
|
|
83
|
+
return;
|
|
84
|
+
stopped = true;
|
|
85
|
+
manager.stop();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
package/dist/http/stream.d.ts
CHANGED
package/dist/http/stream.js
CHANGED
|
@@ -17,6 +17,7 @@ export class StreamManager {
|
|
|
17
17
|
this.initialConnectionAttempts = 0;
|
|
18
18
|
this.consecutiveErrors = 0;
|
|
19
19
|
this.isConnected = false;
|
|
20
|
+
this.hasEverConnected = false;
|
|
20
21
|
this.isStopped = false;
|
|
21
22
|
this.eventListeners = new Map();
|
|
22
23
|
this.options = {
|
|
@@ -53,6 +54,8 @@ export class StreamManager {
|
|
|
53
54
|
source.addEventListener(eventName, (e) => {
|
|
54
55
|
if (this.isStopped)
|
|
55
56
|
return;
|
|
57
|
+
// A delivered event proves the connection works, same as onmessage.
|
|
58
|
+
this.consecutiveErrors = 0;
|
|
56
59
|
try {
|
|
57
60
|
const parsed = JSON.parse(e.data);
|
|
58
61
|
const listeners = this.eventListeners.get(eventName);
|
|
@@ -114,15 +117,15 @@ export class StreamManager {
|
|
|
114
117
|
if (!this.options.autoReconnect || this.isStopped)
|
|
115
118
|
return false;
|
|
116
119
|
const maxReconnects = this.options.maxReconnects ?? 5;
|
|
117
|
-
if (!this.
|
|
120
|
+
if (!this.hasEverConnected && this.initialConnectionAttempts >= maxReconnects) {
|
|
118
121
|
return false;
|
|
119
122
|
}
|
|
120
|
-
if (this.
|
|
123
|
+
if (this.hasEverConnected && this.consecutiveErrors >= maxReconnects) {
|
|
121
124
|
return false;
|
|
122
125
|
}
|
|
123
126
|
this.reconnectTimeout = setTimeout(() => {
|
|
124
127
|
if (!this.isStopped) {
|
|
125
|
-
if (!this.
|
|
128
|
+
if (!this.hasEverConnected) {
|
|
126
129
|
this.initialConnectionAttempts++;
|
|
127
130
|
}
|
|
128
131
|
this.consecutiveErrors++;
|
|
@@ -142,6 +145,7 @@ export class StreamManager {
|
|
|
142
145
|
return;
|
|
143
146
|
this.eventSource = source;
|
|
144
147
|
this.isConnected = true;
|
|
148
|
+
this.hasEverConnected = true;
|
|
145
149
|
this.options.onStart?.();
|
|
146
150
|
// Set up typed event listeners (for events with `event: eventName` header)
|
|
147
151
|
this.setupAllEventListeners(source);
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
* This is the NDJSON alternative to SSE/EventSource that avoids
|
|
4
4
|
* the browser's ~6 connection limit per domain.
|
|
5
5
|
*/
|
|
6
|
+
/** What a stream asks of whatever sends its request. */
|
|
7
|
+
export interface StreamRequestInit {
|
|
8
|
+
method: 'GET' | 'POST';
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
body?: string;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sends a stream's request in place of a plain fetch of a URL. With
|
|
15
|
+
* `(init) => http.fetch(endpoint, init)` the request carries the client's
|
|
16
|
+
* auth and a refusal goes through its error handler.
|
|
17
|
+
*/
|
|
18
|
+
export type StreamRequest = (init: StreamRequestInit) => Promise<Response>;
|
|
6
19
|
export interface StreamableOptions {
|
|
7
20
|
/** Additional headers to include */
|
|
8
21
|
headers?: Record<string, string>;
|
|
@@ -14,7 +27,7 @@ export interface StreamableOptions {
|
|
|
14
27
|
signal?: AbortSignal;
|
|
15
28
|
/** Skip heartbeat messages (default: true) */
|
|
16
29
|
skipHeartbeats?: boolean;
|
|
17
|
-
/** Request credentials mode (for cookie-based auth) */
|
|
30
|
+
/** Request credentials mode (for cookie-based auth); a StreamRequest brings its own */
|
|
18
31
|
credentials?: RequestCredentials;
|
|
19
32
|
}
|
|
20
33
|
export interface StreamableMessage<T = unknown> {
|
|
@@ -30,19 +43,26 @@ export interface StreamableMessage<T = unknown> {
|
|
|
30
43
|
/**
|
|
31
44
|
* Stream NDJSON from an HTTP endpoint using fetch + ReadableStream.
|
|
32
45
|
* Yields parsed JSON objects, automatically filtering heartbeats.
|
|
46
|
+
* `source` is the URL to fetch, or a StreamRequest that sends the request.
|
|
33
47
|
*/
|
|
34
|
-
export declare function streamable<T = unknown>(
|
|
48
|
+
export declare function streamable<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<T>;
|
|
35
49
|
/**
|
|
36
50
|
* StreamableManager provides a callback-based API similar to StreamManager
|
|
37
51
|
* but uses fetch + ReadableStream instead of EventSource.
|
|
38
52
|
*/
|
|
39
|
-
|
|
53
|
+
/** Where a StreamableManager's stream comes from: a URL it fetches, or a StreamRequest. */
|
|
54
|
+
export type StreamableSource = {
|
|
40
55
|
/** URL to connect to */
|
|
41
56
|
url: string;
|
|
42
57
|
/** Additional headers */
|
|
43
58
|
headers?: Record<string, string>;
|
|
44
59
|
/** Request credentials mode (for cookie-based auth) */
|
|
45
60
|
credentials?: RequestCredentials;
|
|
61
|
+
} | {
|
|
62
|
+
/** Sends the request, e.g. `(init) => http.fetch(endpoint, init)` */
|
|
63
|
+
request: StreamRequest;
|
|
64
|
+
};
|
|
65
|
+
export type StreamableManagerOptions<T> = StreamableSource & {
|
|
46
66
|
/** Request body */
|
|
47
67
|
body?: unknown;
|
|
48
68
|
/** Called for each message */
|
|
@@ -57,7 +77,7 @@ export interface StreamableManagerOptions<T> {
|
|
|
57
77
|
onEnd?: () => void;
|
|
58
78
|
/** Called for delta events (streaming token deltas) */
|
|
59
79
|
onDelta?: (delta: Record<string, any>, seq: number) => void;
|
|
60
|
-
}
|
|
80
|
+
};
|
|
61
81
|
export declare class StreamableManager<T> {
|
|
62
82
|
private options;
|
|
63
83
|
private abortController;
|
|
@@ -76,4 +96,4 @@ export declare class StreamableManager<T> {
|
|
|
76
96
|
* Raw streamable that yields the full message including wrapper.
|
|
77
97
|
* Use this when you need access to event type or fields.
|
|
78
98
|
*/
|
|
79
|
-
export declare function streamableRaw<T = unknown>(
|
|
99
|
+
export declare function streamableRaw<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;
|