runbios-sdk 0.2.1-dev.62
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/README.md +460 -0
- package/dist/client.d.ts +202 -0
- package/dist/client.js +408 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +93 -0
- package/dist/resources/datasets.d.ts +180 -0
- package/dist/resources/datasets.js +358 -0
- package/dist/resources/gpu-priorities.d.ts +23 -0
- package/dist/resources/gpu-priorities.js +63 -0
- package/dist/resources/gpu.d.ts +60 -0
- package/dist/resources/gpu.js +101 -0
- package/dist/resources/inference.d.ts +224 -0
- package/dist/resources/inference.js +794 -0
- package/dist/resources/models.d.ts +113 -0
- package/dist/resources/models.js +171 -0
- package/dist/resources/training.d.ts +166 -0
- package/dist/resources/training.js +419 -0
- package/dist/resources/wallet.d.ts +53 -0
- package/dist/resources/wallet.js +61 -0
- package/dist/types.d.ts +1916 -0
- package/dist/types.js +4 -0
- package/package.json +52 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { ApiError, GpuRejectionError, gpuRejectionCodeForReason, envApiKey, envBaseUrl, envInferenceKey } from '../client.js';
|
|
2
|
+
import { readNativeMaxContext } from './models.js';
|
|
3
|
+
import { normalizeGPUPlacement } from './gpu-priorities.js';
|
|
4
|
+
const FUNCTION_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
5
|
+
const ROLES = new Set(['system', 'developer', 'user', 'assistant', 'tool', 'function']);
|
|
6
|
+
const INFERENCE_IDEMPOTENCY_KEY = /^[A-Za-z0-9._:-]{8,128}$/;
|
|
7
|
+
/**
|
|
8
|
+
* Serving context-length policy, owned and enforced by the server. Mirrored
|
|
9
|
+
* here for documentation only -- never to pre-empt a server verdict.
|
|
10
|
+
*
|
|
11
|
+
* default = min(nativeMax, CONTEXT_DEFAULT_CEILING)
|
|
12
|
+
* the window is editable only when nativeMax exceeds CONTEXT_EDITABLE_FLOOR,
|
|
13
|
+
* and a request can NEVER exceed the model's own nativeMax (that is a 400).
|
|
14
|
+
*
|
|
15
|
+
* CONTEXT_EDITABLE_FLOOR doubles as the default when the native window is
|
|
16
|
+
* unknown; a genuinely small-context model keeps its own native window rather
|
|
17
|
+
* than being raised to the floor.
|
|
18
|
+
*/
|
|
19
|
+
export const CONTEXT_DEFAULT_CEILING = 262_144;
|
|
20
|
+
export const CONTEXT_EDITABLE_FLOOR = 32_768;
|
|
21
|
+
export function contextSizingBasis(requested) {
|
|
22
|
+
if (requested === undefined || requested === null)
|
|
23
|
+
return ['server_default', 0];
|
|
24
|
+
if (typeof requested !== 'number' || !Number.isInteger(requested))
|
|
25
|
+
return ['unprovable', 0];
|
|
26
|
+
if (requested <= 0)
|
|
27
|
+
return ['server_default', 0];
|
|
28
|
+
return ['explicit', requested];
|
|
29
|
+
}
|
|
30
|
+
const INFERENCE_TOOL_CALL_PARSERS = new Set([
|
|
31
|
+
'deepseekv3', 'deepseekv31', 'deepseekv32',
|
|
32
|
+
'glm', 'glm45', 'glm47', 'gpt-oss', 'kimi_k2',
|
|
33
|
+
'lfm2', 'llama3', 'mimo', 'mistral',
|
|
34
|
+
'omega17', 'omega17_exp', 'omega17_vl_exp', 'pythonic',
|
|
35
|
+
'qwen', 'qwen25', 'qwen3_coder', 'step3', 'step3p5',
|
|
36
|
+
'minimax-m2', 'trinity', 'interns1', 'hermes', 'gigachat3',
|
|
37
|
+
'usf_omega', 'usf_milli', 'usf_mini',
|
|
38
|
+
]);
|
|
39
|
+
function inferenceIdempotencyKey(value) {
|
|
40
|
+
const key = value?.trim();
|
|
41
|
+
if (!INFERENCE_IDEMPOTENCY_KEY.test(key)) {
|
|
42
|
+
throw new Error("RunBiOS: idempotencyKey is required and must be 8-128 characters using letters, numbers, '.', '_', ':', or '-'");
|
|
43
|
+
}
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
function buildInferenceRequest(params) {
|
|
47
|
+
const queueEnabled = params.allowCapacityQueue ?? false;
|
|
48
|
+
const placement = normalizeGPUPlacement(params.gpuPriorities, queueEnabled, params.gpuType, params.gpuCount, 'allowCapacityQueue',
|
|
49
|
+
// Backup rungs are decoupled from queue consent (book-first §4): a
|
|
50
|
+
// non-queued deployment may rank 1-5 placements. Only the selected rung
|
|
51
|
+
// is booked up front; the extras become the after-start replacement
|
|
52
|
+
// ladder if a GPU is ever lost.
|
|
53
|
+
true);
|
|
54
|
+
if (!params.name?.trim())
|
|
55
|
+
throw new Error('RunBiOS: deployment name is required');
|
|
56
|
+
if (!placement.gpuType)
|
|
57
|
+
throw new Error('RunBiOS: gpuType is required');
|
|
58
|
+
if (!Number.isInteger(placement.gpuCount) || placement.gpuCount < 1 || placement.gpuCount > 8) {
|
|
59
|
+
throw new Error('RunBiOS: gpuCount must be an integer between 1 and 8');
|
|
60
|
+
}
|
|
61
|
+
if (params.sourceType === 'checkpoint') {
|
|
62
|
+
if (!params.sourceJobId || !params.sourceCheckpointId) {
|
|
63
|
+
throw new Error('RunBiOS: checkpoint deployments require sourceJobId and sourceCheckpointId');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (params.sourceType === 'hf_model') {
|
|
67
|
+
if (!params.hfModelId)
|
|
68
|
+
throw new Error('RunBiOS: hf_model deployments require hfModelId');
|
|
69
|
+
if (params.servingMode !== undefined && params.servingMode !== 'full') {
|
|
70
|
+
throw new Error('RunBiOS: hf_model deployments use servingMode=full; checkpoint modes are derived from the verified artifact');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw new Error('RunBiOS: sourceType must be checkpoint or hf_model');
|
|
75
|
+
}
|
|
76
|
+
if (params.servingMode !== undefined && !['full', 'adapter', 'merged'].includes(params.servingMode)) {
|
|
77
|
+
throw new Error('RunBiOS: servingMode must be full, adapter, or merged');
|
|
78
|
+
}
|
|
79
|
+
// modelTask is NOT validated here. It is server-DERIVED from the resolved
|
|
80
|
+
// model (deriveModelTask), and whether a given value is accepted depends on
|
|
81
|
+
// that derivation — which only the server can do. Live dev proves both
|
|
82
|
+
// directions: 'completions'/'Completion' are ACCEPTED for a base model
|
|
83
|
+
// (openai-community/gpt2 preflights 200) while 'completion' is REJECTED for a
|
|
84
|
+
// chat model, so any client vocabulary is simultaneously too narrow and too
|
|
85
|
+
// wide. The value rides through untouched and the server answers; read the
|
|
86
|
+
// derived task back from preflight's canonical_request.model_task.
|
|
87
|
+
if (params.gpuTier !== undefined && params.gpuTier !== 'secure') {
|
|
88
|
+
throw new Error('RunBiOS: gpuTier must be secure; other deployment capacity tiers are not supported');
|
|
89
|
+
}
|
|
90
|
+
const requestedParser = params.servingConfig?.tool_call_parser;
|
|
91
|
+
if (requestedParser !== undefined && !INFERENCE_TOOL_CALL_PARSERS.has(requestedParser)) {
|
|
92
|
+
throw new Error('RunBiOS: servingConfig.tool_call_parser must name a parser the Run BiOS serving runtime supports');
|
|
93
|
+
}
|
|
94
|
+
const body = {
|
|
95
|
+
name: params.name.trim(),
|
|
96
|
+
source_type: params.sourceType,
|
|
97
|
+
gpu_type: placement.gpuType,
|
|
98
|
+
gpu_count: placement.gpuCount,
|
|
99
|
+
gpu_tier: params.gpuTier || 'secure',
|
|
100
|
+
allow_capacity_queue: queueEnabled,
|
|
101
|
+
};
|
|
102
|
+
if (placement.priorities !== undefined)
|
|
103
|
+
body.gpu_priorities = placement.priorities;
|
|
104
|
+
if (params.sourceJobId !== undefined)
|
|
105
|
+
body.source_job_id = params.sourceJobId;
|
|
106
|
+
if (params.sourceCheckpointId !== undefined)
|
|
107
|
+
body.source_checkpoint_id = params.sourceCheckpointId;
|
|
108
|
+
if (params.hfModelId !== undefined)
|
|
109
|
+
body.hf_model_id = params.hfModelId;
|
|
110
|
+
if (params.hfModelRevision !== undefined)
|
|
111
|
+
body.hf_model_revision = params.hfModelRevision;
|
|
112
|
+
if (params.hfIntegrationId !== undefined)
|
|
113
|
+
body.hf_integration_id = params.hfIntegrationId;
|
|
114
|
+
if (params.baseModelId !== undefined)
|
|
115
|
+
body.base_model_id = params.baseModelId;
|
|
116
|
+
if (params.baseModelRevision !== undefined)
|
|
117
|
+
body.base_model_revision = params.baseModelRevision;
|
|
118
|
+
if (params.servingMode !== undefined)
|
|
119
|
+
body.serving_mode = params.servingMode;
|
|
120
|
+
if (params.modelTask !== undefined)
|
|
121
|
+
body.model_task = params.modelTask;
|
|
122
|
+
// supportsImages is deliberately NOT forwarded: image-input support is
|
|
123
|
+
// derived server-side from the resolved model's own config and the wire field
|
|
124
|
+
// is IGNORED, so sending a client value would only advertise control the
|
|
125
|
+
// caller does not have. See InferenceCreateParams.
|
|
126
|
+
if (params.storageGb !== undefined)
|
|
127
|
+
body.storage_gb = params.storageGb;
|
|
128
|
+
if (params.contextLength !== undefined)
|
|
129
|
+
body.context_length = params.contextLength;
|
|
130
|
+
if (params.quant !== undefined)
|
|
131
|
+
body.quant = params.quant;
|
|
132
|
+
if (params.servingConfig !== undefined)
|
|
133
|
+
body.serving_config = params.servingConfig;
|
|
134
|
+
if (params.maxPriceHourCents !== undefined)
|
|
135
|
+
body.max_price_hour_cents = params.maxPriceHourCents;
|
|
136
|
+
return body;
|
|
137
|
+
}
|
|
138
|
+
export function validateChatRequest(body) {
|
|
139
|
+
const messages = body.messages;
|
|
140
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
141
|
+
throw new Error('messages must be a non-empty array');
|
|
142
|
+
}
|
|
143
|
+
const tools = body.tools ?? [];
|
|
144
|
+
if (!Array.isArray(tools))
|
|
145
|
+
throw new Error('tools must be an array');
|
|
146
|
+
const names = [];
|
|
147
|
+
for (const rawTool of tools) {
|
|
148
|
+
if (!rawTool || typeof rawTool !== 'object')
|
|
149
|
+
throw new Error('each tool must be an object');
|
|
150
|
+
const tool = rawTool;
|
|
151
|
+
if ((tool.type ?? 'function') !== 'function')
|
|
152
|
+
throw new Error('each tool must have type="function"');
|
|
153
|
+
const fn = tool.function;
|
|
154
|
+
if (!fn || typeof fn !== 'object')
|
|
155
|
+
throw new Error('each tool requires a function object');
|
|
156
|
+
const definition = fn;
|
|
157
|
+
if (typeof definition.name !== 'string' || !FUNCTION_NAME.test(definition.name)) {
|
|
158
|
+
throw new Error('tool function names must be 1-64 safe characters');
|
|
159
|
+
}
|
|
160
|
+
const parameters = definition.parameters;
|
|
161
|
+
if (parameters !== undefined && (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)
|
|
162
|
+
|| (parameters.type ?? 'object') !== 'object')) {
|
|
163
|
+
throw new Error('tool parameters must be a JSON Schema object');
|
|
164
|
+
}
|
|
165
|
+
names.push(definition.name);
|
|
166
|
+
}
|
|
167
|
+
if (new Set(names).size !== names.length)
|
|
168
|
+
throw new Error('tool function names must be unique');
|
|
169
|
+
const choice = body.tool_choice;
|
|
170
|
+
if (choice && typeof choice === 'object') {
|
|
171
|
+
const fn = choice.function;
|
|
172
|
+
const selected = fn && typeof fn === 'object' ? fn.name : undefined;
|
|
173
|
+
if (typeof selected !== 'string' || !names.includes(selected)) {
|
|
174
|
+
throw new Error('tool_choice references an undefined function');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if ((choice === 'auto' || choice === 'required') && tools.length === 0) {
|
|
178
|
+
throw new Error('tool_choice requires at least one tool');
|
|
179
|
+
}
|
|
180
|
+
const pending = new Set();
|
|
181
|
+
for (const rawMessage of messages) {
|
|
182
|
+
if (!rawMessage || typeof rawMessage !== 'object')
|
|
183
|
+
throw new Error('every message must be an object');
|
|
184
|
+
const message = rawMessage;
|
|
185
|
+
const role = message.role;
|
|
186
|
+
if (typeof role !== 'string' || !ROLES.has(role))
|
|
187
|
+
throw new Error('every message requires a valid role');
|
|
188
|
+
const calls = message.tool_calls;
|
|
189
|
+
if (calls !== undefined && role !== 'assistant')
|
|
190
|
+
throw new Error('tool_calls are only valid on assistant messages');
|
|
191
|
+
if (role === 'assistant' && Array.isArray(calls) && calls.length > 0) {
|
|
192
|
+
if (pending.size > 0)
|
|
193
|
+
throw new Error('tool calls must be resolved before the next turn');
|
|
194
|
+
for (const rawCall of calls) {
|
|
195
|
+
if (!rawCall || typeof rawCall !== 'object')
|
|
196
|
+
throw new Error('assistant tool calls must be objects');
|
|
197
|
+
const call = rawCall;
|
|
198
|
+
const id = call.id;
|
|
199
|
+
const fn = call.function;
|
|
200
|
+
if (typeof id !== 'string' || !id)
|
|
201
|
+
throw new Error('assistant tool calls require a non-empty id');
|
|
202
|
+
if (pending.has(id))
|
|
203
|
+
throw new Error('assistant tool call ids must be unique');
|
|
204
|
+
if (!fn || typeof fn !== 'object')
|
|
205
|
+
throw new Error('assistant tool calls require a function');
|
|
206
|
+
const functionCall = fn;
|
|
207
|
+
if (tools.length > 0 && !names.includes(String(functionCall.name ?? ''))) {
|
|
208
|
+
throw new Error('assistant tool call references an undefined function');
|
|
209
|
+
}
|
|
210
|
+
let args = functionCall.arguments;
|
|
211
|
+
if (typeof args === 'string') {
|
|
212
|
+
try {
|
|
213
|
+
args = JSON.parse(args);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
throw new Error('tool call arguments must be valid JSON');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
220
|
+
throw new Error('tool call arguments must encode a JSON object');
|
|
221
|
+
}
|
|
222
|
+
pending.add(id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (role === 'tool') {
|
|
226
|
+
const id = message.tool_call_id;
|
|
227
|
+
if (typeof id !== 'string' || !pending.delete(id)) {
|
|
228
|
+
throw new Error('tool message references an unknown tool_call_id');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (pending.size > 0) {
|
|
232
|
+
throw new Error('tool calls must be resolved before the next turn');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (pending.size > 0)
|
|
236
|
+
throw new Error('assistant tool calls are missing tool response messages');
|
|
237
|
+
}
|
|
238
|
+
function extractFrames(buffer, final = false) {
|
|
239
|
+
const frames = [];
|
|
240
|
+
let rest = buffer;
|
|
241
|
+
while (true) {
|
|
242
|
+
const match = /\r\n\r\n|\n\n|\r\r/.exec(rest);
|
|
243
|
+
if (!match || match.index === undefined)
|
|
244
|
+
break;
|
|
245
|
+
frames.push(rest.slice(0, match.index));
|
|
246
|
+
rest = rest.slice(match.index + match[0].length);
|
|
247
|
+
}
|
|
248
|
+
if (final && rest) {
|
|
249
|
+
frames.push(rest);
|
|
250
|
+
rest = '';
|
|
251
|
+
}
|
|
252
|
+
return { frames, rest };
|
|
253
|
+
}
|
|
254
|
+
function dataFromFrame(frame) {
|
|
255
|
+
const data = [];
|
|
256
|
+
for (const line of frame.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n')) {
|
|
257
|
+
if (!line || line.startsWith(':'))
|
|
258
|
+
continue;
|
|
259
|
+
const colon = line.indexOf(':');
|
|
260
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
261
|
+
let value = colon < 0 ? '' : line.slice(colon + 1);
|
|
262
|
+
if (value.startsWith(' '))
|
|
263
|
+
value = value.slice(1);
|
|
264
|
+
if (field === 'data')
|
|
265
|
+
data.push(value);
|
|
266
|
+
}
|
|
267
|
+
return data.length > 0 ? data.join('\n') : undefined;
|
|
268
|
+
}
|
|
269
|
+
export async function* parseSSE(body) {
|
|
270
|
+
const reader = body.getReader();
|
|
271
|
+
const decoder = new TextDecoder();
|
|
272
|
+
let buffer = '';
|
|
273
|
+
try {
|
|
274
|
+
while (true) {
|
|
275
|
+
const { done, value } = await reader.read();
|
|
276
|
+
if (done)
|
|
277
|
+
break;
|
|
278
|
+
buffer += decoder.decode(value, { stream: true });
|
|
279
|
+
const extracted = extractFrames(buffer);
|
|
280
|
+
buffer = extracted.rest;
|
|
281
|
+
for (const frame of extracted.frames) {
|
|
282
|
+
const data = dataFromFrame(frame);
|
|
283
|
+
if (data !== undefined)
|
|
284
|
+
yield data;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
buffer += decoder.decode();
|
|
288
|
+
const extracted = extractFrames(buffer, true);
|
|
289
|
+
for (const frame of extracted.frames) {
|
|
290
|
+
const data = dataFromFrame(frame);
|
|
291
|
+
if (data !== undefined)
|
|
292
|
+
yield data;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
await reader.cancel().catch(() => undefined);
|
|
297
|
+
reader.releaseLock();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function apiError(response) {
|
|
301
|
+
const text = await response.text().catch(() => '');
|
|
302
|
+
let body = { error: text || `Inference API error ${response.status}` };
|
|
303
|
+
try {
|
|
304
|
+
body = JSON.parse(text);
|
|
305
|
+
}
|
|
306
|
+
catch { /* use text envelope */ }
|
|
307
|
+
return new ApiError(response.status, body);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Inference surface. Combines control-plane management of model-serving
|
|
311
|
+
* deployments (`/api/inference*`) with OpenAI-compatible key-scoped inference
|
|
312
|
+
* (`/v1/chat/completions`). Requests are dispatched once; an idempotency header
|
|
313
|
+
* is forwarded but server-side replay is not assumed.
|
|
314
|
+
*/
|
|
315
|
+
export class Inference {
|
|
316
|
+
key;
|
|
317
|
+
baseUrl;
|
|
318
|
+
timeout;
|
|
319
|
+
_http;
|
|
320
|
+
constructor(config = {}, http) {
|
|
321
|
+
// Explicit key, then RUNBIOS_INFERENCE_KEY (legacy BIOS_INFERENCE_KEY),
|
|
322
|
+
// then RUNBIOS_API_KEY (legacy BIOS_API_KEY): a platform key
|
|
323
|
+
// carrying the serverless scope calls /v1 directly, so the same key must
|
|
324
|
+
// never have to be supplied twice. The SDK client resolves its own apiKey
|
|
325
|
+
// ahead of this (see RunBiOS's constructor).
|
|
326
|
+
this.key = config.inferenceKey || envInferenceKey() || envApiKey();
|
|
327
|
+
// Same default host as HttpClient (api.runbios.ai); api.runbios.ai cutover
|
|
328
|
+
// is planned once its DNS exists — update both call sites together.
|
|
329
|
+
this.baseUrl = (config.baseUrl || envBaseUrl() || 'https://api-dev.runbios.ai').replace(/\/+$/, '');
|
|
330
|
+
this.timeout = config.timeout ?? 900_000;
|
|
331
|
+
this._http = http;
|
|
332
|
+
}
|
|
333
|
+
/** @internal Control-plane transport; present when constructed by the SDK client. */
|
|
334
|
+
get http() {
|
|
335
|
+
if (!this._http) {
|
|
336
|
+
throw new Error('RunBiOS: control-plane inference management requires an SDK client (apiKey or accessToken)');
|
|
337
|
+
}
|
|
338
|
+
return this._http;
|
|
339
|
+
}
|
|
340
|
+
// --------------------------------------------------------------------------
|
|
341
|
+
// Control-plane deployment management — `/api/inference*`
|
|
342
|
+
// --------------------------------------------------------------------------
|
|
343
|
+
/** Side-effect-free validation with authoritative stock, prices, alternatives, and hold terms. */
|
|
344
|
+
preflight(params) {
|
|
345
|
+
return this.http.fetchPost('/api/inference/preflight', buildInferenceRequest(params));
|
|
346
|
+
}
|
|
347
|
+
async create(params, idempotencyKey, options) {
|
|
348
|
+
const body = buildInferenceRequest(params);
|
|
349
|
+
// Validate the RESOLVED placement (gpu_type/gpu_count may come from the
|
|
350
|
+
// first ranked priority rather than the top-level fields).
|
|
351
|
+
await this.validateGpuSelectionBeforeSubmit(params, String(body.gpu_type ?? ''), Number(body.gpu_count ?? NaN));
|
|
352
|
+
const response = await this.http.fetchPost('/api/inference', body, { 'Idempotency-Key': inferenceIdempotencyKey(idempotencyKey) });
|
|
353
|
+
const handle = response?.booking?.handle;
|
|
354
|
+
if (handle && (options?.waitForBooking ?? true)) {
|
|
355
|
+
return this.waitForBooking(handle, options?.bookingTimeoutMs ?? 300_000);
|
|
356
|
+
}
|
|
357
|
+
return response;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Poll a pre-reveal booking handle once: `{ booking: {...} }` while pending,
|
|
361
|
+
* or the full create payload once the GPU is secured (the one-time
|
|
362
|
+
* inference_key is present exactly once). Throws
|
|
363
|
+
* {@link GpuRejectionError} on the definitive 409 stock miss and ApiError
|
|
364
|
+
* 503 on a transient market outage (retry — never a capacity verdict).
|
|
365
|
+
*/
|
|
366
|
+
getBooking(handle) {
|
|
367
|
+
return this.http.fetchGet(`/api/inference/bookings/${encodeURIComponent(handle)}`);
|
|
368
|
+
}
|
|
369
|
+
/** Poll a booking handle to its terminal outcome (see {@link create}). */
|
|
370
|
+
async waitForBooking(handle, timeoutMs = 300_000, pollIntervalMs = 2_000) {
|
|
371
|
+
const deadline = Date.now() + Math.max(timeoutMs, pollIntervalMs);
|
|
372
|
+
for (;;) {
|
|
373
|
+
if (Date.now() > deadline) {
|
|
374
|
+
throw new ApiError(408, {
|
|
375
|
+
error: {
|
|
376
|
+
code: 'BOOKING_POLL_TIMEOUT',
|
|
377
|
+
message: `RunBiOS: the GPU booking did not conclude within ${Math.round(timeoutMs / 1000)}s. `
|
|
378
|
+
+ `Nothing was charged and no deployment exists until a GPU is confirmed; `
|
|
379
|
+
+ `poll getBooking('${handle}') to continue waiting.`,
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
384
|
+
let outcome;
|
|
385
|
+
try {
|
|
386
|
+
outcome = await this.getBooking(handle);
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
if (err instanceof ApiError && err.status === 503)
|
|
390
|
+
continue; // transient — never fail-closed
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
393
|
+
if (outcome?.booking?.status === 'booking')
|
|
394
|
+
continue;
|
|
395
|
+
return outcome;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Whether the advisory capacity check may run on this context length.
|
|
400
|
+
*
|
|
401
|
+
* `contextLength` is an INPUT to KV-cache sizing. An over-native value
|
|
402
|
+
* inflates the KV estimate, raises the computed `min_gpus`, and would make
|
|
403
|
+
* {@link validateGpuSelectionBeforeSubmit} synthesize a 409 telling the
|
|
404
|
+
* caller to buy more GPUs — when the real problem is one parameter and the
|
|
405
|
+
* server's own verdict is a 400 (`context_length N exceeds the model's
|
|
406
|
+
* maximum of M`). A client must never fabricate a capacity verdict the server
|
|
407
|
+
* would not give.
|
|
408
|
+
*
|
|
409
|
+
* True only when the sizing basis is sound: no explicit `contextLength` (the
|
|
410
|
+
* server sizes from `min(nativeMax, 262144)`, which can never exceed native),
|
|
411
|
+
* or a window PROVEN to fit the registry's recorded native max. False when
|
|
412
|
+
* the registry says the context is over native, when the native max cannot be
|
|
413
|
+
* read at all, or when the value is not one this client can read as a window
|
|
414
|
+
* (see {@link contextSizingBasis}) — all of them hand the question to the
|
|
415
|
+
* server, which answers authoritatively before any wallet hold or GPU
|
|
416
|
+
* booking. The decision never depends on the runtime type the caller passed:
|
|
417
|
+
* `'999999'` from a JSON config skips the check exactly like `999999` does.
|
|
418
|
+
* @internal
|
|
419
|
+
*/
|
|
420
|
+
async contextSizingIsTrustworthy(params) {
|
|
421
|
+
const [basis, requested] = contextSizingBasis(params.contextLength);
|
|
422
|
+
if (basis === 'server_default')
|
|
423
|
+
return true;
|
|
424
|
+
if (basis === 'unprovable')
|
|
425
|
+
return false;
|
|
426
|
+
const model = params.hfModelId || params.baseModelId;
|
|
427
|
+
// No control-plane transport: leave the pre-existing failure point where it
|
|
428
|
+
// was (the POST) rather than throwing a step earlier from an advisory check.
|
|
429
|
+
if (!model || !this._http)
|
|
430
|
+
return false;
|
|
431
|
+
const native = await readNativeMaxContext(this._http, model);
|
|
432
|
+
if (native === undefined)
|
|
433
|
+
return false; // unknown basis — the server decides
|
|
434
|
+
return requested <= native;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Advisory model-addressed min/valid-count check before any POST. Throws the
|
|
438
|
+
* typed GpuRejectionError only when the selection can NEVER be booked for
|
|
439
|
+
* this model, so it always carries a permanent code and no queue offer; every
|
|
440
|
+
* failure to ANSWER (endpoint unreachable, unknown shape, an untrustworthy
|
|
441
|
+
* context length) is silent — the create gate re-validates authoritatively
|
|
442
|
+
* and unknown never fails closed. @internal
|
|
443
|
+
*/
|
|
444
|
+
async validateGpuSelectionBeforeSubmit(params, gpuType, gpuCount) {
|
|
445
|
+
const model = params.hfModelId || params.baseModelId;
|
|
446
|
+
if (!model || !gpuType || !Number.isInteger(gpuCount))
|
|
447
|
+
return;
|
|
448
|
+
if (!(await this.contextSizingIsTrustworthy(params)))
|
|
449
|
+
return;
|
|
450
|
+
let options;
|
|
451
|
+
try {
|
|
452
|
+
options = await this.getGPUOptions({
|
|
453
|
+
model,
|
|
454
|
+
revision: params.hfModelRevision || params.baseModelRevision,
|
|
455
|
+
quant: params.quant,
|
|
456
|
+
contextLength: params.contextLength,
|
|
457
|
+
hfIntegrationId: params.hfIntegrationId,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return; // advisory only — the create gate is the enforcement floor
|
|
462
|
+
}
|
|
463
|
+
const chosen = options?.options?.find(option => option.gpu_type === gpuType);
|
|
464
|
+
if (!chosen)
|
|
465
|
+
return;
|
|
466
|
+
const minGpus = chosen.min_gpus || 0;
|
|
467
|
+
const validCounts = (chosen.valid_counts || []).filter(count => Number.isInteger(count));
|
|
468
|
+
let reason;
|
|
469
|
+
let message = '';
|
|
470
|
+
if (chosen.selectable === false) {
|
|
471
|
+
reason = chosen.reason_code === 'gpu_unsupported' ? 'gpu_unsupported' : 'model_too_large';
|
|
472
|
+
message = reason === 'gpu_unsupported'
|
|
473
|
+
? `RunBiOS: ${gpuType} cannot run this model, whatever GPU count you pick. Choose one of the GPUs listed in available_gpus.`
|
|
474
|
+
: `RunBiOS: this model does not fit on ${gpuType}, at any GPU count. Pick one of the GPUs listed in available_gpus, which all have room for it.`;
|
|
475
|
+
}
|
|
476
|
+
else if (minGpus > 0 && gpuCount < minGpus) {
|
|
477
|
+
reason = 'below_model_minimum';
|
|
478
|
+
message = `RunBiOS: this model needs at least ${minGpus} ${gpuType}${minGpus === 1 ? '' : ' GPUs'} to run, and this request asked for ${gpuCount}. Raise the GPU count, or pick one of the GPUs listed in available_gpus.`;
|
|
479
|
+
}
|
|
480
|
+
else if (validCounts.length > 0 && !validCounts.includes(gpuCount)) {
|
|
481
|
+
reason = 'invalid_gpu_count';
|
|
482
|
+
message = `RunBiOS: this model cannot be split across ${gpuCount} ${gpuType}${gpuCount === 1 ? '' : ' GPUs'}. Use one of these GPU counts instead: ${validCounts.join(', ')}.`;
|
|
483
|
+
}
|
|
484
|
+
if (!reason)
|
|
485
|
+
return;
|
|
486
|
+
const selectableOptions = (options.options || []).filter((option) => !!option && option.selectable !== false);
|
|
487
|
+
const alternatives = selectableOptions
|
|
488
|
+
.filter(option => option.market?.availability_status === 'available')
|
|
489
|
+
.map(option => ({
|
|
490
|
+
gpu_type: option.gpu_type,
|
|
491
|
+
gpu_count: option.min_gpus,
|
|
492
|
+
min_gpus: option.min_gpus,
|
|
493
|
+
valid_counts: option.valid_counts,
|
|
494
|
+
tier: 'secure',
|
|
495
|
+
available_count: option.market?.available_count ?? undefined,
|
|
496
|
+
price_hour_cents: (option.market?.price_per_gpu_hour_cents || 0) * (option.min_gpus || 1),
|
|
497
|
+
}));
|
|
498
|
+
// Every reason this guard can raise is a PERMANENT property of the request,
|
|
499
|
+
// so it must speak the same 400 + own-code + no-queue contract the service
|
|
500
|
+
// does. Claiming CAPACITY_UNAVAILABLE here would tell the caller to wait for
|
|
501
|
+
// stock that would never make the request bookable.
|
|
502
|
+
throw new GpuRejectionError(400, {
|
|
503
|
+
error: {
|
|
504
|
+
code: gpuRejectionCodeForReason(reason),
|
|
505
|
+
message,
|
|
506
|
+
},
|
|
507
|
+
reason,
|
|
508
|
+
checked_at: options.availability_checked_at,
|
|
509
|
+
queue_offered: false,
|
|
510
|
+
queue_eligible: false,
|
|
511
|
+
selected: {
|
|
512
|
+
gpu_type: gpuType,
|
|
513
|
+
gpu_count: gpuCount,
|
|
514
|
+
tier: params.gpuTier || 'secure',
|
|
515
|
+
availability_status: 'unknown',
|
|
516
|
+
},
|
|
517
|
+
minimum_requirement: {
|
|
518
|
+
selected_gpu_min: minGpus,
|
|
519
|
+
selected_valid_counts: validCounts,
|
|
520
|
+
per_type: selectableOptions
|
|
521
|
+
.filter(option => (option.min_gpus || 0) >= 1)
|
|
522
|
+
.map(option => ({
|
|
523
|
+
gpu_type: option.gpu_type,
|
|
524
|
+
min_gpus: option.min_gpus,
|
|
525
|
+
valid_counts: option.valid_counts || [],
|
|
526
|
+
})),
|
|
527
|
+
},
|
|
528
|
+
available_gpus: alternatives,
|
|
529
|
+
available_alternatives: alternatives,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/** Fetch one bounded newest-first page. Reuse next_cursor with unchanged filters. */
|
|
533
|
+
async listPage(params = {}) {
|
|
534
|
+
const query = new URLSearchParams();
|
|
535
|
+
if (params.limit !== undefined) {
|
|
536
|
+
if (!Number.isInteger(params.limit) || params.limit < 1) {
|
|
537
|
+
throw new Error('RunBiOS: deployment list limit must be a positive integer');
|
|
538
|
+
}
|
|
539
|
+
query.set('limit', String(Math.min(params.limit, 200)));
|
|
540
|
+
}
|
|
541
|
+
if (params.cursor)
|
|
542
|
+
query.set('cursor', params.cursor);
|
|
543
|
+
if (params.status && params.status !== 'all')
|
|
544
|
+
query.set('status', params.status);
|
|
545
|
+
if (params.search?.trim())
|
|
546
|
+
query.set('search', params.search.trim());
|
|
547
|
+
const suffix = query.toString();
|
|
548
|
+
const response = await this.http.fetchGet(`/api/inference${suffix ? `?${suffix}` : ''}`);
|
|
549
|
+
// Compatibility with pre-pagination servers during a rolling release.
|
|
550
|
+
if (Array.isArray(response)) {
|
|
551
|
+
return { deployments: response, has_more: false, next_cursor: null, limit: response.length };
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
deployments: response.deployments || [],
|
|
555
|
+
has_more: response.has_more === true,
|
|
556
|
+
next_cursor: response.next_cursor ?? null,
|
|
557
|
+
limit: response.limit || params.limit || 50,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Compatibility helper returning only one bounded page. Prefer listPage for
|
|
562
|
+
* pagination.
|
|
563
|
+
*
|
|
564
|
+
* These are LIST rows, not details: the model handle is `model_ref` (there is
|
|
565
|
+
* no `model` key) and the per-row serving settings are absent. Call
|
|
566
|
+
* {@link get} for the full deployment.
|
|
567
|
+
*/
|
|
568
|
+
async list(params = {}) {
|
|
569
|
+
return (await this.listPage(params)).deployments;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Lazily traverse pages without materializing an unbounded tenant list.
|
|
573
|
+
* Yields LIST rows (see {@link list}), not details.
|
|
574
|
+
*/
|
|
575
|
+
async *iterate(params = {}) {
|
|
576
|
+
let cursor;
|
|
577
|
+
do {
|
|
578
|
+
const page = await this.listPage({ ...params, cursor });
|
|
579
|
+
for (const deployment of page.deployments)
|
|
580
|
+
yield deployment;
|
|
581
|
+
cursor = page.has_more && page.next_cursor ? page.next_cursor : undefined;
|
|
582
|
+
} while (cursor);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Current durable lifecycle, queue, price-cap, and wallet-authorization state.
|
|
586
|
+
*
|
|
587
|
+
* Unlike a {@link list} row this resolves the per-deployment serving settings
|
|
588
|
+
* — `context_length` and its `native_max_context` ceiling, `quantization`,
|
|
589
|
+
* `serving_config`, the applied-vs-requested snapshot, tool/reasoning
|
|
590
|
+
* capability, and `available_actions` — and it exposes the model handle as
|
|
591
|
+
* `hf_model_id` / `base_model_id` while `model` aliases the deployment name.
|
|
592
|
+
*/
|
|
593
|
+
get(id) {
|
|
594
|
+
return this.http.fetchGet(`/api/inference/${encodeURIComponent(id)}`);
|
|
595
|
+
}
|
|
596
|
+
/** Alias for get(), useful in polling automations. */
|
|
597
|
+
status(id) {
|
|
598
|
+
return this.get(id);
|
|
599
|
+
}
|
|
600
|
+
/** Durable email delivery history, including bounded retries and dead letters. */
|
|
601
|
+
async notifications(id, limit = 50) {
|
|
602
|
+
const safeLimit = Math.max(1, Math.min(100, Math.trunc(limit)));
|
|
603
|
+
const response = await this.http.fetchGet(`/api/inference/${encodeURIComponent(id)}/notifications?limit=${safeLimit}`);
|
|
604
|
+
return response.notifications || [];
|
|
605
|
+
}
|
|
606
|
+
stop(id) {
|
|
607
|
+
return this.http.fetchPost(`/api/inference/${encodeURIComponent(id)}/stop`);
|
|
608
|
+
}
|
|
609
|
+
resume(id) {
|
|
610
|
+
return this.http.fetchPost(`/api/inference/${encodeURIComponent(id)}/resume`);
|
|
611
|
+
}
|
|
612
|
+
restart(id) {
|
|
613
|
+
return this.http.fetchPost(`/api/inference/${encodeURIComponent(id)}/restart`);
|
|
614
|
+
}
|
|
615
|
+
update(id, params) {
|
|
616
|
+
const body = {};
|
|
617
|
+
if (params.allowCapacityQueue !== undefined)
|
|
618
|
+
body.allow_capacity_queue = params.allowCapacityQueue;
|
|
619
|
+
if (params.maxPriceHourCents !== undefined)
|
|
620
|
+
body.max_price_hour_cents = params.maxPriceHourCents;
|
|
621
|
+
if (params.contextLength !== undefined)
|
|
622
|
+
body.context_length = params.contextLength;
|
|
623
|
+
if (params.quant !== undefined)
|
|
624
|
+
body.quant = params.quant;
|
|
625
|
+
if (params.servingConfig !== undefined)
|
|
626
|
+
body.serving_config = params.servingConfig;
|
|
627
|
+
return this.http.fetchPatch(`/api/inference/${encodeURIComponent(id)}`, body);
|
|
628
|
+
}
|
|
629
|
+
delete(id) {
|
|
630
|
+
return this.http.fetchDelete(`/api/inference/${encodeURIComponent(id)}`);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Model-fit GPU choices joined to the authoritative deployment market
|
|
634
|
+
* snapshot. MODEL-ADDRESSED (recommended, book-first §2): pass `model` (or
|
|
635
|
+
* `inferenceId`) and the SERVER resolves the facts and computes
|
|
636
|
+
* min_gpus/valid_counts/bookable_counts — the same single implementation the
|
|
637
|
+
* create gate enforces, so client facts can never understate a minimum. The
|
|
638
|
+
* client-fact params (`paramsB` & friends) are DEPRECATED, kept one release.
|
|
639
|
+
*/
|
|
640
|
+
getGPUOptions(params) {
|
|
641
|
+
if (params.gpuTier !== undefined && params.gpuTier !== 'secure') {
|
|
642
|
+
throw new Error('RunBiOS: gpuTier must be secure; other deployment capacity tiers are not supported');
|
|
643
|
+
}
|
|
644
|
+
if (params.model === undefined && params.inferenceId === undefined && params.paramsB === undefined) {
|
|
645
|
+
throw new Error('RunBiOS: pass model (or inferenceId) for server-resolved sizing, or the deprecated paramsB client facts');
|
|
646
|
+
}
|
|
647
|
+
const query = new URLSearchParams();
|
|
648
|
+
if (params.model !== undefined)
|
|
649
|
+
query.set('model', params.model);
|
|
650
|
+
if (params.revision !== undefined)
|
|
651
|
+
query.set('revision', params.revision);
|
|
652
|
+
if (params.inferenceId !== undefined)
|
|
653
|
+
query.set('inference_id', params.inferenceId);
|
|
654
|
+
if (params.hfIntegrationId !== undefined)
|
|
655
|
+
query.set('hf_integration_id', params.hfIntegrationId);
|
|
656
|
+
if (params.paramsB !== undefined)
|
|
657
|
+
query.set('params_b', String(params.paramsB));
|
|
658
|
+
if (params.activeParamsB !== undefined)
|
|
659
|
+
query.set('active_params_b', String(params.activeParamsB));
|
|
660
|
+
if (params.isMoe !== undefined)
|
|
661
|
+
query.set('is_moe', String(params.isMoe));
|
|
662
|
+
if (params.quant !== undefined)
|
|
663
|
+
query.set('quant', params.quant);
|
|
664
|
+
if (params.contextLength !== undefined)
|
|
665
|
+
query.set('context_length', String(params.contextLength));
|
|
666
|
+
if (params.kvHeads !== undefined)
|
|
667
|
+
query.set('kv_heads', String(params.kvHeads));
|
|
668
|
+
if (params.numLayers !== undefined)
|
|
669
|
+
query.set('num_layers', String(params.numLayers));
|
|
670
|
+
if (params.kvLayers !== undefined)
|
|
671
|
+
query.set('kv_layers', String(params.kvLayers));
|
|
672
|
+
if (params.headDim !== undefined)
|
|
673
|
+
query.set('head_dim', String(params.headDim));
|
|
674
|
+
if (params.attention !== undefined)
|
|
675
|
+
query.set('attn', params.attention);
|
|
676
|
+
if (params.numAttentionHeads !== undefined)
|
|
677
|
+
query.set('num_attention_heads', String(params.numAttentionHeads));
|
|
678
|
+
if (params.kvLoraRank !== undefined)
|
|
679
|
+
query.set('kv_lora_rank', String(params.kvLoraRank));
|
|
680
|
+
if (params.qkRopeHeadDim !== undefined)
|
|
681
|
+
query.set('qk_rope_head_dim', String(params.qkRopeHeadDim));
|
|
682
|
+
if (params.gpuTier !== undefined)
|
|
683
|
+
query.set('gpu_tier', params.gpuTier);
|
|
684
|
+
return this.http.fetchGet(`/api/inference/gpu-options?${query}`);
|
|
685
|
+
}
|
|
686
|
+
// --------------------------------------------------------------------------
|
|
687
|
+
// OpenAI-compatible key-scoped inference — `/v1/chat/completions`
|
|
688
|
+
// --------------------------------------------------------------------------
|
|
689
|
+
prepare(params, stream) {
|
|
690
|
+
const { messages, model, tools, toolChoice, reasoningEffort, inferenceKey, idempotencyKey, requestId, signal, ...extra } = params;
|
|
691
|
+
const key = inferenceKey || this.key;
|
|
692
|
+
if (!key)
|
|
693
|
+
throw new Error('an inferenceKey is required');
|
|
694
|
+
const body = { ...extra, messages, stream };
|
|
695
|
+
if (model)
|
|
696
|
+
body.model = model;
|
|
697
|
+
if (tools !== undefined)
|
|
698
|
+
body.tools = tools;
|
|
699
|
+
if (toolChoice !== undefined)
|
|
700
|
+
body.tool_choice = toolChoice;
|
|
701
|
+
if (reasoningEffort !== undefined)
|
|
702
|
+
body.reasoning_effort = reasoningEffort;
|
|
703
|
+
validateChatRequest(body);
|
|
704
|
+
const headers = {
|
|
705
|
+
Authorization: `Bearer ${key}`,
|
|
706
|
+
Accept: stream ? 'text/event-stream' : 'application/json',
|
|
707
|
+
'Content-Type': 'application/json',
|
|
708
|
+
'X-Request-ID': requestId || crypto.randomUUID(),
|
|
709
|
+
};
|
|
710
|
+
if (idempotencyKey)
|
|
711
|
+
headers['Idempotency-Key'] = idempotencyKey;
|
|
712
|
+
return { body, key, headers, signal };
|
|
713
|
+
}
|
|
714
|
+
abortContext(signal) {
|
|
715
|
+
const controller = new AbortController();
|
|
716
|
+
const relay = () => controller.abort(signal?.reason);
|
|
717
|
+
if (signal?.aborted)
|
|
718
|
+
relay();
|
|
719
|
+
else
|
|
720
|
+
signal?.addEventListener('abort', relay, { once: true });
|
|
721
|
+
const timeoutId = setTimeout(() => controller.abort(new Error(`Inference timed out after ${this.timeout}ms`)), this.timeout);
|
|
722
|
+
return {
|
|
723
|
+
controller,
|
|
724
|
+
timeoutId,
|
|
725
|
+
remove: () => signal?.removeEventListener('abort', relay),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
async chatCompletions(params) {
|
|
729
|
+
const prepared = this.prepare(params, false);
|
|
730
|
+
const abort = this.abortContext(prepared.signal);
|
|
731
|
+
try {
|
|
732
|
+
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
733
|
+
method: 'POST', headers: prepared.headers, body: JSON.stringify(prepared.body), signal: abort.controller.signal,
|
|
734
|
+
});
|
|
735
|
+
if (!response.ok)
|
|
736
|
+
throw await apiError(response);
|
|
737
|
+
return await response.json();
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
if (abort.controller.signal.aborted && !(error instanceof ApiError)) {
|
|
741
|
+
throw new ApiError(0, { error: String(abort.controller.signal.reason || 'Inference request aborted') });
|
|
742
|
+
}
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
clearTimeout(abort.timeoutId);
|
|
747
|
+
abort.remove();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async *streamChatCompletions(params) {
|
|
751
|
+
const prepared = this.prepare(params, true);
|
|
752
|
+
const abort = this.abortContext(prepared.signal);
|
|
753
|
+
try {
|
|
754
|
+
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
755
|
+
method: 'POST', headers: prepared.headers, body: JSON.stringify(prepared.body), signal: abort.controller.signal,
|
|
756
|
+
});
|
|
757
|
+
if (!response.ok)
|
|
758
|
+
throw await apiError(response);
|
|
759
|
+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
760
|
+
if (!contentType.includes('text/event-stream')) {
|
|
761
|
+
throw new ApiError(response.status, { error: `Expected text/event-stream, received ${contentType || 'no content type'}` });
|
|
762
|
+
}
|
|
763
|
+
if (!response.body)
|
|
764
|
+
throw new ApiError(0, { error: 'Inference response has no body' });
|
|
765
|
+
for await (const data of parseSSE(response.body)) {
|
|
766
|
+
if (data === '[DONE]')
|
|
767
|
+
return;
|
|
768
|
+
let event;
|
|
769
|
+
try {
|
|
770
|
+
event = JSON.parse(data);
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
throw new ApiError(0, { error: 'Invalid JSON SSE event' });
|
|
774
|
+
}
|
|
775
|
+
if (event && typeof event === 'object' && 'error' in event) {
|
|
776
|
+
throw new ApiError(0, event);
|
|
777
|
+
}
|
|
778
|
+
yield event;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
if (abort.controller.signal.aborted && !(error instanceof ApiError)) {
|
|
783
|
+
throw new ApiError(0, { error: String(abort.controller.signal.reason || 'Inference request aborted') });
|
|
784
|
+
}
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
finally {
|
|
788
|
+
abort.controller.abort('stream closed');
|
|
789
|
+
clearTimeout(abort.timeoutId);
|
|
790
|
+
abort.remove();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
export { buildInferenceRequest };
|