bios-sdk 0.1.1-dev.13 → 0.1.1-dev.17

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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BiOSConfig, ApiErrorBody, AvailableGpuAlternative } from './types.js';
1
+ import type { BiOSConfig, ApiErrorBody, AvailableGpuAlternative, CapacityMinimumRequirement } from './types.js';
2
2
  /**
3
3
  * Typed error thrown by every SDK method when the API returns a non-2xx status.
4
4
  *
@@ -52,6 +52,32 @@ export declare class ApiError extends Error {
52
52
  readonly checkedAt: string | undefined;
53
53
  constructor(status: number, body: ApiErrorBody);
54
54
  }
55
+ /**
56
+ * Typed 409 for the standard CAPACITY_UNAVAILABLE contract (ONE code family
57
+ * across training and inference). Carries the rejection class (`reason`), the
58
+ * explicit server-computed `minimumRequirement` (min_gpus / valid_counts —
59
+ * never pick below them), and the canonical bookable-now alternatives via
60
+ * {@link ApiError.availableGpus}. Instanceof-compatible with ApiError so
61
+ * existing handlers keep working.
62
+ */
63
+ export declare class CapacityUnavailableError extends ApiError {
64
+ /** insufficient_stock | below_model_minimum | invalid_gpu_count | model_too_large. */
65
+ readonly reason: string | undefined;
66
+ /** The explicit minimum block for the model (selected type + per-type table). */
67
+ readonly minimumRequirement: CapacityMinimumRequirement | undefined;
68
+ /** The selection the rejection was about. */
69
+ readonly selected: ApiErrorBody['selected'];
70
+ /** Whether the capacity queue may be joined instead. */
71
+ readonly queueEligible: boolean;
72
+ constructor(status: number, body: ApiErrorBody);
73
+ }
74
+ /**
75
+ * Map a non-2xx body to the most specific typed error. The service is the
76
+ * authority: a CAPACITY_UNAVAILABLE code becomes {@link CapacityUnavailableError}
77
+ * so callers read `.availableGpus`/`.minimumRequirement` without string
78
+ * matching. @internal
79
+ */
80
+ export declare function buildApiError(status: number, body: ApiErrorBody): ApiError;
55
81
  /** Default API key from `BIOS_API_KEY` when the config omits one. @internal */
56
82
  export declare function envApiKey(): string | undefined;
57
83
  /** Default base URL from `BIOS_BASE_URL` when the config omits one. @internal */
package/dist/client.js CHANGED
@@ -67,11 +67,65 @@ export class ApiError extends Error {
67
67
  this.code = (nested ? String(nested.code || '') : body.code) || undefined;
68
68
  this.requestId = body.request_id;
69
69
  this.body = body;
70
- const alts = body.available_gpus ?? body.available_alternatives;
70
+ const flat = nestedErrorBody(body);
71
+ const alts = flat.available_gpus ?? flat.available_alternatives;
71
72
  this.availableGpus = Array.isArray(alts) && alts.length > 0 ? alts : undefined;
72
- this.checkedAt = body.checked_at;
73
+ this.checkedAt = flat.checked_at;
73
74
  }
74
75
  }
76
+ /**
77
+ * Flatten the standard `{ error: { ... } }` envelope over the legacy top-level
78
+ * fields so structured capacity data is found in either shape. @internal
79
+ */
80
+ function nestedErrorBody(body) {
81
+ const err = body.error;
82
+ if (typeof err === 'object' && err !== null) {
83
+ return { ...body, ...err };
84
+ }
85
+ return body;
86
+ }
87
+ /**
88
+ * Typed 409 for the standard CAPACITY_UNAVAILABLE contract (ONE code family
89
+ * across training and inference). Carries the rejection class (`reason`), the
90
+ * explicit server-computed `minimumRequirement` (min_gpus / valid_counts —
91
+ * never pick below them), and the canonical bookable-now alternatives via
92
+ * {@link ApiError.availableGpus}. Instanceof-compatible with ApiError so
93
+ * existing handlers keep working.
94
+ */
95
+ export class CapacityUnavailableError extends ApiError {
96
+ /** insufficient_stock | below_model_minimum | invalid_gpu_count | model_too_large. */
97
+ reason;
98
+ /** The explicit minimum block for the model (selected type + per-type table). */
99
+ minimumRequirement;
100
+ /** The selection the rejection was about. */
101
+ selected;
102
+ /** Whether the capacity queue may be joined instead. */
103
+ queueEligible;
104
+ constructor(status, body) {
105
+ super(status, body);
106
+ this.name = 'CapacityUnavailableError';
107
+ const flat = nestedErrorBody(body);
108
+ this.reason = flat.reason;
109
+ this.minimumRequirement = flat.minimum_requirement;
110
+ this.selected = flat.selected;
111
+ this.queueEligible = flat.queue_eligible === true;
112
+ }
113
+ }
114
+ /**
115
+ * Map a non-2xx body to the most specific typed error. The service is the
116
+ * authority: a CAPACITY_UNAVAILABLE code becomes {@link CapacityUnavailableError}
117
+ * so callers read `.availableGpus`/`.minimumRequirement` without string
118
+ * matching. @internal
119
+ */
120
+ export function buildApiError(status, body) {
121
+ const err = body.error;
122
+ const nested = typeof err === 'object' && err !== null ? err : null;
123
+ const code = (nested ? String(nested.code || '') : body.code) || undefined;
124
+ if (code === 'CAPACITY_UNAVAILABLE') {
125
+ return new CapacityUnavailableError(status, body);
126
+ }
127
+ return new ApiError(status, body);
128
+ }
75
129
  // ============================================================================
