@inferencesh/sdk 0.20.1 → 0.21.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 CHANGED
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.21.0] - 2026-10-08
11
+
12
+ Breaking: `AgentClient.http` requires `fetch`, the `AppTag*` constants are removed, and `StreamableManagerOptions` is a type alias. See Changed and Removed.
13
+
14
+ ### Added
15
+
16
+ - `HttpClient.fetch(endpoint, init?)`: fetches an endpoint with the client's auth, headers and credentials mode and resolves with the raw `Response`, for bodies `request()` does not parse (streams, text). A refused response throws the error `request()` would (`InferenceError`, `RequirementsNotMetException`). Type `HttpFetchInit`.
17
+ - `StreamRequest` and `StreamRequestInit`: a function that sends a stream's request. `StreamableManager` takes `{ request }` in place of `{ url, headers, credentials }`, and `streamable()` / `streamableRaw()` take one in place of the URL. With `(init) => http.fetch(endpoint, init)` the stream carries the token of the moment it connects and a refusal goes through `onError`. Type `StreamableSource`.
18
+ - `onError` gets a third argument, `FailedRequest` (`{ token }`): the bearer token the failed request carried, so a handler that refreshes credentials can tell a request sent before the refresh from one sent after it.
19
+ - `getToken` may be async (`() => string | null | undefined | Promise<string | null | undefined>`). `request()`, `fetch()` and the EventSource handshake wait for it.
20
+ - `HttpClient.request()` takes `handleErrors` (default `true`). With `handleErrors: false` the caller handles the request's failure itself: `onError` is not called for it, nothing is retried, and the error is thrown as it is.
21
+
22
+ - Types: `StoreCategoryDTO` and `StoreTagDTO` (`GET /store/categories`, `GET /store/tags`); the decision app contract (`DecisionInput`, `DecisionVisionInput`, `DecisionOutput` and its question and answer types); `ErrorCodeImpersonationReasonRequired`.
23
+
24
+ ### Changed
25
+
26
+ - `AgentClient.http` requires `fetch` and no longer requires `getStreamableConfig`. A hand-built `AgentClient` must add `http.fetch`; `Inference` and `HttpClient` already have it.
27
+ - The streams the SDK opens itself (`tasks.run()` and `tasks.watch()`, `Agent` chat streams, the agent chat in `@inferencesh/sdk/agent`) go through `HttpClient.fetch`. A stream the api refuses now reaches `onError` like a refused request, and fails with an `InferenceError` (`statusCode`, `responseBody`) where it failed with `Error('HTTP <status>: <body>')`.
28
+ - A request that got no response (the network failed) goes to `onError` from `fetch()` and the EventSource handshake too, as it already did from `request()`. The failure is thrown unless the handler's retry resolves with a response. A request aborted through its signal is not handed to `onError`.
29
+ - The EventSource handshake and `fetch()` take what `onError` resolves with only when it is a `Response`; anything else leaves the refused response in place.
30
+ - `StreamableManagerOptions` is a type alias (`StreamableSource & { ... }`), no longer an interface: it cannot be extended with `interface X extends StreamableManagerOptions<T>`; use an intersection.
31
+
32
+ ### Removed
33
+
34
+ - The `AppTag*` constants (`AppTagTextToImage` and the rest). App tags come from the store: `GET /store/tags`, typed `StoreTagDTO`.
35
+
36
+ ### Deprecated
37
+
38
+ - `HttpClient.getStreamableConfig()`: a request made from the config bypasses `onError` and carries the token of the moment the config was read. Use `fetch()` or a `StreamRequest`. It throws when `getToken` is async.
39
+
40
+ ## [0.20.1] - 2026-10-08
41
+
42
+ ### Added
43
+
44
+ - Types: app tags and the decision app category.
45
+
46
+ ## [0.20.0] - 2026-10-07
47
+
48
+ ### Added
49
+
50
+ - `tasks.files(id)`, `tasks.deleteFiles(id)` and `tasks.delete(id, { files })`: list a task's files and delete them with or without the task.
51
+ - Types: task files, MCP Apps (`_meta.ui`, `AppUIRef`, the MCP tool call UI fields), remote tags and policy rules, `AppDTO.resolved_function`, `DescriptionLimit`.
52
+
53
+ ### Changed
54
+
55
+ - Types: forms lose the bounty and reward fields; a bounty's proof form drops `proof_min_length`.
56
+
57
+ ### Security
58
+
59
+ - `next` ^16.3.6 (resolves 16.3.8) for GHSA-vcvr-r3jv-pc5j.
60
+
61
+ ## [0.19.0] - 2026-10-04
62
+
63
+ ### Changed
64
+
65
+ - Types regenerated; the run parameters docs drop `params.variant`.
66
+
10
67
  ## [0.18.0] - 2026-10-04
