flexinference 0.1.0 → 1.0.1
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 +59 -0
- package/README.md +38 -23
- package/dist/index.d.ts +34 -3
- package/dist/index.js +164 -22
- package/dist/types.d.ts +4 -4
- package/package.json +4 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
Gemini now supports **image input** and **web search** (send a Responses `web_search`
|
|
6
|
+
tool; the router maps it to Gemini's `google_search`), in addition to text, streaming,
|
|
7
|
+
structured outputs, and function calling. No API or type changes - translation is
|
|
8
|
+
handled server-side, so existing code keeps working.
|
|
9
|
+
|
|
10
|
+
## 1.0.0
|
|
11
|
+
|
|
12
|
+
The stable v1 of the FlexInference TypeScript SDK. This release makes `start_within`
|
|
13
|
+
required and aligns its values with OpenAI's `service_tier` vocabulary.
|
|
14
|
+
|
|
15
|
+
### Breaking changes
|
|
16
|
+
|
|
17
|
+
- **`start_within` is now required on every request.** It is typed as
|
|
18
|
+
`StartWithin = "default" | "priority" | "auto" | \`${number}h-${number}m-${number}s\``,
|
|
19
|
+
so omitting it (or passing a value outside those shapes) is a **compile-time** error.
|
|
20
|
+
A duration that fits the shape but is malformed or out of range is caught at **runtime**;
|
|
21
|
+
plain-JS callers get every check at **runtime** (a thrown `Error` before any network call).
|
|
22
|
+
- **Accepted values changed.** `start_within` now takes `"default"`, `"priority"`,
|
|
23
|
+
`"auto"`, or a duration `"HHh-MMm-SSs"` (5s-10m). The old `"standard"` value is
|
|
24
|
+
removed; use `"default"` (it maps to the same OpenAI service tier).
|
|
25
|
+
- **Any model proxies for the non-flex values.** `"default"`, `"priority"`, and
|
|
26
|
+
`"auto"` proxy any model to OpenAI. Only the duration (flex race) requires a
|
|
27
|
+
flex-capable model; a duration on a non-flex-capable model returns the router's new
|
|
28
|
+
`model_not_flex_capable` error (the old `unsupported_model` is retired).
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Default request timeout.** Clients now bound every request: 10s to connect and
|
|
33
|
+
600s (10 min) total, matching the router. Override the total with the new `timeout`
|
|
34
|
+
client option, still composable with a per-call `AbortSignal`.
|
|
35
|
+
- **`outputText(res)` helper.** Pulls the assistant's text out of a raw response
|
|
36
|
+
(Responses or chat completion) without hand-walking `output[].content[]`. The raw
|
|
37
|
+
wire body has no `output_text` field (that is computed by OpenAI's SDKs, not sent),
|
|
38
|
+
so this fills the gap. Read-only; nothing is reshaped.
|
|
39
|
+
|
|
40
|
+
### Notes
|
|
41
|
+
|
|
42
|
+
- An arbitrary `string` is not assignable to `StartWithin`; use `value as StartWithin`
|
|
43
|
+
for a dynamically-built value.
|
|
44
|
+
- Validation is request-only: unknown fields pass straight through to the provider,
|
|
45
|
+
and responses are never validated or reshaped.
|
|
46
|
+
|
|
47
|
+
### Migration
|
|
48
|
+
|
|
49
|
+
```diff
|
|
50
|
+
- await client.responses.create({ model: "gpt-5.5", input: "hi" });
|
|
51
|
+
+ await client.responses.create({ model: "gpt-5.5", input: "hi", start_within: "default" });
|
|
52
|
+
|
|
53
|
+
- start_within: "standard"
|
|
54
|
+
+ start_within: "default"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 0.1.0
|
|
58
|
+
|
|
59
|
+
- Initial release.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# FlexInference (TypeScript)
|
|
2
2
|
|
|
3
|
-
The official TypeScript SDK for [FlexInference](https://flexinference.com) - a deadline-aware, OpenAI-compatible inference router. Send the OpenAI requests you already send, bring your own OpenAI key, and
|
|
3
|
+
The official TypeScript SDK for [FlexInference](https://flexinference.com) - a deadline-aware, OpenAI-compatible inference router. Send the OpenAI requests you already send, bring your own OpenAI key, and set one required field - `start_within` - to trade latency for cost.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install flexinference
|
|
@@ -9,7 +9,7 @@ npm install flexinference
|
|
|
9
9
|
## Quickstart
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
import { FlexInference } from "flexinference";
|
|
12
|
+
import { FlexInference, outputText } from "flexinference";
|
|
13
13
|
|
|
14
14
|
const client = new FlexInference({ apiKey: "flex_live_..." });
|
|
15
15
|
|
|
@@ -19,10 +19,23 @@ const res = await client.responses.create({
|
|
|
19
19
|
start_within: "00h-00m-30s", // typed - no cast needed
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
console.log(res
|
|
22
|
+
console.log(outputText(res));
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Responses come back as the **raw OpenAI JSON** (we never reshape the body), so there is no `output_text` field on the wire - that is computed by OpenAI's own SDKs. `outputText(res)` pulls the assistant's text out of either a response or a chat completion for you.
|
|
26
|
+
|
|
27
|
+
`start_within` is **required** on every request. It takes `"default"`, `"priority"`, `"auto"`, or a duration `"HHh-MMm-SSs"` (5s-10m). The duration races OpenAI's flex tier on a flex-capable model and falls back to standard if it can't start in time; `"default"`, `"priority"`, and `"auto"` map to those OpenAI service tiers and proxy any model. It is typed, so a missing value or a value outside the allowed shapes is a **compile-time** error; a duration that fits the shape but is malformed or out of range is caught at **runtime**, the same check plain-JavaScript callers get. See the [docs](https://flexinference.mintlify.app/deadline-routing).
|
|
28
|
+
|
|
29
|
+
## Providers (OpenAI and Gemini)
|
|
30
|
+
|
|
31
|
+
FlexInference routes to **OpenAI** and **Google Gemini**. Send the same OpenAI-shaped request and pass whichever model id you want - `gpt-5.5`, `o4-mini`, `gemini-3.5-flash`, and so on. We run Gemini through its Interactions API and translate it back to the OpenAI shape, so your code is identical for both.
|
|
32
|
+
|
|
33
|
+
- **OpenAI:** `default` (standard tier), `priority`, `auto`, and the flex race (a duration) on flex-capable models.
|
|
34
|
+
- **Gemini:** `default` maps to Gemini's **standard** tier, plus `priority` and the flex race on the Gemini flex models (`gemini-3.5-flash`, `gemini-3.1-flash-lite`, `gemini-3.1-pro-preview`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`). Gemini has no `auto` tier, so `start_within: "auto"` on a Gemini model returns `400`.
|
|
35
|
+
|
|
36
|
+
Add the provider key you'll use (OpenAI and/or Gemini) in the [dashboard](https://www.flexinference.com/dashboard). Text, streaming, structured outputs, function calling, image input, and web search work on both providers (send a Responses `web_search` tool; we map it to Gemini's `google_search`).
|
|
37
|
+
|
|
38
|
+
Don't send `service_tier` - the router controls the tier from `start_within` and rejects a caller-supplied `service_tier` with `400 service_tier_not_allowed`.
|
|
26
39
|
|
|
27
40
|
## Streaming
|
|
28
41
|
|
|
@@ -47,27 +60,28 @@ The Chat Completions endpoint works identically:
|
|
|
47
60
|
const res = await client.chat.completions.create({
|
|
48
61
|
model: "gpt-5.5",
|
|
49
62
|
messages: [{ role: "user", content: "Hello!" }],
|
|
50
|
-
start_within: "
|
|
63
|
+
start_within: "default",
|
|
51
64
|
});
|
|
52
65
|
```
|
|
53
66
|
|
|
54
|
-
##
|
|
67
|
+
## Timeouts and cancellation
|
|
55
68
|
|
|
56
|
-
Every
|
|
69
|
+
Every client has a built-in timeout: **10 seconds to connect** and **600 seconds (10 min) total**, matching the router's budget, so a request can never hang forever. Override the total with the `timeout` option (raise it for very long streams):
|
|
57
70
|
|
|
58
71
|
```ts
|
|
59
|
-
const
|
|
60
|
-
|
|
72
|
+
const client = new FlexInference({ apiKey: "flex_live_...", timeout: 120_000 });
|
|
73
|
+
```
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
Every call also accepts an `AbortSignal` as a second argument, so you can cancel a request yourself. Cancelling a stream stops it mid-flight:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const res = await client.responses.create(
|
|
80
|
+
{ model: "gpt-5.5", input: "Write a haiku about cheap GPUs.", start_within: "priority" },
|
|
81
|
+
{ signal: controller.signal },
|
|
82
|
+
);
|
|
83
|
+
// controller.abort() from elsewhere to cancel
|
|
84
|
+
console.log(outputText(res));
|
|
71
85
|
```
|
|
72
86
|
|
|
73
87
|
## Errors
|
|
@@ -90,11 +104,12 @@ try {
|
|
|
90
104
|
|
|
91
105
|
## Configuration
|
|
92
106
|
|
|
93
|
-
| Option | Default | Description
|
|
94
|
-
| --------- | ---------------------------------- |
|
|
95
|
-
| `apiKey` | - | Your `flex_live_` key (required).
|
|
96
|
-
| `baseURL` | `https://api.flexinference.com/v1` | Override the router endpoint.
|
|
97
|
-
| `fetch` | global `fetch` | Provide a custom fetch implementation.
|
|
107
|
+
| Option | Default | Description |
|
|
108
|
+
| --------- | ---------------------------------- | ------------------------------------------------ |
|
|
109
|
+
| `apiKey` | - | Your `flex_live_` key (required). |
|
|
110
|
+
| `baseURL` | `https://api.flexinference.com/v1` | Override the router endpoint. |
|
|
111
|
+
| `fetch` | global `fetch` | Provide a custom fetch implementation. |
|
|
112
|
+
| `timeout` | `600000` | Total request budget in ms (connect capped 10s). |
|
|
98
113
|
|
|
99
114
|
## License
|
|
100
115
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import type { components } from "./types.js";
|
|
2
2
|
type Schemas = components["schemas"];
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The required FlexInference routing field. "default", "priority", and "auto" map to
|
|
5
|
+
* those OpenAI service tiers and proxy any model; a duration "HHh-MMm-SSs" (5s-10m)
|
|
6
|
+
* runs the flex race on a flex-capable model. An arbitrary string needs `as StartWithin`.
|
|
7
|
+
*/
|
|
8
|
+
export type StartWithin = "default" | "priority" | "auto" | `${number}h-${number}m-${number}s`;
|
|
9
|
+
/** Make `start_within` a required, typed field on a generated request body. */
|
|
10
|
+
type WithStartWithin<T> = Omit<T, "start_within"> & {
|
|
11
|
+
start_within: StartWithin;
|
|
12
|
+
};
|
|
13
|
+
export type ResponseCreateParams = WithStartWithin<Schemas["CreateResponse"]>;
|
|
4
14
|
export type ResponseObject = Schemas["Response"];
|
|
5
15
|
export type ResponseStreamEvent = Schemas["ResponseStreamEvent"];
|
|
6
|
-
|
|
16
|
+
type RawChatRequest = Schemas["CreateChatCompletionRequest"];
|
|
17
|
+
export type ChatCompletionCreateParams = WithStartWithin<Partial<RawChatRequest> & Required<Pick<RawChatRequest, "model" | "messages">>>;
|
|
7
18
|
export type ChatCompletion = Schemas["CreateChatCompletionResponse"];
|
|
8
19
|
export type ChatCompletionChunk = Schemas["CreateChatCompletionStreamResponse"];
|
|
20
|
+
/**
|
|
21
|
+
* Concatenate the assistant's text from a non-streamed response. Works on a Responses
|
|
22
|
+
* object (walks `output[].content[]` for `output_text` parts) and on a chat completion
|
|
23
|
+
* (`choices[0].message.content`). Read-only: the raw body is never reshaped. Returns ""
|
|
24
|
+
* when there is no text (e.g. a tool-call-only or refusal response). The raw wire body
|
|
25
|
+
* has no `output_text` field - that is computed by OpenAI's own SDKs, not sent on the
|
|
26
|
+
* wire - so use this to pull the text out. Streamed responses are not objects; collect
|
|
27
|
+
* their `output_text.delta` events instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare function outputText(response: ResponseObject | ChatCompletion): string;
|
|
9
30
|
export interface FlexErrorBody {
|
|
10
31
|
error: {
|
|
11
32
|
message: string;
|
|
@@ -27,17 +48,27 @@ export interface ClientOptions {
|
|
|
27
48
|
baseURL?: string;
|
|
28
49
|
/** Override the global fetch (e.g. for tests or non-standard runtimes). */
|
|
29
50
|
fetch?: typeof fetch;
|
|
51
|
+
/**
|
|
52
|
+
* Total request budget in ms (connect through the full response/stream). Defaults
|
|
53
|
+
* to 600000 (10 min). Connect alone is capped at 10s. Raise it for very long streams.
|
|
54
|
+
*/
|
|
55
|
+
timeout?: number;
|
|
30
56
|
}
|
|
31
57
|
export interface RequestOptions {
|
|
32
58
|
signal?: AbortSignal;
|
|
33
59
|
}
|
|
34
|
-
|
|
60
|
+
interface PostResult {
|
|
61
|
+
res: Response;
|
|
62
|
+
cleanup: () => void;
|
|
63
|
+
}
|
|
64
|
+
type Post = (path: string, body: unknown, signal: AbortSignal | undefined) => Promise<PostResult>;
|
|
35
65
|
export declare class FlexInference {
|
|
36
66
|
readonly responses: Responses;
|
|
37
67
|
readonly chat: Chat;
|
|
38
68
|
private readonly apiKey;
|
|
39
69
|
private readonly baseURL;
|
|
40
70
|
private readonly fetchImpl;
|
|
71
|
+
private readonly timeoutMs;
|
|
41
72
|
constructor(options: ClientOptions);
|
|
42
73
|
private post;
|
|
43
74
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Concatenate the assistant's text from a non-streamed response. Works on a Responses
|
|
6
|
+
* object (walks `output[].content[]` for `output_text` parts) and on a chat completion
|
|
7
|
+
* (`choices[0].message.content`). Read-only: the raw body is never reshaped. Returns ""
|
|
8
|
+
* when there is no text (e.g. a tool-call-only or refusal response). The raw wire body
|
|
9
|
+
* has no `output_text` field - that is computed by OpenAI's own SDKs, not sent on the
|
|
10
|
+
* wire - so use this to pull the text out. Streamed responses are not objects; collect
|
|
11
|
+
* their `output_text.delta` events instead.
|
|
12
|
+
*/
|
|
13
|
+
export function outputText(response) {
|
|
14
|
+
const obj = response;
|
|
15
|
+
if (!isRecord(obj))
|
|
16
|
+
return "";
|
|
17
|
+
const output = obj.output;
|
|
18
|
+
if (Array.isArray(output)) {
|
|
19
|
+
let text = "";
|
|
20
|
+
for (const item of output) {
|
|
21
|
+
if (!isRecord(item) || item.type !== "message" || !Array.isArray(item.content))
|
|
22
|
+
continue;
|
|
23
|
+
for (const part of item.content) {
|
|
24
|
+
if (isRecord(part) && part.type === "output_text" && typeof part.text === "string") {
|
|
25
|
+
text += part.text;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
const choices = obj.choices;
|
|
32
|
+
if (Array.isArray(choices) && isRecord(choices[0])) {
|
|
33
|
+
const message = choices[0].message;
|
|
34
|
+
if (isRecord(message) && typeof message.content === "string")
|
|
35
|
+
return message.content;
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
1
39
|
export class FlexInferenceError extends Error {
|
|
2
40
|
status;
|
|
3
41
|
type;
|
|
@@ -14,17 +52,51 @@ export class FlexInferenceError extends Error {
|
|
|
14
52
|
}
|
|
15
53
|
}
|
|
16
54
|
const DEFAULT_BASE_URL = "https://api.flexinference.com/v1";
|
|
55
|
+
const DEFAULT_TIMEOUT_MS = 600_000; // 10 min total budget, matching the router's generosity
|
|
56
|
+
const CONNECT_TIMEOUT_MS = 10_000; // fail fast if the router is unreachable
|
|
57
|
+
const MIN_DEADLINE_SECONDS = 5;
|
|
58
|
+
const MAX_DEADLINE_SECONDS = 600;
|
|
59
|
+
const SECONDS_PER_HOUR = 3600;
|
|
60
|
+
const SECONDS_PER_MINUTE = 60;
|
|
61
|
+
const DURATION_RE = /^(\d{2})h-(\d{2})m-(\d{2})s$/;
|
|
62
|
+
const START_WITHIN_VALUES = '"default", "priority", "auto", or a duration "HHh-MMm-SSs"';
|
|
63
|
+
// Runtime guard so plain-JS callers (who get no compile-time check) still fail
|
|
64
|
+
// locally with a clear message instead of a provider round-trip. TypeScript callers
|
|
65
|
+
// are already protected by the required StartWithin field above.
|
|
66
|
+
function assertStartWithin(body) {
|
|
67
|
+
const value = typeof body === "object" && body !== null
|
|
68
|
+
? body["start_within"]
|
|
69
|
+
: undefined;
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
throw new Error(`FlexInference: \`start_within\` is required. Set it to ${START_WITHIN_VALUES}.`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof value !== "string") {
|
|
74
|
+
throw new Error(`FlexInference: \`start_within\` must be a string (${START_WITHIN_VALUES}).`);
|
|
75
|
+
}
|
|
76
|
+
if (value === "default" || value === "priority" || value === "auto")
|
|
77
|
+
return;
|
|
78
|
+
const match = DURATION_RE.exec(value);
|
|
79
|
+
if (match === null) {
|
|
80
|
+
throw new Error(`FlexInference: \`start_within\` "${value}" is not ${START_WITHIN_VALUES}.`);
|
|
81
|
+
}
|
|
82
|
+
const seconds = Number(match[1]) * SECONDS_PER_HOUR + Number(match[2]) * SECONDS_PER_MINUTE + Number(match[3]);
|
|
83
|
+
if (seconds < MIN_DEADLINE_SECONDS || seconds > MAX_DEADLINE_SECONDS) {
|
|
84
|
+
throw new Error("FlexInference: `start_within` duration must be between 5s and 10m.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
17
87
|
export class FlexInference {
|
|
18
88
|
responses;
|
|
19
89
|
chat;
|
|
20
90
|
apiKey;
|
|
21
91
|
baseURL;
|
|
22
92
|
fetchImpl;
|
|
93
|
+
timeoutMs;
|
|
23
94
|
constructor(options) {
|
|
24
95
|
if (!options.apiKey)
|
|
25
96
|
throw new Error("FlexInference: `apiKey` is required.");
|
|
26
97
|
this.apiKey = options.apiKey;
|
|
27
98
|
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
99
|
+
this.timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
28
100
|
const f = options.fetch ?? globalThis.fetch;
|
|
29
101
|
if (typeof f !== "function") {
|
|
30
102
|
throw new Error("FlexInference: no global fetch found; pass `fetch` in options.");
|
|
@@ -35,17 +107,54 @@ export class FlexInference {
|
|
|
35
107
|
this.chat = new Chat(post);
|
|
36
108
|
}
|
|
37
109
|
async post(path, body, signal) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
signal
|
|
47
|
-
|
|
110
|
+
// One AbortController fed by three sources: the caller's signal, a 10s connect
|
|
111
|
+
// timer (cleared once headers arrive), and the total budget (cleared by the
|
|
112
|
+
// consumer once the body/stream is done). No dangling timers after completion.
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const onCallerAbort = () => {
|
|
115
|
+
controller.abort(signal?.reason);
|
|
116
|
+
};
|
|
117
|
+
if (signal) {
|
|
118
|
+
if (signal.aborted)
|
|
119
|
+
controller.abort(signal.reason);
|
|
120
|
+
else
|
|
121
|
+
signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
122
|
+
}
|
|
123
|
+
const connectTimer = setTimeout(() => {
|
|
124
|
+
controller.abort();
|
|
125
|
+
}, CONNECT_TIMEOUT_MS);
|
|
126
|
+
const totalTimer = setTimeout(() => {
|
|
127
|
+
controller.abort();
|
|
128
|
+
}, this.timeoutMs);
|
|
129
|
+
const cleanup = () => {
|
|
130
|
+
clearTimeout(connectTimer);
|
|
131
|
+
clearTimeout(totalTimer);
|
|
132
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
133
|
+
};
|
|
134
|
+
let res;
|
|
135
|
+
try {
|
|
136
|
+
res = await this.fetchImpl(`${this.baseURL}${path}`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
Accept: "text/event-stream, application/json",
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify(body),
|
|
144
|
+
signal: controller.signal,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
cleanup();
|
|
149
|
+
if (controller.signal.aborted && signal?.aborted !== true) {
|
|
150
|
+
throw new Error(`FlexInference: request to the router timed out (connect ${String(CONNECT_TIMEOUT_MS)}ms / total ${String(this.timeoutMs)}ms).`);
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
clearTimeout(connectTimer); // headers arrived; only the total budget governs the body now
|
|
48
155
|
if (!res.ok) {
|
|
156
|
+
// Read the error body under the same timeout, then clear the timers. Clearing
|
|
157
|
+
// before the read would leave a stalled error body with no budget to abort it.
|
|
49
158
|
let parsed;
|
|
50
159
|
try {
|
|
51
160
|
parsed = (await res.json());
|
|
@@ -53,9 +162,12 @@ export class FlexInference {
|
|
|
53
162
|
catch {
|
|
54
163
|
parsed = undefined;
|
|
55
164
|
}
|
|
165
|
+
finally {
|
|
166
|
+
cleanup();
|
|
167
|
+
}
|
|
56
168
|
throw new FlexInferenceError(res.status, parsed, `HTTP ${String(res.status)} ${res.statusText}`);
|
|
57
169
|
}
|
|
58
|
-
return res;
|
|
170
|
+
return { res, cleanup };
|
|
59
171
|
}
|
|
60
172
|
}
|
|
61
173
|
class Responses {
|
|
@@ -64,11 +176,17 @@ class Responses {
|
|
|
64
176
|
this.post = post;
|
|
65
177
|
}
|
|
66
178
|
async create(body, options) {
|
|
67
|
-
|
|
179
|
+
assertStartWithin(body);
|
|
180
|
+
const { res, cleanup } = await this.post("/responses", body, options?.signal);
|
|
68
181
|
if (body.stream) {
|
|
69
|
-
return streamSSE(res);
|
|
182
|
+
return streamSSE(res, cleanup);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return (await res.json());
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
cleanup();
|
|
70
189
|
}
|
|
71
|
-
return (await res.json());
|
|
72
190
|
}
|
|
73
191
|
}
|
|
74
192
|
class Chat {
|
|
@@ -83,24 +201,35 @@ class ChatCompletions {
|
|
|
83
201
|
this.post = post;
|
|
84
202
|
}
|
|
85
203
|
async create(body, options) {
|
|
86
|
-
|
|
204
|
+
assertStartWithin(body);
|
|
205
|
+
const { res, cleanup } = await this.post("/chat/completions", body, options?.signal);
|
|
87
206
|
if (body.stream) {
|
|
88
|
-
return streamSSE(res);
|
|
207
|
+
return streamSSE(res, cleanup);
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
return (await res.json());
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
cleanup();
|
|
89
214
|
}
|
|
90
|
-
return (await res.json());
|
|
91
215
|
}
|
|
92
216
|
}
|
|
93
|
-
async function* streamSSE(res) {
|
|
94
|
-
if (!res.body)
|
|
217
|
+
async function* streamSSE(res, cleanup) {
|
|
218
|
+
if (!res.body) {
|
|
219
|
+
cleanup();
|
|
95
220
|
throw new Error("FlexInference: streaming response has no body.");
|
|
221
|
+
}
|
|
96
222
|
const reader = res.body.getReader();
|
|
97
223
|
const decoder = new TextDecoder();
|
|
98
224
|
let buffer = "";
|
|
225
|
+
let exhausted = false;
|
|
99
226
|
try {
|
|
100
227
|
for (;;) {
|
|
101
228
|
const { done, value } = await reader.read();
|
|
102
|
-
if (done)
|
|
229
|
+
if (done) {
|
|
230
|
+
exhausted = true;
|
|
103
231
|
break;
|
|
232
|
+
}
|
|
104
233
|
buffer += decoder.decode(value, { stream: true }).replace(/\r/g, "");
|
|
105
234
|
let sep;
|
|
106
235
|
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
@@ -114,13 +243,26 @@ async function* streamSSE(res) {
|
|
|
114
243
|
if (dataLines.length === 0)
|
|
115
244
|
continue;
|
|
116
245
|
const data = dataLines.join("\n");
|
|
117
|
-
if (data === "[DONE]")
|
|
246
|
+
if (data === "[DONE]") {
|
|
247
|
+
exhausted = true;
|
|
118
248
|
return;
|
|
249
|
+
}
|
|
119
250
|
yield JSON.parse(data);
|
|
120
251
|
}
|
|
121
252
|
}
|
|
122
253
|
}
|
|
123
254
|
finally {
|
|
124
|
-
|
|
255
|
+
// A consumer that breaks early (or a parse that throws) leaves the body undrained.
|
|
256
|
+
// Cancel it so the socket is released now instead of lingering until GC; a fully
|
|
257
|
+
// drained stream just releases the lock.
|
|
258
|
+
if (exhausted) {
|
|
259
|
+
reader.releaseLock();
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
await reader.cancel().catch(() => {
|
|
263
|
+
// already cancelled or the body errored; nothing to release
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
cleanup();
|
|
125
267
|
}
|
|
126
268
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1105,7 +1105,7 @@ export interface components {
|
|
|
1105
1105
|
*/
|
|
1106
1106
|
top_logprobs?: number;
|
|
1107
1107
|
/**
|
|
1108
|
-
* @description FlexInference deadline control. "priority"
|
|
1108
|
+
* @description FlexInference deadline control. Required on every request. "default", "priority", and "auto" map to those OpenAI service tiers ("auto" lets OpenAI pick) and proxy any model. A duration "HHh-MMm-SSs" between 5 seconds and 10 minutes runs the flex tier on a flex-capable model and automatically falls back to standard if it has not started within the window. The retired value "standard" is no longer accepted (use "default").
|
|
1109
1109
|
* @example 00h-00m-30s
|
|
1110
1110
|
*/
|
|
1111
1111
|
start_within?: string;
|
|
@@ -1773,7 +1773,7 @@ export interface components {
|
|
|
1773
1773
|
/**
|
|
1774
1774
|
* @description Labels an `assistant` message as intermediate commentary (`commentary`) or the final answer (`final_answer`).
|
|
1775
1775
|
* For models like `gpt-5.3-codex` and beyond, when sending follow-up requests, preserve and resend
|
|
1776
|
-
* phase on all assistant messages
|
|
1776
|
+
* phase on all assistant messages — dropping it can degrade performance. Not used for user messages.
|
|
1777
1777
|
* @enum {string}
|
|
1778
1778
|
*/
|
|
1779
1779
|
MessagePhase: "commentary" | "final_answer";
|
|
@@ -5398,7 +5398,7 @@ export interface operations {
|
|
|
5398
5398
|
"text/event-stream": components["schemas"]["CreateChatCompletionStreamResponse"];
|
|
5399
5399
|
};
|
|
5400
5400
|
};
|
|
5401
|
-
/** @description Bad request. A FlexInference-layer rejection (type flexinference_error, e.g. invalid_start_within,
|
|
5401
|
+
/** @description Bad request. A FlexInference-layer rejection (type flexinference_error, e.g. missing_start_within, invalid_start_within, model_not_flex_capable, no_byok_key) or an OpenAI rejection passed through unchanged. */
|
|
5402
5402
|
400: {
|
|
5403
5403
|
headers: {
|
|
5404
5404
|
[name: string]: unknown;
|
|
@@ -5441,7 +5441,7 @@ export interface operations {
|
|
|
5441
5441
|
"text/event-stream": components["schemas"]["ResponseStreamEvent"];
|
|
5442
5442
|
};
|
|
5443
5443
|
};
|
|
5444
|
-
/** @description Bad request. A FlexInference-layer rejection (type flexinference_error, e.g. invalid_start_within,
|
|
5444
|
+
/** @description Bad request. A FlexInference-layer rejection (type flexinference_error, e.g. missing_start_within, invalid_start_within, model_not_flex_capable, no_byok_key) or an OpenAI rejection passed through unchanged. */
|
|
5445
5445
|
400: {
|
|
5446
5446
|
headers: {
|
|
5447
5447
|
[name: string]: unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flexinference",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
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>",
|
|
@@ -27,11 +27,14 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
34
|
"build": "tsc -p tsconfig.json",
|
|
34
35
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"test:types": "tsc -p tsconfig.test.json --noEmit",
|
|
37
|
+
"test": "node test/smoke.mjs && npm run test:types",
|
|
35
38
|
"lint": "eslint .",
|
|
36
39
|
"lint:fix": "eslint . --fix",
|
|
37
40
|
"format": "prettier --write .",
|