runbios-sdk 0.2.22 → 0.3.0-dev.323
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 +53 -0
- package/README.md +74 -1
- package/dist/client.d.ts +11 -0
- package/dist/client.js +29 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/resources/inference.d.ts +74 -2
- package/dist/resources/inference.js +73 -5
- package/dist/resources/loop.d.ts +95 -71
- package/dist/resources/loop.js +168 -18
- package/dist/resources/training.js +12 -2
- package/dist/types.d.ts +283 -69
- package/package.json +3 -2
package/dist/resources/loop.d.ts
CHANGED
|
@@ -1,63 +1,11 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { LoopTrace, LoopTraceDeleteResponse, LoopTraceListResponse, LoopTraceListParams, LoopCaptureParams, LoopCaptureResult, LoopImportParams, LoopImportResult, LoopSignal, LoopSignalParams, LoopLabel, LoopLabelCount, LoopConfig, LoopConfigParams, LoopStats, LoopTracePipelinesResponse, LoopMetricsHistory, LoopMetricsHistoryParams, TrainingRuleDeleteResponse, TrainingRunListParams, TrainingRunListResponse, TrainingRunResponse, TrainingRunActionResponse, TrainingRunPromoteRequest, TrainingRunRejectRequest, TrainingRunRollbackRequest, TrainingRunCancelRequest, Evaluation, EvaluationItemListParams, EvaluationItemsResponse, Benchmark, BenchmarkCreateParams, BenchmarkHistoryParams, BenchmarkHistoryResponse, BenchmarkItemListParams, BenchmarkItemsResponse, BenchmarkListParams, BenchmarkListResponse, BenchmarkRetireParams, BenchmarkRun, BenchmarkRunItemListParams, BenchmarkRunItemsResponse, Pipeline, PipelineListParams, PipelineListResponse, PipelineWriteRequest, PipelineMutationResponse, LoopModelsResponse, PipelineImportParams, PipelineBenchmarksResponse, PipelineBenchmarkAttachParams, PipelineBenchmarkMoveParams, VersionCompareResponse, VersionCompareParams, AttemptCompareResponse, AttemptCompareParams } from '../types.js';
|
|
3
|
-
/**
|
|
4
|
-
* The Conscious Loop -- pipelines that keep improving a model from the
|
|
5
|
-
* conversations you send them.
|
|
6
|
-
*
|
|
7
|
-
* A pipeline is one use case: a model, the tags whose samples train it, when
|
|
8
|
-
* to train, and how a new version is promoted. Every time it trains it makes
|
|
9
|
-
* an attempt; an attempt that goes live becomes the next version: one that
|
|
10
|
-
* beats the live model, one a member approves, or, after a deleted
|
|
11
|
-
* conversation, a rebuild without it that is not clearly worse (a `manual`
|
|
12
|
-
* pipeline waits for a member even then).
|
|
13
|
-
*
|
|
14
|
-
* Nothing is captured until a source is switched on, and you can switch it
|
|
15
|
-
* off again at any time. Creating a pipeline switches on the source named by
|
|
16
|
-
* its own tag (`own_tag`), stamping that tag, and deleting it switches that
|
|
17
|
-
* source off again. Anything that looks like a credential or a personal detail
|
|
18
|
-
* is removed before the record is written, never afterwards.
|
|
19
|
-
*
|
|
20
|
-
* Requires an API key carrying `loop:read` and/or `loop:write`. Those scopes are
|
|
21
|
-
* deliberately absent from the Read Only preset, because what they return is
|
|
22
|
-
* your raw prompts and completions rather than catalog metadata.
|
|
23
|
-
*
|
|
24
|
-
* Offered on the development environment only for now. Anywhere else every
|
|
25
|
-
* method here, reads included, rejects with a `ComingSoonError` whose
|
|
26
|
-
* `code` is `LOOP_COMING_SOON` (HTTP 403): a deliberate product state, not an
|
|
27
|
-
* outage, so retrying cannot succeed there.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```ts
|
|
31
|
-
* // 1. A pipeline: a model it can train, and its own tag.
|
|
32
|
-
* const { models } = await client.loop.listModels();
|
|
33
|
-
* const { pipeline } = await client.loop.createPipeline({
|
|
34
|
-
* name: 'Support replies',
|
|
35
|
-
* model: { id: models[0].id },
|
|
36
|
-
* });
|
|
37
|
-
*
|
|
38
|
-
* // 2. Send samples from your app to the pipeline's own source: its create
|
|
39
|
-
* // switched that source on, and it tags everything it records.
|
|
40
|
-
* const sent = await client.loop.capture({
|
|
41
|
-
* deployment_id: pipeline.own_tag!,
|
|
42
|
-
* model: 'gpt-4o',
|
|
43
|
-
* messages: [{ role: 'user', content: 'what is our refund window' }],
|
|
44
|
-
* completion: 'thirty days',
|
|
45
|
-
* });
|
|
46
|
-
*
|
|
47
|
-
* // 3. Say whether it was right, on the trace_id the capture answered with.
|
|
48
|
-
* // A correction is the most valuable answer.
|
|
49
|
-
* if (sent.trace_id) {
|
|
50
|
-
* await client.loop.correct(sent.trace_id, 'Thirty days from delivery, no questions asked.');
|
|
51
|
-
* }
|
|
52
|
-
*
|
|
53
|
-
* // 4. Once it has its minimum, it trains; versions appear on the pipeline.
|
|
54
|
-
* const p = await client.loop.getPipeline(pipeline.rule_id);
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
1
|
+
import { type HttpClient } from '../client.js';
|
|
2
|
+
import type { LoopTrace, LoopTraceDeleteResponse, LoopTraceListResponse, LoopTraceListParams, LoopCaptureParams, LoopCaptureResult, LoopImportParams, LoopImportResult, LoopSignal, LoopSignalParams, LoopSignalManyItem, LoopSignalManyResult, LoopLabel, LoopLabelCount, LoopConfig, LoopConfigParams, LoopStats, LoopTracePipelinesResponse, LoopMetricsHistory, LoopMetricsHistoryParams, TrainingRuleDeleteResponse, TrainingRunListParams, TrainingRunListResponse, TrainingRunResponse, TrainingRunActionResponse, TrainingRunPromoteRequest, TrainingRunRejectRequest, TrainingRunRollbackRequest, TrainingRunCancelRequest, Evaluation, EvaluationItemListParams, EvaluationItemsResponse, Benchmark, BenchmarkCreateParams, BenchmarkHistoryParams, BenchmarkHistoryResponse, BenchmarkItemListParams, BenchmarkItemsResponse, BenchmarkListParams, BenchmarkListResponse, BenchmarkRetireParams, BenchmarkRun, BenchmarkRunItemListParams, BenchmarkRunItemsResponse, Pipeline, PipelineAttemptListParams, PipelineAttemptListResponse, PipelineListParams, PipelineListResponse, PipelineWriteRequest, PipelineMutationResponse, LoopModelsResponse, PipelineImportParams, PipelineBenchmarksResponse, PipelineBenchmarkAttachParams, PipelineBenchmarkMoveParams, VersionCompareResponse, VersionCompareParams, AttemptCompareResponse, AttemptCompareParams } from '../types.js';
|
|
57
3
|
export declare class Loop {
|
|
58
4
|
private readonly _http;
|
|
59
5
|
/** @internal */
|
|
60
6
|
constructor(_http: HttpClient);
|
|
7
|
+
/** @internal How feedback waits between tries; tests replace it so nothing sleeps. */
|
|
8
|
+
_sleep: (ms: number) => Promise<void>;
|
|
61
9
|
/**
|
|
62
10
|
* Send one exchange to be captured.
|
|
63
11
|
*
|
|
@@ -79,6 +27,10 @@ export declare class Loop {
|
|
|
79
27
|
* When the source is not switched on this resolves with `captured: false`
|
|
80
28
|
* and a reason instead of throwing -- capture must never be the thing that
|
|
81
29
|
* breaks your application.
|
|
30
|
+
*
|
|
31
|
+
* Already know the answer was good, bad or wrong? Pass `feedback` and it is
|
|
32
|
+
* written with the exchange, so no second call is needed:
|
|
33
|
+
* `feedback: { verdict: 'corrected', correction: 'Thirty days from delivery.' }`.
|
|
82
34
|
*/
|
|
83
35
|
capture(params: LoopCaptureParams): Promise<LoopCaptureResult>;
|
|
84
36
|
/**
|
|
@@ -139,7 +91,14 @@ export declare class Loop {
|
|
|
139
91
|
* a shorter list moves every row after the gap and imports it again.
|
|
140
92
|
*/
|
|
141
93
|
importRows(params: LoopImportParams): Promise<LoopImportResult>;
|
|
142
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* List captured conversations, newest first.
|
|
96
|
+
*
|
|
97
|
+
* `q` searches the text: 2 to 200 characters, any case, matched as written
|
|
98
|
+
* in every message, the answer and its tool calls. Samples kept in your own
|
|
99
|
+
* storage cannot be searched; a search answers how many were left out as
|
|
100
|
+
* `unsearched`.
|
|
101
|
+
*/
|
|
143
102
|
listTraces(params?: LoopTraceListParams): Promise<LoopTraceListResponse>;
|
|
144
103
|
/**
|
|
145
104
|
* Read one conversation, with every verdict and tag recorded on it.
|
|
@@ -176,8 +135,36 @@ export declare class Loop {
|
|
|
176
135
|
* `edited` or `gold` verdict is the example, in place of the model's answer;
|
|
177
136
|
* a `rejected` answer is left out. Nothing is learned from an answer that was
|
|
178
137
|
* rejected or replaced. `source` defaults to `human`.
|
|
138
|
+
*
|
|
139
|
+
* Feedback on an `X-Loop-Sample-Id` sent right after the answer can reach
|
|
140
|
+
* the service before the sample does, which answers 409
|
|
141
|
+
* `SAMPLE_NOT_YET_RECORDED` and stores nothing. This method, and so `upvote`,
|
|
142
|
+
* `downvote`, `correct` and `gold`, sends it again after the `Retry-After`
|
|
143
|
+
* seconds (2 when none is given), at most 6 times in all, and then throws
|
|
144
|
+
* that error. No other error is retried.
|
|
179
145
|
*/
|
|
180
146
|
signal(traceId: string, params: LoopSignalParams): Promise<LoopSignal>;
|
|
147
|
+
/**
|
|
148
|
+
* Send many feedback items at once, several in flight together.
|
|
149
|
+
*
|
|
150
|
+
* Each item is `signal(item.trace_id, item)`: the same call, the same
|
|
151
|
+
* `SAMPLE_NOT_YET_RECORDED` retry. At most `concurrency` calls are in flight
|
|
152
|
+
* (8 by default, at most 32). It never throws for one item: every item
|
|
153
|
+
* comes back, in the order given, with its `signal` or the `error` it ended
|
|
154
|
+
* with, so one refused item costs no other.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const results = await client.loop.signalMany([
|
|
159
|
+
* { trace_id: idA, verdict: 'good' },
|
|
160
|
+
* { trace_id: idB, verdict: 'corrected', correction: 'Refunds take five working days.' },
|
|
161
|
+
* ]);
|
|
162
|
+
* const failed = results.filter((r) => r.error);
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
signalMany(items: LoopSignalManyItem[], options?: {
|
|
166
|
+
concurrency?: number;
|
|
167
|
+
}): Promise<LoopSignalManyResult[]>;
|
|
181
168
|
/**
|
|
182
169
|
* Thumbs up: this answer was good. Shorthand for `signal(id, { verdict:
|
|
183
170
|
* 'accepted' })` — there is one stored vocabulary behind both, so a thumbs-up
|
|
@@ -298,6 +285,13 @@ export declare class Loop {
|
|
|
298
285
|
*
|
|
299
286
|
* `ready.sft` is an upper bound: duplicates, the held-back set and
|
|
300
287
|
* conversations too long to train on come out of it when an attempt trains.
|
|
288
|
+
*
|
|
289
|
+
* `capture` is what the one-header capture did not record over the last
|
|
290
|
+
* `capture.days` days: per tag, `no_pipeline` (no pipeline has the tag),
|
|
291
|
+
* `too_large` or `incomplete` (the answer could not be kept). Empty when
|
|
292
|
+
* every call was recorded. Rows are per tag: a call naming two tags is
|
|
293
|
+
* counted under each, so never add them up as calls. One pipeline's share
|
|
294
|
+
* is `capture` on its detail.
|
|
301
295
|
*/
|
|
302
296
|
stats(): Promise<LoopStats>;
|
|
303
297
|
/**
|
|
@@ -356,15 +350,21 @@ export declare class Loop {
|
|
|
356
350
|
*/
|
|
357
351
|
cancelTrainingRun(id: string, params?: TrainingRunCancelRequest): Promise<TrainingRunActionResponse>;
|
|
358
352
|
/**
|
|
359
|
-
*
|
|
360
|
-
* the live model),
|
|
361
|
-
*
|
|
353
|
+
* The workspace's pipelines, a page at a time: its versions (the attempts
|
|
354
|
+
* that became the live model), its latest 5 attempts (`attempts_total` says
|
|
355
|
+
* how many; {@link listPipelineAttempts} pages through every one), which
|
|
356
|
+
* version is live, the attempt in flight, and what the pipeline is waiting
|
|
357
|
+
* for.
|
|
358
|
+
*
|
|
359
|
+
* `limit` is 100 when absent and at most 100; `q` matches the name, the tag
|
|
360
|
+
* or the model (case-insensitive); `status` keeps the pipelines `training`,
|
|
361
|
+
* waiting on you (`needs_you`), `serving` or `paused`.
|
|
362
362
|
*
|
|
363
363
|
* RETURNED WITH ITS ENVELOPE, because the list is not always all of them:
|
|
364
|
-
* the service returns
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
364
|
+
* the service returns one page, newest first, `total` is every pipeline
|
|
365
|
+
* that matches and `truncated` is true when more exist after this page. A
|
|
366
|
+
* caller that shows `pipelines` alone presents a short list as the whole of
|
|
367
|
+
* it; the next page is `{ offset: offset + pipelines.length }`.
|
|
368
368
|
*/
|
|
369
369
|
listPipelines(params?: PipelineListParams): Promise<PipelineListResponse>;
|
|
370
370
|
/**
|
|
@@ -373,11 +373,21 @@ export declare class Loop {
|
|
|
373
373
|
* `status` says whether it is the platform working (`running`), the member
|
|
374
374
|
* who has to act (`needs_review`, `needs_funds`), or nothing at all until
|
|
375
375
|
* somebody does (`paused`, with `paused_reason` saying why; `complete` at
|
|
376
|
-
* `max_versions`).
|
|
377
|
-
*
|
|
378
|
-
* at most, not an exact spend.
|
|
376
|
+
* `max_versions`). The monthly safety limit is the platform's and has no
|
|
377
|
+
* field: when it holds the pipeline, `next_reason` says so.
|
|
379
378
|
*/
|
|
380
379
|
getPipeline(id: string): Promise<Pipeline>;
|
|
380
|
+
/**
|
|
381
|
+
* A pipeline's attempts, newest first, a page at a time: every training run
|
|
382
|
+
* it made, winner or not, each the shape `attempts` has on the pipeline
|
|
383
|
+
* (with its `explanation` in plain words). `limit` is 50 when absent and at
|
|
384
|
+
* most 200. Narrow by `result` (`became_version`, `not_better`,
|
|
385
|
+
* `inconclusive`, `failed`, `stopped`, `running` -- which includes one
|
|
386
|
+
* waiting for a decision -- or `waiting_for_review`) and by `model` (`main`,
|
|
387
|
+
* or `second` for the second model's attempts); `total` follows the
|
|
388
|
+
* filters.
|
|
389
|
+
*/
|
|
390
|
+
listPipelineAttempts(id: string, params?: PipelineAttemptListParams): Promise<PipelineAttemptListResponse>;
|
|
381
391
|
/**
|
|
382
392
|
* The base models a pipeline can train: only those the engine trains with
|
|
383
393
|
* LoRA SFT and the platform can serve for the comparison, recommended first.
|
|
@@ -391,9 +401,12 @@ export declare class Loop {
|
|
|
391
401
|
* judged on at most `scored_rows` of them). They are read-only.
|
|
392
402
|
* Where a pipeline builds on its live version (its `chain` is set), each
|
|
393
403
|
* attempt trains a new adapter on top of the live version, on what it has
|
|
394
|
-
* not learned yet plus a replay of what it already learned (about
|
|
404
|
+
* not learned yet plus a replay of what it already learned (about half
|
|
395
405
|
* of what it trains on, picked at random, so it is not forgotten), and a
|
|
396
406
|
* winner is merged in and served as one model.
|
|
407
|
+
* `unavailable` lists every other model the platform knows, each with
|
|
408
|
+
* why it is not offered here (`reason_code`, and `reason`, one plain
|
|
409
|
+
* sentence); `[]` from a service from before it was sent.
|
|
397
410
|
* When the list cannot be read it throws `ApiError` 503
|
|
398
411
|
* `MODELS_UNAVAILABLE`, whose `body.floors` still carries the floors.
|
|
399
412
|
*/
|
|
@@ -408,12 +421,14 @@ export declare class Loop {
|
|
|
408
421
|
* Where the pipeline builds on its live version (`chain`), each attempt
|
|
409
422
|
* waits for 1,000 new samples and trains a new adapter on top of the live
|
|
410
423
|
* version, on what it has not learned plus a replay of what it already
|
|
411
|
-
* learned (about
|
|
424
|
+
* learned (about half of what it trains on, picked at random); a winner
|
|
412
425
|
* is merged in.
|
|
413
426
|
* What is charged is the GPU time the runs actually use; the platform's own
|
|
414
427
|
* per-run and monthly limits stop a run that goes wrong. `holdout.percent`
|
|
415
428
|
* (5 to 20, default 5) is the share of its samples held back to judge each
|
|
416
|
-
* attempt and never trained on.
|
|
429
|
+
* attempt and never trained on. `training` sets any of its training
|
|
430
|
+
* settings (`epochs`, `learning_rate`, `lora_rank`, `lora_alpha`,
|
|
431
|
+
* `replay_percent`); left out, or null, every one is automatic.
|
|
417
432
|
*/
|
|
418
433
|
createPipeline(params: PipelineWriteRequest): Promise<PipelineMutationResponse>;
|
|
419
434
|
/**
|
|
@@ -421,7 +436,13 @@ export declare class Loop {
|
|
|
421
436
|
* the limit and `challenger_model: null` removes the challenger. `promotion`
|
|
422
437
|
* is how the promotion policy is set. A new `holdout.percent` is a new
|
|
423
438
|
* revision: lowering it keeps every sample already held back, and raising it
|
|
424
|
-
* holds back only new samples.
|
|
439
|
+
* holds back only new samples. `training` changes only the settings it
|
|
440
|
+
* carries -- `{ epochs: 8 }` sets the passes and keeps the rest,
|
|
441
|
+
* `{ epochs: null }` puts the passes back to automatic -- and
|
|
442
|
+
* `training: null` puts every one back to automatic; the next attempt
|
|
443
|
+
* trains with them, and earlier attempts keep the settings they trained
|
|
444
|
+
* with. Out of range is 422 `TRAINING_SETTING_OUT_OF_RANGE`, naming the
|
|
445
|
+
* field. Saving is the authorization, as on a create.
|
|
425
446
|
*/
|
|
426
447
|
updatePipeline(id: string, params: PipelineWriteRequest): Promise<PipelineMutationResponse>;
|
|
427
448
|
/**
|
|
@@ -585,6 +606,9 @@ export declare class Loop {
|
|
|
585
606
|
* series that silently dropped the runs where the benchmark could not be
|
|
586
607
|
* scored would read as an unbroken line and not be one, so those points come
|
|
587
608
|
* back with `candidate_score` null and `status_reason` saying why.
|
|
609
|
+
*
|
|
610
|
+
* `pipeline` (a pipeline's id) keeps only the points of that pipeline's
|
|
611
|
+
* attempts, so the trend is one pipeline's attempts by attempt number.
|
|
588
612
|
*/
|
|
589
613
|
getBenchmarkHistory(id: string, params?: BenchmarkHistoryParams): Promise<BenchmarkHistoryResponse>;
|
|
590
614
|
/**
|
package/dist/resources/loop.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ApiError } from '../client.js';
|
|
1
2
|
/**
|
|
2
3
|
* The Conscious Loop -- pipelines that keep improving a model from the
|
|
3
4
|
* conversations you send them.
|
|
@@ -52,12 +53,28 @@
|
|
|
52
53
|
* const p = await client.loop.getPipeline(pipeline.rule_id);
|
|
53
54
|
* ```
|
|
54
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* The code feedback is refused with (409, `Retry-After`) when it names a sample
|
|
58
|
+
* id the gateway issued moments ago (`X-Loop-Sample-Id`) and the sample is not
|
|
59
|
+
* recorded yet. Nothing was stored, so sending the same feedback again is safe.
|
|
60
|
+
*/
|
|
61
|
+
const SAMPLE_NOT_YET_RECORDED = 'SAMPLE_NOT_YET_RECORDED';
|
|
62
|
+
/** Feedback is sent at most this many times while its sample is on its way. */
|
|
63
|
+
const FEEDBACK_TRIES = 6;
|
|
64
|
+
/** The wait when the 409 names none, and the longest wait honoured. */
|
|
65
|
+
const FEEDBACK_RETRY_DEFAULT_SECONDS = 2;
|
|
66
|
+
const FEEDBACK_RETRY_MAX_SECONDS = 30;
|
|
67
|
+
/** How many feedback calls `signalMany` keeps in flight, by default and at most. */
|
|
68
|
+
const SIGNAL_MANY_CONCURRENCY = 8;
|
|
69
|
+
const SIGNAL_MANY_CONCURRENCY_MAX = 32;
|
|
55
70
|
export class Loop {
|
|
56
71
|
_http;
|
|
57
72
|
/** @internal */
|
|
58
73
|
constructor(_http) {
|
|
59
74
|
this._http = _http;
|
|
60
75
|
}
|
|
76
|
+
/** @internal How feedback waits between tries; tests replace it so nothing sleeps. */
|
|
77
|
+
_sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
78
|
// ── capture ───────────────────────────────────────────────────────────
|
|
62
79
|
/**
|
|
63
80
|
* Send one exchange to be captured.
|
|
@@ -80,6 +97,10 @@ export class Loop {
|
|
|
80
97
|
* When the source is not switched on this resolves with `captured: false`
|
|
81
98
|
* and a reason instead of throwing -- capture must never be the thing that
|
|
82
99
|
* breaks your application.
|
|
100
|
+
*
|
|
101
|
+
* Already know the answer was good, bad or wrong? Pass `feedback` and it is
|
|
102
|
+
* written with the exchange, so no second call is needed:
|
|
103
|
+
* `feedback: { verdict: 'corrected', correction: 'Thirty days from delivery.' }`.
|
|
83
104
|
*/
|
|
84
105
|
async capture(params) {
|
|
85
106
|
return this._http.fetchPost('/api/loop/traces', params);
|
|
@@ -145,9 +166,18 @@ export class Loop {
|
|
|
145
166
|
return this._http.fetchPost('/api/loop/import', params);
|
|
146
167
|
}
|
|
147
168
|
// ── traces ────────────────────────────────────────────────────────────
|
|
148
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* List captured conversations, newest first.
|
|
171
|
+
*
|
|
172
|
+
* `q` searches the text: 2 to 200 characters, any case, matched as written
|
|
173
|
+
* in every message, the answer and its tool calls. Samples kept in your own
|
|
174
|
+
* storage cannot be searched; a search answers how many were left out as
|
|
175
|
+
* `unsearched`.
|
|
176
|
+
*/
|
|
149
177
|
async listTraces(params = {}) {
|
|
150
178
|
const q = new URLSearchParams();
|
|
179
|
+
if (params.q)
|
|
180
|
+
q.set('q', params.q);
|
|
151
181
|
if (params.deployment_id)
|
|
152
182
|
q.set('deployment_id', params.deployment_id);
|
|
153
183
|
if (params.conversation_id)
|
|
@@ -226,10 +256,71 @@ export class Loop {
|
|
|
226
256
|
* `edited` or `gold` verdict is the example, in place of the model's answer;
|
|
227
257
|
* a `rejected` answer is left out. Nothing is learned from an answer that was
|
|
228
258
|
* rejected or replaced. `source` defaults to `human`.
|
|
259
|
+
*
|
|
260
|
+
* Feedback on an `X-Loop-Sample-Id` sent right after the answer can reach
|
|
261
|
+
* the service before the sample does, which answers 409
|
|
262
|
+
* `SAMPLE_NOT_YET_RECORDED` and stores nothing. This method, and so `upvote`,
|
|
263
|
+
* `downvote`, `correct` and `gold`, sends it again after the `Retry-After`
|
|
264
|
+
* seconds (2 when none is given), at most 6 times in all, and then throws
|
|
265
|
+
* that error. No other error is retried.
|
|
229
266
|
*/
|
|
230
267
|
async signal(traceId, params) {
|
|
231
|
-
const
|
|
232
|
-
|
|
268
|
+
const path = `/api/loop/traces/${encodeURIComponent(traceId)}/signals`;
|
|
269
|
+
for (let tries = 1;; tries++) {
|
|
270
|
+
try {
|
|
271
|
+
const res = await this._http.fetchPost(path, params);
|
|
272
|
+
return res.signal;
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
const onItsWay = err instanceof ApiError && err.status === 409 && err.code === SAMPLE_NOT_YET_RECORDED;
|
|
276
|
+
if (!onItsWay || tries >= FEEDBACK_TRIES)
|
|
277
|
+
throw err;
|
|
278
|
+
const seconds = Math.min(err.retryAfter ?? FEEDBACK_RETRY_DEFAULT_SECONDS, FEEDBACK_RETRY_MAX_SECONDS);
|
|
279
|
+
await this._sleep(seconds * 1000);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Send many feedback items at once, several in flight together.
|
|
285
|
+
*
|
|
286
|
+
* Each item is `signal(item.trace_id, item)`: the same call, the same
|
|
287
|
+
* `SAMPLE_NOT_YET_RECORDED` retry. At most `concurrency` calls are in flight
|
|
288
|
+
* (8 by default, at most 32). It never throws for one item: every item
|
|
289
|
+
* comes back, in the order given, with its `signal` or the `error` it ended
|
|
290
|
+
* with, so one refused item costs no other.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```ts
|
|
294
|
+
* const results = await client.loop.signalMany([
|
|
295
|
+
* { trace_id: idA, verdict: 'good' },
|
|
296
|
+
* { trace_id: idB, verdict: 'corrected', correction: 'Refunds take five working days.' },
|
|
297
|
+
* ]);
|
|
298
|
+
* const failed = results.filter((r) => r.error);
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
async signalMany(items, options = {}) {
|
|
302
|
+
const asked = Math.trunc(options.concurrency ?? SIGNAL_MANY_CONCURRENCY);
|
|
303
|
+
const limit = Math.max(1, Math.min(Number.isFinite(asked) ? asked : SIGNAL_MANY_CONCURRENCY, SIGNAL_MANY_CONCURRENCY_MAX));
|
|
304
|
+
const out = new Array(items.length);
|
|
305
|
+
let next = 0;
|
|
306
|
+
const worker = async () => {
|
|
307
|
+
for (;;) {
|
|
308
|
+
const i = next++;
|
|
309
|
+
if (i >= items.length)
|
|
310
|
+
return;
|
|
311
|
+
const { trace_id, ...params } = items[i];
|
|
312
|
+
try {
|
|
313
|
+
if (!trace_id)
|
|
314
|
+
throw new Error('each feedback item needs its trace_id');
|
|
315
|
+
out[i] = { trace_id, signal: await this.signal(trace_id, params) };
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
out[i] = { trace_id, error };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
323
|
+
return out;
|
|
233
324
|
}
|
|
234
325
|
/**
|
|
235
326
|
* Thumbs up: this answer was good. Shorthand for `signal(id, { verdict:
|
|
@@ -383,6 +474,13 @@ export class Loop {
|
|
|
383
474
|
*
|
|
384
475
|
* `ready.sft` is an upper bound: duplicates, the held-back set and
|
|
385
476
|
* conversations too long to train on come out of it when an attempt trains.
|
|
477
|
+
*
|
|
478
|
+
* `capture` is what the one-header capture did not record over the last
|
|
479
|
+
* `capture.days` days: per tag, `no_pipeline` (no pipeline has the tag),
|
|
480
|
+
* `too_large` or `incomplete` (the answer could not be kept). Empty when
|
|
481
|
+
* every call was recorded. Rows are per tag: a call naming two tags is
|
|
482
|
+
* counted under each, so never add them up as calls. One pipeline's share
|
|
483
|
+
* is `capture` on its detail.
|
|
386
484
|
*/
|
|
387
485
|
async stats() {
|
|
388
486
|
const res = await this._http.fetchGet('/api/loop/stats');
|
|
@@ -481,20 +579,32 @@ export class Loop {
|
|
|
481
579
|
}
|
|
482
580
|
// ── pipelines ─────────────────────────────────────────────────────────
|
|
483
581
|
/**
|
|
484
|
-
*
|
|
485
|
-
* the live model),
|
|
486
|
-
*
|
|
582
|
+
* The workspace's pipelines, a page at a time: its versions (the attempts
|
|
583
|
+
* that became the live model), its latest 5 attempts (`attempts_total` says
|
|
584
|
+
* how many; {@link listPipelineAttempts} pages through every one), which
|
|
585
|
+
* version is live, the attempt in flight, and what the pipeline is waiting
|
|
586
|
+
* for.
|
|
587
|
+
*
|
|
588
|
+
* `limit` is 100 when absent and at most 100; `q` matches the name, the tag
|
|
589
|
+
* or the model (case-insensitive); `status` keeps the pipelines `training`,
|
|
590
|
+
* waiting on you (`needs_you`), `serving` or `paused`.
|
|
487
591
|
*
|
|
488
592
|
* RETURNED WITH ITS ENVELOPE, because the list is not always all of them:
|
|
489
|
-
* the service returns
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
593
|
+
* the service returns one page, newest first, `total` is every pipeline
|
|
594
|
+
* that matches and `truncated` is true when more exist after this page. A
|
|
595
|
+
* caller that shows `pipelines` alone presents a short list as the whole of
|
|
596
|
+
* it; the next page is `{ offset: offset + pipelines.length }`.
|
|
493
597
|
*/
|
|
494
598
|
async listPipelines(params = {}) {
|
|
495
599
|
const q = new URLSearchParams();
|
|
600
|
+
if (params.limit != null)
|
|
601
|
+
q.set('limit', String(params.limit));
|
|
496
602
|
if (params.offset)
|
|
497
603
|
q.set('offset', String(params.offset));
|
|
604
|
+
if (params.q)
|
|
605
|
+
q.set('q', params.q);
|
|
606
|
+
if (params.status)
|
|
607
|
+
q.set('status', params.status);
|
|
498
608
|
const qs = q.toString();
|
|
499
609
|
const res = await this._http.fetchGet(`/api/loop/pipelines${qs ? `?${qs}` : ''}`);
|
|
500
610
|
const pipelines = res.pipelines || [];
|
|
@@ -511,14 +621,38 @@ export class Loop {
|
|
|
511
621
|
* `status` says whether it is the platform working (`running`), the member
|
|
512
622
|
* who has to act (`needs_review`, `needs_funds`), or nothing at all until
|
|
513
623
|
* somebody does (`paused`, with `paused_reason` saying why; `complete` at
|
|
514
|
-
* `max_versions`).
|
|
515
|
-
*
|
|
516
|
-
* at most, not an exact spend.
|
|
624
|
+
* `max_versions`). The monthly safety limit is the platform's and has no
|
|
625
|
+
* field: when it holds the pipeline, `next_reason` says so.
|
|
517
626
|
*/
|
|
518
627
|
async getPipeline(id) {
|
|
519
628
|
const res = await this._http.fetchGet(`/api/loop/pipelines/${encodeURIComponent(id)}`);
|
|
520
629
|
return res.pipeline;
|
|
521
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* A pipeline's attempts, newest first, a page at a time: every training run
|
|
633
|
+
* it made, winner or not, each the shape `attempts` has on the pipeline
|
|
634
|
+
* (with its `explanation` in plain words). `limit` is 50 when absent and at
|
|
635
|
+
* most 200. Narrow by `result` (`became_version`, `not_better`,
|
|
636
|
+
* `inconclusive`, `failed`, `stopped`, `running` -- which includes one
|
|
637
|
+
* waiting for a decision -- or `waiting_for_review`) and by `model` (`main`,
|
|
638
|
+
* or `second` for the second model's attempts); `total` follows the
|
|
639
|
+
* filters.
|
|
640
|
+
*/
|
|
641
|
+
async listPipelineAttempts(id, params = {}) {
|
|
642
|
+
const q = new URLSearchParams();
|
|
643
|
+
if (params.limit != null)
|
|
644
|
+
q.set('limit', String(params.limit));
|
|
645
|
+
if (params.offset)
|
|
646
|
+
q.set('offset', String(params.offset));
|
|
647
|
+
if (params.result)
|
|
648
|
+
q.set('result', params.result);
|
|
649
|
+
if (params.model)
|
|
650
|
+
q.set('model', params.model);
|
|
651
|
+
const qs = q.toString();
|
|
652
|
+
const res = await this._http.fetchGet(`/api/loop/pipelines/${encodeURIComponent(id)}/attempts${qs ? `?${qs}` : ''}`);
|
|
653
|
+
const attempts = res.attempts || [];
|
|
654
|
+
return { attempts, total: typeof res.total === 'number' ? res.total : attempts.length };
|
|
655
|
+
}
|
|
522
656
|
// ── creating and changing pipelines ───────────────────────────────────
|
|
523
657
|
//
|
|
524
658
|
// One body -- a name, a model, tags, when to train, how to promote -- and
|
|
@@ -538,15 +672,18 @@ export class Loop {
|
|
|
538
672
|
* judged on at most `scored_rows` of them). They are read-only.
|
|
539
673
|
* Where a pipeline builds on its live version (its `chain` is set), each
|
|
540
674
|
* attempt trains a new adapter on top of the live version, on what it has
|
|
541
|
-
* not learned yet plus a replay of what it already learned (about
|
|
675
|
+
* not learned yet plus a replay of what it already learned (about half
|
|
542
676
|
* of what it trains on, picked at random, so it is not forgotten), and a
|
|
543
677
|
* winner is merged in and served as one model.
|
|
678
|
+
* `unavailable` lists every other model the platform knows, each with
|
|
679
|
+
* why it is not offered here (`reason_code`, and `reason`, one plain
|
|
680
|
+
* sentence); `[]` from a service from before it was sent.
|
|
544
681
|
* When the list cannot be read it throws `ApiError` 503
|
|
545
682
|
* `MODELS_UNAVAILABLE`, whose `body.floors` still carries the floors.
|
|
546
683
|
*/
|
|
547
684
|
async listModels() {
|
|
548
685
|
const res = await this._http.fetchGet('/api/loop/models');
|
|
549
|
-
return { ...res, models: res.models || [] };
|
|
686
|
+
return { ...res, models: res.models || [], unavailable: res.unavailable || [] };
|
|
550
687
|
}
|
|
551
688
|
/**
|
|
552
689
|
* Create a pipeline -- one use case.
|
|
@@ -558,12 +695,14 @@ export class Loop {
|
|
|
558
695
|
* Where the pipeline builds on its live version (`chain`), each attempt
|
|
559
696
|
* waits for 1,000 new samples and trains a new adapter on top of the live
|
|
560
697
|
* version, on what it has not learned plus a replay of what it already
|
|
561
|
-
* learned (about
|
|
698
|
+
* learned (about half of what it trains on, picked at random); a winner
|
|
562
699
|
* is merged in.
|
|
563
700
|
* What is charged is the GPU time the runs actually use; the platform's own
|
|
564
701
|
* per-run and monthly limits stop a run that goes wrong. `holdout.percent`
|
|
565
702
|
* (5 to 20, default 5) is the share of its samples held back to judge each
|
|
566
|
-
* attempt and never trained on.
|
|
703
|
+
* attempt and never trained on. `training` sets any of its training
|
|
704
|
+
* settings (`epochs`, `learning_rate`, `lora_rank`, `lora_alpha`,
|
|
705
|
+
* `replay_percent`); left out, or null, every one is automatic.
|
|
567
706
|
*/
|
|
568
707
|
async createPipeline(params) {
|
|
569
708
|
return this._http.fetchPost('/api/loop/pipelines', params);
|
|
@@ -573,7 +712,13 @@ export class Loop {
|
|
|
573
712
|
* the limit and `challenger_model: null` removes the challenger. `promotion`
|
|
574
713
|
* is how the promotion policy is set. A new `holdout.percent` is a new
|
|
575
714
|
* revision: lowering it keeps every sample already held back, and raising it
|
|
576
|
-
* holds back only new samples.
|
|
715
|
+
* holds back only new samples. `training` changes only the settings it
|
|
716
|
+
* carries -- `{ epochs: 8 }` sets the passes and keeps the rest,
|
|
717
|
+
* `{ epochs: null }` puts the passes back to automatic -- and
|
|
718
|
+
* `training: null` puts every one back to automatic; the next attempt
|
|
719
|
+
* trains with them, and earlier attempts keep the settings they trained
|
|
720
|
+
* with. Out of range is 422 `TRAINING_SETTING_OUT_OF_RANGE`, naming the
|
|
721
|
+
* field. Saving is the authorization, as on a create.
|
|
577
722
|
*/
|
|
578
723
|
async updatePipeline(id, params) {
|
|
579
724
|
return this._http.fetchPut(`/api/loop/pipelines/${encodeURIComponent(id)}`, params);
|
|
@@ -811,9 +956,14 @@ export class Loop {
|
|
|
811
956
|
* series that silently dropped the runs where the benchmark could not be
|
|
812
957
|
* scored would read as an unbroken line and not be one, so those points come
|
|
813
958
|
* back with `candidate_score` null and `status_reason` saying why.
|
|
959
|
+
*
|
|
960
|
+
* `pipeline` (a pipeline's id) keeps only the points of that pipeline's
|
|
961
|
+
* attempts, so the trend is one pipeline's attempts by attempt number.
|
|
814
962
|
*/
|
|
815
963
|
async getBenchmarkHistory(id, params = {}) {
|
|
816
964
|
const q = new URLSearchParams();
|
|
965
|
+
if (params.pipeline)
|
|
966
|
+
q.set('pipeline', params.pipeline);
|
|
817
967
|
if (params.limit != null)
|
|
818
968
|
q.set('limit', String(params.limit));
|
|
819
969
|
if (params.offset != null)
|
|
@@ -9,8 +9,9 @@ function trainingIdempotencyKey(explicit) {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* `start_from` on the wire. The adapters stay in the order given (oldest
|
|
12
|
-
* first: the order they are merged in)
|
|
13
|
-
*
|
|
12
|
+
* first: the order they are merged in). An empty list, an `exportStep` other
|
|
13
|
+
* than 'best' or 'final', and an `exportStep` without `exportMerged: true`
|
|
14
|
+
* are refused here rather than sent, since the API refuses each of them too.
|
|
14
15
|
*/
|
|
15
16
|
function startFromBody(sf) {
|
|
16
17
|
if (!Array.isArray(sf.adapters) || sf.adapters.length === 0) {
|
|
@@ -21,6 +22,15 @@ function startFromBody(sf) {
|
|
|
21
22
|
};
|
|
22
23
|
if (sf.exportMerged !== undefined)
|
|
23
24
|
body.export_merged = sf.exportMerged;
|
|
25
|
+
if (sf.exportStep !== undefined) {
|
|
26
|
+
if (sf.exportStep !== 'best' && sf.exportStep !== 'final') {
|
|
27
|
+
throw new Error("RunBiOS: startFrom.exportStep must be 'best' (the step validation picked) or 'final' (the last step trained)");
|
|
28
|
+
}
|
|
29
|
+
if (sf.exportMerged !== true) {
|
|
30
|
+
throw new Error('RunBiOS: startFrom.exportStep names the step the merged model is made of, so it needs exportMerged: true');
|
|
31
|
+
}
|
|
32
|
+
body.export_step = sf.exportStep;
|
|
33
|
+
}
|
|
24
34
|
return body;
|
|
25
35
|
}
|
|
26
36
|
function buildTrainingRequest(params) {
|