runbios-sdk 0.4.0-rc.337 → 0.5.0-dev.338

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 CHANGED
@@ -5,6 +5,43 @@ surface is added, renamed or removed, the patch for a fix inside an unchanged
5
5
  surface. The SDK, the Python SDK (`runbios-sdk` on PyPI) and the MCP server
6
6
  (`runbios-mcp`) are released together, at one version.
7
7
 
8
+ ## 0.5.0
9
+
10
+ Added
11
+
12
+ - Conscious Loop, `loop.chat({ pipeline, messages, model? })`: one call asks a
13
+ model with the pipeline ID in the `X-Loop-Pipeline` header (it is
14
+ `inference.chatCompletions` with the `loop` option, a normal billed call)
15
+ and returns `{ answer, sample_id, raw, good(), bad(), correct(text) }`.
16
+ `good`, `bad` and `correct` send the feedback words `good`, `bad` and
17
+ `corrected` on the sample ID, and wait and send again while the sample is
18
+ still being saved (`409 SAMPLE_NOT_YET_RECORDED`), as `signal` does; on an
19
+ answer with no sample ID they throw before any request. `model` defaults to
20
+ the pipeline's own model, `loop-<pipeline ID>`; while the pipeline has no
21
+ live version nothing answers to that name, and `chat` then rejects with
22
+ `LoopNoVersionError`, an `ApiError` (the API's 404: its `status`, `code`,
23
+ `requestId` and `body`, with a plain message; `pipeline`, `model`, and the
24
+ original error as `cause`). The pipeline ID is lowercased before it names
25
+ the header and the model, as the platform reads it, and anything that is
26
+ not a pipeline ID (lowercase letters, digits, dot, dash or underscore, at
27
+ most 63) is refused before any request.
28
+ Exported: `LoopChatParams`, `LoopChatResult`, `LoopNoVersionError`.
29
+ - README: a Conscious Loop section, "What it is" and the three steps.
30
+
31
+ Changed
32
+
33
+ - `client.loop` is given the client's `Inference` resource, so `loop.chat`
34
+ uses the same key resolution and base URL as `client.inference`.
35
+ - The loop's module comments say what is saved: a call carrying the
36
+ pipeline ID header is saved whatever the recording switches say (the header
37
+ is the consent for that one call); they no longer say nothing is captured
38
+ until a source is switched on, and no longer offer DPO or GRPO. They no
39
+ longer name an environment: where the platform has the loop switched off,
40
+ every method answers `LOOP_COMING_SOON`.
41
+ - README: the one-header section says pipeline ID and sample ID, not tag and
42
+ trace id; going back to the version before is possible for 30 days (it
43
+ said always).
44
+
8
45
  ## 0.4.0
9
46
 
10
47
  Added
package/README.md CHANGED
@@ -219,27 +219,32 @@ POSTs are retried automatically, and `idempotencyKey` is sent only if supplied.
219
219
 
220
220
  #### Record conversations into a Conscious Loop pipeline (one header)
221
221
 
222
- Add `loop: { pipeline: '<tag>' }` to `chatCompletions`, `streamChatCompletions`,
223
- `messages` or `streamMessages` and the call is sent exactly as before with one
224
- more header, `X-Loop-Pipeline: <tag>` (several tags: `pipeline: ['a', 'b']`).
225
- The gateway records the exchange -- what was asked, what the model answered and
226
- its tool calls -- as a sample of the pipelines those tags feed (a pipeline's
227
- `own_tag`), even where recording is off for that model: the header is your
228
- consent for that one exchange. With `chatCompletions` and `streamChatCompletions`
229
- it works for serverless models and dedicated deployments, including the
230
- pipeline's own `loop-<tag>` model. `messages` and `streamMessages` (the
231
- Anthropic shape) work for serverless models; a dedicated deployment, and so a
232
- pipeline's `loop-<tag>` model, serves the OpenAI shape and refuses
233
- `/v1/messages` with 400, so call it with `chatCompletions`. Only the tags of a
234
- pipeline that exists are recorded: a call naming a misspelt tag, or a deleted
235
- pipeline's, is not recorded under it, and `client.loop.stats()` lists such
236
- calls under `capture`.
222
+ Add `loop: { pipeline: '<pipeline ID>' }` to `chatCompletions`,
223
+ `streamChatCompletions`, `messages` or `streamMessages` and the call is sent
224
+ exactly as before with one more header, `X-Loop-Pipeline: <pipeline ID>`
225
+ (several pipelines: `pipeline: ['a', 'b']`). The gateway records the exchange
226
+ -- what was asked, what the model answered and its tool calls -- as a sample of
227
+ the pipelines those pipeline IDs name (a pipeline's `own_tag`, read in
228
+ lowercase), even where recording is off for that model: the header is your
229
+ consent for that one exchange. With `chatCompletions` and
230
+ `streamChatCompletions` it works for serverless models and dedicated
231
+ deployments, including the pipeline's own `loop-<pipeline ID>` model.
232
+ `messages` and `streamMessages` (the Anthropic shape) work for serverless
233
+ models; a dedicated deployment, and so a pipeline's `loop-<pipeline ID>` model,
234
+ serves the OpenAI shape and refuses `/v1/messages` with 400, so call it with
235
+ `chatCompletions`. Only the pipeline IDs of pipelines that exist are recorded:
236
+ a call naming a misspelt ID, or a deleted pipeline's, is not recorded under it.
237
+ The reply still carries a sample ID: if recording is switched on for that
238
+ model, the call is still recorded the way that model records every call and
239
+ feedback on it is accepted; when nothing records it, feedback on it is
240
+ refused. `client.loop.stats()` lists such calls under `capture`.
237
241
 
238
242
  The gateway answers with the sample's id in `X-Loop-Sample-Id`. A non-streaming