76
130
  // Environment defaults
77
131
  // ============================================================================
@@ -174,7 +228,7 @@ export class HttpClient {
174
228
  }
175
229
  const data = await res.json().catch(() => ({}));
176
230
  if (!res.ok) {
177
- throw new ApiError(res.status, data);
231
+ throw buildApiError(res.status, data);
178
232
  }
179
233
  return data;
180
234
  }
@@ -229,7 +283,7 @@ export class HttpClient {
229
283
  }
230
284
  const data = await res.json().catch(() => ({}));
231
285
  if (!res.ok) {
232
- throw new ApiError(res.status, data);
286
+ throw buildApiError(res.status, data);
233
287
  }
234
288
  return data;
235
289
  }
package/dist/index.d.ts CHANGED
@@ -56,11 +56,11 @@ export declare class BiOS {
56
56
  constructor(config: BiOSConfig);
57
57
  introspect(): Promise<ApiKeyIntrospection>;
58
58
  }
59
- export { ApiError } from './client.js';
59
+ export { ApiError, CapacityUnavailableError } from './client.js';
60
60
  export { Models } from './resources/models.js';
61
61
  export { Datasets } from './resources/datasets.js';
62
62
  export { Training } from './resources/training.js';
63
63
  export { Wallet } from './resources/wallet.js';
64
64
  export { GPU, type GPURecommendation } from './resources/gpu.js';
65
65
  export { Inference, validateChatRequest, parseSSE, type ChatMessage, type FunctionTool, type ChatCompletionParams, type ChatCompletionResponse, type ChatCompletionChunk, } from './resources/inference.js';
