@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 +57 -0
- package/dist/agent/actions.js +1 -4
- package/dist/agent/api.d.ts +2 -5
- package/dist/agent/api.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/api/agents.js +2 -4
- package/dist/api/tasks.js +1 -4
- package/dist/http/client.d.ts +68 -7
- package/dist/http/client.js +136 -96
- package/dist/http/index.d.ts +2 -2
- package/dist/http/streamable.d.ts +25 -5
- package/dist/http/streamable.js +28 -40
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +169 -266
- package/dist/types.js +6 -252
- package/package.json +1 -1
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
|
package/dist/agent/actions.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/agent/api.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
207
|
-
return client.http.
|
|
206
|
+
export function chatStreamRequest(client, chatId) {
|
|
207
|
+
return (init) => client.http.fetch(`/chats/${chatId}/stream`, init);
|
|
208
208
|
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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' | '
|
|
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>;
|
package/dist/api/agents.js
CHANGED
|
@@ -209,13 +209,11 @@ export class Agent {
|
|
|
209
209
|
streamUntilIdle(options, gate) {
|
|
210
210
|
if (!this.chatId)
|
|
211
211
|
return Promise.resolve();
|
|
212
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/http/client.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
20
|
-
|
|
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
|
-
/**
|
|
75
|
-
private
|
|
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
|
|
164
|
+
* @deprecated Use StreamableManager with a StreamRequest instead
|
|
104
165
|
*/
|
|
105
166
|
createEventSource(endpoint: string): Promise<EventSource | null>;
|
|
106
167
|
}
|
package/dist/http/client.js
CHANGED
|
@@ -54,32 +54,139 @@ export class HttpClient {
|
|
|
54
54
|
}
|
|
55
55
|
return resolved;
|
|
56
56
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
if (this.
|
|
60
|
-
return
|
|
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
|
|
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
|
|
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
|
|
93
|
+
return await send(token);
|
|
71
94
|
}
|
|
72
95
|
catch (error) {
|
|
73
|
-
if (this.onError) {
|
|
74
|
-
return await this.onError(error,
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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 {
|
|
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
|
|
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 =
|
|
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 }));
|
|
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
|
/**
|
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';
|
|
@@ -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>;
|