11
68
 
12
69
  ### Added
@@ -109,11 +109,8 @@ export function createActions(ctx) {
109
109
  return;
110
110
  }
111
111
  // Single unified stream with TypedEvents (both Chat and ChatMessage events)
112
- const { url, headers, credentials } = api.getChatStreamConfig(client, id);
113
112
  const manager = new StreamableManager({
114
- url,
115
- headers,
116
- credentials,
113
+ request: api.chatStreamRequest(client, id),
117
114
  onError: (error) => {
118
115
  console.warn('[AgentSDK] Stream error:', error);
119
116
  callbacks.onError?.(error);
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { AlwaysAllowOptionsDTO, AlwaysAllowResultDTO, ChatAgentDTO, ChatDTO, ChatMessageDTO, ChatSettingsDTO, ChatSettingsRequest, InterruptDTO, ToolExplanationDTO } from '../types';
11
11
  import type { AgentOptions, AgentClient, AgentInfo, FileRef } from './types';
12
+ import type { StreamRequest } from '../http/streamable';
12
13
  export interface SendResult {
13
14
  chatId: string;
14
15
  userMessage: ChatMessageDTO;
@@ -69,8 +70,4 @@ export declare function updateChatSettings(client: AgentClient, chatId: string,
69
70
  export declare function resolveInterrupt(client: AgentClient, interruptId: string, decision: 'allow' | 'deny'): Promise<InterruptDTO>;
70
71
  export declare function listRunInterrupts(client: AgentClient, runId: string): Promise<InterruptDTO[]>;
71
72
  export declare function uploadFile(client: AgentClient, file: globalThis.File): Promise<FileRef>;
72
- export declare function getChatStreamConfig(client: AgentClient, chatId: string): {
73
- url: string;
74
- headers: Record<string, string>;
75
- credentials: RequestCredentials;
76
- };
73
+ export declare function chatStreamRequest(client: AgentClient, chatId: string): StreamRequest;
package/dist/agent/api.js CHANGED
@@ -203,6 +203,6 @@ export async function listRunInterrupts(client, runId) {
203
203
  export async function uploadFile(client, file) {
204
204
  return client.files.upload(file);
205
205
  }
206
- export function getChatStreamConfig(client, chatId) {
207
- return client.http.getStreamableConfig(`/chats/${chatId}/stream`);
206
+ export function chatStreamRequest(client, chatId) {
207
+ return (init) => client.http.fetch(`/chats/${chatId}/stream`, init);
208
208
  }
@@ -19,7 +19,7 @@ export interface AlwaysAllowChoice {
19
19
  */
20
20
  export interface AgentClient {
21
21
  /** HTTP client for API requests */
22
- http: Pick<HttpClient, 'request' | 'getStreamableConfig' | 'getStreamDefault' | 'getPollIntervalMs'>;
22
+ http: Pick<HttpClient, 'request' | 'fetch' | 'getStreamDefault' | 'getPollIntervalMs'>;
23
23
  /** Files API for uploads */
24
24
  files: {
25
25
  upload: (data: string | Blob | globalThis.File) => Promise<FileRef>;
@@ -209,13 +209,11 @@ export class Agent {
209
209
  streamUntilIdle(options, gate) {
210
210
  if (!this.chatId)
211
211
  return Promise.resolve();
212
- const { url, headers, credentials } = this.http.getStreamableConfig(`/chats/${this.chatId}/stream`);
212
+ const endpoint = `/chats/${this.chatId}/stream`;
213
213
  return new Promise((resolve) => {
214
214
  this.stream?.stop();
215
215
  this.stream = new StreamableManager({
216
- url,
217
- headers,
218
- credentials,
216
+ request: (init) => this.http.fetch(endpoint, init),
219
217
  });
220
218
  // Last chat/run observation was idle but the gate wasn't settled yet;
221
219
  // a terminal message for this turn can settle it.
package/dist/api/tasks.js CHANGED
@@ -110,7 +110,6 @@ export class TasksAPI {
110
110
  const { onUpdate, onPartialUpdate, onDelta } = options;
111
111
  // Accumulate state across partial updates to preserve fields like session_id
112
112
  let accumulatedTask = { ...task };
113
- const { url, headers, credentials } = this.http.getStreamableConfig(`/tasks/${task.id}/stream`);
114
113
  let streamManager;
115
114
  const done = new Promise((resolve, reject) => {
116
115
  const settle = (data, stripped) => {
@@ -128,9 +127,7 @@ export class TasksAPI {
128
127
  }
129
128
  };
130
129
  streamManager = new StreamableManager({
131
- url,
132
- headers,
133
- credentials,
130
+ request: (init) => this.http.fetch(`/tasks/${task.id}/stream`, init),
134
131
  onDelta,
135
132
  onData: (data) => {
136
133
  // Merge new data, preserving existing fields if not in update
@@ -1,10 +1,26 @@
1
1
  import type { Response } from './response';
2
2
  import { EventSource } from 'eventsource';
3
+ /** What a failed request was sent with, for an error handler that acts on it. */
4
+ export interface FailedRequest {
5
+ /**
6
+ * The bearer token the request carried, or undefined when it carried none
7
+ * (proxy mode, or no apiKey and getToken returned nothing). A handler that
8
+ * refreshes credentials compares it with the token in hand: when they
9
+ * differ, the credentials were already replaced and a retry is enough.
10
+ */
11
+ token: string | undefined;
12
+ }
3
13
  /**
4
14
  * Error handler that can intercept errors and optionally retry the request.
5
15
  * Return a promise to retry with a new result, or throw/rethrow to propagate the error.
16
+ * `retry` sends the request again with the token of that moment and does not
17
+ * come back through the handler. `request` describes the attempt that failed.
6
18
  */
7
- export type ErrorHandler = (error: unknown, retry: () => Promise<unknown>) => Promise<unknown>;
19
+ export type ErrorHandler = (error: unknown, retry: () => Promise<unknown>, request: FailedRequest) => Promise<unknown>;
20
+ /** Options of HttpClient.fetch: a fetch init whose credentials mode is the client's. */
21
+ export type HttpFetchInit = Omit<RequestInit, 'headers' | 'credentials'> & {
22
+ headers?: Record<string, string>;
23
+ };
8
24
  export type MessageHandler = (messages: import('../types').ResponseMessage[]) => void;
9
25
  export interface HttpClientConfig {
10
26
  /** Your inference.sh API key (required unless using proxyUrl) */
@@ -16,8 +32,11 @@ export interface HttpClientConfig {
16
32
  * When set, requests are routed through your proxy server to protect API keys.
17
33
  */
18
34
  proxyUrl?: string;
19
- /** Dynamic token getter (alternative to apiKey) */
20
- getToken?: () => string | null | undefined;
35
+ /**
36
+ * Dynamic token getter (alternative to apiKey), asked before every request.
37
+ * It may be async: the request waits for the token.
38
+ */
39
+ getToken?: () => string | null | undefined | Promise<string | null | undefined>;
21
40
  /** Dynamic headers */
22
41
  headers?: Record<string, string | (() => string | undefined)>;
23
42
  /** Request credentials mode */
@@ -71,15 +90,53 @@ export declare class HttpClient {
71
90
  isProxyMode(): boolean;
72
91
  /** Resolve dynamic headers */
73
92
  private resolveHeaders;
74
- /** Get authorization token */
75
- private getAuthToken;
93
+ /** The bearer token a request sent now carries; none through a proxy, which adds its own. */
94
+ private bearerToken;
95
+ /** The configured headers plus what identifies the request: its bearer, or through a proxy its target. */
96
+ private requestHeaders;
97
+ /**
98
+ * Where a request whose caller cannot set headers (EventSource) or reads
99
+ * the URL (streams) goes: the API, or the proxy with the target as a query
100
+ * parameter.
101
+ */
102
+ private streamUrl;
76
103
  /**
77
104
  * Make an HTTP request to the API
78
105
  */
79
- request<T, P extends object = Record<string, unknown>>(method: 'get' | 'post' | 'put' | 'delete', endpoint: string, options?: {
106
+ request<T, P extends object = Record<string, unknown>>(method: 'get' | 'post' | 'put' | 'delete', endpoint: string, { handleErrors, ...options }?: {
80
107
  params?: P;
81
108
  data?: unknown;
109
+ /**
110
+ * `false` when the caller handles this request's failure itself: the
111
+ * client's error handler (onError) is not consulted, so there is no
112
+ * prompt, no redirect and no retry, and the error is thrown as it is.
113
+ */
114
+ handleErrors?: boolean;
82
115
  }): Promise<Response<T>>;
116
+ /**
117
+ * Fetch an API endpoint with the client's auth, headers and credentials
118
+ * mode and resolve with the raw response, for bodies request() does not
119
+ * parse (streams, text). A failure goes through onError like one of
120
+ * request() does: a refused response, or a request that got no response.
121
+ * It resolves with the response onError's retry got; when onError resolves
122
+ * with anything else the failure stands and is thrown, the error request()
123
+ * would throw. A request aborted through `init.signal` rejects with the
124
+ * abort and is not an error for onError.
125
+ */
126
+ fetch(endpoint: string, init?: HttpFetchInit): Promise<globalThis.Response>;
127
+ /**
128
+ * One fetch of `url` on behalf of `targetUrl`. A refused response, or a
129
+ * request that got none, goes to onError; the result is the response its
130
+ * retry got. Without one the failure stands: the error of a request that
131
+ * got no response is thrown, and a refusal is thrown as the error its body
132
+ * was read into or, for a caller that passes the response on
133
+ * (`refused: 'return'`), handed back with its body unread.
134
+ */
135
+ private fetchHandled;
136
+ /** The error a refused response stands for, read from its body. */
137
+ private readError;
138
+ /** HTTP errors → RFC 9457 problem+json */
139
+ private responseError;
83
140
  /**
84
141
  * Execute the actual HTTP request (internal)
85
142
  */
@@ -87,6 +144,10 @@ export declare class HttpClient {
87
144
  /**
88
145
  * Get URL and headers for NDJSON streaming.
89
146
  * Returns the full URL and auth headers needed for streamable requests.
147
+ * It answers synchronously, so it throws when getToken is async.
148
+ * @deprecated Use fetch(), or a StreamRequest built on it
149
+ * (`(init) => http.fetch(endpoint, init)`): a request made from this config
150
+ * bypasses onError and carries the token of the moment the config was read.
90
151
  */
91
152
  getStreamableConfig(endpoint: string): {
92
153
  url: string;
@@ -100,7 +161,7 @@ export declare class HttpClient {
100
161
  private extractErrorDetail;
101
162
  /**
102
163
  * Create an EventSource for SSE streaming
103
- * @deprecated Use getStreamableConfig() with StreamableManager instead
164
+ * @deprecated Use StreamableManager with a StreamRequest instead
104
165
  */
105
166
  createEventSource(endpoint: string): Promise<EventSource | null>;
106
167
  }
@@ -54,32 +54,139 @@ export class HttpClient {
54
54
  }
55
55
  return resolved;
56
56
  }
57
- /** Get authorization token */
58
- getAuthToken() {
59
- if (this.getToken) {
60
- return this.getToken();
57
+ /** The bearer token a request sent now carries; none through a proxy, which adds its own. */
58
+ async bearerToken() {
59
+ if (this.proxyUrl)
60
+ return undefined;
61
+ return (this.getToken ? await this.getToken() : this.apiKey) || undefined;
62
+ }
63
+ /** The configured headers plus what identifies the request: its bearer, or through a proxy its target. */
64
+ requestHeaders(targetUrl, token) {
65
+ const headers = this.resolveHeaders();
66
+ if (this.proxyUrl) {
67
+ headers['x-inf-target-url'] = targetUrl.toString();
68
+ }
69
+ else if (token) {
70
+ headers['Authorization'] = `Bearer ${token}`;
61
71
  }
62
- return this.apiKey;
72
+ return headers;
73
+ }
74
+ /**
75
+ * Where a request whose caller cannot set headers (EventSource) or reads
76
+ * the URL (streams) goes: the API, or the proxy with the target as a query
77
+ * parameter.
78
+ */
79
+ streamUrl(targetUrl) {
80
+ if (!this.proxyUrl)
81
+ return targetUrl.toString();
82
+ const proxyUrlWithQuery = new URL(this.proxyUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
83
+ proxyUrlWithQuery.searchParams.set('__inf_target', targetUrl.toString());
84
+ return proxyUrlWithQuery.toString();
63
85
  }
64
86
  /**
65
87
  * Make an HTTP request to the API
66
88
  */
67
- async request(method, endpoint, options = {}) {
68
- const doRequest = () => this.executeRequest(method, endpoint, options);
89
+ async request(method, endpoint, { handleErrors = true, ...options } = {}) {
90
+ const send = (token) => this.executeRequest(method, endpoint, options, token);
91
+ const token = await this.bearerToken();
69
92
  try {
70
- return await doRequest();
93
+ return await send(token);
71
94
  }
72
95
  catch (error) {
73
- if (this.onError) {
74
- return await this.onError(error, doRequest);
96
+ if (this.onError && handleErrors) {
97
+ return await this.onError(error, async () => send(await this.bearerToken()), { token });
75
98
  }
76
99
  throw error;
77
100
  }
78
101
  }
102
+ /**
103
+ * Fetch an API endpoint with the client's auth, headers and credentials
104
+ * mode and resolve with the raw response, for bodies request() does not
105
+ * parse (streams, text). A failure goes through onError like one of
106
+ * request() does: a refused response, or a request that got no response.
107
+ * It resolves with the response onError's retry got; when onError resolves
108
+ * with anything else the failure stands and is thrown, the error request()
109
+ * would throw. A request aborted through `init.signal` rejects with the
110
+ * abort and is not an error for onError.
111
+ */
112
+ async fetch(endpoint, init = {}) {
113
+ const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
114
+ const response = await this.fetchHandled(this.streamUrl(targetUrl), targetUrl, init, 'throw');
115
+ // What onError's retry got is handed over unread, and can be a refusal too.
116
+ if (!response.ok)
117
+ throw await this.readError(response);
118
+ return response;
119
+ }
120
+ /**
121
+ * One fetch of `url` on behalf of `targetUrl`. A refused response, or a
122
+ * request that got none, goes to onError; the result is the response its
123
+ * retry got. Without one the failure stands: the error of a request that
124
+ * got no response is thrown, and a refusal is thrown as the error its body
125
+ * was read into or, for a caller that passes the response on
126
+ * (`refused: 'return'`), handed back with its body unread.
127
+ */
128
+ async fetchHandled(url, targetUrl, init, refused) {
129
+ const send = (token) => fetch(url, {
130
+ ...init,
131
+ headers: { ...init.headers, ...this.requestHeaders(targetUrl, token) },
132
+ credentials: this.credentials,
133
+ });
134
+ const retry = async () => send(await this.bearerToken());
135
+ const token = await this.bearerToken();
136
+ let response;
137
+ try {
138
+ response = await send(token);
139
+ }
140
+ catch (error) {
141
+ // The caller aborted the request; nothing failed.
142
+ if (!this.onError || init.signal?.aborted)
143
+ throw error;
144
+ const handled = await this.onError(error, retry, { token });
145
+ if (handled instanceof globalThis.Response)
146
+ return handled;
147
+ throw error;
148
+ }
149
+ if (response.ok || (refused === 'return' && !this.onError))
150
+ return response;
151
+ // The body is read once: from a clone only when the response is handed back.
152
+ const error = await this.readError(refused === 'return' ? response.clone() : response);
153
+ const handled = await this.onError?.(error, retry, { token });
154
+ if (handled instanceof globalThis.Response)
155
+ return handled;
156
+ if (refused === 'return')
157
+ return response;
158
+ throw error;
159
+ }
160
+ /** The error a refused response stands for, read from its body. */
161
+ async readError(response) {
162
+ const responseText = await response.text().catch(() => '');
163
+ let data = null;
164
+ try {
165
+ data = JSON.parse(responseText);
166
+ }
167
+ catch {
168
+ // Not JSON
169
+ }
170
+ return this.responseError(response.status, responseText, data);
171
+ }
172
+ /** HTTP errors → RFC 9457 problem+json */
173
+ responseError(status, responseText, data) {
174
+ if (status === 412 && data && typeof data === 'object' && 'errors' in data && Array.isArray(data.errors)) {
175
+ return RequirementsNotMetException.fromResponse(data, status);
176
+ }
177
+ let errorDetail;
178
+ if (data && typeof data === 'object') {
179
+ errorDetail = this.extractErrorDetail(data) ?? JSON.stringify(data);
180
+ }
181
+ else if (responseText) {
182
+ errorDetail = responseText.slice(0, 500);
183
+ }
184
+ return new InferenceError(status, errorDetail || 'Request failed', responseText);
185
+ }
79
186
  /**
80
187
  * Execute the actual HTTP request (internal)
81
188
  */
82
- async executeRequest(method, endpoint, options = {}) {
189
+ async executeRequest(method, endpoint, options, token) {
83
190
  // Build the target URL (always points to the API)
84
191
  const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
85
192
  if (options.params) {
@@ -92,23 +199,11 @@ export class HttpClient {
92
199
  });
93
200
  }
94
201
  // In proxy mode, requests go to the proxy with target URL in a header
95
- const isProxyMode = !!this.proxyUrl;
96
- const fetchUrl = isProxyMode ? this.proxyUrl : targetUrl.toString();
202
+ const fetchUrl = this.proxyUrl ?? targetUrl.toString();
97
203
  const headers = {
98
204
  'Content-Type': 'application/json',
99
- ...this.resolveHeaders(),
205
+ ...this.requestHeaders(targetUrl, token),
100
206
  };
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
207
  const fetchOptions = {
113
208
  method: method.toUpperCase(),
114
209
  headers,
@@ -126,19 +221,8 @@ export class HttpClient {
126
221
  catch {
127
222
  // Not JSON
128
223
  }
129
- // HTTP errors → RFC 9457 problem+json
130
224
  if (!response.ok) {
131
- if (response.status === 412 && data && 'errors' in data && Array.isArray(data.errors)) {
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);
225
+ throw this.responseError(response.status, responseText, data);
142
226
  }
143
227
  if (response.status === 204 || !responseText) {
144
228
  return { data: undefined, messages: [] };
@@ -156,26 +240,22 @@ export class HttpClient {
156
240
  /**
157
241
  * Get URL and headers for NDJSON streaming.
158
242
  * Returns the full URL and auth headers needed for streamable requests.
243
+ * It answers synchronously, so it throws when getToken is async.
244
+ * @deprecated Use fetch(), or a StreamRequest built on it
245
+ * (`(init) => http.fetch(endpoint, init)`): a request made from this config
246
+ * bypasses onError and carries the token of the moment the config was read.
159
247
  */
160
248
  getStreamableConfig(endpoint) {
161
249
  const targetUrl = new URL(`${this.baseUrl}${endpoint}`);
162
- const isProxyMode = !!this.proxyUrl;
163
- let url;
164
- const headers = { ...this.resolveHeaders() };
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
- }
250
+ const token = this.proxyUrl ? undefined : this.getToken ? this.getToken() : this.apiKey;
251
+ if (token instanceof Promise) {
252
+ throw new Error('getStreamableConfig() cannot wait for an async getToken; use fetch() instead');
177
253
  }
178
- return { url, headers, credentials: this.credentials };
254
+ return {
255
+ url: this.streamUrl(targetUrl),
256
+ headers: this.requestHeaders(targetUrl, token || undefined),
257
+ credentials: this.credentials,
258
+ };
179
259
  }
180
260
  /**
181
261
  * Pull a human-readable detail string from a parsed problem+json / API error
@@ -189,56 +269,16 @@ export class HttpClient {
189
269
  }
190
270
  /**
191
271
  * Create an EventSource for SSE streaming
192
- * @deprecated Use getStreamableConfig() with StreamableManager instead
272
+ * @deprecated Use StreamableManager with a StreamRequest instead
193
273
  */
194
274
  createEventSource(endpoint) {
195
275
  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
276
  // The EventSource error event omits the response body, so a failed initial
210
277
  // connection (e.g. 403 otp_required) is handled here in the fetch wrapper:
211
278
  // route it through onError like request() does, and hand the retried
212
279
  // response (e.g. after OTP verification) back so the stream connects.
213
- const doFetch = async (input, init) => {
214
- const headers = {
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 }));
280
+ const doFetch = (input, init) => this.fetchHandled(input, targetUrl, init ?? {}, 'return');
281
+ return Promise.resolve(new EventSource(this.streamUrl(targetUrl), { fetch: doFetch }));
242
282
  }
243
283
  }
244
284
  /**
@@ -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';
@@ -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>(url: string, options?: StreamableOptions): AsyncGenerator<T>;
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
- export interface StreamableManagerOptions<T> {
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>(url: string, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;
99
+ export declare function streamableRaw<T = unknown>(source: string | StreamRequest, options?: StreamableOptions): AsyncGenerator<StreamableMessage<T> | T>;