66
- export type { BiOSConfig, PaginatedResponse, ApiErrorBody, AvailableGpuAlternative, Model, ModelSearchParams, ModelSearchResponse, ModelConfig, Dataset, DatasetListParams, DatasetListResponse, DatasetUploadParams, DatasetPreview, DatasetPreviewParams, DatasetImportHFParams, DatasetRegisterHFParams, DatasetHubSearchParams, DatasetHubPreviewParams, DatasetValidation, DatasetFormatVariant, DatasetFormatSpec, DatasetFormatSpecs, DatasetStorageUsage, TrainingMethod, RLHFAlgorithm, AdapterType, TrainingJobStatus, TrainingStatusFilter, TrainingCreateParams, TrainingListParams, TrainingListResponse, TrainingJob, TrainingMetrics, MetricPoint, MetricGraphConfig, TrainingCheckpoint, TrainingLogs, TrainingLogEntry, TrainingStopResponse, TrainingResumeResponse, CanonicalTrainingRequest, TrainingPreflightDataset, TrainingPreflightWarning, TrainingPreflightResponse, TrainingCapabilityChoice, TrainingConfigFieldCapability, TrainingCapabilities, GPUChoice, WalletBalance, Transaction, TransactionListParams, GPUInfo, GPUPricingResponse, GPUOptionsParams, GPUOption, GPUOptionSuggestion, GPUOptionsResponse, InferenceStatus, InferenceCreateParams, InferenceUpdateParams, InferenceDeployment, InferenceCreateResponse, InferenceListResponse, InferenceUpdateResponse, InferenceLifecycleResponse, InferenceDeleteResponse, InferenceGPUOptionMarket, InferenceGPUOption, InferenceGPUAlternative, InferenceGPUOptionsParams, InferenceGPUOptionsResponse, InferenceCanonicalRequest, InferencePreflightWarning, InferencePreflightResponse, AdapterCompatibility, AdapterCompatibilityResponse, AdapterCompatibilityParams, SupportedArchitecture, ArchitectureScope, SupportedArchitecturesResponse, ApiKey, ApiKeyScope, ApiKeyIntrospection, Organization, OrgMember, OrgInvite, Workspace, Integration, IntegrationCreateParams, StorageUsage, StorageObject, } from './types.js';
66
+ export type { BiOSConfig, PaginatedResponse, ApiErrorBody, AvailableGpuAlternative, CapacityMinimumRequirement, InferenceBookingAccepted, Model, ModelSearchParams, ModelSearchResponse, ModelConfig, Dataset, DatasetListParams, DatasetListResponse, DatasetUploadParams, DatasetPreview, DatasetPreviewParams, DatasetImportHFParams, DatasetRegisterHFParams, DatasetHubSearchParams, DatasetHubPreviewParams, DatasetValidation, DatasetFormatVariant, DatasetFormatSpec, DatasetFormatSpecs, DatasetStorageUsage, TrainingMethod, RLHFAlgorithm, AdapterType, TrainingJobStatus, TrainingStatusFilter, TrainingCreateParams, TrainingListParams, TrainingListResponse, TrainingJob, TrainingMetrics, MetricPoint, MetricGraphConfig, TrainingCheckpoint, TrainingLogs, TrainingLogEntry, TrainingStopResponse, TrainingResumeResponse, CanonicalTrainingRequest, TrainingPreflightDataset, TrainingPreflightWarning, TrainingPreflightResponse, TrainingCapabilityChoice, TrainingConfigFieldCapability, TrainingCapabilities, GPUChoice, WalletBalance, Transaction, TransactionListParams, GPUInfo, GPUPricingResponse, GPUOptionsParams, GPUOption, GPUOptionSuggestion, GPUOptionsResponse, InferenceStatus, InferenceCreateParams, InferenceUpdateParams, InferenceDeployment, InferenceCreateResponse, InferenceListResponse, InferenceUpdateResponse, InferenceLifecycleResponse, InferenceDeleteResponse, InferenceGPUOptionMarket, InferenceGPUOption, InferenceGPUAlternative, InferenceGPUOptionsParams, InferenceGPUOptionsResponse, InferenceCanonicalRequest, InferencePreflightWarning, InferencePreflightResponse, AdapterCompatibility, AdapterCompatibilityResponse, AdapterCompatibilityParams, SupportedArchitecture, ArchitectureScope, SupportedArchitecturesResponse, ApiKey, ApiKeyScope, ApiKeyIntrospection, Organization, OrgMember, OrgInvite, Workspace, Integration, IntegrationCreateParams, StorageUsage, StorageObject, } from './types.js';
package/dist/index.js CHANGED
@@ -74,7 +74,7 @@ export class BiOS {
74
74
  // ---------------------------------------------------------------------------
75
75
  // Re-exports
76
76
  // ---------------------------------------------------------------------------
77
- export { ApiError } from './client.js';
77
+ export { ApiError, CapacityUnavailableError } from './client.js';
78
78
  export { Models } from './resources/models.js';
79
79
  export { Datasets } from './resources/datasets.js';
80
80
  export { Training } from './resources/training.js';
@@ -10,5 +10,14 @@ export interface NormalizedGPUPlacement {
10
10
  gpuType: string | undefined;
11
11
  gpuCount: number | undefined;
12
12
  }
13
- /** Validate the ranked placement contract shared by training and deployments. */
14
- export declare function normalizeGPUPlacement(choices: GPUChoice[] | undefined, queueEnabled: boolean, gpuType: string | undefined, gpuCount: number | undefined, queueField: 'queueIfUnavailable' | 'allowCapacityQueue'): NormalizedGPUPlacement;
13
+ /**
14
+ * Validate the ranked placement contract shared by training and deployments.
15
+ *
16
+ * `rankedImmediate=true` lets a launch WITHOUT the queue carry 1-5 ranked
17
+ * choices (best first): only the selected rung is booked up front, the extras
18
+ * become backups / the after-start replacement ladder (book-first §4 — backup
19
+ * rungs are decoupled from queue consent; the queue itself stays opt-in).
20
+ * With `rankedImmediate=false` a non-queued launch still needs exactly one
21
+ * choice. Queueing always needs 3 to 5 choices.
22
+ */
23
+ export declare function normalizeGPUPlacement(choices: GPUChoice[] | undefined, queueEnabled: boolean, gpuType: string | undefined, gpuCount: number | undefined, queueField: 'queueIfUnavailable' | 'allowCapacityQueue', rankedImmediate?: boolean): NormalizedGPUPlacement;
@@ -1,5 +1,14 @@
1
- /** Validate the ranked placement contract shared by training and deployments. */
2
- export function normalizeGPUPlacement(choices, queueEnabled, gpuType, gpuCount, queueField) {
1
+ /**
2
+ * Validate the ranked placement contract shared by training and deployments.
3
+ *
4
+ * `rankedImmediate=true` lets a launch WITHOUT the queue carry 1-5 ranked
5
+ * choices (best first): only the selected rung is booked up front, the extras
6
+ * become backups / the after-start replacement ladder (book-first §4 — backup
7
+ * rungs are decoupled from queue consent; the queue itself stays opt-in).
8
+ * With `rankedImmediate=false` a non-queued launch still needs exactly one
9
+ * choice. Queueing always needs 3 to 5 choices.
10
+ */
11
+ export function normalizeGPUPlacement(choices, queueEnabled, gpuType, gpuCount, queueField, rankedImmediate = false) {
3
12
  if (choices === undefined) {
4
13
  if (queueEnabled) {
5
14
  throw new Error(`BiOS: ${queueField}=true requires 3 to 5 gpuPriorities`);
@@ -15,7 +24,7 @@ export function normalizeGPUPlacement(choices, queueEnabled, gpuType, gpuCount,
15
24
  if (queueEnabled && choices.length < 3) {
16
25
  throw new Error(`BiOS: ${queueField}=true requires 3 to 5 gpuPriorities`);
17
26
  }
18
- if (!queueEnabled && choices.length !== 1) {
27
+ if (!queueEnabled && !rankedImmediate && choices.length !== 1) {
19
28
  throw new Error(`BiOS: gpuPriorities must contain exactly one choice when ${queueField} is false`);
20
29
  }
21
30
  const seen = new Set();
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { InferenceDeployment, InferenceCreateParams, InferenceCreateResponse, InferenceDeleteResponse, InferenceGPUOptionsParams, InferenceGPUOptionsResponse, InferenceLifecycleResponse, InferenceListParams, InferenceListResponse, InferenceNotificationListResponse, InferencePreflightResponse, InferenceUpdateResponse, InferenceUpdateParams } from '../types.js';
2
+ import type { InferenceDeployment, InferenceBookingAccepted, InferenceCreateParams, InferenceCreateResponse, InferenceDeleteResponse, InferenceGPUOptionsParams, InferenceGPUOptionsResponse, InferenceLifecycleResponse, InferenceListParams, InferenceListResponse, InferenceNotificationListResponse, InferencePreflightResponse, InferenceUpdateResponse, InferenceUpdateParams } from '../types.js';
3
3
  declare function buildInferenceRequest(params: InferenceCreateParams): Record<string, unknown>;
4
4
  export interface ChatMessage extends Record<string, unknown> {
5
5
  role: 'system' | 'developer' | 'user' | 'assistant' | 'tool' | 'function';
@@ -53,8 +53,50 @@ export declare class Inference {
53
53
  private get http();
54
54
  /** Side-effect-free validation with authoritative stock, prices, alternatives, and hold terms. */
55
55
  preflight(params: InferenceCreateParams): Promise<InferencePreflightResponse>;
56
- /** Create after preflight. Reuse idempotencyKey after a timeout to recover the same deployment and inference key. */
56
+ /**
57
+ * Create after preflight. Reuse idempotencyKey after a timeout to recover
58
+ * the same deployment and inference key.
59
+ *
60
+ * Pre-submit validation (book-first §2): the chosen gpuType/gpuCount are
61
+ * checked against the server's MODEL-ADDRESSED gpu-options (computed
62
+ * min_gpus/valid_counts) before any POST; a below-minimum or TP-invalid
63
+ * selection throws the typed {@link CapacityUnavailableError} with the
64
+ * standard body. The server stays the enforcement floor; an unreadable
65
+ * sizing endpoint never blocks the create.
66
+ *
67
+ * Book-before-reveal (book-first §1): a non-queued create answers 202 with
68
+ * a booking handle while a vendor-accepted GPU is booked (30-40s typical).
69
+ * By default this method POLLS the booking to its terminal outcome and
70
+ * returns the full create payload (the one-time inference_key exactly
71
+ * once); a definitive miss throws {@link CapacityUnavailableError} with
72
+ * FRESH alternatives + the minimum block, and NO deployment exists. Pass
73
+ * `{ waitForBooking: false }` to receive the raw 202 body and poll
74
+ * {@link getBooking} yourself. Transient 503s during the poll are retried —
75
+ * a market outage is never a capacity verdict.
76
+ */
57
77
  create(params: InferenceCreateParams, idempotencyKey: string): Promise<InferenceCreateResponse>;
78
+ create(params: InferenceCreateParams, idempotencyKey: string, options: {
79
+ waitForBooking?: boolean;
80
+ bookingTimeoutMs?: number;
81
+ }): Promise<InferenceCreateResponse | InferenceBookingAccepted>;
82
+ /**
83
+ * Poll a pre-reveal booking handle once: `{ booking: {...} }` while pending,
84
+ * or the full create payload after the vendor accepted (the one-time
85
+ * inference_key is present exactly once). Throws
86
+ * {@link CapacityUnavailableError} on the definitive 409 miss and ApiError
87
+ * 503 on a transient market outage (retry — never a capacity verdict).
88
+ */
89
+ getBooking(handle: string): Promise<InferenceCreateResponse | InferenceBookingAccepted>;
90
+ /** Poll a booking handle to its terminal outcome (see {@link create}). */
91
+ waitForBooking(handle: string, timeoutMs?: number, pollIntervalMs?: number): Promise<InferenceCreateResponse>;
92
+ /**
93
+ * Advisory model-addressed min/valid-count check before any POST. Throws the
94
+ * typed CapacityUnavailableError only when the selection can NEVER be booked
95
+ * for this model; every failure to ANSWER (endpoint unreachable, unknown
96
+ * shape) is silent — the create gate re-validates authoritatively and
97
+ * unknown never fails closed. @internal
98
+ */
99
+ private validateGpuSelectionBeforeSubmit;
58
100
  /** Fetch one bounded newest-first page. Reuse next_cursor with unchanged filters. */
59
101
  listPage(params?: InferenceListParams): Promise<InferenceListResponse>;
60
102
  /** Compatibility helper returning only one bounded page. Prefer listPage for pagination. */
@@ -72,7 +114,14 @@ export declare class Inference {
72
114
  restart(id: string): Promise<InferenceLifecycleResponse>;
73
115
  update(id: string, params: InferenceUpdateParams): Promise<InferenceUpdateResponse>;
74
116
  delete(id: string): Promise<InferenceDeleteResponse>;
75
- /** Model-fit GPU choices joined to the authoritative deployment market snapshot. */
117
+ /**
118
+ * Model-fit GPU choices joined to the authoritative deployment market
119
+ * snapshot. MODEL-ADDRESSED (recommended, book-first §2): pass `model` (or
120
+ * `inferenceId`) and the SERVER resolves the facts and computes
121
+ * min_gpus/valid_counts/bookable_counts — the same single implementation the
122
+ * create gate enforces, so client facts can never understate a minimum. The
123
+ * client-fact params (`paramsB` & friends) are DEPRECATED, kept one release.
124
+ */
76
125
  getGPUOptions(params: InferenceGPUOptionsParams): Promise<InferenceGPUOptionsResponse>;
77
126
  private prepare;
78
127
  private abortContext;
@@ -1,4 +1,4 @@
1
- import { ApiError, envBaseUrl } from '../client.js';
1
+ import { ApiError, CapacityUnavailableError, envBaseUrl } from '../client.js';
2
2
  import { normalizeGPUPlacement } from './gpu-priorities.js';
3
3
  const FUNCTION_NAME = /^[A-Za-z0-9_-]{1,64}$/;
4
4
  const ROLES = new Set(['system', 'developer', 'user', 'assistant', 'tool', 'function']);
@@ -21,7 +21,12 @@ function inferenceIdempotencyKey(value) {
21
21
  }
22
22
  function buildInferenceRequest(params) {
23
23
  const queueEnabled = params.allowCapacityQueue ?? false;
24
- const placement = normalizeGPUPlacement(params.gpuPriorities, queueEnabled, params.gpuType, params.gpuCount, 'allowCapacityQueue');
24
+ const placement = normalizeGPUPlacement(params.gpuPriorities, queueEnabled, params.gpuType, params.gpuCount, 'allowCapacityQueue',
25
+ // Backup rungs are decoupled from queue consent (book-first §4): a
26
+ // non-queued deployment may rank 1-5 placements. Only the selected rung
27
+ // is booked up front; the extras become the after-start replacement
28
+ // ladder if a GPU is ever lost.
29
+ true);
25
30
  if (!params.name?.trim())
26
31
  throw new Error('BiOS: deployment name is required');
27
32
  if (!placement.gpuType)
@@ -298,10 +303,141 @@ export class Inference {
298
303
  preflight(params) {
299
304
  return this.http.fetchPost('/api/inference/preflight', buildInferenceRequest(params));
300
305
  }
301
- /** Create after preflight. Reuse idempotencyKey after a timeout to recover the same deployment and inference key. */
302
- create(params, idempotencyKey) {
303
- return this.http.fetchPost('/api/inference', buildInferenceRequest(params), {
304
- 'Idempotency-Key': inferenceIdempotencyKey(idempotencyKey),
306
+ async create(params, idempotencyKey, options) {
307
+ const body = buildInferenceRequest(params);
308
+ // Validate the RESOLVED placement (gpu_type/gpu_count may come from the
309
+ // first ranked priority rather than the top-level fields).
310
+ await this.validateGpuSelectionBeforeSubmit(params, String(body.gpu_type ?? ''), Number(body.gpu_count ?? NaN));
311
+ const response = await this.http.fetchPost('/api/inference', body, { 'Idempotency-Key': inferenceIdempotencyKey(idempotencyKey) });
312
+ const handle = response?.booking?.handle;
313
+ if (handle && (options?.waitForBooking ?? true)) {
314
+ return this.waitForBooking(handle, options?.bookingTimeoutMs ?? 300_000);
315
+ }
316
+ return response;
317
+ }
318
+ /**
319
+ * Poll a pre-reveal booking handle once: `{ booking: {...} }` while pending,
320
+ * or the full create payload after the vendor accepted (the one-time
321
+ * inference_key is present exactly once). Throws
322
+ * {@link CapacityUnavailableError} on the definitive 409 miss and ApiError
323
+ * 503 on a transient market outage (retry — never a capacity verdict).
324
+ */
325
+ getBooking(handle) {
326
+ return this.http.fetchGet(`/api/inference/bookings/${encodeURIComponent(handle)}`);
327
+ }
328
+ /** Poll a booking handle to its terminal outcome (see {@link create}). */
329
+ async waitForBooking(handle, timeoutMs = 300_000, pollIntervalMs = 2_000) {
330
+ const deadline = Date.now() + Math.max(timeoutMs, pollIntervalMs);
331
+ for (;;) {
332
+ if (Date.now() > deadline) {
333
+ throw new ApiError(408, {
334
+ error: {
335
+ code: 'BOOKING_POLL_TIMEOUT',
336
+ message: `BiOS: the GPU booking did not conclude within ${Math.round(timeoutMs / 1000)}s. `
337
+ + `Nothing was charged and no deployment exists until a GPU is confirmed; `
338
+ + `poll getBooking('${handle}') to continue waiting.`,
339
+ },
340
+ });
341
+ }
342
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
343
+ let outcome;
344
+ try {
345
+ outcome = await this.getBooking(handle);
346
+ }
347
+ catch (err) {
348
+ if (err instanceof ApiError && err.status === 503)
349
+ continue; // transient — never fail-closed
350
+ throw err;
351
+ }
352
+ if (outcome?.booking?.status === 'booking')
353
+ continue;
354
+ return outcome;
355
+ }
356
+ }
357
+ /**
358
+ * Advisory model-addressed min/valid-count check before any POST. Throws the
359
+ * typed CapacityUnavailableError only when the selection can NEVER be booked
360
+ * for this model; every failure to ANSWER (endpoint unreachable, unknown
361
+ * shape) is silent — the create gate re-validates authoritatively and
362
+ * unknown never fails closed. @internal
363
+ */
364
+ async validateGpuSelectionBeforeSubmit(params, gpuType, gpuCount) {
365
+ const model = params.hfModelId || params.baseModelId;
366
+ if (!model || !gpuType || !Number.isInteger(gpuCount))
367
+ return;
368
+ let options;
369
+ try {
370
+ options = await this.getGPUOptions({
371
+ model,
372
+ revision: params.hfModelRevision || params.baseModelRevision,
373
+ quant: params.quant,
374
+ contextLength: params.contextLength,
375
+ hfIntegrationId: params.hfIntegrationId,
376
+ });
377
+ }
378
+ catch {
379
+ return; // advisory only — the create gate is the enforcement floor
380
+ }
381
+ const chosen = options?.options?.find(option => option.gpu_type === gpuType);
382
+ if (!chosen)
383
+ return;
384
+ const minGpus = chosen.min_gpus || 0;
385
+ const validCounts = (chosen.valid_counts || []).filter(count => Number.isInteger(count));
386
+ let reason;
387
+ let message = '';
388
+ if (chosen.selectable === false) {
389
+ reason = 'model_too_large';
390
+ message = `BiOS: this model cannot be served on ${gpuType} at any supported GPU count`;
391
+ }
392
+ else if (minGpus > 0 && gpuCount < minGpus) {
393
+ reason = 'below_model_minimum';
394
+ message = `BiOS: ${gpuType} needs at least ${minGpus} GPU(s) for this model (you chose ${gpuCount})`;
395
+ }
396
+ else if (validCounts.length > 0 && !validCounts.includes(gpuCount)) {
397
+ reason = 'invalid_gpu_count';
398
+ message = `BiOS: ${gpuCount} GPUs is not a valid tensor-parallel size for this model on ${gpuType} — valid choices: ${validCounts.join(', ')}`;
399
+ }
400
+ if (!reason)
401
+ return;
402
+ const selectableOptions = (options.options || []).filter((option) => !!option && option.selectable !== false);
403
+ const alternatives = selectableOptions
404
+ .filter(option => option.market?.availability_status === 'available')
405
+ .map(option => ({
406
+ gpu_type: option.gpu_type,
407
+ gpu_count: option.min_gpus,
408
+ min_gpus: option.min_gpus,
409
+ valid_counts: option.valid_counts,
410
+ tier: 'secure',
411
+ available_count: option.market?.available_count ?? undefined,
412
+ price_hour_cents: (option.market?.price_per_gpu_hour_cents || 0) * (option.min_gpus || 1),
413
+ }));
414
+ throw new CapacityUnavailableError(409, {
415
+ error: {
416
+ code: 'CAPACITY_UNAVAILABLE',
417
+ message,
418
+ },
419
+ reason,
420
+ checked_at: options.availability_checked_at,
421
+ queue_eligible: params.allowCapacityQueue === true,
422
+ selected: {
423
+ gpu_type: gpuType,
424
+ gpu_count: gpuCount,
425
+ tier: params.gpuTier || 'secure',
426
+ availability_status: 'unknown',
427
+ },
428
+ minimum_requirement: {
429
+ selected_gpu_min: minGpus,
430
+ selected_valid_counts: validCounts,
431
+ per_type: selectableOptions
432
+ .filter(option => (option.min_gpus || 0) >= 1)
433
+ .map(option => ({
434
+ gpu_type: option.gpu_type,
435
+ min_gpus: option.min_gpus,
436
+ valid_counts: option.valid_counts || [],
437
+ })),
438
+ },
439
+ available_gpus: alternatives,
440
+ available_alternatives: alternatives,
305
441
  });
306
442
  }
307
443
  /** Fetch one bounded newest-first page. Reuse next_cursor with unchanged filters. */
@@ -386,12 +522,32 @@ export class Inference {
386
522
  delete(id) {
387
523
  return this.http.fetchDelete(`/api/inference/${encodeURIComponent(id)}`);
388
524
  }
389
- /** Model-fit GPU choices joined to the authoritative deployment market snapshot. */
525
+ /**
526
+ * Model-fit GPU choices joined to the authoritative deployment market
527
+ * snapshot. MODEL-ADDRESSED (recommended, book-first §2): pass `model` (or
528
+ * `inferenceId`) and the SERVER resolves the facts and computes
529
+ * min_gpus/valid_counts/bookable_counts — the same single implementation the
530
+ * create gate enforces, so client facts can never understate a minimum. The
531
+ * client-fact params (`paramsB` & friends) are DEPRECATED, kept one release.
532
+ */
390
533
  getGPUOptions(params) {
391
534
  if (params.gpuTier !== undefined && params.gpuTier !== 'secure') {
392
535
  throw new Error('BiOS: gpuTier must be secure; other deployment capacity tiers are not supported');
393
536
  }
394
- const query = new URLSearchParams({ params_b: String(params.paramsB) });
537
+ if (params.model === undefined && params.inferenceId === undefined && params.paramsB === undefined) {
538
+ throw new Error('BiOS: pass model (or inferenceId) for server-resolved sizing, or the deprecated paramsB client facts');
539
+ }
540
+ const query = new URLSearchParams();
541
+ if (params.model !== undefined)
542
+ query.set('model', params.model);
543
+ if (params.revision !== undefined)
544
+ query.set('revision', params.revision);
545
+ if (params.inferenceId !== undefined)
546
+ query.set('inference_id', params.inferenceId);
547
+ if (params.hfIntegrationId !== undefined)
548
+ query.set('hf_integration_id', params.hfIntegrationId);
549
+ if (params.paramsB !== undefined)
550
+ query.set('params_b', String(params.paramsB));
395
551
  if (params.activeParamsB !== undefined)
396
552
  query.set('active_params_b', String(params.activeParamsB));
397
553
  if (params.isMoe !== undefined)
@@ -10,11 +10,26 @@ export declare class Training {
10
10
  /** Return the authoritative BIOS image, method, adapter, and hyperparameter contract. */
11
11
  capabilities(): Promise<TrainingCapabilities>;
12
12
  /**
13
- * Create a new training job.
14
- *
15
- * Provisions GPU resources, downloads the model and dataset, and begins
16
- * training. The job starts in "pending" status and progresses through
17
- * provisioning, downloading, and running states.
13
+ * Create a new training job (book-before-reveal).
14
+ *
15
+ * The call blocks while the ranked GPU ladder is booked (~40s typical). A
16
+ * training id and the "training started" email exist only once a real pod is
17
+ * secured, so the returned `status` is one of:
18
+ *
19
+ * - `"booked"` — a GPU was secured (booked == secured); the job then
20
+ * progresses through provisioning, downloading, and running on its own.
21
+ * - `"securing"` — still booking at the deadline; the async tail continues.
22
+ * Poll {@link get} until it reaches `booked`/`running` (or terminal).
23
+ * Nothing is charged until a pod is real.
24
+ * - `"queued"` — returned only with explicit queue consent
25
+ * (`queueIfUnavailable: true`); waits for stock at zero charge and books
26
+ * via the same path.
27
+ *
28
+ * If the ladder is exhausted at booking time without queue consent, this
29
+ * rejects with a `CAPACITY_UNAVAILABLE` (HTTP 409) carrying neutral
30
+ * alternatives — no phantom job remains and nothing is billed. The SDK never
31
+ * auto-substitutes a GPU; a transient 503 is a retry, never a capacity
32
+ * verdict.
18
33
  *
19
34
  * @example
20
35
  * ```ts
@@ -120,7 +135,12 @@ export declare class Training {
120
135
  */
121
136
  stop(id: string, keepData?: boolean): Promise<TrainingStopResponse>;
122
137
  /**
123
- * Resume a stopped training job from its last checkpoint.
138
+ * Resume a stopped or `interrupted` training job from its last checkpoint.
139
+ *
140
+ * Use this on a `stopped` job, or one resting at `interrupted` — the
141
+ * self-heal state a started job enters when its pod is lost and billing has
142
+ * already been stopped (distinct from `failed`). Resume re-books on secured
143
+ * capacity before reporting resumed, so book-before-reveal still applies.
124
144
  *
125
145
  * @param id - Training job ID.
126
146
  *
@@ -191,11 +191,26 @@ export class Training {
191
191
  return this._http.fetchGet('/api/training/capabilities');
192
192
  }
193
193
  /**
194
- * Create a new training job.
194
+ * Create a new training job (book-before-reveal).
195
195
  *
196
- * Provisions GPU resources, downloads the model and dataset, and begins
197
- * training. The job starts in "pending" status and progresses through
198
- * provisioning, downloading, and running states.
196
+ * The call blocks while the ranked GPU ladder is booked (~40s typical). A
197
+ * training id and the "training started" email exist only once a real pod is
198
+ * secured, so the returned `status` is one of:
199
+ *
200
+ * - `"booked"` — a GPU was secured (booked == secured); the job then
201
+ * progresses through provisioning, downloading, and running on its own.
202
+ * - `"securing"` — still booking at the deadline; the async tail continues.
203
+ * Poll {@link get} until it reaches `booked`/`running` (or terminal).
204
+ * Nothing is charged until a pod is real.
205
+ * - `"queued"` — returned only with explicit queue consent
206
+ * (`queueIfUnavailable: true`); waits for stock at zero charge and books
207
+ * via the same path.
208
+ *
209
+ * If the ladder is exhausted at booking time without queue consent, this
210
+ * rejects with a `CAPACITY_UNAVAILABLE` (HTTP 409) carrying neutral
211
+ * alternatives — no phantom job remains and nothing is billed. The SDK never
212
+ * auto-substitutes a GPU; a transient 503 is a retry, never a capacity
213
+ * verdict.
199
214
  *
200
215
  * @example
201
216
  * ```ts
@@ -363,7 +378,12 @@ export class Training {
363
378
  return this._http.fetchPost(`/api/training/jobs/${encodeURIComponent(id)}/stop`, { keep_data: keepData });
364
379
  }
365
380
  /**
366
- * Resume a stopped training job from its last checkpoint.
381
+ * Resume a stopped or `interrupted` training job from its last checkpoint.
382
+ *
383
+ * Use this on a `stopped` job, or one resting at `interrupted` — the
384
+ * self-heal state a started job enters when its pod is lost and billing has
385
+ * already been stopped (distinct from `failed`). Resume re-books on secured
386
+ * capacity before reporting resumed, so book-before-reveal still applies.
367
387
  *
368
388
  * @param id - Training job ID.
369
389
  *
package/dist/types.d.ts CHANGED
@@ -49,16 +49,55 @@ export interface ApiErrorBody {
49
49
  available_alternatives?: AvailableGpuAlternative[];
50
50
  /** When the availability snapshot behind the rejection was taken. */
51
51
  checked_at?: string;
52
+ /**
53
+ * Rejection class on the standard CAPACITY_UNAVAILABLE contract:
54
+ * insufficient_stock | below_model_minimum | invalid_gpu_count |
55
+ * model_too_large.
56
+ */
57
+ reason?: string;
58
+ /**
59
+ * Explicit server-computed minimum block on CAPACITY_UNAVAILABLE: the
60
+ * selected type's minimum + valid counts and the per-type table for the
61
+ * model. Client-supplied facts can never lower these.
62
+ */
63
+ minimum_requirement?: CapacityMinimumRequirement;
64
+ /** The selection the rejection was about. */
65
+ selected?: {
66
+ gpu_type: string;
67
+ gpu_count: number;
68
+ tier?: string;
69
+ availability_status?: 'available' | 'out_of_stock' | 'unknown';
70
+ available_count?: number;
71
+ };
72
+ /** Whether the capacity queue may be joined for this request. */
73
+ queue_eligible?: boolean;
74
+ }
75
+ /** The explicit minimum-requirement block of the standard capacity contract. */
76
+ export interface CapacityMinimumRequirement {
77
+ selected_gpu_min: number;
78
+ selected_valid_counts?: number[];
79
+ per_type: Array<{
80
+ gpu_type: string;
81
+ min_gpus: number;
82
+ valid_counts: number[];
83
+ }>;
52
84
  }
53
85
  /** One bookable-now GPU alternative carried on an availability rejection. */
54
86
  export interface AvailableGpuAlternative {
55
87
  gpu_type: string;
56
88
  gpu_count?: number;
89
+ /** The model's minimum GPU count for this type (never pick below it). */
90
+ min_gpus?: number;
91
+ /** Every pickable count for this type (>= min and tensor-parallel-valid). */
92
+ valid_counts?: number[];
57
93
  available_count?: number;
58
94
  price_per_hour_cents?: number;
95
+ /** Total hourly price at `gpu_count` on the inference contract. */
96
+ price_hour_cents?: number;
59
97
  provider?: string;
60
98
  region?: string;
61
99
  tier?: string;
100
+ recommended?: boolean;
62
101
  }
63
102
  /** A model returned from the HuggingFace-backed model search. */
64
103
  export interface Model {
@@ -350,9 +389,9 @@ export type RLHFAlgorithm = 'dpo' | 'simpo' | 'cpo' | 'orpo' | 'kto' | 'rm';
350
389
  /** Supported adapter types. */
351
390
  export type AdapterType = 'lora' | 'qlora' | 'adalora' | 'full' | 'loha' | 'lokr' | 'boft' | 'oft' | 'vera' | 'fourierft' | 'bone' | 'adapter' | 'reft' | 'llamapro' | 'longlora';
352
391
  /** Training job status values. */
353
- export type TrainingJobStatus = 'pending' | 'queued' | 'provisioning' | 'preparing' | 'starting' | 'downloading' | 'running' | 'completed' | 'failed' | 'saving' | 'stopped' | 'stopping' | 'resuming' | 'cancelled';
392
+ export type TrainingJobStatus = 'pending' | 'queued' | 'securing' | 'booked' | 'provisioning' | 'preparing' | 'starting' | 'downloading' | 'running' | 'completed' | 'failed' | 'interrupted' | 'saving' | 'stopped' | 'stopping' | 'resuming' | 'cancelled';
354
393
  /** Status values accepted by GET /api/training/jobs?status=. */
355
- export type TrainingStatusFilter = 'pending' | 'queued' | 'provisioning' | 'preparing' | 'starting' | 'running' | 'saving' | 'completed' | 'failed' | 'stopped';
394
+ export type TrainingStatusFilter = 'pending' | 'queued' | 'securing' | 'booked' | 'provisioning' | 'preparing' | 'starting' | 'running' | 'interrupted' | 'saving' | 'completed' | 'failed' | 'stopped';
356
395
  /** One ranked GPU fallback choice. The first choice is the primary. */
357
396
  export interface GPUChoice {
358
397
  gpuType: string;
@@ -1061,6 +1100,18 @@ export interface InferenceNotificationSummary {
1061
1100
  export interface InferenceNotificationListResponse {
1062
1101
  notifications: InferenceNotification[];
1063
1102
  }
1103
+ /**
1104
+ * 202 body of a book-before-reveal create: no id, no key — only the opaque
1105
+ * handle to poll (`inference.getBooking` / `inference.waitForBooking`).
1106
+ */
1107
+ export interface InferenceBookingAccepted {
1108
+ booking: {
1109
+ handle: string;
1110
+ status: 'booking';
1111
+ estimated_seconds: number;
1112
+ poll_url: string;
1113
+ };
1114
+ }
1064
1115
  export interface InferenceCreateResponse {
1065
1116
  id: string;
1066
1117
  deployment_id: string;
@@ -1176,6 +1227,12 @@ export interface InferenceGPUOption {
1176
1227
  min_gpus: number;
1177
1228
  recommended_gpus: number;
1178
1229
  valid_counts?: number[];
1230
+ /**
1231
+ * valid_counts filtered to counts live single-vendor stock can fill RIGHT
1232
+ * NOW (book-first contract). Absent when availability is unknown — never
1233
+ * fabricated. Offer/submit only these when present.
1234
+ */
1235
+ bookable_counts?: number[];
1179
1236
  selectable: boolean;
1180
1237
  reason?: string;
1181
1238
  market?: InferenceGPUOptionMarket;
@@ -1194,7 +1251,21 @@ export interface InferenceGPUAlternative {
1194
1251
  availability_status: 'available' | 'out_of_stock' | 'unknown';
1195
1252
  }
1196
1253
  export interface InferenceGPUOptionsParams {
1197
- paramsB: number;
1254
+ /**
1255
+ * MODEL-ADDRESSED sizing (recommended): the SERVER resolves the model facts
1256
+ * from this HF repo id (optionally with `revision`) and computes
1257
+ * min_gpus/valid_counts/bookable_counts — the same single implementation the
1258
+ * create gate enforces. When set, the deprecated client-fact params below
1259
+ * are unnecessary.
1260
+ */
1261
+ model?: string;
1262
+ revision?: string;
1263
+ /** Size from an existing deployment's pinned facts instead of a model id. */
1264
+ inferenceId?: string;
1265
+ /** HF integration for gated models when using `model`. */
1266
+ hfIntegrationId?: string;
1267
+ /** DEPRECATED client-fact path; required only when `model`/`inferenceId` are absent. */
1268
+ paramsB?: number;
1198
1269
  activeParamsB?: number;
1199
1270
  isMoe?: boolean;
1200
1271
  quant?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bios-sdk",
3
- "version": "0.1.1-dev.13",
3
+ "version": "0.1.1-dev.17",
4
4
  "description": "Official TypeScript SDK for the BIOS training and deployment platform API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "build": "tsc",
23
- "test": "npm run build && node --test test/contracts.test.mjs",
23
+ "test": "npm run build && node --test test/contracts.test.mjs test/capacity-contract.test.mjs",
24
24
  "clean": "rm -rf dist",
25
25
  "prepublishOnly": "npm run clean && npm run build"
26
26
  },