flexinference 1.5.0 → 1.5.2
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 +15 -5
- package/README.md +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +150 -50
- package/dist/types.d.ts +2 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.1
|
|
4
|
+
|
|
5
|
+
Small robustness fixes for the transport, no API changes. A non-streaming request that hits
|
|
6
|
+
its total `timeout` now throws a clear timeout error instead of a bare abort that looked like
|
|
7
|
+
a caller cancellation. A streamed response flushes its decoder and delivers the final frame
|
|
8
|
+
even when the stream ends without a trailing blank line, so the last event is never dropped,
|
|
9
|
+
and the streaming buffer is capped so a stream that never sends a separator cannot grow
|
|
10
|
+
without bound. The `start_within` type now requires the runtime's two-digit duration
|
|
11
|
+
shape, so values like `0h-5m-0s` no longer type-check.
|
|
12
|
+
|
|
3
13
|
## 1.5.0
|
|
4
14
|
|
|
5
15
|
Reworks request timeouts so a healthy stream is never cut off yet a hung request never hangs
|
|
@@ -72,11 +82,11 @@ required and aligns its values with OpenAI's `service_tier` vocabulary.
|
|
|
72
82
|
|
|
73
83
|
### Breaking changes
|
|
74
84
|
|
|
75
|
-
- **`start_within` is now required on every request.** It is typed as
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
A duration that fits the shape but is malformed or out of range is caught at **runtime**;
|
|
79
|
-
plain-JS callers get every check at **runtime** (a thrown `Error` before any network call).
|
|
85
|
+
- **`start_within` is now required on every request.** It is typed as `"default"`,
|
|
86
|
+
`"priority"`, `"auto"`, or a duration shaped like `"00h-05m-00s"`, so omitting it
|
|
87
|
+
(or passing a value outside those shapes) is a **compile-time** error.
|
|
88
|
+
A duration that fits the shape but is malformed or out of range is caught at **runtime**;
|
|
89
|
+
plain-JS callers get every check at **runtime** (a thrown `Error` before any network call).
|
|
80
90
|
- **Accepted values changed.** `start_within` now takes `"default"`, `"priority"`,
|
|
81
91
|
`"auto"`, or a duration `"HHh-MMm-SSs"` (5s-10m). The old `"standard"` value is
|
|
82
92
|
removed; use `"default"` (it maps to the same OpenAI service tier).
|
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ console.log(messageText(res));
|
|
|
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
106
|
|
|
107
|
-
- **Non-streaming** requests have a
|
|
107
|
+
- **Non-streaming** requests have a body-read budget after response headers arrive: `timeout`, default **600000 ms (10 min)**. The worst case is roughly `firstByteTimeout + timeout`.
|
|
108
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
109
|
|
|
110
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,12 @@ type Schemas = components["schemas"];
|
|
|
5
5
|
* those OpenAI service tiers and proxy any model; a duration "HHh-MMm-SSs" (5s-10m)
|
|
6
6
|
* runs the flex race on a flex-capable model. An arbitrary string needs `as StartWithin`.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
9
|
+
type Second00To59 = "00" | "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | `1${Digit}` | `2${Digit}` | `3${Digit}` | `4${Digit}` | `5${Digit}`;
|
|
10
|
+
type Second05To59 = Exclude<Second00To59, "00" | "01" | "02" | "03" | "04">;
|
|
11
|
+
type Minute01To09 = `0${Exclude<Digit, "0">}`;
|
|
12
|
+
type Duration = `00h-00m-${Second05To59}s` | `00h-${Minute01To09}m-${Second00To59}s` | "00h-10m-00s";
|
|
13
|
+
export type StartWithin = "default" | "priority" | "auto" | Duration;
|
|
9
14
|
/** Make `start_within` a required, typed field on a generated request body. */
|
|
10
15
|
type WithStartWithin<T> = Omit<T, "start_within"> & {
|
|
11
16
|
start_within: StartWithin;
|
|
@@ -78,6 +83,7 @@ export declare class FlexInferenceError extends Error {
|
|
|
78
83
|
* FlexInferenceError)` handlers keep working; narrow to this to handle 402 specially.
|
|
79
84
|
*/
|
|
80
85
|
export declare class PaymentRequiredError extends FlexInferenceError {
|
|
86
|
+
constructor(status: number, body: unknown, fallback: string);
|
|
81
87
|
}
|
|
82
88
|
export interface ClientOptions {
|
|
83
89
|
apiKey: string;
|
|
@@ -86,9 +92,10 @@ export interface ClientOptions {
|
|
|
86
92
|
/** Override the global fetch (e.g. for tests or non-standard runtimes). */
|
|
87
93
|
fetch?: typeof fetch;
|
|
88
94
|
/**
|
|
89
|
-
* Non-streaming
|
|
90
|
-
* 600000 (10 min).
|
|
91
|
-
*
|
|
95
|
+
* Non-streaming body-read budget in ms, after response headers arrive. Defaults to
|
|
96
|
+
* 600000 (10 min). The worst case is roughly `firstByteTimeout + timeout`.
|
|
97
|
+
* Streaming requests ignore this: they are governed by `firstByteTimeout` and
|
|
98
|
+
* `idleTimeout` and have no total cap, so a healthy long stream is never cut off.
|
|
92
99
|
*/
|
|
93
100
|
timeout?: number;
|
|
94
101
|
/**
|
package/dist/index.js
CHANGED
|
@@ -120,6 +120,10 @@ export class FlexInferenceError extends Error {
|
|
|
120
120
|
* FlexInferenceError)` handlers keep working; narrow to this to handle 402 specially.
|
|
121
121
|
*/
|
|
122
122
|
export class PaymentRequiredError extends FlexInferenceError {
|
|
123
|
+
constructor(status, body, fallback) {
|
|
124
|
+
super(status, body, fallback);
|
|
125
|
+
this.name = "PaymentRequiredError";
|
|
126
|
+
}
|
|
123
127
|
}
|
|
124
128
|
const DEFAULT_BASE_URL = "https://api.flexinference.com/v1";
|
|
125
129
|
// A streaming request is governed by two clocks and no total: how long we wait for the
|
|
@@ -132,42 +136,114 @@ const FLEX_FIRST_BYTE_MARGIN_MS = 30_000; // headroom over the flex deadline for
|
|
|
132
136
|
const DEFAULT_IDLE_MS = 60_000; // max silence between streamed chunks before the stream is treated as hung
|
|
133
137
|
const DEFAULT_TOTAL_MS = 600_000; // non-streaming wall-clock budget (streaming has none)
|
|
134
138
|
const ERROR_BODY_READ_MS = 30_000; // bound the read of a non-2xx error body
|
|
139
|
+
// Cap the streaming buffer above the largest legitimate single frame (a terminal
|
|
140
|
+
// response object is well under this). A stream that never sends a blank-line separator
|
|
141
|
+
// would otherwise grow the buffer without bound; error clearly instead.
|
|
142
|
+
const MAX_SSE_BUFFER_CHARS = 8 * 1024 * 1024;
|
|
135
143
|
const MIN_DEADLINE_SECONDS = 5;
|
|
136
144
|
const MAX_DEADLINE_SECONDS = 600;
|
|
137
145
|
const SECONDS_PER_HOUR = 3600;
|
|
138
146
|
const SECONDS_PER_MINUTE = 60;
|
|
147
|
+
// Two digits per field so the runtime matches the router, Python SDK, and StartWithin type.
|
|
139
148
|
const DURATION_RE = /^(\d{2})h-(\d{2})m-(\d{2})s$/;
|
|
140
149
|
const START_WITHIN_VALUES = '"default", "priority", "auto", or a duration "HHh-MMm-SSs"';
|
|
150
|
+
function startWithinValue(body) {
|
|
151
|
+
const value = typeof body === "object" && body !== null
|
|
152
|
+
? body["start_within"]
|
|
153
|
+
: undefined;
|
|
154
|
+
return value === undefined ? { kind: "missing" } : { kind: "present", value };
|
|
155
|
+
}
|
|
156
|
+
function parseStartWithin(value) {
|
|
157
|
+
if (typeof value !== "string")
|
|
158
|
+
return { kind: "invalid", reason: "type", value };
|
|
159
|
+
if (value === "default" || value === "priority" || value === "auto") {
|
|
160
|
+
return { kind: "tier", value };
|
|
161
|
+
}
|
|
162
|
+
const match = DURATION_RE.exec(value);
|
|
163
|
+
if (match === null)
|
|
164
|
+
return { kind: "invalid", reason: "malformed", value };
|
|
165
|
+
const seconds = Number(match[1]) * SECONDS_PER_HOUR +
|
|
166
|
+
Number(match[2]) * SECONDS_PER_MINUTE +
|
|
167
|
+
Number(match[3]);
|
|
168
|
+
return { kind: "duration", value, seconds };
|
|
169
|
+
}
|
|
141
170
|
// Runtime guard so plain-JS callers (who get no compile-time check) still fail
|
|
142
171
|
// locally with a clear message instead of a provider round-trip. TypeScript callers
|
|
143
172
|
// are already protected by the required StartWithin field above.
|
|
144
173
|
function assertStartWithin(body) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
: undefined;
|
|
148
|
-
if (value === undefined) {
|
|
174
|
+
const sw = startWithinValue(body);
|
|
175
|
+
if (sw.kind === "missing") {
|
|
149
176
|
throw new Error(`FlexInference: \`start_within\` is required on every request. Set it to ${START_WITHIN_VALUES}, ` +
|
|
150
177
|
`for example "start_within": "default" for standard-tier behavior or "00h-00m-30s" for a 30-second flex race.`);
|
|
151
178
|
}
|
|
152
|
-
|
|
179
|
+
const parsed = parseStartWithin(sw.value);
|
|
180
|
+
if (parsed.kind === "invalid" && parsed.reason === "type") {
|
|
153
181
|
throw new Error(`FlexInference: \`start_within\` must be a string (${START_WITHIN_VALUES}), for example "default".`);
|
|
154
182
|
}
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
const match = DURATION_RE.exec(value);
|
|
158
|
-
if (match === null) {
|
|
159
|
-
throw new Error(`FlexInference: \`start_within\` "${value}" is not ${START_WITHIN_VALUES}. ` +
|
|
183
|
+
if (parsed.kind === "invalid") {
|
|
184
|
+
throw new Error(`FlexInference: \`start_within\` "${parsed.value}" is not ${START_WITHIN_VALUES}. ` +
|
|
160
185
|
`A duration is two digits each for hours, minutes, and seconds, for example "00h-05m-00s" for a 5-minute deadline.`);
|
|
161
186
|
}
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
if (parsed.kind === "duration" &&
|
|
188
|
+
(parsed.seconds < MIN_DEADLINE_SECONDS || parsed.seconds > MAX_DEADLINE_SECONDS)) {
|
|
164
189
|
throw new Error("FlexInference: `start_within` duration must be between 5s and 10m.");
|
|
165
190
|
}
|
|
166
191
|
}
|
|
192
|
+
// Per-endpoint required fields beyond the model/start_within envelope. Mirrors the Python
|
|
193
|
+
// _RequestSpec table in _validation.py: responses/interactions require `input` (presence only);
|
|
194
|
+
// chat requires `messages` (array); messages requires `messages` (array) + `max_tokens` (number).
|
|
195
|
+
const RESPONSES_REQUEST_SPEC = {
|
|
196
|
+
required: [{ name: "input", kind: "present" }],
|
|
197
|
+
};
|
|
198
|
+
const CHAT_REQUEST_SPEC = {
|
|
199
|
+
required: [{ name: "messages", kind: "array" }],
|
|
200
|
+
};
|
|
201
|
+
const INTERACTIONS_REQUEST_SPEC = {
|
|
202
|
+
required: [{ name: "input", kind: "present" }],
|
|
203
|
+
};
|
|
204
|
+
const MESSAGES_REQUEST_SPEC = {
|
|
205
|
+
required: [
|
|
206
|
+
{ name: "messages", kind: "array" },
|
|
207
|
+
{ name: "max_tokens", kind: "finiteNumber" },
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
// Read one field by reference off the raw body without copying it. `undefined` (or a non-object
|
|
211
|
+
// body) is treated as missing; `null` counts as present, matching Python's `name in body`
|
|
212
|
+
// presence check (a JSON `null` is a present key, and JSON serialization drops `undefined`).
|
|
213
|
+
function requestField(body, name) {
|
|
214
|
+
if (!isRecord(body))
|
|
215
|
+
return { kind: "missing" };
|
|
216
|
+
const value = body[name];
|
|
217
|
+
return value === undefined ? { kind: "missing" } : { kind: "present", value };
|
|
218
|
+
}
|
|
219
|
+
// Pre-flight guard: model, then start_within, then the endpoint's required fields in spec order.
|
|
220
|
+
// Pure - it only throws earlier for requests the router/Python already reject; it never mutates,
|
|
221
|
+
// clones, or reshapes the body, so unknown/extra fields pass straight through to `send`.
|
|
222
|
+
function validateRequest(spec, body) {
|
|
223
|
+
const model = requestField(body, "model");
|
|
224
|
+
if (model.kind === "missing" || typeof model.value !== "string" || model.value === "") {
|
|
225
|
+
throw new Error("FlexInference: `model` is required and must be a non-empty string.");
|
|
226
|
+
}
|
|
227
|
+
assertStartWithin(body);
|
|
228
|
+
for (const field of spec.required) {
|
|
229
|
+
const value = requestField(body, field.name);
|
|
230
|
+
if (value.kind === "missing") {
|
|
231
|
+
throw new Error(`FlexInference: \`${field.name}\` is required for this endpoint.`);
|
|
232
|
+
}
|
|
233
|
+
if (field.kind === "array" && !Array.isArray(value.value)) {
|
|
234
|
+
throw new Error(`FlexInference: \`${field.name}\` must be an array.`);
|
|
235
|
+
}
|
|
236
|
+
if (field.kind === "finiteNumber" &&
|
|
237
|
+
(typeof value.value !== "number" || !Number.isFinite(value.value))) {
|
|
238
|
+
throw new Error(`FlexInference: \`${field.name}\` must be a finite number.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
167
242
|
// Distinct abort reasons so an aborted fetch/read can be told apart: a timeout we fired
|
|
168
243
|
// (turned into a clear message) versus the caller's own AbortSignal (propagated as-is).
|
|
169
244
|
const FIRST_BYTE_TIMEOUT = Symbol("first-byte-timeout");
|
|
170
245
|
const IDLE_TIMEOUT = Symbol("idle-timeout");
|
|
246
|
+
const TOTAL_TIMEOUT = Symbol("total-timeout");
|
|
171
247
|
/**
|
|
172
248
|
* Seconds for a flex-duration `start_within`, or null for a tier value ("default" etc.),
|
|
173
249
|
* a missing field, or a malformed one. Used to auto-size the first-byte wait: the router
|
|
@@ -175,31 +251,45 @@ const IDLE_TIMEOUT = Symbol("idle-timeout");
|
|
|
175
251
|
* clock must cover the whole deadline plus the fallback path.
|
|
176
252
|
*/
|
|
177
253
|
function flexDeadlineSeconds(body) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
: undefined;
|
|
181
|
-
if (typeof value !== "string")
|
|
182
|
-
return null;
|
|
183
|
-
const match = DURATION_RE.exec(value);
|
|
184
|
-
if (match === null)
|
|
254
|
+
const sw = startWithinValue(body);
|
|
255
|
+
if (sw.kind === "missing")
|
|
185
256
|
return null;
|
|
186
|
-
|
|
257
|
+
const parsed = parseStartWithin(sw.value);
|
|
258
|
+
return parsed.kind === "duration" ? parsed.seconds : null;
|
|
187
259
|
}
|
|
188
260
|
// Read a non-streamed JSON body under the total wall-clock budget, then release the timer
|
|
189
261
|
// and the abort listener. Non-streaming only: a stream is drained by streamSSE instead.
|
|
190
262
|
async function readJson(open) {
|
|
191
263
|
const { res, controller, cleanup, totalMs } = open;
|
|
264
|
+
// Abort with a dedicated reason so a fired total timeout can be told apart from the
|
|
265
|
+
// caller cancelling on purpose, and translated into a clear message (parity with the
|
|
266
|
+
// first-byte and idle timeouts, and with the Python SDK).
|
|
192
267
|
const totalTimer = setTimeout(() => {
|
|
193
|
-
controller.abort();
|
|
268
|
+
controller.abort(TOTAL_TIMEOUT);
|
|
194
269
|
}, totalMs);
|
|
195
270
|
try {
|
|
196
271
|
return (await res.json());
|
|
197
272
|
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (controller.signal.reason === TOTAL_TIMEOUT) {
|
|
275
|
+
throw new Error(`FlexInference: no full response from the router within ${String(totalMs)}ms ` +
|
|
276
|
+
"(total timeout). Raise `timeout` for very large non-streaming responses, or " +
|
|
277
|
+
"stream the request so a long generation is governed by the idle timeout instead.");
|
|
278
|
+
}
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
198
281
|
finally {
|
|
199
282
|
clearTimeout(totalTimer);
|
|
200
283
|
cleanup();
|
|
201
284
|
}
|
|
202
285
|
}
|
|
286
|
+
async function createEndpoint(send, path, spec, body, options) {
|
|
287
|
+
validateRequest(spec, body);
|
|
288
|
+
const open = await send(path, body, options);
|
|
289
|
+
if (body.stream)
|
|
290
|
+
return streamSSE(open);
|
|
291
|
+
return readJson(open);
|
|
292
|
+
}
|
|
203
293
|
export class FlexInference {
|
|
204
294
|
responses;
|
|
205
295
|
chat;
|
|
@@ -326,11 +416,7 @@ class Responses {
|
|
|
326
416
|
this.send = send;
|
|
327
417
|
}
|
|
328
418
|
async create(body, options) {
|
|
329
|
-
|
|
330
|
-
const open = await this.send("/responses", body, options);
|
|
331
|
-
if (body.stream)
|
|
332
|
-
return streamSSE(open);
|
|
333
|
-
return readJson(open);
|
|
419
|
+
return createEndpoint(this.send, "/responses", RESPONSES_REQUEST_SPEC, body, options);
|
|
334
420
|
}
|
|
335
421
|
}
|
|
336
422
|
class Chat {
|
|
@@ -345,11 +431,7 @@ class ChatCompletions {
|
|
|
345
431
|
this.send = send;
|
|
346
432
|
}
|
|
347
433
|
async create(body, options) {
|
|
348
|
-
|
|
349
|
-
const open = await this.send("/chat/completions", body, options);
|
|
350
|
-
if (body.stream)
|
|
351
|
-
return streamSSE(open);
|
|
352
|
-
return readJson(open);
|
|
434
|
+
return createEndpoint(this.send, "/chat/completions", CHAT_REQUEST_SPEC, body, options);
|
|
353
435
|
}
|
|
354
436
|
}
|
|
355
437
|
class Interactions {
|
|
@@ -358,11 +440,7 @@ class Interactions {
|
|
|
358
440
|
this.send = send;
|
|
359
441
|
}
|
|
360
442
|
async create(body, options) {
|
|
361
|
-
|
|
362
|
-
const open = await this.send("/interactions", body, options);
|
|
363
|
-
if (body.stream)
|
|
364
|
-
return streamSSE(open);
|
|
365
|
-
return readJson(open);
|
|
443
|
+
return createEndpoint(this.send, "/interactions", INTERACTIONS_REQUEST_SPEC, body, options);
|
|
366
444
|
}
|
|
367
445
|
}
|
|
368
446
|
class Messages {
|
|
@@ -371,13 +449,22 @@ class Messages {
|
|
|
371
449
|
this.send = send;
|
|
372
450
|
}
|
|
373
451
|
async create(body, options) {
|
|
374
|
-
|
|
375
|
-
const open = await this.send("/messages", body, options);
|
|
376
|
-
if (body.stream)
|
|
377
|
-
return streamSSE(open);
|
|
378
|
-
return readJson(open);
|
|
452
|
+
return createEndpoint(this.send, "/messages", MESSAGES_REQUEST_SPEC, body, options);
|
|
379
453
|
}
|
|
380
454
|
}
|
|
455
|
+
function parseSSEFrame(frame) {
|
|
456
|
+
const dataLines = [];
|
|
457
|
+
for (const line of frame.split("\n")) {
|
|
458
|
+
if (line.startsWith("data:"))
|
|
459
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
460
|
+
}
|
|
461
|
+
if (dataLines.length === 0)
|
|
462
|
+
return null;
|
|
463
|
+
const data = dataLines.join("\n");
|
|
464
|
+
if (data === "[DONE]")
|
|
465
|
+
return { kind: "done" };
|
|
466
|
+
return { kind: "value", value: JSON.parse(data) };
|
|
467
|
+
}
|
|
381
468
|
async function* streamSSE(open) {
|
|
382
469
|
const { res, controller, idleMs, cleanup } = open;
|
|
383
470
|
if (!res.body) {
|
|
@@ -411,7 +498,18 @@ async function* streamSSE(open) {
|
|
|
411
498
|
}
|
|
412
499
|
const { done, value } = chunk;
|
|
413
500
|
if (done) {
|
|
501
|
+
// The stream ended. Flush any bytes the decoder held back for an incomplete
|
|
502
|
+
// multibyte sequence, then emit a final frame that arrived without its trailing
|
|
503
|
+
// blank line so the last piece of the answer is not dropped.
|
|
414
504
|
exhausted = true;
|
|
505
|
+
buffer += decoder.decode();
|
|
506
|
+
const frame = buffer;
|
|
507
|
+
buffer = "";
|
|
508
|
+
if (frame.length > 0) {
|
|
509
|
+
const parsed = parseSSEFrame(frame);
|
|
510
|
+
if (parsed !== null && parsed.kind === "value")
|
|
511
|
+
yield parsed.value;
|
|
512
|
+
}
|
|
415
513
|
break;
|
|
416
514
|
}
|
|
417
515
|
buffer += decoder.decode(value, { stream: true }).replace(/\r/g, "");
|
|
@@ -419,19 +517,21 @@ async function* streamSSE(open) {
|
|
|
419
517
|
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
420
518
|
const frame = buffer.slice(0, sep);
|
|
421
519
|
buffer = buffer.slice(sep + 2);
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
if (line.startsWith("data:"))
|
|
425
|
-
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
426
|
-
}
|
|
427
|
-
if (dataLines.length === 0)
|
|
520
|
+
const parsed = parseSSEFrame(frame);
|
|
521
|
+
if (parsed === null)
|
|
428
522
|
continue;
|
|
429
|
-
|
|
430
|
-
if (data === "[DONE]") {
|
|
523
|
+
if (parsed.kind === "done") {
|
|
431
524
|
exhausted = true;
|
|
432
525
|
return;
|
|
433
526
|
}
|
|
434
|
-
yield
|
|
527
|
+
yield parsed.value;
|
|
528
|
+
}
|
|
529
|
+
// With every complete frame drained, whatever remains is a single unterminated
|
|
530
|
+
// frame. If it grows past the cap the stream is never sending a separator; error
|
|
531
|
+
// clearly instead of buffering without bound.
|
|
532
|
+
if (buffer.length > MAX_SSE_BUFFER_CHARS) {
|
|
533
|
+
throw new Error(`FlexInference: stream buffer exceeded ${String(MAX_SSE_BUFFER_CHARS)} characters ` +
|
|
534
|
+
"without a frame boundary; aborting to avoid unbounded memory.");
|
|
435
535
|
}
|
|
436
536
|
}
|
|
437
537
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5398,6 +5398,8 @@ export interface components {
|
|
|
5398
5398
|
type: string;
|
|
5399
5399
|
code?: string | null;
|
|
5400
5400
|
param?: string | null;
|
|
5401
|
+
/** @description Additive deep link into the docs for this exact code. Present (a URL or null) on FlexInference-layer errors; absent on verbatim upstream errors. */
|
|
5402
|
+
doc_url?: string | null;
|
|
5401
5403
|
};
|
|
5402
5404
|
};
|
|
5403
5405
|
/** Interaction Text Part */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flexinference",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Official TypeScript SDK for FlexInference - a deadline-aware, OpenAI-compatible inference router.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aditya Perswal <adityaperswal@gmail.com>",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build": "tsc -p tsconfig.json",
|
|
35
35
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
36
|
"test:types": "tsc -p tsconfig.test.json --noEmit",
|
|
37
|
-
"test": "node test/smoke.mjs && npm run test:types",
|
|
37
|
+
"test": "node test/smoke.mjs && node test/fixes.test.mjs && node test/parity.test.mjs && npm run test:types",
|
|
38
38
|
"lint": "eslint .",
|
|
39
39
|
"lint:fix": "eslint . --fix",
|
|
40
40
|
"format": "prettier --write .",
|