239
243
  result carries it as `loop_sample_id` (non-enumerable: it is never part of the
240
244
  answer's JSON), and `loop.onSampleId` hears it as soon as the headers arrive,
241
245
  before the first streamed chunk. It is `null` when the gateway recorded nothing.
242
- Send feedback on that id; it is the trace id the feedback calls take.
246
+ Send feedback on that sample ID: `client.loop.upvote`, `downvote` and `correct`
247
+ take it.
243
248
 
244
249
  ```typescript
245
250
  const reply = await client.inference.chatCompletions({
@@ -319,6 +324,109 @@ includes failed admitted requests. The saved workspace RPM cap is a subdivision
319
324
  of the org's tier allowance, not a claim about effective remaining RPM.
320
325
  An incomplete 200 response is an error, never "zero usage" or "unlimited".
321
326
 
327
+ ### Conscious Loop
328
+
329
+ #### What it is
330
+
331
+ The Conscious Loop keeps improving a model from your app's conversations and
332
+ your feedback. You make a pipeline for one use case (in the console, or with
333
+ `client.loop.createPipeline`). Your app's conversations are saved to it as
334
+ samples, you say how each answer did, and when enough samples have feedback
335
+ a training starts: your model learns from them. Every training is tested before
336
+ it can go live: on questions from your own samples that it never trained on, and on
337
+ your benchmarks if you add any. A training that does better, or that you
338
+ approve, becomes the next version: v1, v2, v3 and on, with no limit. The
339
+ **live version** is the version your app gets answers from right now. It
340
+ answers to the model name `loop-<pipeline ID>`, and each new version takes
341
+ that name over. For 30 days after a new version goes live you can go back to
342
+ the one before.
343
+
344
+ Two names to know:
345
+
346
+ - **Pipeline ID**, e.g. `support-replies`: made from the pipeline's name when
347
+ you created it, and it never changes. The pipeline's Connect page shows it.
348
+ It is the `pipeline` you pass here, sent as the `X-Loop-Pipeline` header.
349
+ - **Sample ID**: comes back in the reply's headers (`X-Loop-Sample-Id`), and
350
+ feedback names it. `client.loop.chat` hands it to you as `sample_id` and
351
+ sends the feedback for you. It is missing only when the platform is not
352
+ recording calls just then; then nothing was saved. A sample ID does not
353
+ prove the pipeline ID is right: if no pipeline has that ID (misspelt, or
354
+ the pipeline was deleted), the conversation is not saved under it. If
355
+ recording is switched on for the model that answered, the call is still
356
+ recorded the way that model records every call, and feedback on it is then
357
+ accepted; when nothing records it, feedback on it is refused (first "not
358
+ recorded yet", then "not found"). `client.loop.stats()` counts such calls
359
+ under `capture` (reason `no_pipeline`), and the pipeline's Connect page
360
+ shows its pipeline ID.
361
+
362
+ The API key needs `serverless` to call a serverless model (`deployments:read`
363
+ for a deployment and for the pipeline's own `loop-` model) and `loop:write` to
364
+ send feedback.
365
+
366
+ #### The three steps
367
+
368
+ **1. Send your app's conversations.** `client.loop.chat` is a normal, billed
369
+ model call with the pipeline ID added, and the conversation is saved as a
370
+ sample of the pipeline:
371
+
372
+ ```typescript
373
+ const reply = await client.loop.chat({
374
+ pipeline: 'support-replies',
375
+ messages: [{ role: 'user', content: 'Where is my order?' }],
376
+ model: 'meta-llama/Llama-3.1-8B-Instruct', // before v1: the model your app uses now
377
+ });
378
+ console.log(reply.answer, reply.sample_id);
379
+ ```
380
+
381
+ Before the pipeline's first version, pass `model`. Left out, `chat` asks
382
+ `loop-support-replies`, and while no version is live nothing answers to that
383
+ name: it rejects with `LoopNoVersionError` (an `ApiError`) and says so in plain
384
+ words. The pipeline ID is sent in lowercase, as the platform reads it. Any
385
+ other chat parameter (`temperature`, `max_tokens`, `tools`, ...) is passed on.
386
+ Already calling `client.inference.chatCompletions`? Add
387
+ `loop: { pipeline: 'support-replies' }` instead (see the one-header section
388
+ above; it also works when streaming).
389
+
390
+ **2. Tell it how each answer did.**
391
+
392
+ ```typescript
393
+ await reply.good(); // Good
394
+ // await reply.bad(); // Wrong
395
+ // await reply.correct('It ships on Monday.'); // Wrong + the right answer
396
+ ```
397
+
398
+ Feedback sent the instant an answer arrives can reach the platform before the
399
+ conversation is saved (`409 SAMPLE_NOT_YET_RECORDED`): `good`, `bad` and
400
+ `correct` wait 2 seconds and send it again, up to 6 tries. With a sample ID
401
+ from elsewhere, `client.loop.upvote(id)`, `downvote(id)` and
402
+ `correct(id, text)` do the same.
403
+
404
+ What your feedback does:
405
+
406
+ | Feedback | What happens |
407
+ |---|---|
408
+ | Good | The answer is learned as it is. |
409
+ | Wrong | The answer is not learned. |
410
+ | Wrong + right answer | The right answer is learned instead. |
411
+ | No feedback | If automatic scoring is on, the judge scores it. The judge is an AI that compares each answer with the right answer; where nobody gave one, as here, it scores the answer on its instructions alone. 0.8 or more is learned like Good. |
412
+
413
+ Training that learns from rankings or rewards (like DPO or GRPO) is not
414
+ available yet; every version learns from your good and corrected answers.
415
+ Samples and feedback are kept 30 days by default. Right answers are cleaned of
416
+ emails, long numbers and keys before they are saved.
417
+
418
+ **3. Use the improved model.** Leave `model` out once a version is live:
419
+
420
+ ```typescript
421
+ const next = await client.loop.chat({
422
+ pipeline: 'support-replies',
423
+ messages: [{ role: 'user', content: 'Can I return a gift?' }],
424
+ }); // asks loop-support-replies, the live version
425
+ ```
426
+
427
+ Any OpenAI-compatible client can call `loop-support-replies` too. It always
428
+ answers with the live version, and each new version takes over the name.
429
+
322
430
  ### Models
323
431
 
324
432
  The catalog lists only models hosted on Run BiOS (the platform's own verified
package/dist/client.d.ts CHANGED
@@ -160,6 +160,26 @@ export declare class ComingSoonError extends ApiError {
160
160
  }
161
161
  /** The code an import answers with when some rows were understood and then could not be stored. */
162
162
  export declare const IMPORT_INCOMPLETE_CODE = "IMPORT_INCOMPLETE";
163
+ /**
164
+ * `client.loop.chat` was asked to use the pipeline's own model
165
+ * (`loop-<pipeline ID>`) and nothing answers to that name: the pipeline has no
166
+ * live version right now -- before its first version, or after its only live
167
+ * version was taken back -- or no pipeline has that ID. Pass `model`, the
168
+ * model your app uses now, until a version is live.
169
+ *
170
+ * It is the API's own 404 with a plain message: an `ApiError` (`status`,
171
+ * `code`, `requestId` and `body` are the refusal's), so `instanceof ApiError`
172
+ * still catches it. `cause` is the original error.
173
+ */
174
+ export declare class LoopNoVersionError extends ApiError {
175
+ /** The pipeline ID that was asked. */
176
+ readonly pipeline: string;
177
+ /** The model name that answered nothing. */
178
+ readonly model: string;
179
+ /** The API's own refusal (a 404). */
180
+ readonly cause: ApiError;
181
+ constructor(pipeline: string, model: string, cause: ApiError);
182
+ }
163
183
  /**
164
184
  * An import that did not finish: some rows could not be stored, or some
165
185
  * verdicts that arrived with them could not be written.
@@ -261,6 +281,22 @@ export declare class HttpClient {
261
281
  * Throws {@link ApiError} on non-2xx responses.
262
282
  */
263
283
  request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
284
+ /**
285
+ * Send a GET and answer the body as text, with the response's headers: for
286
+ * the routes that answer a file rather than JSON (a dataset's export).
287
+ * Throws {@link ApiError} on non-2xx responses, read from the JSON error
288
+ * envelope as every other call is.
289
+ *
290
+ * THE TIMEOUT IS A WAIT FOR THE NEXT PART, NOT FOR THE WHOLE FILE. A file
291
+ * is streamed: a large dataset takes minutes to arrive, steadily. The
292
+ * client's `timeout` bounds the wait for the answer to start and then for
293
+ * each next part of it, so a long file that keeps coming is read whole,
294
+ * and one that stops coming is still given up on.
295
+ */
296
+ fetchText(path: string): Promise<{
297
+ text: string;
298
+ headers: Headers;
299
+ }>;
264
300
  /** Send a GET request. */
265
301
  fetchGet<T>(path: string, extraHeaders?: Record<string, string>): Promise<T>;
266
302
  /** Send a POST request with a JSON body. */
package/dist/client.js CHANGED
@@ -231,6 +231,38 @@ export class ComingSoonError extends ApiError {
231
231
  }
232
232
  /** The code an import answers with when some rows were understood and then could not be stored. */
233
233
  export const IMPORT_INCOMPLETE_CODE = 'IMPORT_INCOMPLETE';
234
+ /**
235
+ * `client.loop.chat` was asked to use the pipeline's own model
236
+ * (`loop-<pipeline ID>`) and nothing answers to that name: the pipeline has no
237
+ * live version right now -- before its first version, or after its only live
238
+ * version was taken back -- or no pipeline has that ID. Pass `model`, the
239
+ * model your app uses now, until a version is live.
240
+ *
241
+ * It is the API's own 404 with a plain message: an `ApiError` (`status`,
242
+ * `code`, `requestId` and `body` are the refusal's), so `instanceof ApiError`
243
+ * still catches it. `cause` is the original error.
244
+ */
245
+ export class LoopNoVersionError extends ApiError {
246
+ /** The pipeline ID that was asked. */
247
+ pipeline;
248
+ /** The model name that answered nothing. */
249
+ model;
250
+ /** The API's own refusal (a 404). */
251
+ cause;
252
+ constructor(pipeline, model, cause) {
253
+ super(cause.status, cause.body);
254
+ this.message =
255
+ `Pipeline "${pipeline}" has no live version right now, so nothing answers to "${model}". ` +
256
+ 'Until a version is live, pass model: the model your app uses now ' +
257
+ '(the conversations still go to the pipeline). ' +
258
+ 'If the pipeline has a live version, check the pipeline ID on its Connect page.';
259
+ this.name = 'LoopNoVersionError';
260
+ this.retryAfter = cause.retryAfter;
261
+ this.pipeline = pipeline;
262
+ this.model = model;
263
+ this.cause = cause;
264
+ }
265
+ }
234
266
  /**
235
267
  * An import that did not finish: some rows could not be stored, or some
236
268
  * verdicts that arrived with them could not be written.
@@ -364,6 +396,27 @@ export function envInferenceKey() {
364
396
  export function envBaseUrl() {
365
397
  return envVar('RUNBIOS_BASE_URL') ?? envVar('BIOS_BASE_URL');
366
398
  }
399
+ /**
400
+ * A response's body as text, read part by part, calling `progressed` before
401
+ * each read so the caller's timer measures the wait for the next part rather
402
+ * than for the whole body. A body that cannot be streamed is read whole.
403
+ */
404
+ async function readTextAsItArrives(res, progressed) {
405
+ if (!res.body)
406
+ return res.text();
407
+ const reader = res.body.getReader();
408
+ const decoder = new TextDecoder();
409
+ const parts = [];
410
+ for (;;) {
411
+ progressed();
412
+ const { done, value } = await reader.read();
413
+ if (done)
414
+ break;
415
+ parts.push(decoder.decode(value, { stream: true }));
416
+ }
417
+ parts.push(decoder.decode());
418
+ return parts.join('');
419
+ }
367
420
  // ============================================================================
368
421
  // HttpClient — shared HTTP transport used by resource modules via composition
369
422
  // ============================================================================
@@ -378,7 +431,7 @@ export class HttpClient {
378
431
  constructor(config) {
379
432
  // Default host stays api.runbios.ai for now; cutover to api.runbios.ai is
380
433
  // planned once its DNS exists.
381
- this.baseUrl = (config.baseUrl || envBaseUrl() || 'https://api-staging.runbios.ai').replace(/\/+$/, '');
434
+ this.baseUrl = (config.baseUrl || envBaseUrl() || 'https://api-dev.runbios.ai').replace(/\/+$/, '');
382
435
  this.apiKey = config.apiKey ?? envApiKey();
383
436
  this.accessToken = config.accessToken;
384
437
  this.orgId = config.orgId;
@@ -469,6 +522,58 @@ export class HttpClient {
469
522
  }
470
523
  return data;
471
524
  }
525
+ /**
526
+ * Send a GET and answer the body as text, with the response's headers: for
527
+ * the routes that answer a file rather than JSON (a dataset's export).
528
+ * Throws {@link ApiError} on non-2xx responses, read from the JSON error
529
+ * envelope as every other call is.
530
+ *
531
+ * THE TIMEOUT IS A WAIT FOR THE NEXT PART, NOT FOR THE WHOLE FILE. A file
532
+ * is streamed: a large dataset takes minutes to arrive, steadily. The
533
+ * client's `timeout` bounds the wait for the answer to start and then for
534
+ * each next part of it, so a long file that keeps coming is read whole,
535
+ * and one that stops coming is still given up on.
536
+ */
537
+ async fetchText(path) {
538
+ const url = `${this.baseUrl}${path}`;
539
+ const headers = this.buildHeaders({ 'Accept': 'application/x-ndjson, application/json' });
540
+ const controller = new AbortController();
541
+ let timeoutId = setTimeout(() => controller.abort(), this.timeout);
542
+ const progressed = () => {
543
+ clearTimeout(timeoutId);
544
+ timeoutId = setTimeout(() => controller.abort(), this.timeout);
545
+ };
546
+ let res;
547
+ let text;
548
+ let reading = false;
549
+ try {
550
+ res = await fetch(url, { method: 'GET', headers, signal: controller.signal });
551
+ reading = true;
552
+ text = await readTextAsItArrives(res, progressed);
553
+ }
554
+ catch (err) {
555
+ if (controller.signal.aborted || (err instanceof DOMException && err.name === 'AbortError')) {
556
+ throw new ApiError(0, {
557
+ error: reading
558
+ ? `Request timed out: nothing more arrived for ${this.timeout}ms`
559
+ : `Request timed out after ${this.timeout}ms`,
560
+ });
561
+ }
562
+ throw err;
563
+ }
564
+ finally {
565
+ clearTimeout(timeoutId);
566
+ }
567
+ if (!res.ok) {
568
+ let data = {};
569
+ try {
570
+ data = JSON.parse(text);
571
+ }
572
+ catch { /* a body that is not JSON carries no envelope */ }
573
+ throw errorFromResponse(res, data);
574
+ }
575
+ return { text, headers: res.headers };
576
+ }
472
577
  /** Send a GET request. */
473
578
  fetchGet(path, extraHeaders) {
474
579
  return this.request('GET', path, undefined, extraHeaders);
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ import { Loop } from './resources/loop.js';
36
36
  * SDK version. Sent as part of the User-Agent header.
37
37
  * Must match package.json "version" -- enforced by a contract test.
38
38
  */
39
- export declare const VERSION = "0.4.0-rc.337";
39
+ export declare const VERSION = "0.5.0-dev.338";
40
40
  export declare class RunBiOS {
41
41
  /** Search models, fetch configs, check adapter compatibility. */
42
42
  readonly models: Models;
@@ -55,9 +55,12 @@ export declare class RunBiOS {
55
55
  */
56
56
  readonly inference: Inference;
57
57
  /**
58
- * The Conscious Loop. Capture what your model was asked and answered, record
59
- * whether it was right, and turn those judgements into training data for
60
- * SFT, DPO or GRPO. Nothing is captured until you turn it on for a source.
58
+ * The Conscious Loop: pipelines that keep improving a model from your app's
59
+ * conversations and your feedback. `loop.chat` asks a model and saves the
60
+ * conversation as a sample; `good()`, `bad()` and `correct(text)` on its
61
+ * answer say how it did. A call is saved only when it carries the pipeline
62
+ * ID (`chat`, or the `loop` option of `inference`), or when recording is
63
+ * switched on for its source.
61
64
  */
62
65
  readonly loop: Loop;
63
66
  /** @internal */
@@ -74,8 +77,9 @@ export { Integrations, type HuggingFaceIntegration, type IntegrationBrowseParams
74
77
  export { Training } from './resources/training.js';
75
78
  export { Wallet } from './resources/wallet.js';
76
79
  export { GPU, type GPURecommendation } from './resources/gpu.js';
80
+ export { LoopNoVersionError, type LoopChatParams, type LoopChatResult, } from './resources/loop-chat.js';
77
81
  export { Inference, validateChatRequest, parseSSE, buildInferenceRequest, contextSizingBasis, type ContextSizingBasis, CONTEXT_DEFAULT_CEILING, CONTEXT_EDITABLE_FLOOR, type ChatMessage, type FunctionTool, type ChatCompletionParams, type ChatCompletionResponse, type ChatCompletionChunk, type InferenceVerbParams, type CompletionParams, type EmbeddingParams, type RerankParams, type AnthropicMessageParams, type AnthropicMessageResponse, type LoopCaptureOption, type LoopSampleIdOnResult, LOOP_PIPELINE_HEADER, LOOP_SAMPLE_ID_HEADER, type ServerlessUsageWindow, type ServerlessTimeseriesMetric, type ServerlessWorkspaceLimits, type ServerlessUsageEnvelope, type ServerlessUsageOverviewResponse, type ServerlessUsageRowsResponse, type ServerlessUsageTimeseriesResponse, type ServerlessUsageDailyResponse, type ServerlessSavingsTotals, type ServerlessSavingsPeriod, type ServerlessUsageSavingsResponse, } from './resources/inference.js';
78
- export type { BiOSConfig, PaginatedResponse, ApiErrorBody, AvailableGpuAlternative, CapacityMinimumRequirement, InferenceBookingAccepted, Model, ModelSearchParams, ModelSearchResponse, ModelDetailResponse, InferenceModel, InferenceModelListResponse, ModelConfig, Dataset, DatasetListParams, DatasetListResponse, DatasetUploadParams, DatasetPreview, DatasetPreviewParams, DatasetImportHFParams, DatasetRegisterHFParams, DatasetHubSearchParams, DatasetHubPreviewParams, DatasetValidation, DatasetFormatVariant, DatasetFormatSpec, DatasetFormatSpecs, DatasetStorageUsage, TrainingMethod, RLHFAlgorithm, AdapterType, TrainingJobStatus, TrainingStatusFilter, TrainingCreateParams, TrainingStartFromParams, TrainingStartFrom, TrainingExportStep, TrainingListParams, TrainingListResponse, TrainingJob, TrainingMetrics, TrainingResourceMetrics, TrainingDeviceMetrics, MetricPoint, MetricGraphConfig, TrainingCheckpoint, TrainingLogs, TrainingLogEntry, TrainingStopResponse, TrainingResumeResponse, CanonicalTrainingRequest, TrainingPreflightDataset, TrainingPreflightWarning, TrainingPreflightResponse, TrainingCapabilityChoice, TrainingConfigFieldCapability, TrainingCapabilities, TrainingFeature, GPUChoice, WalletBalance, Transaction, TransactionListResponse, TransactionListParams, GPUInfo, GPUPricingResponse, GPUOptionsParams, TrainingRecommendParams, TrainingAdvisorVerdict, TrainingAdvisorRecommendation, TrainingAdvisorUserShape, TrainingAdvisorJustification, TrainingAdvisorBasis, GPUOption, GPUOptionSuggestion, GPUOptionsResponse, InferenceStatus, InferenceCreateParams, InferenceUpdateParams, InferenceDeployment, InferenceDeploymentSummary, 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, PromptTokensDetails, ChatCompletionUsage, ServingKind, TrainingRulePausedReason, TrainingTrigger, TrainingRecipe, TrainingRunState, TrainingVerdict, TrainingDecision, EvaluationStatus, EvaluationItemStatus, ItemErrorCode, EvaluationWinner, EvaluationItemWinnerFilter, ReportStatusFilter, ReportSort, EvaluationWarningCode, TrainingToolCall, TrainingMessage, TrainingRuleServing, TrainingRunPromoteRequest, TrainingRunRejectRequest, TrainingRunRollbackRequest, TrainingRunCancelRequest, TrainingRunListParams, TrainingRun, TrainingRunSummary, TrainingRunEvent, TrainingRunLinks, TrainingRunActions, EvaluationRef, EvaluationDecoding, EvaluationJudgeDimension, EvaluationDimensionScore, EvaluationWarning, EvaluationMargin, Evaluation, EvaluationItemJudge, EvaluationReferenceKind, EvaluationReference, EvaluationPairwiseOrder, EvaluationItemPairwise, EvaluationItemSide, EvaluationItem, EvaluationItemListParams, BenchmarkStatus, BenchmarkSourceKind, BenchmarkRunStatus, BenchmarkScoring, BenchmarkCheck, BenchmarkDimensionScore, Benchmark, BenchmarkItem, BenchmarkRun, BenchmarkRunItemSide, BenchmarkRunItem, BenchmarkRunItemListParams, BenchmarkHistoryPoint, BenchmarkSource, BenchmarkCreateParams, BenchmarkListParams, BenchmarkItemListParams, BenchmarkHistoryParams, BenchmarkRetireParams, InferenceAlias, InferenceAliasRequest, TrainingRuleDeleteResponse, TrainingRunListResponse, TrainingRunBenchmarkRun, TrainingRunResponse, TrainingRunActionResponse, EvaluationItemsResponse, BenchmarkRunItemsResponse, InferenceAliasListResponse, InferenceAliasResponse, InferenceAliasDeleteResponse, BenchmarkListResponse, BenchmarkItemsResponse, BenchmarkHistoryResponse, TrainingRuleRunRefusalCode, TrainingRuleSaveRefusalCode, TrainingRuleMonthlyLimitRefusal, PipelineStatus, PipelineActiveRun, PipelineVersion, PipelineAttempt, AttemptExplanation, PipelineAttemptListParams, PipelineAttemptListResponse, PipelineAttemptModelFilter, PipelineAttemptResultFilter, PipelineAttemptTrigger, Lineage, PipelineChain, PipelineChainRebuild, TrainingRunRollbackRefusalCode, LoopTraceRebuild, LoopTraceDeleteResponse, PipelineAttemptOutcome, PipelineLiveDeploymentStatus, PipelineLiveDeployment, LoopStorageWhere, LoopStorage, Pipeline, PipelineListParams, PipelineListStatus, PipelineListResponse, PipelineTrainingSettings, PipelineTrainingDefaults, PipelineTrainingRequest, PipelineResponse, PipelineTrainWhen, PipelineSchedule, PipelineWriteRequest, PipelineMutationResponse, PipelineImportParams, PipelineFloors, HeldBackFloors, LoopModel, LoopModelsResponse, LoopUnavailableModel, LoopUnavailableReasonCode, LoopTracePipeline, LoopTracePipelinesResponse, LoopMetricsHistoryParams, LoopMetricPoint, LoopSignalDay, LoopMetricsHistory, LoopCaptureFeedback, LoopFeedbackWord, LoopCaptureHealth, LoopCaptureMiss, LoopSignalManyItem, LoopSignalManyResult, PromotionPolicyState, RuleBenchmark, PipelineBenchmarksResponse, PipelineBenchmarkAttachParams, PipelineBenchmarkMoveParams, CompareBenchmark, VersionBenchmarkScore, PromotionMeasurement, PromotionOutcome, VersionHoldout, VersionScores, VersionScoreDifference, VersionCompareResponse, VersionCompareParams, TrainingRunPromotion, VerdictCheck, VerdictCheckKey, VerdictChecksWithheld, TrainingRunCost, AttemptDirection, AttemptHoldout, AttemptCompareSide, AttemptSharedSample, AttemptSharedFigures, AttemptBenchmarkSide, AttemptCompareBenchmark, AttemptCompareItemSide, AttemptCompareItem, AttemptCompareResponse, AttemptCompareParams, LoopTraceJudge, LoopJudgeFilter, LoopSampleState, LoopSampleStateName, LoopSampleDay, LoopImportRefusal, PipelineAutoScoring, PipelineAutoScoringStatus, LoopDatasetFeedback, LoopSampleOrigin, LoopFeedbackCounts, LoopPipelineCounts, LoopDatasetPipelineRef, LoopDataset, LoopDatasetWrite, LoopDatasetCreate, LoopDatasetPreviewParams, LoopDatasetPreviewModel, LoopDatasetPreview, LoopDatasetListParams, LoopDatasetListResponse, LoopDatasetDeleteResponse, PipelineDatasetRef, PipelineConditions, PipelineConditionsWrite, TrainedOnDataset, TrainedOn, } from './types.js';
82
+ export type { BiOSConfig, PaginatedResponse, ApiErrorBody, AvailableGpuAlternative, CapacityMinimumRequirement, InferenceBookingAccepted, Model, ModelSearchParams, ModelSearchResponse, ModelDetailResponse, InferenceModel, InferenceModelListResponse, ModelConfig, Dataset, DatasetListParams, DatasetListResponse, DatasetUploadParams, DatasetPreview, DatasetPreviewParams, DatasetImportHFParams, DatasetRegisterHFParams, DatasetHubSearchParams, DatasetHubPreviewParams, DatasetValidation, DatasetFormatVariant, DatasetFormatSpec, DatasetFormatSpecs, DatasetStorageUsage, TrainingMethod, RLHFAlgorithm, AdapterType, TrainingJobStatus, TrainingStatusFilter, TrainingCreateParams, TrainingStartFromParams, TrainingStartFrom, TrainingExportStep, TrainingListParams, TrainingListResponse, TrainingJob, TrainingMetrics, TrainingResourceMetrics, TrainingDeviceMetrics, MetricPoint, MetricGraphConfig, TrainingCheckpoint, TrainingLogs, TrainingLogEntry, TrainingStopResponse, TrainingResumeResponse, CanonicalTrainingRequest, TrainingPreflightDataset, TrainingPreflightWarning, TrainingPreflightResponse, TrainingCapabilityChoice, TrainingConfigFieldCapability, TrainingCapabilities, TrainingFeature, GPUChoice, WalletBalance, Transaction, TransactionListResponse, TransactionListParams, GPUInfo, GPUPricingResponse, GPUOptionsParams, TrainingRecommendParams, TrainingAdvisorVerdict, TrainingAdvisorRecommendation, TrainingAdvisorUserShape, TrainingAdvisorJustification, TrainingAdvisorBasis, GPUOption, GPUOptionSuggestion, GPUOptionsResponse, InferenceStatus, InferenceCreateParams, InferenceUpdateParams, InferenceDeployment, InferenceDeploymentSummary, 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, PromptTokensDetails, ChatCompletionUsage, ServingKind, TrainingRulePausedReason, TrainingTrigger, TrainingRecipe, TrainingRunState, TrainingVerdict, TrainingDecision, EvaluationStatus, EvaluationItemStatus, ItemErrorCode, EvaluationWinner, EvaluationItemWinnerFilter, ReportStatusFilter, ReportSort, EvaluationWarningCode, TrainingToolCall, TrainingMessage, TrainingRuleServing, TrainingRunPromoteRequest, TrainingRunRejectRequest, TrainingRunRollbackRequest, TrainingRunCancelRequest, TrainingRunListParams, TrainingRun, TrainingRunSummary, TrainingRunEvent, TrainingRunLinks, TrainingRunActions, EvaluationRef, EvaluationDecoding, EvaluationJudgeDimension, EvaluationDimensionScore, EvaluationWarning, EvaluationMargin, Evaluation, EvaluationItemJudge, EvaluationReferenceKind, EvaluationReference, EvaluationPairwiseOrder, EvaluationItemPairwise, EvaluationItemSide, EvaluationItem, EvaluationItemListParams, BenchmarkStatus, BenchmarkSourceKind, BenchmarkRunStatus, BenchmarkScoring, BenchmarkCheck, BenchmarkDimensionScore, Benchmark, BenchmarkItem, BenchmarkRun, BenchmarkRunItemSide, BenchmarkRunItem, BenchmarkRunItemListParams, BenchmarkHistoryPoint, BenchmarkSource, BenchmarkCreateParams, BenchmarkListParams, BenchmarkItemListParams, BenchmarkHistoryParams, BenchmarkRetireParams, InferenceAlias, InferenceAliasRequest, TrainingRuleDeleteResponse, TrainingRunListResponse, TrainingRunBenchmarkRun, TrainingRunResponse, TrainingRunActionResponse, EvaluationItemsResponse, BenchmarkRunItemsResponse, InferenceAliasListResponse, InferenceAliasResponse, InferenceAliasDeleteResponse, BenchmarkListResponse, BenchmarkItemsResponse, BenchmarkHistoryResponse, BenchmarkQuestionsParams, BenchmarkQuestionsTraining, BenchmarkQuestionResult, BenchmarkQuestion, BenchmarkQuestionsResponse, TrainingCurvePoint, TrainingCurve, TrainingRuleRunRefusalCode, TrainingRuleSaveRefusalCode, TrainingRuleMonthlyLimitRefusal, PipelineStatus, PipelineActiveRun, PipelineVersion, PipelineAttempt, AttemptExplanation, AttemptMissedBar, PipelineModelRole, PipelineComparedWith, PipelineTotals, PipelineTrainingsByResult, PipelineTrainingsByModel, PipelineSpentParts, PipelineAttemptListParams, PipelineAttemptListResponse, PipelineAttemptModelFilter, PipelineAttemptResultFilter, PipelineAttemptTrigger, PipelineSecondModel, PipelineSecondModelState, Lineage, PipelineChain, PipelineChainRebuild, TrainingRunRollbackRefusalCode, TrainingRunRetestRefusalCode, LoopTraceRebuild, LoopTraceDeleteResponse, PipelineAttemptOutcome, PipelineLiveDeploymentStatus, PipelineLiveDeployment, LoopStorageWhere, LoopStorage, Pipeline, PipelineListParams, PipelineListStatus, PipelineListResponse, PipelineTrainingSettings, PipelineTrainingDefaults, PipelineTrainingRequest, PipelineResponse, PipelineTrainWhen, PipelineSchedule, PipelineWriteRequest, PipelineMutationResponse, PipelineImportParams, PipelineFloors, HeldBackFloors, LoopModel, LoopModelsResponse, LoopUnavailableModel, LoopUnavailableReasonCode, LoopTracePipeline, LoopTracePipelinesResponse, LoopMetricsHistoryParams, LoopMetricPoint, LoopSignalDay, LoopMetricsHistory, LoopCaptureFeedback, LoopFeedbackWord, LoopCaptureHealth, LoopCaptureMiss, LoopSignalManyItem, LoopSignalManyResult, PromotionPolicyState, RuleBenchmark, PipelineBenchmarksResponse, PipelineBenchmarkAttachParams, PipelineBenchmarkMoveParams, CompareBenchmark, VersionBenchmarkScore, PromotionMeasurement, PromotionOutcome, VersionHoldout, VersionCommonSet, VersionScores, VersionScoreDifference, VersionCompareResponse, VersionCompareParams, TrainingRunPromotion, VerdictCheck, VerdictCheckKey, VerdictChecksWithheld, TrainingRunCost, AttemptDirection, AttemptHoldout, AttemptCompareSide, AttemptSharedSample, AttemptSharedFigures, AttemptBenchmarkSide, AttemptCompareBenchmark, AttemptCompareItemSide, AttemptCompareItem, AttemptCompareResponse, AttemptCompareParams, LoopTraceJudge, LoopJudgeFilter, LoopSampleState, LoopSampleStateName, LoopSampleDay, LoopImportRefusal, PipelineAutoScoring, PipelineAutoScoringStatus, LoopDatasetFeedback, LoopSampleOrigin, LoopFeedbackCounts, LoopPipelineCounts, LoopDatasetPipelineRef, LoopDataset, LoopDatasetWrite, LoopDatasetCreate, LoopDatasetPreviewParams, LoopDatasetPreviewModel, LoopDatasetPreview, LoopDatasetListParams, LoopDatasetListResponse, LoopDatasetCreator, LoopDatasetLearnedBy, LoopDatasetExportParams, LoopDatasetExportLine, LoopDatasetExport, LoopDatasetDeleteResponse, PipelineDatasetRef, PipelineConditions, PipelineConditionsWrite, TrainedOnDataset, TrainedOn, } from './types.js';
79
83
  /** The closed set of run states a training run never leaves. */
80
84
  export { TERMINAL_RUN_STATES } from './types.js';
81
85
  /** The most versions a pipeline may be set to make. */
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ import { Loop } from './resources/loop.js';
36
36
  * SDK version. Sent as part of the User-Agent header.
37
37
  * Must match package.json "version" -- enforced by a contract test.
38
38
  */
39
- export const VERSION = '0.4.0-rc.337';
39
+ export const VERSION = '0.5.0-dev.338';
40
40
  export class RunBiOS {
41
41
  /** Search models, fetch configs, check adapter compatibility. */
42
42
  models;
@@ -55,9 +55,12 @@ export class RunBiOS {
55
55
  */
56
56
  inference;
57
57
  /**
58
- * The Conscious Loop. Capture what your model was asked and answered, record
59
- * whether it was right, and turn those judgements into training data for
60
- * SFT, DPO or GRPO. Nothing is captured until you turn it on for a source.
58
+ * The Conscious Loop: pipelines that keep improving a model from your app's
59
+ * conversations and your feedback. `loop.chat` asks a model and saves the
60
+ * conversation as a sample; `good()`, `bad()` and `correct(text)` on its
61
+ * answer say how it did. A call is saved only when it carries the pipeline
62
+ * ID (`chat`, or the `loop` option of `inference`), or when recording is
63
+ * switched on for its source.
61
64
  */
62
65
  loop;
63
66
  /** @internal */
@@ -71,7 +74,6 @@ export class RunBiOS {
71
74
  this.training = new Training(http);
72
75
  this.wallet = new Wallet(http);
73
76
  this.gpu = new GPU(http);
74
- this.loop = new Loop(http);
75
77
  // Inference-key resolution, most specific first:
76
78
  // 1. an explicit inferenceKey (a per-deployment sk-bios-... key)
77
79
  // 2. RUNBIOS_INFERENCE_KEY from the environment (legacy: BIOS_INFERENCE_KEY)
@@ -85,6 +87,9 @@ export class RunBiOS {
85
87
  baseUrl: config.inferenceBaseUrl || config.baseUrl,
86
88
  timeout: config.inferenceTimeout,
87
89
  }, http);
90
+ // Loop.chat asks a model through the same Inference resource, so it uses
91
+ // the same key resolution and base URL as client.inference.
92
+ this.loop = new Loop(http, this.inference);
88
93
  }
89
94
  async introspect() {
90
95
  return this._http.fetchGet('/api/api-keys/introspect');
@@ -102,6 +107,7 @@ export { Integrations } from './resources/integrations.js';
102
107
  export { Training } from './resources/training.js';
103
108
  export { Wallet } from './resources/wallet.js';
104
109
  export { GPU } from './resources/gpu.js';
110
+ export { LoopNoVersionError, } from './resources/loop-chat.js';
105
111
  export { Inference, validateChatRequest, parseSSE, buildInferenceRequest, contextSizingBasis, CONTEXT_DEFAULT_CEILING, CONTEXT_EDITABLE_FLOOR, LOOP_PIPELINE_HEADER, LOOP_SAMPLE_ID_HEADER, } from './resources/inference.js';
106
112
  /** The closed set of run states a training run never leaves. */
107
113
  export { TERMINAL_RUN_STATES } from './types.js';
@@ -373,7 +373,7 @@ export class Inference {
373
373
  this.key = config.inferenceKey || envInferenceKey() || envApiKey();
374
374
  // Same default host as HttpClient (api.runbios.ai); api.runbios.ai cutover
375
375
  // is planned once its DNS exists — update both call sites together.
376
- this.baseUrl = (config.baseUrl || envBaseUrl() || 'https://api-staging.runbios.ai').replace(/\/+$/, '');
376
+ this.baseUrl = (config.baseUrl || envBaseUrl() || 'https://api-dev.runbios.ai').replace(/\/+$/, '');
377
377
  this.timeout = config.timeout ?? 900_000;
378
378
  this._http = http;
379
379
  }
@@ -0,0 +1,87 @@
1
+ import type { LoopSignal } from '../types.js';
2
+ import type { ChatMessage, ChatCompletionParams, ChatCompletionResponse } from './inference.js';
3
+ /** The model name a pipeline's live version answers to: `loop-` and its pipeline ID. */
4
+ export declare const PIPELINE_MODEL_PREFIX = "loop-";
5
+ /**
6
+ * A pipeline ID as the platform reads it. The gateways trim and lowercase the
7
+ * `X-Loop-Pipeline` header and keep only this shape, and the pipeline's own
8
+ * model name is `loop-` and the ID exactly, matched case for case. So `chat`
9
+ * lowercases the ID before it names the header and the model, and refuses
10
+ * anything else before any request: sent, it would be dropped and nothing saved.
11
+ */
12
+ export declare const PIPELINE_ID_PATTERN: RegExp;
13
+ /**
14
+ * What `client.loop.chat` takes: the pipeline ID, the conversation, and
15
+ * optionally the model to ask. Anything else (`temperature`, `max_tokens`,
16
+ * `tools`, `requestId`, ...) is passed to `client.inference.chatCompletions`
17
+ * as it is.
18
+ */
19
+ export interface LoopChatParams extends Record<string, unknown> {
20
+ /**
21
+ * The pipeline ID (its `own_tag`, e.g. `support-replies`): made from the
22
+ * pipeline's name when it was created, and it never changes. The Connect
23
+ * page shows it. Sent as the `X-Loop-Pipeline` header, lowercased as the
24
+ * platform reads it.
25
+ */
26
+ pipeline: string;
27
+ /** The conversation to send, as for `chatCompletions`. */
28
+ messages: ChatMessage[];
29
+ /**
30
+ * The model to ask. Leave it out to ask the pipeline's own model,
31
+ * `loop-<pipeline ID>`, which answers with the live version. Before the
32
+ * pipeline's first version nothing answers to that name: pass the model your
33
+ * app uses now (a serverless model id or a deployment name).
34
+ */
35
+ model?: string;
36
+ tools?: ChatCompletionParams['tools'];
37
+ toolChoice?: ChatCompletionParams['toolChoice'];
38
+ reasoningEffort?: ChatCompletionParams['reasoningEffort'];
39
+ inferenceKey?: string;
40
+ idempotencyKey?: string;
41
+ requestId?: string;
42
+ signal?: AbortSignal;
43
+ }
44
+ /**
45
+ * The answer `client.loop.chat` returns, with the feedback for it one call
46
+ * away. `good()`, `bad()` and `correct(text)` send feedback on `sample_id`,
47
+ * and wait and send again while the sample is still being saved (409
48
+ * `SAMPLE_NOT_YET_RECORDED`), as `signal` does.
49
+ */
50
+ export interface LoopChatResult {
51
+ /**
52
+ * The model's answer as text: `choices[0].message.content`. Empty when the
53
+ * model answered only with tool calls; `raw` has them.
54
+ */
55
+ readonly answer: string;
56
+ /**
57
+ * The sample ID the reply's `X-Loop-Sample-Id` header carried, which
58
+ * feedback names. null when no sample ID came back (the platform was not
59
+ * recording calls just then), so the answer was not saved as a sample and
60
+ * cannot take feedback. A sample ID does not prove the pipeline ID is
61
+ * right: when no pipeline has it (misspelt, or the pipeline was deleted),
62
+ * the conversation is not saved under it. If recording is switched on for
63
+ * the model that answered, the call is still recorded the way that model
64
+ * records every call, and feedback on it is then accepted; when nothing
65
+ * records it, feedback on it is refused (409 `SAMPLE_NOT_YET_RECORDED` at
66
+ * first, 404 later). `stats()` counts such calls under `capture` (reason
67
+ * `no_pipeline`); the pipeline's Connect page shows its pipeline ID.
68
+ */
69
+ readonly sample_id: string | null;
70
+ /** The whole `/v1/chat/completions` answer, exactly as the endpoint sent it. */
71
+ readonly raw: ChatCompletionResponse;
72
+ /** Good: the answer is learned as it is. */
73
+ good(reason?: string): Promise<LoopSignal>;
74
+ /** Wrong: the answer is not learned. If you know the right answer, `correct` is worth far more. */
75
+ bad(reason?: string): Promise<LoopSignal>;
76
+ /** Wrong, and here is the right answer: the right answer is learned instead of the model's. */
77
+ correct(rightAnswer: string, reason?: string): Promise<LoopSignal>;
78
+ }
79
+ /**
80
+ * The error `client.loop.chat` rejects with when the pipeline's own model
81
+ * answers nothing. It is defined in client.ts beside the SDK's other
82
+ * `ApiError` subclasses: a class that extends `ApiError` is built when its
83
+ * module loads, and client.ts must have finished loading by then.
84
+ */
85
+ export { LoopNoVersionError } from '../client.js';
86
+ /** The answer's text: a string content, or the text parts of a list of parts. */
87
+ export declare function chatAnswerText(raw: ChatCompletionResponse): string;
@@ -0,0 +1,32 @@
1
+ /** The model name a pipeline's live version answers to: `loop-` and its pipeline ID. */
2
+ export const PIPELINE_MODEL_PREFIX = 'loop-';
3
+ /**
4
+ * A pipeline ID as the platform reads it. The gateways trim and lowercase the
5
+ * `X-Loop-Pipeline` header and keep only this shape, and the pipeline's own
6
+ * model name is `loop-` and the ID exactly, matched case for case. So `chat`
7
+ * lowercases the ID before it names the header and the model, and refuses
8
+ * anything else before any request: sent, it would be dropped and nothing saved.
9
+ */
10
+ export const PIPELINE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,62}$/;
11
+ /**
12
+ * The error `client.loop.chat` rejects with when the pipeline's own model
13
+ * answers nothing. It is defined in client.ts beside the SDK's other
14
+ * `ApiError` subclasses: a class that extends `ApiError` is built when its
15
+ * module loads, and client.ts must have finished loading by then.
16
+ */
17
+ export { LoopNoVersionError } from '../client.js';
18
+ /** The answer's text: a string content, or the text parts of a list of parts. */
19
+ export function chatAnswerText(raw) {
20
+ const choices = raw.choices;
21
+ const first = Array.isArray(choices) ? choices[0] : undefined;
22
+ const content = first?.message?.content;
23
+ if (typeof content === 'string')
24
+ return content;
25
+ if (Array.isArray(content)) {
26
+ return content
27
+ .map((part) => (part && typeof part === 'object' && typeof part.text === 'string'
28
+ ? part.text : ''))
29
+ .join('');
30
+ }
31
+ return '';
32
+ }