flexinference 1.4.1 → 1.5.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 +18 -0
- package/README.md +26 -12
- package/dist/index.d.ts +42 -16
- package/dist/index.js +168 -90
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
Reworks request timeouts so a healthy stream is never cut off yet a hung request never hangs
|
|
6
|
+
forever. Streaming requests now have **no total cap** -- they run as long as tokens keep
|
|
7
|
+
arriving -- and are bounded instead by two new options: `firstByteTimeout` (how long to wait
|
|
8
|
+
for the first response, default `60000` ms) and `idleTimeout` (max silence between chunks
|
|
9
|
+
before the stream is treated as hung, default `60000` ms). Non-streaming requests keep the
|
|
10
|
+
total `timeout` (default `600000` ms). The first-byte wait is **auto-raised for a flex
|
|
11
|
+
`start_within`**, fixing flex requests whose deadline exceeded the old hardcoded 10s connect
|
|
12
|
+
cap and aborted before the router replied. All three timeouts are configurable on the client
|
|
13
|
+
and per request. Behavior change, no API breaks: `timeout` no longer caps a stream, and the
|
|
14
|
+
old 10s first-response cap is now 60s (auto-sized for flex).
|
|
15
|
+
|
|
16
|
+
Also teaches `FlexInferenceError` to parse the router's per-surface error shapes: the router now
|
|
17
|
+
returns errors in the dialect of the endpoint you called (OpenAI on responses/chat, Anthropic on
|
|
18
|
+
`messages`, Google on `interactions`), and the error class reads all three (`message` always,
|
|
19
|
+
`type` from the OpenAI/Anthropic `type` or Gemini `status`).
|
|
20
|
+
|
|
3
21
|
## 1.4.1
|
|
4
22
|
|
|
5
23
|
Rewrites the README in plain language from the copy audit. It explains what `start_within` does, how the cheaper flex tier runs first and falls back up to your standard tier when it cannot start in time, and how you only pay a share of what a flex request saves you. No code or API changes.
|
package/README.md
CHANGED
|
@@ -102,19 +102,29 @@ console.log(messageText(res));
|
|
|
102
102
|
|
|
103
103
|
## Timeouts and cancellation
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
Streaming and non-streaming requests are timed differently, so a long generation is never cut off yet a hung request never hangs forever:
|
|
106
|
+
|
|
107
|
+
- **Non-streaming** requests have a total wall-clock budget: `timeout`, default **600000 ms (10 min)**, from send through the full body.
|
|
108
|
+
- **Streaming** requests have **no total cap** (a healthy stream runs as long as tokens keep arriving). They are bounded by two clocks: `firstByteTimeout` (wait for the first response, default **60000 ms**) and `idleTimeout` (max silence between chunks before the stream is treated as hung, default **60000 ms**).
|
|
109
|
+
|
|
110
|
+
The first-byte wait is **auto-raised for a flex `start_within`** (the router withholds response headers until the flex race resolves), so a long deadline just works. Raise `firstByteTimeout` yourself only for very large non-flex contexts whose first token is slow.
|
|
106
111
|
|
|
107
112
|
```ts
|
|
108
|
-
const client = new FlexInference({
|
|
113
|
+
const client = new FlexInference({
|
|
114
|
+
apiKey: "flex_live_...",
|
|
115
|
+
timeout: 120_000, // non-streaming total budget
|
|
116
|
+
firstByteTimeout: 90_000, // wait for the first response
|
|
117
|
+
idleTimeout: 30_000, // max silence between streamed chunks
|
|
118
|
+
});
|
|
109
119
|
```
|
|
110
120
|
|
|
111
|
-
|
|
121
|
+
Any of these can be overridden per request, alongside an `AbortSignal` to cancel yourself. Cancelling a stream stops it mid-flight:
|
|
112
122
|
|
|
113
123
|
```ts
|
|
114
124
|
const controller = new AbortController();
|
|
115
125
|
const res = await client.responses.create(
|
|
116
126
|
{ model: "gpt-5.5", input: "Write a haiku about cheap GPUs.", start_within: "priority" },
|
|
117
|
-
{ signal: controller.signal },
|
|
127
|
+
{ signal: controller.signal, idleTimeout: 15_000 },
|
|
118
128
|
);
|
|
119
129
|
// controller.abort() from elsewhere to cancel
|
|
120
130
|
console.log(outputText(res));
|
|
@@ -122,7 +132,7 @@ console.log(outputText(res));
|
|
|
122
132
|
|
|
123
133
|
## Errors
|
|
124
134
|
|
|
125
|
-
Non-2xx responses throw `FlexInferenceError`, carrying the OpenAI
|
|
135
|
+
Non-2xx responses throw `FlexInferenceError`, carrying `status`, `type`, `code`, and `param`. The router shapes error bodies to match the endpoint you called (OpenAI on `responses`/`chat`, Anthropic on `messages`, Google on `interactions`) so the SDK you would use for that surface parses them; `FlexInferenceError` reads all three. `message` and `status` are always set; `code`, `param`, and `docUrl` are populated on the OpenAI surface.
|
|
126
136
|
|
|
127
137
|
```ts
|
|
128
138
|
import { FlexInferenceError } from "flexinference";
|
|
@@ -138,7 +148,7 @@ try {
|
|
|
138
148
|
}
|
|
139
149
|
```
|
|
140
150
|
|
|
141
|
-
Every FlexInference error tells you four things. It says what went wrong, why it went wrong, how to fix it, and it shows an example of a request that works. The `message` reads like a note from a person, so an agent can act on it instead of guessing.
|
|
151
|
+
Every FlexInference error tells you four things. It says what went wrong, why it went wrong, how to fix it, and it shows an example of a request that works. The `message` reads like a note from a person, so an agent can act on it instead of guessing. Provider errors are reshaped into the same surface envelope (and normalized into `FlexInferenceError`), so you get one consistent error type no matter which model ran. For instance, a duration on a `claude-*` model returns `400 flex_unsupported_for_anthropic` with a message that tells you to drop the duration or switch to `default`, `priority`, or `auto`.
|
|
142
152
|
|
|
143
153
|
## Billing / 402
|
|
144
154
|
|
|
@@ -169,12 +179,16 @@ Because `PaymentRequiredError extends FlexInferenceError`, existing
|
|
|
169
179
|
|
|
170
180
|
## Configuration
|
|
171
181
|
|
|
172
|
-
| Option
|
|
173
|
-
|
|
|
174
|
-
| `apiKey`
|
|
175
|
-
| `baseURL`
|
|
176
|
-
| `fetch`
|
|
177
|
-
| `timeout`
|
|
182
|
+
| Option | Default | Description |
|
|
183
|
+
| ------------------ | ---------------------------------- | ------------------------------------------------------------------------------- |
|
|
184
|
+
| `apiKey` | - | Your `flex_live_` key (required). |
|
|
185
|
+
| `baseURL` | `https://api.flexinference.com/v1` | Override the router endpoint. |
|
|
186
|
+
| `fetch` | global `fetch` | Provide a custom fetch implementation. |
|
|
187
|
+
| `timeout` | `600000` | Non-streaming total budget in ms (streaming has no total). |
|
|
188
|
+
| `firstByteTimeout` | `60000` | Wait for the first response in ms; auto-raised for a flex `start_within`. |
|
|
189
|
+
| `idleTimeout` | `60000` | Max silence between streamed chunks in ms before the stream is treated as hung. |
|
|
190
|
+
|
|
191
|
+
All three timeouts can also be passed per request (with `signal`) in the second argument.
|
|
178
192
|
|
|
179
193
|
## License
|
|
180
194
|
|
package/dist/index.d.ts
CHANGED
|
@@ -70,7 +70,7 @@ export declare class FlexInferenceError extends Error {
|
|
|
70
70
|
readonly param: string | null | undefined;
|
|
71
71
|
/** Deep link into the docs for this error's code, when the router supplied one. */
|
|
72
72
|
readonly docUrl: string | null | undefined;
|
|
73
|
-
constructor(status: number, body:
|
|
73
|
+
constructor(status: number, body: unknown, fallback: string);
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Thrown on HTTP 402: billing is past due, so billable flex is paused. Free routing
|
|
@@ -86,19 +86,41 @@ export interface ClientOptions {
|
|
|
86
86
|
/** Override the global fetch (e.g. for tests or non-standard runtimes). */
|
|
87
87
|
fetch?: typeof fetch;
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
89
|
+
* Non-streaming wall-clock budget in ms (from send through the full body). Defaults to
|
|
90
|
+
* 600000 (10 min). Streaming requests ignore this: they are governed by `firstByteTimeout`
|
|
91
|
+
* and `idleTimeout` and have no total cap, so a healthy long stream is never cut off.
|
|
91
92
|
*/
|
|
92
93
|
timeout?: number;
|
|
94
|
+
/**
|
|
95
|
+
* How long to wait in ms for the first response (headers) before the router has sent
|
|
96
|
+
* anything. Defaults to 60000. For a flex `start_within` it is auto-raised to cover the
|
|
97
|
+
* deadline (the router withholds headers during the race), so you rarely set this; raise
|
|
98
|
+
* it for very large non-flex contexts whose first token is slow.
|
|
99
|
+
*/
|
|
100
|
+
firstByteTimeout?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Max silence in ms between streamed chunks before the stream is treated as hung and
|
|
103
|
+
* aborted. Defaults to 60000. Streaming requests only.
|
|
104
|
+
*/
|
|
105
|
+
idleTimeout?: number;
|
|
93
106
|
}
|
|
94
107
|
export interface RequestOptions {
|
|
95
108
|
signal?: AbortSignal;
|
|
109
|
+
/** Override the client `firstByteTimeout` for this request (ms). */
|
|
110
|
+
firstByteTimeout?: number;
|
|
111
|
+
/** Override the client `idleTimeout` for this streaming request (ms). */
|
|
112
|
+
idleTimeout?: number;
|
|
113
|
+
/** Override the client `timeout` (non-streaming total budget) for this request (ms). */
|
|
114
|
+
timeout?: number;
|
|
96
115
|
}
|
|
97
|
-
interface
|
|
116
|
+
interface OpenResult {
|
|
98
117
|
res: Response;
|
|
118
|
+
controller: AbortController;
|
|
99
119
|
cleanup: () => void;
|
|
120
|
+
idleMs: number;
|
|
121
|
+
totalMs: number;
|
|
100
122
|
}
|
|
101
|
-
type
|
|
123
|
+
type Send = (path: string, body: unknown, options: RequestOptions | undefined) => Promise<OpenResult>;
|
|
102
124
|
export declare class FlexInference {
|
|
103
125
|
readonly responses: Responses;
|
|
104
126
|
readonly chat: Chat;
|
|
@@ -107,13 +129,17 @@ export declare class FlexInference {
|
|
|
107
129
|
private readonly apiKey;
|
|
108
130
|
private readonly baseURL;
|
|
109
131
|
private readonly fetchImpl;
|
|
110
|
-
private readonly
|
|
132
|
+
private readonly firstByteMs;
|
|
133
|
+
private readonly idleMs;
|
|
134
|
+
private readonly totalMs;
|
|
111
135
|
constructor(options: ClientOptions);
|
|
112
|
-
private
|
|
136
|
+
private timeoutsFor;
|
|
137
|
+
private send;
|
|
138
|
+
private open;
|
|
113
139
|
}
|
|
114
140
|
declare class Responses {
|
|
115
|
-
private readonly
|
|
116
|
-
constructor(
|
|
141
|
+
private readonly send;
|
|
142
|
+
constructor(send: Send);
|
|
117
143
|
create(body: ResponseCreateParams & {
|
|
118
144
|
stream?: false | null;
|
|
119
145
|
}, options?: RequestOptions): Promise<ResponseObject>;
|
|
@@ -123,11 +149,11 @@ declare class Responses {
|
|
|
123
149
|
}
|
|
124
150
|
declare class Chat {
|
|
125
151
|
readonly completions: ChatCompletions;
|
|
126
|
-
constructor(
|
|
152
|
+
constructor(send: Send);
|
|
127
153
|
}
|
|
128
154
|
declare class ChatCompletions {
|
|
129
|
-
private readonly
|
|
130
|
-
constructor(
|
|
155
|
+
private readonly send;
|
|
156
|
+
constructor(send: Send);
|
|
131
157
|
create(body: ChatCompletionCreateParams & {
|
|
132
158
|
stream?: false | null;
|
|
133
159
|
}, options?: RequestOptions): Promise<ChatCompletion>;
|
|
@@ -136,8 +162,8 @@ declare class ChatCompletions {
|
|
|
136
162
|
}, options?: RequestOptions): Promise<AsyncIterable<ChatCompletionChunk>>;
|
|
137
163
|
}
|
|
138
164
|
declare class Interactions {
|
|
139
|
-
private readonly
|
|
140
|
-
constructor(
|
|
165
|
+
private readonly send;
|
|
166
|
+
constructor(send: Send);
|
|
141
167
|
create(body: InteractionCreateParams & {
|
|
142
168
|
stream?: false | null;
|
|
143
169
|
}, options?: RequestOptions): Promise<InteractionObject>;
|
|
@@ -146,8 +172,8 @@ declare class Interactions {
|
|
|
146
172
|
}, options?: RequestOptions): Promise<AsyncIterable<InteractionStreamEvent>>;
|
|
147
173
|
}
|
|
148
174
|
declare class Messages {
|
|
149
|
-
private readonly
|
|
150
|
-
constructor(
|
|
175
|
+
private readonly send;
|
|
176
|
+
constructor(send: Send);
|
|
151
177
|
create(body: MessageCreateParams & {
|
|
152
178
|
stream?: false | null;
|
|
153
179
|
}, options?: RequestOptions): Promise<MessageObject>;
|
package/dist/index.js
CHANGED
|
@@ -92,15 +92,26 @@ export class FlexInferenceError extends Error {
|
|
|
92
92
|
param;
|
|
93
93
|
/** Deep link into the docs for this error's code, when the router supplied one. */
|
|
94
94
|
docUrl;
|
|
95
|
+
// The router shapes errors by the endpoint you called, so `body` may be the OpenAI
|
|
96
|
+
// envelope (responses/chat), the Anthropic one (messages: `{type:"error", error:{...}}`),
|
|
97
|
+
// or the Google one (interactions: `{error:{code,message,status}}`). All three nest the
|
|
98
|
+
// detail under `error`, so this parses whichever arrived. `message` is always populated;
|
|
99
|
+
// `type` reads OpenAI/Anthropic `error.type` or falls back to Gemini's `error.status`;
|
|
100
|
+
// `code`/`param`/`doc_url` are OpenAI-only and undefined on the other shapes.
|
|
95
101
|
constructor(status, body, fallback) {
|
|
96
|
-
const err = body
|
|
97
|
-
super(err?.message
|
|
102
|
+
const err = isRecord(body) && isRecord(body.error) ? body.error : undefined;
|
|
103
|
+
super(typeof err?.message === "string" ? err.message : fallback);
|
|
98
104
|
this.name = "FlexInferenceError";
|
|
99
105
|
this.status = status;
|
|
100
|
-
this.type =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
this.type =
|
|
107
|
+
typeof err?.type === "string"
|
|
108
|
+
? err.type
|
|
109
|
+
: typeof err?.status === "string"
|
|
110
|
+
? err.status
|
|
111
|
+
: undefined;
|
|
112
|
+
this.code = typeof err?.code === "string" ? err.code : undefined;
|
|
113
|
+
this.param = typeof err?.param === "string" ? err.param : undefined;
|
|
114
|
+
this.docUrl = typeof err?.doc_url === "string" ? err.doc_url : undefined;
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
/**
|
|
@@ -111,8 +122,16 @@ export class FlexInferenceError extends Error {
|
|
|
111
122
|
export class PaymentRequiredError extends FlexInferenceError {
|
|
112
123
|
}
|
|
113
124
|
const DEFAULT_BASE_URL = "https://api.flexinference.com/v1";
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
// A streaming request is governed by two clocks and no total: how long we wait for the
|
|
126
|
+
// first response (headers), and the maximum silence between chunks once it is flowing. A
|
|
127
|
+
// healthy long generation is allowed to run as long as tokens keep arriving. A
|
|
128
|
+
// non-streaming request has no stream to idle-watch, so it keeps a total wall-clock budget
|
|
129
|
+
// instead.
|
|
130
|
+
const DEFAULT_FIRST_BYTE_MS = 60_000; // wait for the first response; auto-raised for a flex start_within
|
|
131
|
+
const FLEX_FIRST_BYTE_MARGIN_MS = 30_000; // headroom over the flex deadline for the fallback-to-standard path
|
|
132
|
+
const DEFAULT_IDLE_MS = 60_000; // max silence between streamed chunks before the stream is treated as hung
|
|
133
|
+
const DEFAULT_TOTAL_MS = 600_000; // non-streaming wall-clock budget (streaming has none)
|
|
134
|
+
const ERROR_BODY_READ_MS = 30_000; // bound the read of a non-2xx error body
|
|
116
135
|
const MIN_DEADLINE_SECONDS = 5;
|
|
117
136
|
const MAX_DEADLINE_SECONDS = 600;
|
|
118
137
|
const SECONDS_PER_HOUR = 3600;
|
|
@@ -145,6 +164,42 @@ function assertStartWithin(body) {
|
|
|
145
164
|
throw new Error("FlexInference: `start_within` duration must be between 5s and 10m.");
|
|
146
165
|
}
|
|
147
166
|
}
|
|
167
|
+
// Distinct abort reasons so an aborted fetch/read can be told apart: a timeout we fired
|
|
168
|
+
// (turned into a clear message) versus the caller's own AbortSignal (propagated as-is).
|
|
169
|
+
const FIRST_BYTE_TIMEOUT = Symbol("first-byte-timeout");
|
|
170
|
+
const IDLE_TIMEOUT = Symbol("idle-timeout");
|
|
171
|
+
/**
|
|
172
|
+
* Seconds for a flex-duration `start_within`, or null for a tier value ("default" etc.),
|
|
173
|
+
* a missing field, or a malformed one. Used to auto-size the first-byte wait: the router
|
|
174
|
+
* withholds response headers until the flex race commits or falls back, so the first-byte
|
|
175
|
+
* clock must cover the whole deadline plus the fallback path.
|
|
176
|
+
*/
|
|
177
|
+
function flexDeadlineSeconds(body) {
|
|
178
|
+
const value = typeof body === "object" && body !== null
|
|
179
|
+
? body["start_within"]
|
|
180
|
+
: undefined;
|
|
181
|
+
if (typeof value !== "string")
|
|
182
|
+
return null;
|
|
183
|
+
const match = DURATION_RE.exec(value);
|
|
184
|
+
if (match === null)
|
|
185
|
+
return null;
|
|
186
|
+
return (Number(match[1]) * SECONDS_PER_HOUR + Number(match[2]) * SECONDS_PER_MINUTE + Number(match[3]));
|
|
187
|
+
}
|
|
188
|
+
// Read a non-streamed JSON body under the total wall-clock budget, then release the timer
|
|
189
|
+
// and the abort listener. Non-streaming only: a stream is drained by streamSSE instead.
|
|
190
|
+
async function readJson(open) {
|
|
191
|
+
const { res, controller, cleanup, totalMs } = open;
|
|
192
|
+
const totalTimer = setTimeout(() => {
|
|
193
|
+
controller.abort();
|
|
194
|
+
}, totalMs);
|
|
195
|
+
try {
|
|
196
|
+
return (await res.json());
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
clearTimeout(totalTimer);
|
|
200
|
+
cleanup();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
148
203
|
export class FlexInference {
|
|
149
204
|
responses;
|
|
150
205
|
chat;
|
|
@@ -153,28 +208,54 @@ export class FlexInference {
|
|
|
153
208
|
apiKey;
|
|
154
209
|
baseURL;
|
|
155
210
|
fetchImpl;
|
|
156
|
-
|
|
211
|
+
firstByteMs;
|
|
212
|
+
idleMs;
|
|
213
|
+
totalMs;
|
|
157
214
|
constructor(options) {
|
|
158
215
|
if (!options.apiKey)
|
|
159
216
|
throw new Error("FlexInference: `apiKey` is required.");
|
|
160
217
|
this.apiKey = options.apiKey;
|
|
161
218
|
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
162
|
-
this.
|
|
219
|
+
this.firstByteMs = options.firstByteTimeout ?? DEFAULT_FIRST_BYTE_MS;
|
|
220
|
+
this.idleMs = options.idleTimeout ?? DEFAULT_IDLE_MS;
|
|
221
|
+
this.totalMs = options.timeout ?? DEFAULT_TOTAL_MS;
|
|
163
222
|
const f = options.fetch ?? globalThis.fetch;
|
|
164
223
|
if (typeof f !== "function") {
|
|
165
224
|
throw new Error("FlexInference: no global fetch found; pass `fetch` in options.");
|
|
166
225
|
}
|
|
167
226
|
this.fetchImpl = f;
|
|
168
|
-
const
|
|
169
|
-
this.responses = new Responses(
|
|
170
|
-
this.chat = new Chat(
|
|
171
|
-
this.interactions = new Interactions(
|
|
172
|
-
this.messages = new Messages(
|
|
227
|
+
const send = (path, body, options) => this.send(path, body, options);
|
|
228
|
+
this.responses = new Responses(send);
|
|
229
|
+
this.chat = new Chat(send);
|
|
230
|
+
this.interactions = new Interactions(send);
|
|
231
|
+
this.messages = new Messages(send);
|
|
232
|
+
}
|
|
233
|
+
// Resolve the three per-request clocks. A per-request override always wins. Otherwise the
|
|
234
|
+
// first-byte wait is auto-sized for a flex `start_within`: it can never be shorter than
|
|
235
|
+
// the deadline plus the fallback margin (headers are withheld for the whole race), but a
|
|
236
|
+
// larger client default still wins over a short deadline.
|
|
237
|
+
timeoutsFor(body, options) {
|
|
238
|
+
const deadline = flexDeadlineSeconds(body);
|
|
239
|
+
const autoFirstByte = deadline === null
|
|
240
|
+
? this.firstByteMs
|
|
241
|
+
: Math.max(this.firstByteMs, deadline * 1000 + FLEX_FIRST_BYTE_MARGIN_MS);
|
|
242
|
+
return {
|
|
243
|
+
firstByteMs: options?.firstByteTimeout ?? autoFirstByte,
|
|
244
|
+
idleMs: options?.idleTimeout ?? this.idleMs,
|
|
245
|
+
totalMs: options?.timeout ?? this.totalMs,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async send(path, body, options) {
|
|
249
|
+
const { firstByteMs, idleMs, totalMs } = this.timeoutsFor(body, options);
|
|
250
|
+
const { res, controller, cleanup } = await this.open(path, body, options?.signal, firstByteMs);
|
|
251
|
+
return { res, controller, cleanup, idleMs, totalMs };
|
|
173
252
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
253
|
+
// Open the request and return once response headers arrive (or throw on an error status).
|
|
254
|
+
// One AbortController is fed by two sources here: the caller's signal, and a first-byte
|
|
255
|
+
// timer (cleared once headers arrive). The idle timer (streaming) or total timer
|
|
256
|
+
// (non-streaming) is armed by the caller against the returned controller. Distinct abort
|
|
257
|
+
// reasons let a fired timeout be told apart from a caller abort.
|
|
258
|
+
async open(path, body, signal, firstByteMs) {
|
|
178
259
|
const controller = new AbortController();
|
|
179
260
|
const onCallerAbort = () => {
|
|
180
261
|
controller.abort(signal?.reason);
|
|
@@ -185,17 +266,12 @@ export class FlexInference {
|
|
|
185
266
|
else
|
|
186
267
|
signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
187
268
|
}
|
|
188
|
-
const connectTimer = setTimeout(() => {
|
|
189
|
-
controller.abort();
|
|
190
|
-
}, CONNECT_TIMEOUT_MS);
|
|
191
|
-
const totalTimer = setTimeout(() => {
|
|
192
|
-
controller.abort();
|
|
193
|
-
}, this.timeoutMs);
|
|
194
269
|
const cleanup = () => {
|
|
195
|
-
clearTimeout(connectTimer);
|
|
196
|
-
clearTimeout(totalTimer);
|
|
197
270
|
signal?.removeEventListener("abort", onCallerAbort);
|
|
198
271
|
};
|
|
272
|
+
const firstByteTimer = setTimeout(() => {
|
|
273
|
+
controller.abort(FIRST_BYTE_TIMEOUT);
|
|
274
|
+
}, firstByteMs);
|
|
199
275
|
let res;
|
|
200
276
|
try {
|
|
201
277
|
res = await this.fetchImpl(`${this.baseURL}${path}`, {
|
|
@@ -210,24 +286,30 @@ export class FlexInference {
|
|
|
210
286
|
});
|
|
211
287
|
}
|
|
212
288
|
catch (err) {
|
|
289
|
+
clearTimeout(firstByteTimer);
|
|
213
290
|
cleanup();
|
|
214
|
-
if (controller.signal.
|
|
215
|
-
throw new Error(`FlexInference:
|
|
291
|
+
if (controller.signal.reason === FIRST_BYTE_TIMEOUT) {
|
|
292
|
+
throw new Error(`FlexInference: no response from the router within ${String(firstByteMs)}ms ` +
|
|
293
|
+
"(time to first byte). Raise `firstByteTimeout` for very large contexts or long " +
|
|
294
|
+
"`start_within` deadlines.");
|
|
216
295
|
}
|
|
217
296
|
throw err;
|
|
218
297
|
}
|
|
219
|
-
clearTimeout(
|
|
298
|
+
clearTimeout(firstByteTimer); // headers arrived
|
|
220
299
|
if (!res.ok) {
|
|
221
|
-
// Read the error body under
|
|
222
|
-
|
|
300
|
+
// Read the error body under a bounded budget, then release the abort listener.
|
|
301
|
+
const errTimer = setTimeout(() => {
|
|
302
|
+
controller.abort();
|
|
303
|
+
}, ERROR_BODY_READ_MS);
|
|
223
304
|
let parsed;
|
|
224
305
|
try {
|
|
225
|
-
parsed =
|
|
306
|
+
parsed = await res.json();
|
|
226
307
|
}
|
|
227
308
|
catch {
|
|
228
309
|
parsed = undefined;
|
|
229
310
|
}
|
|
230
311
|
finally {
|
|
312
|
+
clearTimeout(errTimer);
|
|
231
313
|
cleanup();
|
|
232
314
|
}
|
|
233
315
|
if (res.status === 402) {
|
|
@@ -235,92 +317,69 @@ export class FlexInference {
|
|
|
235
317
|
}
|
|
236
318
|
throw new FlexInferenceError(res.status, parsed, `HTTP ${String(res.status)} ${res.statusText}`);
|
|
237
319
|
}
|
|
238
|
-
return { res, cleanup };
|
|
320
|
+
return { res, controller, cleanup };
|
|
239
321
|
}
|
|
240
322
|
}
|
|
241
323
|
class Responses {
|
|
242
|
-
|
|
243
|
-
constructor(
|
|
244
|
-
this.
|
|
324
|
+
send;
|
|
325
|
+
constructor(send) {
|
|
326
|
+
this.send = send;
|
|
245
327
|
}
|
|
246
328
|
async create(body, options) {
|
|
247
329
|
assertStartWithin(body);
|
|
248
|
-
const
|
|
249
|
-
if (body.stream)
|
|
250
|
-
return streamSSE(
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
return (await res.json());
|
|
254
|
-
}
|
|
255
|
-
finally {
|
|
256
|
-
cleanup();
|
|
257
|
-
}
|
|
330
|
+
const open = await this.send("/responses", body, options);
|
|
331
|
+
if (body.stream)
|
|
332
|
+
return streamSSE(open);
|
|
333
|
+
return readJson(open);
|
|
258
334
|
}
|
|
259
335
|
}
|
|
260
336
|
class Chat {
|
|
261
337
|
completions;
|
|
262
|
-
constructor(
|
|
263
|
-
this.completions = new ChatCompletions(
|
|
338
|
+
constructor(send) {
|
|
339
|
+
this.completions = new ChatCompletions(send);
|
|
264
340
|
}
|
|
265
341
|
}
|
|
266
342
|
class ChatCompletions {
|
|
267
|
-
|
|
268
|
-
constructor(
|
|
269
|
-
this.
|
|
343
|
+
send;
|
|
344
|
+
constructor(send) {
|
|
345
|
+
this.send = send;
|
|
270
346
|
}
|
|
271
347
|
async create(body, options) {
|
|
272
348
|
assertStartWithin(body);
|
|
273
|
-
const
|
|
274
|
-
if (body.stream)
|
|
275
|
-
return streamSSE(
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
return (await res.json());
|
|
279
|
-
}
|
|
280
|
-
finally {
|
|
281
|
-
cleanup();
|
|
282
|
-
}
|
|
349
|
+
const open = await this.send("/chat/completions", body, options);
|
|
350
|
+
if (body.stream)
|
|
351
|
+
return streamSSE(open);
|
|
352
|
+
return readJson(open);
|
|
283
353
|
}
|
|
284
354
|
}
|
|
285
355
|
class Interactions {
|
|
286
|
-
|
|
287
|
-
constructor(
|
|
288
|
-
this.
|
|
356
|
+
send;
|
|
357
|
+
constructor(send) {
|
|
358
|
+
this.send = send;
|
|
289
359
|
}
|
|
290
360
|
async create(body, options) {
|
|
291
361
|
assertStartWithin(body);
|
|
292
|
-
const
|
|
293
|
-
if (body.stream)
|
|
294
|
-
return streamSSE(
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
return (await res.json());
|
|
298
|
-
}
|
|
299
|
-
finally {
|
|
300
|
-
cleanup();
|
|
301
|
-
}
|
|
362
|
+
const open = await this.send("/interactions", body, options);
|
|
363
|
+
if (body.stream)
|
|
364
|
+
return streamSSE(open);
|
|
365
|
+
return readJson(open);
|
|
302
366
|
}
|
|
303
367
|
}
|
|
304
368
|
class Messages {
|
|
305
|
-
|
|
306
|
-
constructor(
|
|
307
|
-
this.
|
|
369
|
+
send;
|
|
370
|
+
constructor(send) {
|
|
371
|
+
this.send = send;
|
|
308
372
|
}
|
|
309
373
|
async create(body, options) {
|
|
310
374
|
assertStartWithin(body);
|
|
311
|
-
const
|
|
312
|
-
if (body.stream)
|
|
313
|
-
return streamSSE(
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
return (await res.json());
|
|
317
|
-
}
|
|
318
|
-
finally {
|
|
319
|
-
cleanup();
|
|
320
|
-
}
|
|
375
|
+
const open = await this.send("/messages", body, options);
|
|
376
|
+
if (body.stream)
|
|
377
|
+
return streamSSE(open);
|
|
378
|
+
return readJson(open);
|
|
321
379
|
}
|
|
322
380
|
}
|
|
323
|
-
async function* streamSSE(
|
|
381
|
+
async function* streamSSE(open) {
|
|
382
|
+
const { res, controller, idleMs, cleanup } = open;
|
|
324
383
|
if (!res.body) {
|
|
325
384
|
cleanup();
|
|
326
385
|
throw new Error("FlexInference: streaming response has no body.");
|
|
@@ -331,7 +390,26 @@ async function* streamSSE(res, cleanup) {
|
|
|
331
390
|
let exhausted = false;
|
|
332
391
|
try {
|
|
333
392
|
for (;;) {
|
|
334
|
-
|
|
393
|
+
// Idle timeout: reset on every chunk. Fires only when the stream goes silent for
|
|
394
|
+
// idleMs, aborting the underlying fetch so a hung upstream can't pin the stream. A
|
|
395
|
+
// healthy stream (chunks flowing) keeps resetting it, so a long generation is fine.
|
|
396
|
+
const idleTimer = setTimeout(() => {
|
|
397
|
+
controller.abort(IDLE_TIMEOUT);
|
|
398
|
+
}, idleMs);
|
|
399
|
+
let chunk;
|
|
400
|
+
try {
|
|
401
|
+
chunk = await reader.read();
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
if (controller.signal.reason === IDLE_TIMEOUT) {
|
|
405
|
+
throw new Error(`FlexInference: stream stalled - no data for ${String(idleMs)}ms (idle timeout).`);
|
|
406
|
+
}
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
clearTimeout(idleTimer);
|
|
411
|
+
}
|
|
412
|
+
const { done, value } = chunk;
|
|
335
413
|
if (done) {
|
|
336
414
|
exhausted = true;
|
|
337
415
|
break;
|
package/package.json
CHANGED