runbios-mcp 0.2.1-rc.98 → 0.2.2-dev.171
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/api-client.d.ts +2 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +56 -2
- package/dist/api-client.js.map +1 -1
- package/dist/config.d.ts +34 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +72 -1
- package/dist/config.js.map +1 -1
- package/dist/http/mcp-handler.d.ts.map +1 -1
- package/dist/http/mcp-handler.js +8 -1
- package/dist/http/mcp-handler.js.map +1 -1
- package/dist/http/metadata.js +1 -1
- package/dist/http/metadata.js.map +1 -1
- package/dist/http/oauth.d.ts +1 -1
- package/dist/http/oauth.d.ts.map +1 -1
- package/dist/http/oauth.js +7 -0
- package/dist/http/oauth.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +907 -120
- package/dist/server.js.map +1 -1
- package/dist/training-contract.d.ts +22 -1
- package/dist/training-contract.d.ts.map +1 -1
- package/dist/training-contract.js +21 -2
- package/dist/training-contract.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -594,9 +594,82 @@ launchGates = resolveLaunchGates()) {
|
|
|
594
594
|
// The registrations below are moved verbatim from the original single-file
|
|
595
595
|
// entry and intentionally keep their original indentation. `server.tool(...)`
|
|
596
596
|
// is a thin delegate that adds the onToolCall hook around each handler.
|
|
597
|
+
const registeredTools = new Set();
|
|
598
|
+
// MODEL_CATALOG_PATH is the ONE model-discovery endpoint the platform serves:
|
|
599
|
+
// GET /api/public/model-catalog (model-service/cmd/main.go registers it, with
|
|
600
|
+
// /stats and /{author}/{name} beside it).
|
|
601
|
+
//
|
|
602
|
+
// list_models and search_models both used to call `/api/public/model-search`,
|
|
603
|
+
// which no service has ever registered. Measured on production 2026-09-19 it
|
|
604
|
+
// answered HTTP 463 with an empty body from the load balancer, so BOTH tools
|
|
605
|
+
// failed for every caller — and they are the two the descriptions tell an agent
|
|
606
|
+
// to call FIRST, before create_training_job or create_inference. Any agent
|
|
607
|
+
// following the documented flow hit a dead end on step one.
|
|
608
|
+
//
|
|
609
|
+
// One constant, not two string literals: this endpoint moved once already
|
|
610
|
+
// (catalog_detail_route_test.go records the last time a prefix and its
|
|
611
|
+
// registrations drifted apart), and two copies is how one of them gets missed.
|
|
612
|
+
const MODEL_CATALOG_PATH = "/api/public/model-catalog";
|
|
613
|
+
// billionsParam coerces a human parameter count ("7B", "70b", " 13B ") into the
|
|
614
|
+
// bare number the catalog filter parses.
|
|
615
|
+
//
|
|
616
|
+
// The tool advertises "e.g. '7B', '13B', '70B'" and the handler runs ParseFloat,
|
|
617
|
+
// which fails on a trailing B and yields 0 — and 0 means "no filter" there. So
|
|
618
|
+
// every documented spelling of max_params was accepted and then silently
|
|
619
|
+
// ignored, which is worse than rejecting it: the caller gets a plausible,
|
|
620
|
+
// unfiltered answer. A value with no digits is left alone for the server to
|
|
621
|
+
// judge rather than silently dropped here.
|
|
622
|
+
function billionsParam(v) {
|
|
623
|
+
if (v === undefined)
|
|
624
|
+
return undefined;
|
|
625
|
+
const m = /^\s*([0-9]*\.?[0-9]+)\s*[bB]?\s*$/.exec(v);
|
|
626
|
+
return m ? m[1] : v;
|
|
627
|
+
}
|
|
628
|
+
const datasetMixingSchema = z.object({
|
|
629
|
+
mode: z.enum(["sequential", "shuffle", "interleave", "phased"]),
|
|
630
|
+
seed: z.number().int().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
631
|
+
weights: z.array(z.number().nonnegative()).min(1).max(64).optional(),
|
|
632
|
+
phases: z.array(z.object({
|
|
633
|
+
name: z.string().max(255).optional(),
|
|
634
|
+
portion: z.number().positive().max(1),
|
|
635
|
+
weights: z.array(z.number().nonnegative()).min(1).max(64).optional(),
|
|
636
|
+
shuffle: z.boolean().optional(),
|
|
637
|
+
})).min(1).optional(),
|
|
638
|
+
});
|
|
639
|
+
const trainingConfigurationSchema = {
|
|
640
|
+
dataset_sample_limits: z.record(z.string(), z.number().int().min(0)).optional().describe("Per-dataset first-N row caps; keys must be selected dataset IDs. Omit or use 0 for the full source. Prefer dataset_sampling."),
|
|
641
|
+
dataset_sampling: z.record(z.string(), z.object({
|
|
642
|
+
rows: z.number().int().min(1).max(50_000_000).optional(),
|
|
643
|
+
percent: z.number().gt(0).max(100).optional(),
|
|
644
|
+
})).optional().describe("Per-dataset sampling, keyed by selected dataset ID: exactly one of rows or percent (of the dataset's usable rows for the method). Asking for more than the dataset holds uses all of it and the run says so. Recorded in the mix manifest; a resume reproduces the same rows."),
|
|
645
|
+
dataset_sampling_strategy: z.enum(["first", "random"]).optional().describe("How sampled rows are chosen: 'first' (default) keeps the first N rows in file order (a curriculum); 'random' draws a seeded uniform sample kept in file order."),
|
|
646
|
+
epochs: z.number().positive().max(1000).optional().describe("Number of training epochs. Governs the full run when max_steps is omitted (the default)."),
|
|
647
|
+
max_steps: z.number().int().min(1).max(10_000_000).optional().describe("Optional hard step cap that overrides epochs. Omit for no cap and all configured epochs; setting less than one epoch intentionally trains only part of the data."),
|
|
648
|
+
learning_rate: z.number().positive().max(1).optional().describe("Learning rate."),
|
|
649
|
+
batch_size: z.number().int().min(1).max(65536).optional().describe("Per-device train batch size."),
|
|
650
|
+
gradient_accumulation_steps: z.number().int().min(1).max(65536).optional(),
|
|
651
|
+
max_seq_length: trainingSeqLengthSchema(),
|
|
652
|
+
lora_rank: z.number().int().min(1).max(4096).optional(),
|
|
653
|
+
lora_alpha: z.number().positive().max(1_000_000).optional(),
|
|
654
|
+
warmup_ratio: z.number().min(0).max(1).optional(),
|
|
655
|
+
weight_decay: z.number().min(0).max(10).optional(),
|
|
656
|
+
scheduler: z
|
|
657
|
+
.enum(["linear", "cosine", "cosine_with_restarts", "polynomial", "constant", "constant_with_warmup"])
|
|
658
|
+
.optional()
|
|
659
|
+
.describe("Learning-rate scheduler."),
|
|
660
|
+
job_name: z.string().max(255).optional(),
|
|
661
|
+
eval_split_ratio: z.number().min(0).max(0.99).optional(),
|
|
662
|
+
eval_steps: z.number().int().positive().optional().describe("Evaluate every N optimizer steps when a validation split exists."),
|
|
663
|
+
save_steps: z.number().int().positive().optional().describe("Checkpoint interval in optimizer steps."),
|
|
664
|
+
early_stopping_patience: z.number().int().min(0).max(20).optional()
|
|
665
|
+
.describe("Stop after N evaluations without a better validation loss (0 = off; default 3 when eval_split_ratio is set). The best checkpoint is always saved."),
|
|
666
|
+
overlong_policy: z.enum(["truncate", "drop"]).optional()
|
|
667
|
+
.describe("SFT only. 'truncate' trims oldest context while preserving a trainable answer; 'drop' skips overlong samples. CPT packs raw text and does not use this policy."),
|
|
668
|
+
};
|
|
597
669
|
const server = {
|
|
598
670
|
tool(name, description, paramsSchema, cb) {
|
|
599
671
|
mcp.tool(name, description, paramsSchema, wrapToolHandler(name, cb, hooks?.onToolCall));
|
|
672
|
+
registeredTools.add(name);
|
|
600
673
|
},
|
|
601
674
|
};
|
|
602
675
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
@@ -628,12 +701,10 @@ launchGates = resolveLaunchGates()) {
|
|
|
628
701
|
a stand-in that says what launches soon and names only tools that ARE
|
|
629
702
|
registered (reads/lifecycle stay live). At launch the gates open and the
|
|
630
703
|
real topics return verbatim. */
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
// coming-soon gate test pins this claim to the wire count
|
|
634
|
-
const liveToolCount =
|
|
635
|
-
- (launchGates.training ? 1 : 0)
|
|
636
|
-
- (launchGates.datasets ? 2 : 0);
|
|
704
|
+
// Count the actual registrations, including the conditional creation tools.
|
|
705
|
+
// This is read when the guide is called, after registration has completed.
|
|
706
|
+
// The coming-soon gate test also pins this claim to the wire count.
|
|
707
|
+
const liveToolCount = registeredTools.size;
|
|
637
708
|
const comingSoonNote = `# Coming soon
|
|
638
709
|
|
|
639
710
|
${gatedSurfaces.join(" and ")} ${gatedSurfaces.length > 1 ? "are" : "is"} not available in this build yet — the creation tools are not registered, so no tool call (and no workaround) can start one today. Do not retry or search for one.
|
|
@@ -690,9 +761,9 @@ get_inference_status instead of asserting them.
|
|
|
690
761
|
4. search_models / list_models - Find a model from the hosted catalog
|
|
691
762
|
5. get_model_config - Recommended settings for a chosen model
|
|
692
763
|
6. list_supported_architectures - Which architectures can be served
|
|
693
|
-
7. get_gpu_pricing / get_recommended_gpu - GPU options and
|
|
694
|
-
8. list_integrations -
|
|
695
|
-
9. list_datasets / preview_dataset / delete_dataset - Read and clean up existing datasets
|
|
764
|
+
7. get_gpu_pricing / get_recommended_gpu / recommend_training_config - GPU options, pricing, and the trainer's own batch/memory/time recommendation
|
|
765
|
+
8. list_integrations / search_huggingface_datasets / preview_huggingface_dataset - Discover Hub sources and inspect subsets/splits
|
|
766
|
+
9. list_datasets / get_dataset_status / preview_dataset / delete_dataset - Read and clean up existing datasets
|
|
696
767
|
10. get_training_capabilities - Read the authoritative training contract
|
|
697
768
|
11. preflight_training_job - Validate a proposed job payload (no side effects)
|
|
698
769
|
12. list_training_jobs / get_training_status - Monitor existing jobs
|
|
@@ -711,7 +782,38 @@ get_inference_status instead of asserting them.
|
|
|
711
782
|
8. update_inference_policy - Change explicit queue consent or the maximum accepted hourly price
|
|
712
783
|
9. stop_inference / resume_inference / restart_inference - Control serving lifecycle
|
|
713
784
|
10. chat_with_inference - Call a serverless catalog model by id or a dedicated deployment via the unified /v1 endpoint (optional streaming)
|
|
714
|
-
11. delete_inference - Permanently remove a deployment after verified infrastructure teardown
|
|
785
|
+
11. delete_inference - Permanently remove a deployment after verified infrastructure teardown
|
|
786
|
+
|
|
787
|
+
## Conscious Loop MCP Tools
|
|
788
|
+
The loop turns what a deployed model actually did into what it learns next.
|
|
789
|
+
NOTHING IS RECORDED until loop_set_capture turns a source on — capture is a
|
|
790
|
+
consent decision, not a default, and these tools cannot bypass it.
|
|
791
|
+
1. loop_set_capture - Turn recording on or off for a source and set how long conversations are kept. Start here; every other loop tool is inert until this is done.
|
|
792
|
+
2. loop_capture_trace / loop_import_rows - Record one exchange as it happens (what the model was asked, what it answered, any tool calls), whoever served the model — Run BiOS, another provider, or your own servers. loop_import_rows brings in data that already exists instead: an export, a spreadsheet of past answers, preference pairs. Imported rows arrive as conversations NEEDING REVIEW, never as finished training data.
|
|
793
|
+
3. loop_list_traces / loop_get_trace - Read what has been recorded. Credentials and personal details are stripped before storage, never after.
|
|
794
|
+
4. loop_add_signal - Record whether an answer was right. Append-only. Verdict 'edited' with a correction is the most valuable feedback there is.
|
|
795
|
+
5. loop_add_candidate / loop_list_candidates - Submit ALTERNATIVE answers to a recorded prompt. Sampling the model N times and scoring the samples produces DPO pairs without a human writing each one; a stronger model's answers do distillation the same way.
|
|
796
|
+
6. loop_label_trace / loop_remove_label / loop_list_labels - Carve the corpus into slices. A bare tag with a parent makes a master group; NAMED DIMENSIONS (category, task, language, anything you like) are what let ONE captured corpus become a DIFFERENT dataset for every task, because filters on different dimensions AND together and nothing has to be re-labelled.
|
|
797
|
+
7. loop_create_judge / loop_list_judges - A RUBRIC a model applies, for the question no rule can answer: was this actually helpful. The rubric doubles as a GRPO reward function, which makes prompts with no checkable answer trainable.
|
|
798
|
+
8. loop_start_judge_run then loop_take_judge_work then loop_post_judge_verdicts - Run a rubric over a slice. By default YOU call the model: take the rendered prompt, send it, post the scores back. A judge created with auto=true is run by the platform's agent instead, on the workspace's own serverless account. loop_list_judge_run_items says what happened to each conversation in a run and why a failed one failed, which the work list cannot: it hands out only what is still pending, so a finished run answers it with nothing. loop_stop_judge_run closes a run that will NOT finish, which is what unblocks the judge — an open run blocks the next one, and closing it by posting an empty verdict list with finish set writes 'done' on a pass that covered three of forty.
|
|
799
|
+
9. loop_create_grader / loop_list_graders - Deterministic rules that score answers with no person reading them, weighted so the criteria that matter count more, and aimable at one label so a rule about shipping does not mark down every billing answer.
|
|
800
|
+
10. loop_grade_trace - Apply those rules to an answer AND its samples in one pass. This is what closes the loop automatically.
|
|
801
|
+
11. loop_build_dataset / loop_create_build_rule / loop_list_build_rules / loop_delete_build_rule - Curate the feedback into an sft, dpo, grpo or kto training set, once or STANDING. Always reports why rows were left out. kto takes a bare thumbs-down, which the other three cannot use at all. A build rule is the same curation left running: it fires when enough NEW reviewed work exists, and last_reason says why it did not — a rule quiet because it is waiting reads exactly like one that is broken.
|
|
802
|
+
12. loop_agent_status / loop_enable_agent / loop_disable_agent - The AGENT: the worker that calls a model for the workspace (automatic judges, sample answers). It spends through the workspace's OWN serverless key, so every call is billed like one of theirs; enabling it is SPEND CONSENT and you say so first. Status tells you whether an agent exists in this environment at all.
|
|
803
|
+
13. loop_create_sample_run / loop_list_sample_runs - Have the agent write N alternative answers to each conversation in a slice and score them with a judge. This is how DPO pairs and GRPO groups are made with no human writing each one. State the upper bound of model calls (sample × n × 2) before starting.
|
|
804
|
+
14. loop_list_datasets - The sets built so far, each with how many rows it kept and why the rest were dropped.
|
|
805
|
+
15. loop_preview_dataset - Read the actual rows before anyone trains on them; each carries the address of the conversation it came from.
|
|
806
|
+
16. loop_stats - How much is recorded, how much is reviewed, and what each method could use right now.
|
|
807
|
+
17. loop_preflight_training_rule - PRICE a standing training rule without creating anything: the pinned revision, the worst hourly price each GPU ladder can reach, how many hours each ceiling buys, the refusals, and terms_text — the sentence the member is agreeing to, with their own figures in it. Always first.
|
|
808
|
+
18. loop_create_training_rule - Create it. SPEND CONSENT, and recorded: accept_terms is the member's signature on that sentence. Read the estimate and terms_text back VERBATIM, wait for an explicit yes, and never state a ceiling, a price cap or an hour count preflight did not return.
|
|
809
|
+
19. loop_list_training_rules / loop_get_training_rule / loop_update_training_rule / loop_consent_training_rule / loop_run_training_rule / loop_delete_training_rule - Read, edit, re-consent, fire now, retire. A money-bearing edit bumps the rule's revision, drops the consent and STOPS FUTURE FIRINGS until somebody accepts the new terms.
|
|
810
|
+
20. loop_list_training_runs / loop_get_training_run - What each firing did, its timeline, and available_actions — which says what this reader may actually do next, rather than what the state looks like it allows.
|
|
811
|
+
21. loop_get_evaluation / loop_list_evaluation_items / loop_get_judge_agreement - The comparison behind a verdict: the win rate and the per-dimension means, the paired conversations themselves, and how far the judge already agrees with this workspace's own reviewers. Read the warnings out, not just the verdict.
|
|
812
|
+
22. loop_promote_training_run / loop_reject_training_run / loop_rollback_training_run / loop_cancel_training_run - The decision. Promotion re-points the serving name at the candidate and changes what real customers get; it is undoable for 30 days.
|
|
813
|
+
23. loop_get_agent_settings / loop_update_agent_settings / loop_update_build_rule - The agent's default model, its system prompts and the monthly evaluation cap; and the standing curation rule a training rule draws from.
|
|
814
|
+
24. set_inference_alias / list_inference_aliases / delete_inference_alias - The re-pointable public handles a promotion writes: a name customers call that can be moved to different weights without them changing anything.
|
|
815
|
+
|
|
816
|
+
END TO END: capture what a model was asked and answered, have people or judges say whether it was right, write ONE training rule that curates the reviewed work and trains on it, let the platform compare the result against what serves today, read the comparison, then decide — promote, reject, or leave it. Everything before the rule costs nothing; the rule is where money starts, which is why it carries a recorded consent. Two fields carry the state a user actually asks about and BOTH must be read back in plain words rather than summarised as 'fine': last_reason says why a quiet rule is quiet, because a rule waiting for rows looks identical to a broken one; and accepted_revision against revision (with paused_reason) says whether the rule still has a consent to spend under, because a money-bearing edit pauses it until somebody agrees again.`;
|
|
715
817
|
const guides = {
|
|
716
818
|
overview: `# Run BiOS Fine-Tuning Platform
|
|
717
819
|
|
|
@@ -756,7 +858,7 @@ get_inference_status instead of asserting them.
|
|
|
756
858
|
- JWT token: Use \`Authorization: Bearer <token>\` header with \`X-Org-ID\` and \`X-Workspace-ID\` headers
|
|
757
859
|
- Call introspect_api_key to see your permissions, scopes, and which tools you can use
|
|
758
860
|
|
|
759
|
-
## Available MCP Tools (
|
|
861
|
+
## Available MCP Tools (${liveToolCount} in this build, listed in recommended order)
|
|
760
862
|
1. get_platform_guide - You're reading this! Context for all capabilities
|
|
761
863
|
2. introspect_api_key - Discover your API key's permissions, scopes, and bound workspace
|
|
762
864
|
3. get_wallet_balance - Check if you have enough credits before training
|
|
@@ -765,10 +867,11 @@ get_inference_status instead of asserting them.
|
|
|
765
867
|
6. list_supported_architectures - Check which architectures can be trained or served
|
|
766
868
|
7. get_training_capabilities - Read the authoritative training contract; call this before building automated payloads
|
|
767
869
|
8. get_gpu_pricing - See GPU options and pricing
|
|
768
|
-
9. get_recommended_gpu -
|
|
769
|
-
|
|
870
|
+
9. get_recommended_gpu - Read default-config sizing; use full preflight for the actual run
|
|
871
|
+
9a. recommend_training_config - Ask the trainer's sizing model for batch, accumulation, LR, predicted peak memory, minimum GPU count and wall clock on the GPU you picked; check your own batch size against it
|
|
872
|
+
10. list_integrations / search_huggingface_datasets / preview_huggingface_dataset - Choose the Hub account, dataset, subset and split
|
|
770
873
|
11. list_datasets / upload_dataset / import_huggingface_dataset - Prepare training data
|
|
771
|
-
12. preview_dataset - Verify dataset format before training
|
|
874
|
+
12. get_dataset_status / preview_dataset - Verify readiness and dataset format before training
|
|
772
875
|
13. delete_dataset - Remove a dataset you no longer need
|
|
773
876
|
14. preflight_training_job - Validate the full job before creating it
|
|
774
877
|
15. create_training_job - Launch the fine-tuning job
|
|
@@ -792,7 +895,38 @@ get_inference_status instead of asserting them.
|
|
|
792
895
|
8. update_inference_policy - Change explicit queue consent or the maximum accepted hourly price
|
|
793
896
|
9. stop_inference / resume_inference / restart_inference - Control serving lifecycle
|
|
794
897
|
10. chat_with_inference - Call a serverless catalog model by id or a dedicated deployment via the unified /v1 endpoint (optional streaming)
|
|
795
|
-
11. delete_inference - Permanently remove a deployment after verified infrastructure teardown
|
|
898
|
+
11. delete_inference - Permanently remove a deployment after verified infrastructure teardown
|
|
899
|
+
|
|
900
|
+
## Conscious Loop MCP Tools
|
|
901
|
+
The loop turns what a deployed model actually did into what it learns next.
|
|
902
|
+
NOTHING IS RECORDED until loop_set_capture turns a source on — capture is a
|
|
903
|
+
consent decision, not a default, and these tools cannot bypass it.
|
|
904
|
+
1. loop_set_capture - Turn recording on or off for a source and set how long conversations are kept. Start here; every other loop tool is inert until this is done.
|
|
905
|
+
2. loop_capture_trace / loop_import_rows - Record one exchange as it happens (what the model was asked, what it answered, any tool calls), whoever served the model — Run BiOS, another provider, or your own servers. loop_import_rows brings in data that already exists instead: an export, a spreadsheet of past answers, preference pairs. Imported rows arrive as conversations NEEDING REVIEW, never as finished training data.
|
|
906
|
+
3. loop_list_traces / loop_get_trace - Read what has been recorded. Credentials and personal details are stripped before storage, never after.
|
|
907
|
+
4. loop_add_signal - Record whether an answer was right. Append-only. Verdict 'edited' with a correction is the most valuable feedback there is.
|
|
908
|
+
5. loop_add_candidate / loop_list_candidates - Submit ALTERNATIVE answers to a recorded prompt. Sampling the model N times and scoring the samples produces DPO pairs without a human writing each one; a stronger model's answers do distillation the same way.
|
|
909
|
+
6. loop_label_trace / loop_remove_label / loop_list_labels - Carve the corpus into slices. A bare tag with a parent makes a master group; NAMED DIMENSIONS (category, task, language, anything you like) are what let ONE captured corpus become a DIFFERENT dataset for every task, because filters on different dimensions AND together and nothing has to be re-labelled.
|
|
910
|
+
7. loop_create_judge / loop_list_judges - A RUBRIC a model applies, for the question no rule can answer: was this actually helpful. The rubric doubles as a GRPO reward function, which makes prompts with no checkable answer trainable.
|
|
911
|
+
8. loop_start_judge_run then loop_take_judge_work then loop_post_judge_verdicts - Run a rubric over a slice. By default YOU call the model: take the rendered prompt, send it, post the scores back. A judge created with auto=true is run by the platform's agent instead, on the workspace's own serverless account. loop_list_judge_run_items says what happened to each conversation in a run and why a failed one failed, which the work list cannot: it hands out only what is still pending, so a finished run answers it with nothing. loop_stop_judge_run closes a run that will NOT finish, which is what unblocks the judge — an open run blocks the next one, and closing it by posting an empty verdict list with finish set writes 'done' on a pass that covered three of forty.
|
|
912
|
+
9. loop_create_grader / loop_list_graders - Deterministic rules that score answers with no person reading them, weighted so the criteria that matter count more, and aimable at one label so a rule about shipping does not mark down every billing answer.
|
|
913
|
+
10. loop_grade_trace - Apply those rules to an answer AND its samples in one pass. This is what closes the loop automatically.
|
|
914
|
+
11. loop_build_dataset / loop_create_build_rule / loop_list_build_rules / loop_delete_build_rule - Curate the feedback into an sft, dpo, grpo or kto training set, once or STANDING. Always reports why rows were left out. kto takes a bare thumbs-down, which the other three cannot use at all. A build rule is the same curation left running: it fires when enough NEW reviewed work exists, and last_reason says why it did not — a rule quiet because it is waiting reads exactly like one that is broken.
|
|
915
|
+
12. loop_agent_status / loop_enable_agent / loop_disable_agent - The AGENT: the worker that calls a model for the workspace (automatic judges, sample answers). It spends through the workspace's OWN serverless key, so every call is billed like one of theirs; enabling it is SPEND CONSENT and you say so first. Status tells you whether an agent exists in this environment at all.
|
|
916
|
+
13. loop_create_sample_run / loop_list_sample_runs - Have the agent write N alternative answers to each conversation in a slice and score them with a judge. This is how DPO pairs and GRPO groups are made with no human writing each one. State the upper bound of model calls (sample × n × 2) before starting.
|
|
917
|
+
14. loop_list_datasets - The sets built so far, each with how many rows it kept and why the rest were dropped.
|
|
918
|
+
15. loop_preview_dataset - Read the actual rows before anyone trains on them; each carries the address of the conversation it came from.
|
|
919
|
+
16. loop_stats - How much is recorded, how much is reviewed, and what each method could use right now.
|
|
920
|
+
17. loop_preflight_training_rule - PRICE a standing training rule without creating anything: the pinned revision, the worst hourly price each GPU ladder can reach, how many hours each ceiling buys, the refusals, and terms_text — the sentence the member is agreeing to, with their own figures in it. Always first.
|
|
921
|
+
18. loop_create_training_rule - Create it. SPEND CONSENT, and recorded: accept_terms is the member's signature on that sentence. Read the estimate and terms_text back VERBATIM, wait for an explicit yes, and never state a ceiling, a price cap or an hour count preflight did not return.
|
|
922
|
+
19. loop_list_training_rules / loop_get_training_rule / loop_update_training_rule / loop_consent_training_rule / loop_run_training_rule / loop_delete_training_rule - Read, edit, re-consent, fire now, retire. A money-bearing edit bumps the rule's revision, drops the consent and STOPS FUTURE FIRINGS until somebody accepts the new terms.
|
|
923
|
+
20. loop_list_training_runs / loop_get_training_run - What each firing did, its timeline, and available_actions — which says what this reader may actually do next, rather than what the state looks like it allows.
|
|
924
|
+
21. loop_get_evaluation / loop_list_evaluation_items / loop_get_judge_agreement - The comparison behind a verdict: the win rate and the per-dimension means, the paired conversations themselves, and how far the judge already agrees with this workspace's own reviewers. Read the warnings out, not just the verdict.
|
|
925
|
+
22. loop_promote_training_run / loop_reject_training_run / loop_rollback_training_run / loop_cancel_training_run - The decision. Promotion re-points the serving name at the candidate and changes what real customers get; it is undoable for 30 days.
|
|
926
|
+
23. loop_get_agent_settings / loop_update_agent_settings / loop_update_build_rule - The agent's default model, its system prompts and the monthly evaluation cap; and the standing curation rule a training rule draws from.
|
|
927
|
+
24. set_inference_alias / list_inference_aliases / delete_inference_alias - The re-pointable public handles a promotion writes: a name customers call that can be moved to different weights without them changing anything.
|
|
928
|
+
|
|
929
|
+
END TO END: capture what a model was asked and answered, have people or judges say whether it was right, write ONE training rule that curates the reviewed work and trains on it, let the platform compare the result against what serves today, read the comparison, then decide — promote, reject, or leave it. Everything before the rule costs nothing; the rule is where money starts, which is why it carries a recorded consent. Two fields carry the state a user actually asks about and BOTH must be read back in plain words rather than summarised as 'fine': last_reason says why a quiet rule is quiet, because a rule waiting for rows looks identical to a broken one; and accepted_revision against revision (with paused_reason) says whether the rule still has a consent to spend under, because a money-bearing edit pauses it until somebody agrees again.`,
|
|
796
930
|
quick_start: `# Quick Start Guide
|
|
797
931
|
|
|
798
932
|
## Fastest path to a fine-tuned model:
|
|
@@ -808,12 +942,15 @@ For beginners: start with meta-llama/Llama-3.1-8B-Instruct (good balance of qual
|
|
|
808
942
|
Option A: Upload your own file — call upload_dataset with the file path
|
|
809
943
|
Option B: Import from HuggingFace — call import_huggingface_dataset with a repo ID
|
|
810
944
|
|
|
811
|
-
Dataset
|
|
945
|
+
Dataset upload accepts one file type: JSONL. Each line is {"messages": [...]} for SFT (with a final assistant response), or {"text": "..."} for CPT. A Hugging Face split may be stored upstream as Parquet or Arrow, but its ROWS must already have the same canonical shape. Other layouts are refused with conversion guidance before the dataset can become Ready.
|
|
812
946
|
|
|
813
|
-
### Step 4:
|
|
814
|
-
Call get_model_config
|
|
947
|
+
### Step 4: Choose method, adapter, and the complete training configuration
|
|
948
|
+
Call get_model_config and get_training_capabilities. Choose SFT or CPT, adapter, sequence length, per-device batch, gradient accumulation, optimizer, precision, epochs/optional max_steps, validation and checkpoint settings BEFORE asking for a GPU. Empty max_steps means no cap: all configured epochs train. Early stopping defaults on when validation exists.
|
|
815
949
|
|
|
816
|
-
### Step 5:
|
|
950
|
+
### Step 5: Preflight the complete request and choose a GPU LAST
|
|
951
|
+
Call preflight_training_job with the model, dataset(s), method, adapter and complete config. Its GPU choices are sized from ALL of those inputs and joined to live inventory. Choose only a returned configuration; never guess a GPU count or use a choice below min_gpus.
|
|
952
|
+
|
|
953
|
+
### Step 6: Create the training job
|
|
817
954
|
Call create_training_job with:
|
|
818
955
|
- model: the model name from step 2
|
|
819
956
|
- dataset_id: the ID from step 3
|
|
@@ -821,7 +958,7 @@ Call create_training_job with:
|
|
|
821
958
|
- adapter: "lora" (fastest, recommended for first jobs)
|
|
822
959
|
- Leave other params as defaults unless you have specific needs
|
|
823
960
|
|
|
824
|
-
### Step
|
|
961
|
+
### Step 7: Monitor progress
|
|
825
962
|
create_training_job books a GPU before returning (~40s). A 'booked' status means the GPU is secured (booked==secured) and training is starting; 'securing' means it is still booking — poll get_training_status until 'booked'/'running'. A booking-time capacity miss returns CAPACITY_UNAVAILABLE and no job is created.
|
|
826
963
|
Call get_training_status periodically. Training typically takes 1-4 hours for small models.
|
|
827
964
|
If a started job loses its pod it rests at 'interrupted' (billing already stopped, not 'failed') — call resume_training_job to continue from the last checkpoint.
|
|
@@ -891,11 +1028,23 @@ Example JSONL:
|
|
|
891
1028
|
- ShareGPT conversations (from/value) -> role/content with human -> user, gpt -> assistant
|
|
892
1029
|
- Preference pairs (chosen/rejected) and prompt-only RL formats are not accepted for training.
|
|
893
1030
|
|
|
1031
|
+
## Discover, preview and import
|
|
1032
|
+
1. Call list_integrations to choose a connected account, or use public Hub access.
|
|
1033
|
+
2. Call search_huggingface_datasets; use has_more and page for additional results.
|
|
1034
|
+
3. Call preview_huggingface_dataset. Choose from available_configs/available_splits and repeat the preview with that exact subset/split.
|
|
1035
|
+
4. Preserve integration_id, subset and split in import_huggingface_dataset. An explicit revision pins the import to that source; otherwise the service resolves and stores an immutable commit when importing.
|
|
1036
|
+
5. Poll get_dataset_status until ready. A returned dataset ID or temporarily unavailable preview does not mean validation succeeded. Preview the registered dataset before training, especially when importing an explicit older revision.
|
|
1037
|
+
|
|
1038
|
+
## Composition and resume
|
|
1039
|
+
- dataset_ids is an ordered array. Sequential mode keeps that order; shuffle randomizes it with a recorded seed (default 42).
|
|
1040
|
+
- mixing supplies mode, seed, weights and optional phases. Weight positions match dataset_ids exactly. Use phased for an ordered curriculum and interleave for weighted draws.
|
|
1041
|
+
- Preflight and create receive the same plan. Source pins, plan and checksum are stored; resume rebuilds data from metadata and verifies it rather than reusing a composed-data cache.
|
|
1042
|
+
|
|
894
1043
|
## Best Practices
|
|
895
|
-
-
|
|
896
|
-
- Maximum 500MB file size through
|
|
897
|
-
-
|
|
898
|
-
-
|
|
1044
|
+
- Keep a representative held-out validation set; quality matters more than a fixed row-count target.
|
|
1045
|
+
- Maximum 500MB file size through the current MCP upload tool.
|
|
1046
|
+
- Keep row structure consistent; remove unintended duplicates and low-quality examples.
|
|
1047
|
+
- SFT defaults to truncating oldest context while retaining a trainable answer; unusable samples are dropped and reported. CPT packs raw text and has no per-sample truncate/drop option.`,
|
|
899
1048
|
training_methods: `# Training Methods
|
|
900
1049
|
|
|
901
1050
|
## SFT — Supervised Fine-Tuning ⭐ Most Common
|
|
@@ -909,49 +1058,30 @@ Best for: Injecting domain knowledge into the base model before fine-tuning.
|
|
|
909
1058
|
Input: Raw text documents.
|
|
910
1059
|
When to use: Before SFT, when your domain has specialized vocabulary/knowledge.
|
|
911
1060
|
|
|
912
|
-
##
|
|
913
|
-
|
|
914
|
-
|
|
1061
|
+
## Current limits
|
|
1062
|
+
- The current tools expose SFT and text-corpus CPT with full, LoRA and QLoRA; read get_training_capabilities for deployment-specific restrictions.
|
|
1063
|
+
- A VLM architecture does not imply support for image-only pretraining or multimodal preference objectives.
|
|
1064
|
+
- DPO, PPO and GRPO are not enabled by these training tools. They require separately verified training implementations; PPO/GRPO also require rollout inference and reward handling.
|
|
1065
|
+
- Training starts from catalog models. Do not promise a CPT-to-SFT pipeline using a customer's trained checkpoint as a new training source: that workflow is not exposed here.`,
|
|
915
1066
|
gpu_selection: `# GPU Selection Guide
|
|
916
1067
|
|
|
917
|
-
##
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
-
|
|
934
|
-
- 30B-70B models: A100 80GB
|
|
935
|
-
- 70B+ models: H100 or multi-GPU A100
|
|
936
|
-
|
|
937
|
-
### Full Fine-Tuning (no adapter, maximum quality)
|
|
938
|
-
- 1B-3B models: A100 80GB
|
|
939
|
-
- 7B models: 2-4x A100 80GB
|
|
940
|
-
- 13B+ models: 4-8x A100 or H100
|
|
941
|
-
|
|
942
|
-
## Cost Optimization Tips
|
|
943
|
-
- Run BiOS bills per-SECOND, not per-hour — stop jobs early to save money
|
|
944
|
-
- LoRA training is 5-10x cheaper than full fine-tune for comparable quality
|
|
945
|
-
- QLoRA adds ~10% training time but halves VRAM needs
|
|
946
|
-
- Start with fewer epochs (1-2) to validate, then train full if results look good
|
|
947
|
-
- Use the get_recommended_gpu tool to get the optimal GPU for your specific setup
|
|
948
|
-
|
|
949
|
-
## Multi-GPU
|
|
950
|
-
For models that don't fit in a single GPU's VRAM, the platform automatically shards across GPUs.
|
|
951
|
-
Specify gpu_count in create_training_job (default: 1).
|
|
952
|
-
|
|
953
|
-
## Infrastructure and Price Caps
|
|
954
|
-
The platform selects and manages the underlying infrastructure automatically. You choose only the GPU type, the GPU count, and a maximum total hourly price cap. Preflight returns ranked alternatives and the exact price that will be honored. A price above the approved cap is rejected rather than silently accepted.`,
|
|
1068
|
+
## Configure first, choose the GPU last
|
|
1069
|
+
1. Select the catalog model and revision, method, adapter and compatible datasets.
|
|
1070
|
+
2. Set supported configuration fields from get_training_capabilities: sequence length, batch and accumulation, validation and run-length bounds. Review effective precision/optimizer settings; leave disabled engine-managed controls such as activation checkpointing to the engine.
|
|
1071
|
+
3. The current preflight API requires a concrete gpu_type/gpu_count. Use get_recommended_gpu only to obtain a provisional candidate, then call preflight_training_job with that candidate and the complete configuration. Never treat default-config sizing as approval of the final run.
|
|
1072
|
+
4. Review the service-calculated minimum, supported counts and current quote. Choose only a qualifying available type/count. Fit is not stock, and an unknown availability result is not permission to book.
|
|
1073
|
+
5. Re-run preflight after changing any sizing input or GPU choice; create only after valid=true with the final selection. GPU-free preflight discovery is not currently supported by the deployed API.
|
|
1074
|
+
|
|
1075
|
+
## Booking and recovery
|
|
1076
|
+
- Create rechecks capacity. Do not report a run as started while it is only securing; booked means capacity was accepted, not that the trainer has begun executing steps.
|
|
1077
|
+
- A capacity miss must show fresh qualifying alternatives and preserve the configuration. Ask before substituting hardware or exceeding a price cap.
|
|
1078
|
+
- Queueing is explicit consent and applies only when no qualifying alternative is available. Waiting is unbilled.
|
|
1079
|
+
- Reuse the idempotency key after timeouts instead of creating a second run.
|
|
1080
|
+
|
|
1081
|
+
## Memory and quality
|
|
1082
|
+
- Memory depends on the full configuration and input shapes, not parameter count alone. Do not invent a smaller minimum or claim that quantization always halves total memory.
|
|
1083
|
+
- Full fine-tuning is not a guarantee of higher accuracy than adapters. Use representative validation to compare outcomes.
|
|
1084
|
+
- Preflight admission is not proof of universal freedom from hardware faults or OOM. Report actual tested configurations and measured headroom.`,
|
|
955
1085
|
inference: `# Model Inference Guide
|
|
956
1086
|
|
|
957
1087
|
Deployments serve either a verified fine-tuning checkpoint or a base model from the catalog through an OpenAI-compatible endpoint.
|
|
@@ -1241,8 +1371,8 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1241
1371
|
// ("author/name"), so folding the provider into q turns it into a real
|
|
1242
1372
|
// author filter instead of a silently ignored parameter.
|
|
1243
1373
|
const q = [provider, query].filter(Boolean).join(" ").trim() || undefined;
|
|
1244
|
-
const data = await client.api(
|
|
1245
|
-
params: { q, max_params },
|
|
1374
|
+
const data = await client.api(MODEL_CATALOG_PATH, {
|
|
1375
|
+
params: { q, max_params: billionsParam(max_params) },
|
|
1246
1376
|
});
|
|
1247
1377
|
return json(data);
|
|
1248
1378
|
});
|
|
@@ -1257,7 +1387,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1257
1387
|
limit: z.number().int().min(1).max(60).optional().describe("Page size (default 24, max 60)."),
|
|
1258
1388
|
offset: z.number().int().min(0).optional().describe("Pagination offset (default 0)."),
|
|
1259
1389
|
}, async ({ type, sort, limit, offset }) => {
|
|
1260
|
-
const data = await client.api(
|
|
1390
|
+
const data = await client.api(MODEL_CATALOG_PATH, {
|
|
1261
1391
|
params: {
|
|
1262
1392
|
type: type && type !== "all" ? type : undefined,
|
|
1263
1393
|
sort,
|
|
@@ -1382,7 +1512,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1382
1512
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1383
1513
|
/* TOOL: get_recommended_gpu */
|
|
1384
1514
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1385
|
-
server.tool("get_recommended_gpu", "Get authoritative, model-aware GPU options from the training control plane. Returns required counts, live availability, total hourly prices, the cheapest currently bookable recommendation, and compatible alternatives when no GPU is available. It does not invent a duration or total-cost estimate.", {
|
|
1515
|
+
server.tool("get_recommended_gpu", "Get authoritative, model-aware GPU options from the training control plane. Returns required counts, live availability, total hourly prices, the cheapest currently bookable recommendation, and compatible alternatives when no GPU is available. Pass effective_batch (samples per optimizer step -- the quality decision, 64 is standard) and max_length, and every option also carries the per-device split that runs it there: recommended_micro_batch x recommended_grad_accum x required_count. Use that split as per_device_train_batch_size / gradient_accumulation_steps when creating the job. It does not invent a duration or total-cost estimate.", {
|
|
1386
1516
|
model: z.string().describe("Full model name (e.g., 'meta-llama/Llama-3.1-8B-Instruct')"),
|
|
1387
1517
|
adapter: z.enum(TRAINING_ADAPTERS).optional().describe("Adapter type (default: 'lora')."),
|
|
1388
1518
|
method: z.enum(["sft", "pt"]).optional().describe("Canonical training method."),
|
|
@@ -1392,6 +1522,9 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1392
1522
|
.positive()
|
|
1393
1523
|
.optional()
|
|
1394
1524
|
.describe("Optional active-parameter hint for MoE models in billions."),
|
|
1525
|
+
max_length: z.number().int().min(128).max(262144).optional().describe("Training sequence length the sizing assumes (tokens)."),
|
|
1526
|
+
effective_batch: z.number().int().min(1).max(4096).optional().describe("Samples per optimizer step; each option returns its per-device split for this."),
|
|
1527
|
+
per_device_train_batch_size: z.number().int().min(1).optional().describe("Size a typed per-device batch as-is instead (ignored when effective_batch is set)."),
|
|
1395
1528
|
}, async (params) => {
|
|
1396
1529
|
const data = await client.api("/api/training/gpu-options", {
|
|
1397
1530
|
params: buildGPUOptionsParams(params),
|
|
@@ -1399,6 +1532,50 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1399
1532
|
return json(data);
|
|
1400
1533
|
});
|
|
1401
1534
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1535
|
+
/* TOOL: recommend_training_config */
|
|
1536
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1537
|
+
server.tool("recommend_training_config", "Ask the trainer's own sizing model what to run for a model on a GPU type: per-device batch size, gradient accumulation, learning rate, warmup, activation checkpointing, predicted peak GPU memory, the minimum GPU count that fits, a wall-clock estimate, and a verdict on the batch size you intend to use (with a same-global-batch alternative when it will not fit). Every number carries its basis (measured, derived, judgement). Side-effect free. When 'available' is false no advisor is deployed in this environment and no recommendation is returned -- do not treat that as 'anything fits'.", {
|
|
1538
|
+
model: z.string().describe("Full model name (e.g., 'Qwen/Qwen3.6-27B')"),
|
|
1539
|
+
gpu_type: z.string().describe("GPU type from get_recommended_gpu (e.g., 'B300_288GB')"),
|
|
1540
|
+
gpu_count: z.number().int().min(1).max(8).optional().describe("GPUs you intend to book (default 1)."),
|
|
1541
|
+
adapter: z.enum(TRAINING_ADAPTERS).optional().describe("Adapter type (default: 'lora')."),
|
|
1542
|
+
method: z.enum(["sft", "pt"]).optional().describe("Canonical training method (default: 'sft')."),
|
|
1543
|
+
max_length: z.number().int().min(128).max(262144).optional().describe("Training sequence length (default 2048)."),
|
|
1544
|
+
num_train_epochs: z.number().positive().optional().describe("Epochs, for the wall-clock estimate."),
|
|
1545
|
+
max_steps: z.number().int().positive().optional().describe("Step cap, if you set one."),
|
|
1546
|
+
per_device_train_batch_size: z.number().int().min(1).optional().describe("Your intended per-device batch; judged against the model."),
|
|
1547
|
+
gradient_accumulation_steps: z.number().int().min(1).optional(),
|
|
1548
|
+
dataset_ids: z.array(z.string()).max(20).optional().describe("Datasets the job will train on; their measured token statistics feed the sizing."),
|
|
1549
|
+
model_revision: z.string().optional(),
|
|
1550
|
+
}, async (params) => {
|
|
1551
|
+
const q = { model_id: params.model, gpu_type: params.gpu_type };
|
|
1552
|
+
if (params.gpu_count !== undefined)
|
|
1553
|
+
q.gpu_count = String(params.gpu_count);
|
|
1554
|
+
if (params.adapter)
|
|
1555
|
+
q.train_type = params.adapter;
|
|
1556
|
+
if (params.method)
|
|
1557
|
+
q.method = params.method;
|
|
1558
|
+
if (params.max_length !== undefined)
|
|
1559
|
+
q.max_length = String(params.max_length);
|
|
1560
|
+
if (params.num_train_epochs !== undefined)
|
|
1561
|
+
q.num_train_epochs = String(params.num_train_epochs);
|
|
1562
|
+
if (params.max_steps !== undefined)
|
|
1563
|
+
q.max_steps = String(params.max_steps);
|
|
1564
|
+
if (params.per_device_train_batch_size !== undefined)
|
|
1565
|
+
q.per_device_train_batch_size = String(params.per_device_train_batch_size);
|
|
1566
|
+
if (params.gradient_accumulation_steps !== undefined)
|
|
1567
|
+
q.gradient_accumulation_steps = String(params.gradient_accumulation_steps);
|
|
1568
|
+
if (params.dataset_ids?.length) {
|
|
1569
|
+
for (const id of params.dataset_ids)
|
|
1570
|
+
validateId(id, "dataset_ids");
|
|
1571
|
+
q.dataset_ids = params.dataset_ids.join(",");
|
|
1572
|
+
}
|
|
1573
|
+
if (params.model_revision)
|
|
1574
|
+
q.model_revision = params.model_revision;
|
|
1575
|
+
const data = await client.api("/api/training/recommend", { params: q });
|
|
1576
|
+
return json(data);
|
|
1577
|
+
});
|
|
1578
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1402
1579
|
/* TOOL: get_inference_gpu_options */
|
|
1403
1580
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1404
1581
|
server.tool("get_inference_gpu_options", "Get inference model-fit GPU choices joined to the authoritative deployment inventory and pricing snapshot. PREFER model-addressed sizing: pass model=<model_id> (optionally revision / hf_integration_id) and the SERVER resolves the facts (vision-aware) and returns computed min_gpus, valid_counts, and bookable_counts — the exact minimums the create gate enforces. Returns valid tensor-parallel counts, exact total hourly prices, availability as available/out_of_stock/unknown, and ranked alternatives. Never interpret unknown inventory as available, never offer a count below min_gpus or outside bookable_counts, and never substitute a SKU without user approval.", {
|
|
@@ -1760,8 +1937,14 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1760
1937
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1761
1938
|
/* TOOL: list_datasets */
|
|
1762
1939
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1763
|
-
server.tool("list_datasets", "
|
|
1764
|
-
|
|
1940
|
+
server.tool("list_datasets", "Read one page of workspace datasets with metadata and readiness. Use offset/limit for more results; list membership does not mean the dataset is compatible with every training method.", {
|
|
1941
|
+
query: z.string().max(256).optional(),
|
|
1942
|
+
status: z.string().max(64).optional(),
|
|
1943
|
+
dataset_type: z.string().max(64).optional(),
|
|
1944
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
1945
|
+
offset: z.number().int().min(0).optional(),
|
|
1946
|
+
}, async ({ query, status, dataset_type, limit, offset }) => {
|
|
1947
|
+
const data = await client.api("/api/datasets", { params: { q: query, status, dataset_type, limit: String(limit ?? 50), offset: String(offset ?? 0) } });
|
|
1765
1948
|
return json(data);
|
|
1766
1949
|
});
|
|
1767
1950
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
@@ -1773,7 +1956,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1773
1956
|
// server-side (403 DATASETS_COMING_SOON) as the authority. At launch the flag
|
|
1774
1957
|
// opens and the registration returns verbatim.
|
|
1775
1958
|
if (!launchGates.datasets) {
|
|
1776
|
-
server.tool("upload_dataset", "Upload
|
|
1959
|
+
server.tool("upload_dataset", "Upload one canonical JSONL dataset (maximum 500MB): one JSON object per line, with a messages array and final assistant response for SFT, or one non-empty text field for CPT. JSON arrays, CSV, Parquet, TXT, Alpaca, ShareGPT, prompt/completion, question/answer and preference-pair layouts are refused with conversion guidance. The server validates the rows before the dataset becomes Ready or selectable for training. Returns the dataset ID needed for create_training_job.", {
|
|
1777
1960
|
file_path: z.string().describe("Absolute path to the dataset file on disk"),
|
|
1778
1961
|
name: z.string().optional().describe("Display name for the dataset (defaults to filename)"),
|
|
1779
1962
|
}, async ({ file_path, name }) => {
|
|
@@ -1819,7 +2002,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1819
2002
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1820
2003
|
server.tool("preview_dataset", "Preview the first rows of a dataset to inspect its structure, column names, and content before training. Useful for verifying the dataset format is correct.", {
|
|
1821
2004
|
dataset_id: z.string().describe("Dataset ID (e.g., 'ds_abc123')"),
|
|
1822
|
-
rows: z.number().optional().describe("Number of rows to preview (default: 10, max: 50)"),
|
|
2005
|
+
rows: z.number().int().min(1).max(50).optional().describe("Number of rows to preview (default: 10, max: 50)"),
|
|
1823
2006
|
}, async ({ dataset_id, rows }) => {
|
|
1824
2007
|
validateId(dataset_id, "dataset_id");
|
|
1825
2008
|
const data = await client.api(`/api/datasets/${dataset_id}/preview`, {
|
|
@@ -1838,8 +2021,41 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1838
2021
|
return result(`Dataset ${dataset_id} deleted successfully.`);
|
|
1839
2022
|
});
|
|
1840
2023
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1841
|
-
/* TOOL:
|
|
2024
|
+
/* TOOL: dataset discovery and status */
|
|
1842
2025
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2026
|
+
server.tool("get_dataset_status", "Read dataset import/upload readiness, progress and validation failure. Do not select the dataset for training until status is ready; a dataset ID alone does not prove validation completed.", { dataset_id: z.string().min(1).max(128) }, async ({ dataset_id }) => {
|
|
2027
|
+
validateId(dataset_id, "dataset_id");
|
|
2028
|
+
return json(await client.api(`/api/datasets/${dataset_id}/status`));
|
|
2029
|
+
});
|
|
2030
|
+
server.tool("search_huggingface_datasets", "Search a bounded page of public Hub datasets, or browse with a selected connected account. Use list_integrations to choose the account; private/gated visibility depends on its permissions. Preserve integration_id for preview and import. Public search needs datasets:read; connected browsing needs integrations:read.", {
|
|
2031
|
+
query: z.string().max(256).optional(),
|
|
2032
|
+
integration_id: z.string().min(1).max(128).optional(),
|
|
2033
|
+
page: z.number().int().min(1).max(100).optional(),
|
|
2034
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
2035
|
+
sort: z.enum(["downloads", "likes"]).optional(),
|
|
2036
|
+
}, async ({ query, integration_id, page, limit, sort }) => {
|
|
2037
|
+
if (integration_id)
|
|
2038
|
+
validateId(integration_id, "integration_id");
|
|
2039
|
+
return json(await client.api(integration_id ? `/api/datasets/integrations/${integration_id}/browse` : "/api/datasets/hub-search", {
|
|
2040
|
+
params: {
|
|
2041
|
+
[integration_id ? "search" : "q"]: query,
|
|
2042
|
+
page: String(page ?? 1), limit: String(limit ?? 20), sort: sort ?? "downloads",
|
|
2043
|
+
},
|
|
2044
|
+
}));
|
|
2045
|
+
});
|
|
2046
|
+
server.tool("preview_huggingface_dataset", "Preview a Hub dataset before importing. Returns available configs/subsets and splits plus sample-format validation. If configuration selection is required, repeat with one returned subset and split. Use the same integration_id as search/import; authentication failures never fall back to public access. A preview outage is not proof of valid data.", {
|
|
2047
|
+
repo_id: z.string().min(1).max(256),
|
|
2048
|
+
integration_id: z.string().min(1).max(128).optional(),
|
|
2049
|
+
subset: z.string().max(256).optional(),
|
|
2050
|
+
split: z.string().min(1).max(128).optional(),
|
|
2051
|
+
rows: z.number().int().min(1).max(20).optional(),
|
|
2052
|
+
}, async ({ repo_id, integration_id, subset, split, rows }) => {
|
|
2053
|
+
if (integration_id)
|
|
2054
|
+
validateId(integration_id, "integration_id");
|
|
2055
|
+
return json(await client.api("/api/datasets/hub-preview", {
|
|
2056
|
+
params: { dataset_id: repo_id, integration_id, subset, split, limit: String(rows ?? 10), soft_errors: "true" },
|
|
2057
|
+
}));
|
|
2058
|
+
});
|
|
1843
2059
|
server.tool("list_integrations", "List the integrations connected to your workspace, such as Hugging Face accounts. Returns each integration's ID, label, provider type, and status. Use an integration ID wherever a tool accepts integration_id: importing HuggingFace datasets. This is how you pick WHICH connected account an import should use when several are connected. This read is workspace-scoped: without workspace context it fails with WORKSPACE_CONTEXT_REQUIRED, which is a configuration fix (BIOS_WORKSPACE_ID or a workspace-bound API key), not something to retry.", {}, async () => {
|
|
1844
2060
|
const data = await client.api("/api/datasets/integrations");
|
|
1845
2061
|
return json(data);
|
|
@@ -1851,27 +2067,34 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1851
2067
|
// gated, exactly like upload_dataset above; the service's 403
|
|
1852
2068
|
// DATASETS_COMING_SOON stays the authority.
|
|
1853
2069
|
if (!launchGates.datasets) {
|
|
1854
|
-
server.tool("import_huggingface_dataset", "Import
|
|
2070
|
+
server.tool("import_huggingface_dataset", "Import the Hub dataset, subset and split selected through search/preview. Omit integration_id only for anonymous public access. The source is revision-pinned and either referenced or materialized in the background; poll get_dataset_status until ready before training. Canonical messages rows are required for SFT and text rows for CPT. This does not enable preference/RL training.", {
|
|
1855
2071
|
repo_id: z
|
|
1856
|
-
.string()
|
|
2072
|
+
.string().min(1).max(256)
|
|
1857
2073
|
.describe("HuggingFace dataset repo whose rows already carry a messages array (SFT) or a text field (CPT)"),
|
|
1858
2074
|
integration_id: z
|
|
1859
|
-
.string()
|
|
1860
|
-
.describe("
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
|
|
2075
|
+
.string().min(1).max(128).optional()
|
|
2076
|
+
.describe("Connected account ID; retain it from search/preview for private or gated datasets"),
|
|
2077
|
+
subset: z.string().max(256).optional().describe("Exact subset/config selected from the preview response"),
|
|
2078
|
+
split: z.string().min(1).max(128).optional().describe("Exact selected dataset split (default: 'train')"),
|
|
2079
|
+
revision: z.string().min(1).max(256).optional().describe("Requested source commit/tag/branch; the service records an immutable commit for training and resume"),
|
|
2080
|
+
max_samples: z.number().int().positive().optional().describe("Max rows to import (imports all if omitted)"),
|
|
2081
|
+
sample_strategy: z.enum(["first", "random"]).optional(),
|
|
2082
|
+
import_mode: z.enum(["auto", "reference", "materialize"]).optional(),
|
|
2083
|
+
name: z.string().max(255).optional().describe("Display name for the imported dataset"),
|
|
2084
|
+
}, async ({ repo_id, integration_id, subset, split, revision, max_samples, sample_strategy, import_mode, name }) => {
|
|
2085
|
+
const common = { name: name ?? repo_id.split("/").pop(), max_samples, sample_strategy, import_mode };
|
|
2086
|
+
if (integration_id) {
|
|
2087
|
+
validateId(integration_id, "integration_id");
|
|
2088
|
+
return json(await client.api(`/api/datasets/integrations/${integration_id}/import`, {
|
|
2089
|
+
method: "POST", body: { ...common, dataset_id: repo_id, subset, split: split ?? "train", revision },
|
|
2090
|
+
}));
|
|
2091
|
+
}
|
|
2092
|
+
const workspaceId = client.workspaceId || (await client.api("/api/api-keys/introspect"))?.workspace?.id;
|
|
2093
|
+
if (!workspaceId)
|
|
2094
|
+
throw new Error("Workspace context is required: configure RUNBIOS_WORKSPACE_ID or use a workspace-bound API key.");
|
|
2095
|
+
return json(await client.api("/api/datasets/register-hf", {
|
|
2096
|
+
method: "POST", body: { ...common, workspace_id: workspaceId, hf_dataset_id: repo_id, hf_subset: subset, hf_split: split ?? "train", hf_revision: revision },
|
|
2097
|
+
}));
|
|
1875
2098
|
});
|
|
1876
2099
|
}
|
|
1877
2100
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
@@ -1893,7 +2116,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1893
2116
|
model: z.string().describe("Base model id from the Run BiOS catalog (e.g., 'meta-llama/Llama-3.1-8B-Instruct'). Must be hosted on Run BiOS — pick from list_models/search_models. Training always starts from a catalog base model; training FROM a fine-tuned model (adapter stacking) is not supported."),
|
|
1894
2117
|
model_revision: z.string().max(256).optional().describe("Requested model branch, tag, or commit. Copy the exact 40-hex value returned in canonical_request into create."),
|
|
1895
2118
|
dataset_id: z.string().optional(),
|
|
1896
|
-
dataset_ids: z.array(z.string()).min(1).optional(),
|
|
2119
|
+
dataset_ids: z.array(z.string()).min(1).max(64).optional(),
|
|
1897
2120
|
method: z.enum(TRAINING_METHODS),
|
|
1898
2121
|
adapter: z.enum(TRAINING_ADAPTERS).optional(),
|
|
1899
2122
|
gpu_type: z.string().optional(),
|
|
@@ -1915,8 +2138,9 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1915
2138
|
num_checkpoints: z.number().int().min(1).max(100).optional(),
|
|
1916
2139
|
integration_id: z.string().optional(),
|
|
1917
2140
|
network_volume_id: z.string().optional(),
|
|
1918
|
-
cache_dataset: z.boolean().optional(),
|
|
1919
2141
|
dataset_mixing: z.enum(["shuffle", "sequential", "interleave"]).optional(),
|
|
2142
|
+
mixing: datasetMixingSchema.optional().describe("Composition plan. Dataset IDs define source order and weight positions; default seed is 42. The source pins, plan and checksum are retained for metadata-only resume."),
|
|
2143
|
+
...trainingConfigurationSchema,
|
|
1920
2144
|
config: z.record(z.string(), z.unknown()).optional(),
|
|
1921
2145
|
}, async (params) => {
|
|
1922
2146
|
await assertModelHosted(params.model, "preflight_training_job");
|
|
@@ -1933,7 +2157,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1933
2157
|
// server-side (403 TRAINING_COMING_SOON before any billing or booking) as the
|
|
1934
2158
|
// authority. At launch the flag opens and the registration returns verbatim.
|
|
1935
2159
|
if (!launchGates.training) {
|
|
1936
|
-
server.tool("create_training_job", "Create a
|
|
2160
|
+
server.tool("create_training_job", "Create a training job using the same BOOK-BEFORE-REVEAL contract as the web UI. Call preflight_training_job first with the complete model/method/adapter/dataset/config request, then choose one returned GPU configuration: GPU is last because its minimum depends on every earlier choice. The call does not reveal a run as started until a real pod is secured; status is booked (secured) or securing (poll get_training_status). A booking-time miss returns CAPACITY_UNAVAILABLE with fresh qualifying available_gpus and no job/no charge: choose one and retry with the same setup. The rejected GPU type is never recommended back. Queue consent is valid only when available_gpus is empty; waiting is unbilled and expires automatically. Reuse one idempotency key after timeouts.", {
|
|
1937
2161
|
model: z.string().describe("Base model id from the Run BiOS catalog (e.g., 'meta-llama/Llama-3.1-8B-Instruct'). Must be hosted on Run BiOS — pick from list_models/search_models. Training always starts from a catalog base model; training FROM a fine-tuned model (adapter stacking) is not supported."),
|
|
1938
2162
|
model_revision: z.string().max(256).optional().describe("Exact model_revision from preflight canonical_request; a branch or tag is accepted but resolved again before any paid mutation."),
|
|
1939
2163
|
idempotency_key: z
|
|
@@ -1944,10 +2168,10 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1944
2168
|
.optional()
|
|
1945
2169
|
.describe("Stable retry key. Reuse it after a timeout; omit to generate one for this tool call."),
|
|
1946
2170
|
dataset_id: z.string().optional().describe("Compatibility field for one dataset ID."),
|
|
1947
|
-
dataset_ids: z.array(z.string()).min(1).optional().describe("One or more dataset IDs to compose for training."),
|
|
2171
|
+
dataset_ids: z.array(z.string()).min(1).max(64).optional().describe("One or more dataset IDs to compose for training."),
|
|
1948
2172
|
method: z.enum(TRAINING_METHODS).describe("Training method: sft, or pt/cpt for continued pre-training."),
|
|
1949
2173
|
adapter: z.enum(TRAINING_ADAPTERS).optional().describe("Any adapter supported by the Run BiOS contract."),
|
|
1950
|
-
gpu_type: z.string().optional().describe("GPU type
|
|
2174
|
+
gpu_type: z.string().optional().describe("GPU type returned by preflight_training_job after the complete training configuration is known. Do not guess or use a pre-configuration recommendation."),
|
|
1951
2175
|
gpu_count: z.number().int().min(1).max(8).optional().describe("Number of GPUs for the primary choice."),
|
|
1952
2176
|
gpu_priorities: z
|
|
1953
2177
|
.array(z.object({
|
|
@@ -1964,29 +2188,11 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
1964
2188
|
max_price_hour_cents: priceCapSchema(true),
|
|
1965
2189
|
storage_gb: z.number().int().min(1).max(10000).optional(),
|
|
1966
2190
|
num_checkpoints: z.number().int().min(1).max(100).optional(),
|
|
1967
|
-
epochs: z.number().positive().max(1000).optional().describe("Number of training epochs."),
|
|
1968
|
-
learning_rate: z.number().positive().max(1).optional().describe("Learning rate."),
|
|
1969
|
-
batch_size: z.number().int().min(1).max(65536).optional().describe("Per-device train batch size."),
|
|
1970
|
-
gradient_accumulation_steps: z.number().int().min(1).max(65536).optional(),
|
|
1971
|
-
max_seq_length: trainingSeqLengthSchema(),
|
|
1972
|
-
lora_rank: z.number().int().min(1).max(4096).optional(),
|
|
1973
|
-
lora_alpha: z.number().positive().max(1_000_000).optional(),
|
|
1974
|
-
warmup_ratio: z.number().min(0).max(1).optional(),
|
|
1975
|
-
weight_decay: z.number().min(0).max(10).optional(),
|
|
1976
|
-
scheduler: z
|
|
1977
|
-
.enum(["linear", "cosine", "cosine_with_restarts", "polynomial", "constant", "constant_with_warmup"])
|
|
1978
|
-
.optional()
|
|
1979
|
-
.describe("Learning-rate scheduler."),
|
|
1980
|
-
job_name: z.string().max(255).optional(),
|
|
1981
|
-
eval_split_ratio: z.number().min(0).max(0.99).optional(),
|
|
1982
|
-
early_stopping_patience: z.number().int().min(0).max(20).optional()
|
|
1983
|
-
.describe("Stop after N evaluations without a better validation loss (0 = off; default 3 when eval_split_ratio is set). The best checkpoint is always saved."),
|
|
1984
|
-
overlong_policy: z.enum(["truncate", "drop"]).optional()
|
|
1985
|
-
.describe("Samples longer than max_seq_length: 'truncate' keeps the last tokens so the answer is trained (default); 'drop' skips them."),
|
|
1986
2191
|
integration_id: z.string().optional(),
|
|
1987
2192
|
network_volume_id: z.string().optional(),
|
|
1988
|
-
cache_dataset: z.boolean().optional(),
|
|
1989
2193
|
dataset_mixing: z.enum(["shuffle", "sequential", "interleave"]).optional(),
|
|
2194
|
+
mixing: datasetMixingSchema.optional().describe("Composition plan. Dataset IDs define source order and weight positions; default seed is 42. The source pins, plan and checksum are retained for metadata-only resume."),
|
|
2195
|
+
...trainingConfigurationSchema,
|
|
1990
2196
|
config: z.record(z.string(), z.unknown()).optional().describe("Additional Run BiOS training configuration fields."),
|
|
1991
2197
|
}, async (params) => {
|
|
1992
2198
|
await assertModelHosted(params.model, "create_training_job");
|
|
@@ -2002,11 +2208,11 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
2002
2208
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2003
2209
|
/* TOOL: list_training_jobs */
|
|
2004
2210
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2005
|
-
server.tool("list_training_jobs", "List
|
|
2211
|
+
server.tool("list_training_jobs", "List training jobs with their status, progress percentage, base model, training method, GPU type, cost so far, and timestamps. The default list holds running, in-progress and completed jobs only: stopped runs (ended because someone asked; each carries stop_origin 'user' or 'platform') are returned only with status='stopped', and failed runs (ended by an error) only with status='failed'. The two are different facts and never share a filter.", {
|
|
2006
2212
|
status: z
|
|
2007
2213
|
.enum(["running", "completed", "stopped", "queued", "securing", "booked", "interrupted", "failed"])
|
|
2008
2214
|
.optional()
|
|
2009
|
-
.describe("Filter by job status"),
|
|
2215
|
+
.describe("Filter by job status. 'stopped' and 'failed' each reveal runs the default list hides."),
|
|
2010
2216
|
}, async ({ status }) => {
|
|
2011
2217
|
const data = await client.api("/api/training/jobs", { params: { status } });
|
|
2012
2218
|
return json(data);
|
|
@@ -2028,7 +2234,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
2028
2234
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2029
2235
|
/* TOOL: get_training_metrics */
|
|
2030
2236
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2031
|
-
server.tool("get_training_metrics", "Get training metrics history for a job: training loss over time, eval loss, learning rate schedule, and evaluation results. Use this to check if training is progressing well (loss should decrease)
|
|
2237
|
+
server.tool("get_training_metrics", "Get training metrics history for a job: training loss over time, eval loss, learning rate schedule, and evaluation results, plus resource_metrics (the latest worker snapshot: per-GPU memory and utilization, system RAM, CPU when the worker reports it, disk). Fields the worker did not report are null, never zero. Use this to check if training is progressing well (loss should decrease), if the model is overfitting (eval loss increasing while train loss decreases), or whether memory headroom allows a larger batch.", {
|
|
2032
2238
|
job_id: z.string().describe("Training job ID"),
|
|
2033
2239
|
}, async ({ job_id }) => {
|
|
2034
2240
|
validateId(job_id, "job_id");
|
|
@@ -2234,6 +2440,587 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
2234
2440
|
return json(data);
|
|
2235
2441
|
});
|
|
2236
2442
|
}
|
|
2443
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2444
|
+
/* TOOLS: Conscious Loop */
|
|
2445
|
+
/* */
|
|
2446
|
+
/* Capture what a model was asked and answered, record whether it was right, */
|
|
2447
|
+
/* and curate the result into training data. */
|
|
2448
|
+
/* */
|
|
2449
|
+
/* Every one of these is workspace-scoped and needs an API key carrying */
|
|
2450
|
+
/* loop:read or loop:write. Those scopes are absent from the Read Only */
|
|
2451
|
+
/* preset on purpose: what they return is raw prompts and completions, not */
|
|
2452
|
+
/* catalog metadata, so a key holds them only by an explicit grant. */
|
|
2453
|
+
/* */
|
|
2454
|
+
/* NOTHING IS RECORDED until loop_set_capture turns a source on. An agent */
|
|
2455
|
+
/* calling loop_capture_trace against a source nobody enabled gets */
|
|
2456
|
+
/* captured:false back, not an error and not a stored conversation. */
|
|
2457
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2458
|
+
server.tool("loop_set_capture", "Turn conversation capture on or off for one source, and choose how long recordings are kept. THIS IS THE CONSENT DECISION the rest of the Conscious Loop rests on: until it is made, nothing is recorded. The source is a deployment id for models served on Run BiOS, or any label you choose for traffic served elsewhere (another provider, your own servers, an agent framework). Recordings are deleted automatically once the retention window passes.", {
|
|
2459
|
+
source: z.string().describe("The source to record: a Run BiOS deployment id, or a label you choose for traffic served elsewhere (e.g. 'my-support-agent')"),
|
|
2460
|
+
enabled: z.boolean().describe("true starts recording new conversations from this source; false stops. Turning it off never deletes what is already stored."),
|
|
2461
|
+
retention_days: z.number().int().optional().describe("How many days recordings are kept before automatic deletion (1-3650, default 30)"),
|
|
2462
|
+
sample_rate: z.number().optional().describe("Record only a fraction of conversations, 0 to 1. Deterministic per conversation, so a multi-turn exchange is never split in half."),
|
|
2463
|
+
auto_grade: z.boolean().optional().describe("Turn on continuous rule-based scoring for this source: the platform applies the workspace's graders to conversations nobody has reviewed, for free — a rule is arithmetic, not a model call. Off by default. OMIT IT to leave the current setting alone; sending false switches it off."),
|
|
2464
|
+
}, async ({ source, enabled, retention_days, sample_rate, auto_grade }) => {
|
|
2465
|
+
const data = await client.api(`/api/loop/configs/${encodeURIComponent(source)}`, {
|
|
2466
|
+
method: "PUT",
|
|
2467
|
+
body: { enabled, retention_days, sample_rate, auto_grade },
|
|
2468
|
+
});
|
|
2469
|
+
return json(data);
|
|
2470
|
+
});
|
|
2471
|
+
server.tool("loop_capture_trace", "Record one exchange — what the model was asked and what it answered — so it can be reviewed and later trained on. Works whoever served the model. Pass tool_calls and tools when the turn invoked a function: without them a tool-using exchange trains the model to reply in prose exactly where it should have called something. The returned trace_id is Run BiOS's own; if you pass request_id, replaying it returns the same trace instead of storing a duplicate. If the source has not been enabled with loop_set_capture this returns captured:false and stores nothing.", {
|
|
2472
|
+
source: z.string().describe("The source being recorded — must already be enabled with loop_set_capture"),
|
|
2473
|
+
model: z.string().describe("The model that produced the answer, e.g. 'gpt-4o' or a Run BiOS model id"),
|
|
2474
|
+
messages: z.array(z.object({
|
|
2475
|
+
role: z.string().describe("system | user | assistant | tool"),
|
|
2476
|
+
content: z.string().optional(),
|
|
2477
|
+
tool_call_id: z.string().optional().describe("On a tool result turn, the call it answers"),
|
|
2478
|
+
name: z.string().optional().describe("On a tool result turn, the tool's name"),
|
|
2479
|
+
})).describe("The conversation as it was sent to the model"),
|
|
2480
|
+
completion: z.string().optional().describe("The assistant's answer text. Omit when the answer was purely a tool call."),
|
|
2481
|
+
tool_calls: z.array(z.object({
|
|
2482
|
+
id: z.string().optional(),
|
|
2483
|
+
type: z.string().optional(),
|
|
2484
|
+
function: z.object({
|
|
2485
|
+
name: z.string(),
|
|
2486
|
+
arguments: z.string().describe("JSON-encoded string, as the chat-completions wire format uses"),
|
|
2487
|
+
}),
|
|
2488
|
+
})).optional().describe("What the answering turn invoked, if anything"),
|
|
2489
|
+
tools: z.any().optional().describe("The tool schema the model was offered. Training tool use without it teaches the model to invent function names."),
|
|
2490
|
+
conversation_id: z.string().optional().describe("Ties the turns of one conversation together"),
|
|
2491
|
+
request_id: z.string().optional().describe("Your own idempotency handle — replaying it will not create a second trace"),
|
|
2492
|
+
latency_ms: z.number().int().optional(),
|
|
2493
|
+
}, async ({ source, model, messages, completion, tool_calls, tools, conversation_id, request_id, latency_ms }) => {
|
|
2494
|
+
const data = await client.api("/api/loop/traces", {
|
|
2495
|
+
method: "POST",
|
|
2496
|
+
body: {
|
|
2497
|
+
deployment_id: source, model, messages,
|
|
2498
|
+
completion: completion ?? "",
|
|
2499
|
+
tool_calls, tools, conversation_id, request_id, latency_ms,
|
|
2500
|
+
},
|
|
2501
|
+
});
|
|
2502
|
+
return json(data);
|
|
2503
|
+
});
|
|
2504
|
+
server.tool("loop_import_rows", "Bring data the user ALREADY HAS into the loop — an export from another provider, a spreadsheet of past answers, a set of preference pairs. This does NOT create a training set: it creates conversations, in the same place captured ones live, subject to the same review, rules, judges and labels. A row becomes trainable when something says it is good, never because it arrived in a file. Each row is read for what it is: a prompt/chosen/rejected triple becomes a preference pair; an answer plus a yes-or-no becomes a thumbs verdict; a question and an answer waits for review; a question with no answer waits for an answer; a paragraph of prose is refused, because it is not a conversation. A verdict that arrives with the file is kept, but recorded as having come from the user's earlier process rather than from a reviewer here. WHAT COMES BACK, and what to tell the user: imported is what this call created and trace_ids names the conversations from the file that are now in the loop; already_present is rows an earlier import of the same file already had, so they were not stored twice; by_shape counts the shapes this call CREATED, not what was merely recognised and not rows that were already here, so a file that was silently the wrong shape shows up there rather than merely importing fewer rows than expected, and reviewed and needs_review likewise count only what this call wrote; refused with refused_why is rows whose shape could not be read, which the user fixes by changing the file; not_saved with not_saved_rows is rows that were read and understood and then could not be stored, which the file cannot fix; verdicts_not_saved with verdicts_not_saved_rows is rows whose conversation stored and whose VERDICT did not, so those conversations are in the loop waiting for review rather than reviewed. A call with any not_saved or verdicts_not_saved rows fails with code IMPORT_INCOMPLETE and carries all of the same counts. HOW TO RETRY SAFELY: every response carries import_id, the token that call was filed under. Send the SAME rows again with that import_id and anything already imported comes back under already_present instead of being stored a second time, while a verdict that failed to write is attempted again. Do NOT retry without repeating import_id: a call that repeats no token is its own import and stores everything again, deliberately, because two calls carrying the same rows are as likely to be two pages of one export as one call sent twice. SENDING MORE THAN 5000 ROWS: split the file and either give every row its own row_ids entry, after which chunking and ordering stop mattering, or send one import_id with the row_offset each page starts at. Without row_ids, do not send back only the rows not_saved_rows names: a shorter list moves every row after the gap and imports it again.", {
|
|
2505
|
+
source: z.string().describe("Where this came from, e.g. 'zendesk-2026' or 'gpt4-history'. Required, and becomes the source every imported conversation is filed under — do not use a generic word like 'import', because that produces a corpus nobody can slice later."),
|
|
2506
|
+
rows: z.array(z.record(z.string(), z.any())).describe("The rows themselves, already parsed. At most 5000 per call: the call is synchronous. Pass each row in whatever shape it already has rather than reshaping it — the shape is how the server decides whether it carries a verdict."),
|
|
2507
|
+
model: z.string().optional().describe("Which model produced these answers, when the rows do not say per-row"),
|
|
2508
|
+
labels: z.array(z.string()).optional().describe("Applied to every row. Importing a dump is usually the moment somebody knows what all of it is, and labels are what make a slice of it selectable later."),
|
|
2509
|
+
attributes: z.record(z.string(), z.string()).optional().describe("Key/value facts applied to every row, e.g. {\"category\": \"billing\"}"),
|
|
2510
|
+
import_id: z.string().optional().describe("Repeat the import_id a previous call reported to make this call a RETRY of it: rows that already arrived come back under already_present rather than being stored again, and a verdict that failed to write is attempted again. Leave it out for a fresh import or for the next page of one."),
|
|
2511
|
+
row_offset: z.number().int().min(0).optional().describe("Where this page starts in the file, counting from 0. Send it when one import is split across several calls under one import_id, so two pages are not matched against each other row for row."),
|
|
2512
|
+
row_ids: z.array(z.string()).optional().describe("Your own id per row, in the same order as rows: a ticket number, a conversation id, whatever the export already carries. The strongest identity and the one to use for a file big enough to page — chunking, ordering and subsets all stop mattering. One per row or none at all, and no two rows may share one."),
|
|
2513
|
+
}, async ({ source, rows, model, labels, attributes, import_id, row_offset, row_ids }) => {
|
|
2514
|
+
const data = await client.api("/api/loop/import", {
|
|
2515
|
+
method: "POST",
|
|
2516
|
+
body: { source, rows, model, labels, attributes, import_id, row_offset, row_ids },
|
|
2517
|
+
});
|
|
2518
|
+
return json(data);
|
|
2519
|
+
});
|
|
2520
|
+
server.tool("loop_list_traces", "List recorded conversations for this workspace, newest first. Use signalled_only to see just the ones that already carry a verdict, which is what a training set can actually be built from.", {
|
|
2521
|
+
source: z.string().optional().describe("Only conversations from this source"),
|
|
2522
|
+
model: z.string().optional().describe("Only what THIS model answered, e.g. 'kimi-k3'. The filter distillation is made of — every other filter selects by what was asked or what somebody said about the answer, never by who produced it."),
|
|
2523
|
+
origin: z.enum(["captured", "imported"]).optional().describe("captured = the workspace's own model behaviour; imported = a file somebody brought in. Pooling them makes 'train only on what we actually served' unanswerable."),
|
|
2524
|
+
conversation_id: z.string().optional(),
|
|
2525
|
+
label: z.string().optional().describe("Only conversations carrying this bare tag. A tag with children matches them too."),
|
|
2526
|
+
attributes: z.record(z.string(), z.string()).optional().describe("Only conversations matching every one of these dimensions, e.g. {\"category\":\"billing\",\"language\":\"es\"}"),
|
|
2527
|
+
unlabelled: z.boolean().optional().describe("Only the conversations nobody has described yet — the pile worth looking at before deciding what the dimensions should be"),
|
|
2528
|
+
signalled_only: z.boolean().optional().describe("Only conversations that already have feedback on them"),
|
|
2529
|
+
limit: z.number().int().optional().describe("Default 50, maximum 200"),
|
|
2530
|
+
offset: z.number().int().optional(),
|
|
2531
|
+
}, async ({ source, model, origin, conversation_id, label, attributes, unlabelled, signalled_only, limit, offset }) => {
|
|
2532
|
+
const attrs = Object.entries(attributes ?? {})
|
|
2533
|
+
.filter(([k, v]) => k && v)
|
|
2534
|
+
.map(([k, v]) => `${k}:${v}`);
|
|
2535
|
+
const data = await client.api("/api/loop/traces", {
|
|
2536
|
+
params: {
|
|
2537
|
+
deployment_id: source,
|
|
2538
|
+
model,
|
|
2539
|
+
origin,
|
|
2540
|
+
conversation_id,
|
|
2541
|
+
label,
|
|
2542
|
+
attr: attrs.length ? attrs : undefined,
|
|
2543
|
+
unlabelled: unlabelled ? "true" : undefined,
|
|
2544
|
+
signalled: signalled_only ? "true" : undefined,
|
|
2545
|
+
limit: limit?.toString(),
|
|
2546
|
+
offset: offset?.toString(),
|
|
2547
|
+
},
|
|
2548
|
+
});
|
|
2549
|
+
return json(data);
|
|
2550
|
+
});
|
|
2551
|
+
server.tool("loop_get_trace", "Read one recorded conversation in full, with every verdict anyone has recorded on it. Anything that looked like a credential or a personal detail was removed before it was stored, and redaction_report says what.", { trace_id: z.string().describe("The trace id returned by loop_capture_trace or loop_list_traces") }, async ({ trace_id }) => {
|
|
2552
|
+
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}`);
|
|
2553
|
+
return json(data);
|
|
2554
|
+
});
|
|
2555
|
+
server.tool("loop_add_signal", "Record a verdict on a conversation: was the answer right? Feedback is APPEND-ONLY — a second verdict does not replace the first, because two reviewers disagreeing is information worth keeping.\n\nWHICH VERDICT TO USE, because they train different things:\n• accepted (a thumbs up) — the answer was good. It becomes an example to learn from.\n• rejected (a thumbs down) — it was wrong and you have nothing better. This REMOVES the answer from training rather than teaching anything; prefer edited whenever you know the better answer.\n• edited — the model was WRONG and here is what it should have said. The most valuable single action: it produces both halves of a preference pair, so the model learns your answer and learns to avoid its own.\n• gold — the REFERENCE answer for this question, recorded whatever the model happened to say. Unlike edited it asserts nothing about the model, so if it matches what was said no preference is invented — but it still trains SFT, and it stands in as the GRPO reference when no separate ground_truth was given. It is the most reusable feedback there is.\n• scored — a number, with ground_truth when the answer is checkable.", {
|
|
2556
|
+
trace_id: z.string(),
|
|
2557
|
+
verdict: z.enum(["accepted", "rejected", "edited", "scored", "gold"]).describe("accepted = thumbs up; rejected = thumbs down with nothing better to offer; edited = the model was wrong and you wrote the better answer; gold = the reference answer for this question regardless of what the model said; scored = a number or checkable fact"),
|
|
2558
|
+
source: z.enum(["human", "verifier", "judge", "behavioural"]).optional().describe("Who judged. A human outranks a verifier, a verifier outranks a judge, a judge outranks a behavioural hint. Default human."),
|
|
2559
|
+
correction: z.string().optional().describe("REQUIRED when verdict is 'edited' (the answer it should have given) or 'gold' (the reference answer for this question)"),
|
|
2560
|
+
score: z.number().optional().describe("REQUIRED when verdict is 'scored'"),
|
|
2561
|
+
ground_truth: z.string().optional().describe("The value or fact the answer can be checked against"),
|
|
2562
|
+
reason: z.string().optional().describe("Why, in your own words"),
|
|
2563
|
+
author: z.string().optional(),
|
|
2564
|
+
}, async ({ trace_id, verdict, source, correction, score, ground_truth, reason, author }) => {
|
|
2565
|
+
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/signals`, {
|
|
2566
|
+
method: "POST",
|
|
2567
|
+
body: { verdict, source: source ?? "human", correction, score, ground_truth, reason, author },
|
|
2568
|
+
});
|
|
2569
|
+
return json(data);
|
|
2570
|
+
});
|
|
2571
|
+
server.tool("loop_add_candidate", "Submit an ALTERNATIVE answer to a prompt that was already recorded, and score it. This is how preference training scales without a person writing every answer: sample the model several times for the same prompt, score each sample, and a DPO pair falls out automatically — the best-scoring sample becomes 'chosen' and the worst becomes 'rejected'. Point a stronger model at the prompt instead and the same mechanism does distillation. SCORE EVERY CANDIDATE: an unscored alternative cannot pair, because it says nothing about which answer is preferred. A human correction recorded with loop_add_signal always outranks any score you submit here.", {
|
|
2572
|
+
trace_id: z.string().describe("The recorded conversation this is an alternative answer to"),
|
|
2573
|
+
completion: z.string().optional().describe("The alternative answer's text"),
|
|
2574
|
+
tool_calls: z.array(z.object({
|
|
2575
|
+
id: z.string().optional(),
|
|
2576
|
+
type: z.string().optional(),
|
|
2577
|
+
function: z.object({ name: z.string(), arguments: z.string() }),
|
|
2578
|
+
})).optional().describe("When the alternative was itself a tool call"),
|
|
2579
|
+
model: z.string().optional().describe("Which model produced this alternative — the teacher, when distilling"),
|
|
2580
|
+
score: z.number().optional().describe("How good it is. Higher is better. Required for the candidate to take part in a pair."),
|
|
2581
|
+
score_source: z.enum(["human", "verifier", "judge", "behavioural"]).optional().describe("What produced the score. Required whenever a score is given."),
|
|
2582
|
+
reason: z.string().optional(),
|
|
2583
|
+
}, async ({ trace_id, completion, tool_calls, model, score, score_source, reason }) => {
|
|
2584
|
+
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/candidates`, {
|
|
2585
|
+
method: "POST",
|
|
2586
|
+
body: { completion: completion ?? "", tool_calls, model, score, score_source, reason },
|
|
2587
|
+
});
|
|
2588
|
+
return json(data);
|
|
2589
|
+
});
|
|
2590
|
+
server.tool("loop_list_candidates", "Read every alternative answer recorded for one prompt, with its score and what produced that score. Use this to see whether sampling has produced a spread worth training on — if every sample scored the same, the reward function cannot separate them and no pair will be built.", { trace_id: z.string() }, async ({ trace_id }) => {
|
|
2591
|
+
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/candidates`);
|
|
2592
|
+
return json(data);
|
|
2593
|
+
});
|
|
2594
|
+
server.tool("loop_label_trace", "Label a recorded conversation so it can be selected later. An average over everything is the least useful thing to train on — a model weak at refunds is fixed with refund examples, and you can only select those if the conversation was labelled when it arrived. A label may name a PARENT, which makes a master group: labelling something 'refunds' with parent 'billing' makes it selectable as either, and selecting 'billing' later gathers every child without anybody maintaining a list of them. Labels are lowercased and trimmed, so 'Refunds' and 'refunds' are one label.", {
|
|
2595
|
+
trace_id: z.string(),
|
|
2596
|
+
labels: z.array(z.string()).optional().describe("Bare tags, e.g. ['refunds','escalated']. Lowercase letters, digits, dot, dash or underscore."),
|
|
2597
|
+
attributes: z.record(z.string(), z.string()).optional().describe("NAMED DIMENSIONS, e.g. {\"category\":\"billing\",\"task\":\"refund-handling\",\"language\":\"es\"}. This is what lets one captured corpus become a different dataset for every task somebody trains for: filters on different dimensions AND together, and a dimension is matched EXACTLY within its key, so source=support never matches team=support. Setting one dimension leaves the others untouched, so correcting a mistake needs no delete first."),
|
|
2598
|
+
parent: z.string().optional().describe("The master group these belong to, e.g. 'billing'"),
|
|
2599
|
+
}, async ({ trace_id, labels, attributes, parent }) => {
|
|
2600
|
+
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/labels`, {
|
|
2601
|
+
method: "POST", body: { labels, attributes, parent },
|
|
2602
|
+
});
|
|
2603
|
+
return json(data);
|
|
2604
|
+
});
|
|
2605
|
+
server.tool("loop_remove_label", "Remove one label from a conversation. The counterpart to loop_label_trace, which until now had none — an agent could label something and then had no way to correct it. 'key' names the dimension; leave it out for a bare tag, which is what a plain name has always been.", {
|
|
2606
|
+
trace_id: z.string(),
|
|
2607
|
+
label: z.string().describe("The value to remove, e.g. 'refunds' or 'billing'"),
|
|
2608
|
+
key: z.string().optional().describe("The dimension it belongs to, e.g. 'category'. Omit for a bare tag."),
|
|
2609
|
+
}, async ({ trace_id, label, key }) => {
|
|
2610
|
+
const qs = key ? `?key=${encodeURIComponent(key)}` : "";
|
|
2611
|
+
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/labels/${encodeURIComponent(label)}${qs}`, { method: "DELETE" });
|
|
2612
|
+
return json(data);
|
|
2613
|
+
});
|
|
2614
|
+
server.tool("loop_list_labels", "Every label in this workspace with how many conversations carry it. Use it to see where the corpus actually is before choosing a slice to train on — and to discover the master groups, since a parent may have no conversations labelled with it directly. Two fields answer the group question and they answer different ones: 'parent' is the group ALL of a label's conversations are in, and is null the moment they disagree; 'parents' is every group ANY of them are in, each with how many of this label's conversations are in it. LIST THE MASTER GROUPS FROM 'parents' — a label with 12 conversations, 5 of them under support, reports parent null and parents [{support, 5}], so reading only 'parent' finds no group for it. Never state that a label is in a group on the strength of 'parents' alone; say how many of it are.", {}, async () => json(await client.api("/api/loop/labels")));
|
|
2615
|
+
server.tool("loop_create_judge", "Write a RUBRIC a model applies to answers nobody has time to read. A grader is a rule — deterministic, cheap, and blind to anything it was not told to look for; a judge is instructions in your own words, and it is the only thing that can answer 'was this answer actually helpful'. Score several things SEPARATELY: 'helpful but wrong' is not expressible in one number, and an average that hides it is worse than no score. The rubric also becomes a REWARD FUNCTION for GRPO — a prompt with a rubric attached is trainable even though no string the answer has to equal exists, which is most real work.", {
|
|
2616
|
+
name: z.string().describe("What this judge is for, e.g. 'Billing tone'"),
|
|
2617
|
+
instructions: z.string().describe("What makes an answer good here, in your own words"),
|
|
2618
|
+
dimensions: z.array(z.object({
|
|
2619
|
+
key: z.string().describe("Lowercase name, e.g. 'accuracy'"),
|
|
2620
|
+
description: z.string().optional().describe("What that dimension means"),
|
|
2621
|
+
})).describe("What to score, separately. At most 12."),
|
|
2622
|
+
label: z.string().optional().describe("Only judge this slice. A parent label gathers its children."),
|
|
2623
|
+
deployment_id: z.string().optional().describe("Only judge conversations from this source"),
|
|
2624
|
+
sample: z.number().optional().describe("At most this many conversations per run"),
|
|
2625
|
+
only_unscored: z.boolean().optional().describe("Skip conversations a judge has already scored"),
|
|
2626
|
+
model: z.string().optional().describe("Which model judges with. Recorded so two runs are comparable; REQUIRED when auto is true, because that is the model the workspace pays for."),
|
|
2627
|
+
write_gold: z.boolean().optional().describe("Let the judge write the answer that SHOULD have been given. Off by default: training on a model-written reference is distillation, and that is a decision you make deliberately."),
|
|
2628
|
+
auto: z.boolean().optional().describe("SPEND CONSENT. Let the platform's agent run this judge: new conversations in its slice are scored as they arrive, one model call each, billed to THIS WORKSPACE'S OWN serverless account through a managed key of theirs ('Conscious Loop' in their key list). Say this to the user before setting it. Requires model. Off (default), the user runs the model: loop_start_judge_run, loop_take_judge_work, loop_post_judge_verdicts."),
|
|
2629
|
+
}, async ({ name, instructions, dimensions, label, deployment_id, sample, only_unscored, model, write_gold, auto }) => {
|
|
2630
|
+
const data = await client.api("/api/loop/judges", {
|
|
2631
|
+
method: "POST",
|
|
2632
|
+
body: {
|
|
2633
|
+
name, instructions, dimensions, model, write_gold, auto,
|
|
2634
|
+
selection: { label, deployment_id, sample, only_unscored },
|
|
2635
|
+
},
|
|
2636
|
+
});
|
|
2637
|
+
return json(data);
|
|
2638
|
+
});
|
|
2639
|
+
server.tool("loop_list_judges", "Every rubric this workspace has written, with what it scores and which slice it covers.", {}, async () => json(await client.api("/api/loop/judges")));
|
|
2640
|
+
server.tool("loop_start_judge_run", "Open a run of one judge: select the conversations NOW and freeze the rubric onto them. The selection is fixed at this moment on purpose — a run whose selection is a live query silently grows as conversations arrive, so 'we scored the refunds slice' becomes a claim about a set that no longer exists. Follow with loop_take_judge_work.", {
|
|
2641
|
+
judge_id: z.string(),
|
|
2642
|
+
limit: z.number().optional().describe("Cap this pass below the judge's own sample, for a cheap look first"),
|
|
2643
|
+
}, async ({ judge_id, limit }) => {
|
|
2644
|
+
const data = await client.api(`/api/loop/judges/${encodeURIComponent(judge_id)}/runs`, {
|
|
2645
|
+
method: "POST", body: limit ? { limit } : {},
|
|
2646
|
+
});
|
|
2647
|
+
return json(data);
|
|
2648
|
+
});
|
|
2649
|
+
server.tool("loop_take_judge_work", "Take the next conversations to score. Each item arrives with a 'prompt' ALREADY RENDERED — send that text to a model as-is and reply with the JSON it asks for. Do not assemble your own: two callers that build the prompt differently are not applying one rubric. YOU run the model here on purpose; the service that stores these conversations holds no credentials to any model, which is most of the reason it is safe to store them there. Nothing is marked taken, so asking again returns the same items.", { run_id: z.string(), limit: z.number().optional() }, async ({ run_id, limit }) => {
|
|
2650
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
2651
|
+
return json(await client.api(`/api/loop/runs/${encodeURIComponent(run_id)}/work${qs}`));
|
|
2652
|
+
});
|
|
2653
|
+
server.tool("loop_list_judge_run_items", "What happened to each conversation in a judge run, and why. loop_take_judge_work hands out what is still PENDING, so a finished run answers it with nothing; this answers with every conversation the run selected, its outcome (pending, scored or failed), the reason a failed one could not be scored, and when it was scored. A run that ends 'scored 0, failed 3' is otherwise a number with no detail behind it, and the reason is usually the whole answer: a model name nothing serves, a refused key, a reply that would not parse. Ask with status 'failed' when you are looking into a run that went wrong. At most 500 conversations come back at a time; when 'has_more' is true, ask again with the 'next_offset' from the reply.", {
|
|
2654
|
+
run_id: z.string(),
|
|
2655
|
+
status: z.enum(["pending", "scored", "failed"]).optional().describe("Only conversations with this outcome. 'failed' is the usual question."),
|
|
2656
|
+
limit: z.number().optional().describe("How many to return, up to 500. Default 100."),
|
|
2657
|
+
offset: z.number().optional().describe("Where to carry on from, for a run longer than one page"),
|
|
2658
|
+
}, async ({ run_id, status, limit, offset }) => {
|
|
2659
|
+
const qs = new URLSearchParams();
|
|
2660
|
+
if (status)
|
|
2661
|
+
qs.set("status", status);
|
|
2662
|
+
if (limit != null)
|
|
2663
|
+
qs.set("limit", String(limit));
|
|
2664
|
+
if (offset)
|
|
2665
|
+
qs.set("offset", String(offset));
|
|
2666
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
2667
|
+
return json(await client.api(`/api/loop/runs/${encodeURIComponent(run_id)}/items${suffix}`));
|
|
2668
|
+
});
|
|
2669
|
+
server.tool("loop_post_judge_verdicts", "Hand the scores back. Each verdict carries one score per dimension the rubric asked for, each between 0 and 1, plus a reason — a score nobody can argue with is one nobody can correct. Scoring something the rubric never asked for is REFUSED rather than averaged in, and reported in 'rejected'. If you could not score an item, send it with 'error' instead of dropping it: a run that quietly shrinks is one whose coverage nobody can state.", {
|
|
2670
|
+
run_id: z.string(),
|
|
2671
|
+
verdicts: z.array(z.object({
|
|
2672
|
+
trace_id: z.string(),
|
|
2673
|
+
scores: z.record(z.string(), z.number()).optional().describe("One score per dimension, 0 to 1"),
|
|
2674
|
+
overall: z.number().optional().describe("Your own combination. Left out, the plain mean is used."),
|
|
2675
|
+
reason: z.string().optional().describe("Why, in a sentence or two"),
|
|
2676
|
+
gold: z.string().optional().describe("The answer that should have been given. Stored only if the judge has write_gold."),
|
|
2677
|
+
error: z.string().optional().describe("Mark an item you could not score"),
|
|
2678
|
+
})),
|
|
2679
|
+
finish: z.boolean().optional().describe("Close the run in this same call, once you have nothing left to send. To close a run WITHOUT scores use loop_stop_judge_run: an empty verdict list with finish set writes 'done' on a run that never finished."),
|
|
2680
|
+
}, async ({ run_id, verdicts, finish }) => {
|
|
2681
|
+
const data = await client.api(`/api/loop/runs/${encodeURIComponent(run_id)}/verdicts`, {
|
|
2682
|
+
method: "POST", body: { verdicts, finish: finish ?? false },
|
|
2683
|
+
});
|
|
2684
|
+
return json(data);
|
|
2685
|
+
});
|
|
2686
|
+
server.tool("loop_stop_judge_run", "Close a run that has not finished. This is what the refusal means when it says a judge already has a run going: an open run BLOCKS the next one, and the runs that most need closing are the ones nobody can wait out — a run parked on an empty balance or a refused key stays open until the reason is fixed or somebody stops it. Nothing is deleted: every verdict already recorded stays recorded, the counters keep saying how much of the selection was covered, and the conversations the run was holding are free for the next run. Do NOT close a run by posting an empty verdict list with finish set: that writes 'done' on it, and a run that scored 3 of 40 then reads like one that finished its work. A stopped run reads 'stopped'. A run that finished on its own answers 409 and is left alone.", { run_id: z.string() }, async ({ run_id }) => {
|
|
2687
|
+
const data = await client.api(`/api/loop/runs/${encodeURIComponent(run_id)}/stop`, {
|
|
2688
|
+
method: "POST", body: {},
|
|
2689
|
+
});
|
|
2690
|
+
return json(data);
|
|
2691
|
+
});
|
|
2692
|
+
server.tool("loop_agent_status", "Whether the platform's AGENT can run in this environment, whether it is running, and whether THIS workspace has turned it on. The agent is the worker that calls a model on the workspace's behalf — applying automatic judges and writing sample answers — and it spends through the workspace's OWN serverless key ('Conscious Loop' in their key list), so every call is billed exactly like a member's own. `available` is about the environment, `online` about the worker, `credential` about this workspace. Read this before promising a user that a judge will run itself.", {}, async () => json(await client.api("/api/loop/agent")));
|
|
2693
|
+
server.tool("loop_enable_agent", "SPEND CONSENT. Turn the agent on for this workspace: mints its managed serverless key and resumes the automatic judges a previous turn-off paused (judges_resumed in the response). A judge whose model the serving gateway will not route is NOT resumed — making it automatic would buy a run that fails every conversation — so it stays paused, judges_still_paused counts those, and each carries auto_pause_reason saying so; point it at a served model and turn the agent on again. After this, automatic judges and sample runs make model calls billed to the workspace. Tell the user that before calling it. Idempotent — an already-on agent answers with its existing key's metadata (never the secret). `monthly_spend_cap_cents` caps what the key may spend on model calls per calendar month; omitted = no cap (the agent can spend up to the workspace's serverless balance), and passing it while the agent is already on moves the cap on the existing key. When the cap is reached the agent's model calls are refused until next month and its runs pause with that reason. Offer the cap when the user is unsure how much the agent should spend. A freshly minted key can be refused by the gateway for up to a minute; the agent waits that out on its own and a run's last_error says the key is still propagating.", {
|
|
2694
|
+
monthly_spend_cap_cents: z.number().int().min(1).optional().describe("Monthly spend cap for the agent's key, in cents (e.g. 5000 = $50). Omit for no cap."),
|
|
2695
|
+
}, async ({ monthly_spend_cap_cents }) => json(await client.api("/api/loop/agent", {
|
|
2696
|
+
method: "POST",
|
|
2697
|
+
body: monthly_spend_cap_cents != null ? { monthly_spend_cap_cents } : {},
|
|
2698
|
+
})));
|
|
2699
|
+
server.tool("loop_disable_agent", "Turn the agent off for this workspace: revokes its key and PAUSES every automatic judge in the workspace (auto=false, auto_paused=true; the response says how many as judges_paused). loop_enable_agent restores exactly those judges (judges_resumed). Open runs stop where they are and continue if it is turned back on. Nothing already scored or written is removed.", {}, async () => json(await client.api("/api/loop/agent", { method: "DELETE" })));
|
|
2700
|
+
server.tool("loop_create_sample_run", "SPEND CONSENT. Ask the agent to write `n` ALTERNATIVE answers to each conversation in a slice with `model`, score each with `judge_id`, and store them as candidates. This is how preference pairs (DPO) and reward-scored groups (GRPO) are made WITHOUT a person writing each one: the curation pass pairs the best sample against the worst wherever the gap is real. Choose a stronger model than the one being trained and it is distillation. COST: up to n calls to write plus n to judge, per conversation, at the workspace's serverless rate — state the upper bound (sample × n × 2) to the user before starting. `sample` caps at 200 conversations and `n` at 8. Opening a run turns the agent on if it is off.", {
|
|
2701
|
+
model: z.string().describe("The serverless model that writes the alternatives"),
|
|
2702
|
+
n: z.number().int().min(1).max(8).optional().describe("Alternatives per conversation, 1-8. Default 4."),
|
|
2703
|
+
temperature: z.number().min(0).max(2).optional().describe("Default 0.8. 0 makes every sample the same answer, which defeats the point."),
|
|
2704
|
+
max_tokens: z.number().int().min(16).max(8192).optional().describe("Default 1024"),
|
|
2705
|
+
judge_id: z.string().optional().describe("Score each sample with this judge as it is written. Omit to store them unscored; scoring rules still apply."),
|
|
2706
|
+
label: z.string().optional().describe("Only this slice. A parent label gathers its children."),
|
|
2707
|
+
deployment_id: z.string().optional().describe("Only conversations from this source"),
|
|
2708
|
+
sample: z.number().int().min(1).max(200).optional().describe("At most this many conversations. Default and maximum 200."),
|
|
2709
|
+
only_unsampled: z.boolean().optional().describe("Skip conversations that already have alternatives (default true)"),
|
|
2710
|
+
}, async ({ model, n, temperature, max_tokens, judge_id, label, deployment_id, sample, only_unsampled }) => {
|
|
2711
|
+
const data = await client.api("/api/loop/sample-runs", {
|
|
2712
|
+
method: "POST",
|
|
2713
|
+
body: {
|
|
2714
|
+
model, n, temperature, max_tokens, judge_id,
|
|
2715
|
+
selection: { label, deployment_id, sample, only_unsampled: only_unsampled ?? true },
|
|
2716
|
+
},
|
|
2717
|
+
});
|
|
2718
|
+
return json(data);
|
|
2719
|
+
});
|
|
2720
|
+
server.tool("loop_list_sample_runs", "Every sample run in this workspace, newest first: selected, done, failed, alternatives written, and last_error when the agent had to pause one (a refused key, an empty balance, an unknown model). A run that stops moving says why here.", {}, async () => json(await client.api("/api/loop/sample-runs")));
|
|
2721
|
+
server.tool("loop_create_grader", "Write a rule that scores answers automatically, with no person reading them. Human review is the most trustworthy feedback and the least available; a grader is written once and applied to every answer afterwards. Every kind is DETERMINISTIC — no model call — which is why a verifier's score outranks a judge's when they disagree: it cannot be flattered and it cannot drift between runs. 'weight' is the multiplier: a criterion that matters twice as much gets twice the weight, and the combined score is the weighted mean over the rules that actually applied. CAUTION WHEN WRITING A SET: a rule made only of 'forbidden' phrases is satisfied VACUOUSLY by an answer that says nothing, so pair it with a required phrase or you are rewarding silence. 'matches_gold' is the one kind with no expected value of its own: it compares each answer to the gold answer recorded on that SAME conversation (or the human correction when there is no gold), scores sampled alternatives against the same gold, and is NOT APPLIED to a conversation carrying neither, so one rule checks every labelled question without marking down the unlabelled ones.", {
|
|
2722
|
+
name: z.string().describe("What this rule checks, in your words"),
|
|
2723
|
+
kind: z.enum(["matches_gold", "exact_match", "contains", "regex", "json_valid", "numeric", "tool_called"]),
|
|
2724
|
+
expected: z.string().optional().describe("For exact_match and numeric: the value to compare against"),
|
|
2725
|
+
required: z.array(z.string()).optional().describe("For contains: every phrase that must appear"),
|
|
2726
|
+
forbidden: z.array(z.string()).optional().describe("For contains: phrases that must not appear"),
|
|
2727
|
+
pattern: z.string().optional().describe("For regex: the pattern the answer must match"),
|
|
2728
|
+
keys: z.array(z.string()).optional().describe("For json_valid: keys the parsed answer must carry"),
|
|
2729
|
+
tolerance: z.number().optional().describe("For numeric: how far from expected still counts. For matches_gold: how far from the gold answer still counts when both are bare numbers"),
|
|
2730
|
+
function_name: z.string().optional().describe("For tool_called: the function that must have been invoked"),
|
|
2731
|
+
case_sensitive: z.boolean().optional().describe("Turn off the default case and whitespace normalisation"),
|
|
2732
|
+
label: z.string().optional().describe("Aim the rule at one slice. A master group covers its children. Outside that slice the rule is NOT APPLIED rather than failed, so it stays out of the score entirely. Without this, a rule like 'must name the order number' marks down every billing answer for not mentioning a shipment."),
|
|
2733
|
+
weight: z.number().optional().describe("The multiplier, greater than zero. Default 1."),
|
|
2734
|
+
source: z.string().optional().describe("Limit the rule to one capture source. Omit to apply it everywhere."),
|
|
2735
|
+
}, async (input) => {
|
|
2736
|
+
const data = await client.api("/api/loop/graders", {
|
|
2737
|
+
method: "POST",
|
|
2738
|
+
body: {
|
|
2739
|
+
name: input.name, kind: input.kind, weight: input.weight,
|
|
2740
|
+
deployment_id: input.source, label: input.label,
|
|
2741
|
+
config: {
|
|
2742
|
+
expected: input.expected, required: input.required, forbidden: input.forbidden,
|
|
2743
|
+
pattern: input.pattern, keys: input.keys, tolerance: input.tolerance,
|
|
2744
|
+
function: input.function_name, case_sensitive: input.case_sensitive,
|
|
2745
|
+
},
|
|
2746
|
+
},
|
|
2747
|
+
});
|
|
2748
|
+
return json(data);
|
|
2749
|
+
});
|
|
2750
|
+
server.tool("loop_list_graders", "Every scoring rule this workspace has written, with its weight and whether it is enabled.", {}, async () => json(await client.api("/api/loop/graders")));
|
|
2751
|
+
server.tool("loop_grade_trace", "Apply this workspace's rules to one recorded answer AND to every alternative sampled for it, in one pass. That pairing is the point: scoring only the original gives a verdict, while scoring the samples too gives a preference pair — so sampling the model and then calling this produces DPO training data with nobody reading anything. A run where NO rule could apply writes nothing at all: no verdict, no scores. Recording a zero that no rule produced would poison the training data with a judgement nobody reached, so you get applied:0 back instead. Read the per-rule 'detail' to see which criterion an answer failed; a single combined number tells you nothing about what to fix.", { trace_id: z.string() }, async ({ trace_id }) => {
|
|
2752
|
+
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/grade`, {
|
|
2753
|
+
method: "POST", body: {},
|
|
2754
|
+
});
|
|
2755
|
+
return json(data);
|
|
2756
|
+
});
|
|
2757
|
+
server.tool("loop_build_dataset", "Turn the recorded feedback into a training set. The three methods need genuinely different feedback, so a set built for one CANNOT be reshaped into another: 'sft' uses answers marked right and answers that were rewritten; 'dpo' needs answers that were REWRITTEN, so a better and a worse version of the same reply exist; 'grpo' needs answers with a checkable ground truth OR a judge rubric; 'kto' needs only a yes or a no on an answer, INCLUDING a bare thumbs-down that trains nothing under the other three — which is why it usually has the most rows. The result always reports rejected_counts — why rows were left out — so a small set can be explained rather than guessed at. holdout_percent holds back the most RECENT work rather than a random slice, so an evaluation measures whether the model generalised instead of memorising the same week.", {
|
|
2758
|
+
name: z.string().describe("A name you will recognise later"),
|
|
2759
|
+
method: z.enum(["sft", "dpo", "grpo", "kto"]),
|
|
2760
|
+
attributes: z.record(z.string(), z.string()).optional().describe("Narrow to named dimensions, ANDed together: {\"category\":\"billing\",\"language\":\"es\"}. This is how ONE captured corpus becomes a DIFFERENT dataset for every task — nothing is re-labelled to do it."),
|
|
2761
|
+
unlabelled: z.boolean().optional().describe("Only the conversations nobody has described yet"),
|
|
2762
|
+
source: z.string().optional().describe("Only conversations from this source"),
|
|
2763
|
+
label: z.string().optional().describe("Only conversations carrying this label. A label with children selects them too, so 'billing' picks up 'refunds'."),
|
|
2764
|
+
from: z.string().optional().describe("RFC3339 timestamp, earliest conversation to consider"),
|
|
2765
|
+
to: z.string().optional().describe("RFC3339 timestamp, latest conversation to consider"),
|
|
2766
|
+
sample: z.number().int().optional().describe("Take this many of what the filters matched. Which ones is arbitrary but repeatable: the same conversations always give the same slice. Applied before the holdout split, so the held-back slice is still the newest of what was chosen."),
|
|
2767
|
+
holdout_percent: z.number().int().optional().describe("0-50. Holds back the newest slice for evaluation."),
|
|
2768
|
+
}, async ({ name, method, source, label, attributes, unlabelled, from, to, sample, holdout_percent }) => {
|
|
2769
|
+
const data = await client.api("/api/loop/datasets", {
|
|
2770
|
+
method: "POST",
|
|
2771
|
+
body: {
|
|
2772
|
+
name, method, deployment_id: source, label, attributes, unlabelled,
|
|
2773
|
+
from, to, sample, holdout_percent,
|
|
2774
|
+
},
|
|
2775
|
+
});
|
|
2776
|
+
return json(data);
|
|
2777
|
+
});
|
|
2778
|
+
server.tool("loop_create_build_rule", "Stand up a STANDING instruction to build a training set whenever enough new reviewed work exists — the automatic version of loop_build_dataset. The manual path asks somebody to notice that enough conversations have been reviewed, remember which filters describe the slice they care about, and press build, every time; nobody does that six weeks in, which is how a workspace accumulates reviewed feedback that never becomes a dataset. The `spec` is the same selection loop_build_dataset takes and is replayed verbatim, so an automatic set is identical to a hand-made one. Tell the user what `min_new_rows` means when you set it: it counts rows reviewed SINCE THE LAST BUILD, not the whole corpus — counting the total would fire the rule forever, because a total that has crossed a threshold stays across it.", {
|
|
2779
|
+
name: z.string().describe("What to call the series of sets this rule produces, e.g. 'nightly support sft'. Each build is named after it plus a timestamp. Must be unique in the workspace: two rules writing the same name make the series unreadable."),
|
|
2780
|
+
method: z.enum(["sft", "dpo", "grpo", "kto"]).describe("Which training method the sets are curated for"),
|
|
2781
|
+
min_new_rows: z.number().int().optional().describe("Rows reviewed since the last build before this fires again. Minimum 10, default 50. A set built from fewer trains nothing and buries the useful ones."),
|
|
2782
|
+
enabled: z.boolean().optional().describe("Default true. false stores the rule without running it."),
|
|
2783
|
+
spec: z.record(z.string(), z.any()).optional().describe("The selection, same shape as loop_build_dataset: deployment_id, label, attributes, model, origin, holdout_percent, max_items, sample. Omit for the whole corpus."),
|
|
2784
|
+
}, async ({ name, method, min_new_rows, enabled, spec }) => {
|
|
2785
|
+
const data = await client.api("/api/loop/build-rules", {
|
|
2786
|
+
method: "POST",
|
|
2787
|
+
body: { name, method, min_new_rows, enabled, spec },
|
|
2788
|
+
});
|
|
2789
|
+
return json(data);
|
|
2790
|
+
});
|
|
2791
|
+
server.tool("loop_list_build_rules", "The standing build rules, with what each one last did and why. READ last_reason BACK TO THE USER when they ask why no dataset appeared: a rule that is quiet because it is waiting looks exactly like one that is quiet because it is broken, and that field is the only thing that tells them apart — 'waiting: 3 reviewed since the last build, needs 10'.", {}, async () => json(await client.api("/api/loop/build-rules")));
|
|
2792
|
+
server.tool("loop_delete_build_rule", "Stop a standing build rule. The training sets it already produced are untouched — this stops future builds, it does not undo past ones.", { rule_id: z.string().describe("The rule's id, from loop_list_build_rules") }, async ({ rule_id }) => json(await client.api(`/api/loop/build-rules/${encodeURIComponent(rule_id)}`, { method: "DELETE" })));
|
|
2793
|
+
server.tool("loop_list_datasets", "List the training sets built in this workspace, each with how many rows it kept, how many conversations were considered, and why the rest were left out.", {
|
|
2794
|
+
method: z.enum(["sft", "dpo", "grpo", "kto"]).optional(),
|
|
2795
|
+
limit: z.number().int().optional(),
|
|
2796
|
+
}, async ({ method, limit }) => {
|
|
2797
|
+
const data = await client.api("/api/loop/datasets", {
|
|
2798
|
+
params: { method, limit: limit?.toString() },
|
|
2799
|
+
});
|
|
2800
|
+
return json(data);
|
|
2801
|
+
});
|
|
2802
|
+
server.tool("loop_preview_dataset", "Read the actual training rows in a set before anyone spends money training on it. Each row carries the address of the conversation it was built from, so a suspicious row can be traced back to what produced it.", {
|
|
2803
|
+
dataset_id: z.string(),
|
|
2804
|
+
limit: z.number().int().optional().describe("Default 50, maximum 200"),
|
|
2805
|
+
}, async ({ dataset_id, limit }) => {
|
|
2806
|
+
const data = await client.api(`/api/loop/datasets/${encodeURIComponent(dataset_id)}/items`, {
|
|
2807
|
+
params: { limit: limit?.toString() },
|
|
2808
|
+
});
|
|
2809
|
+
return json(data);
|
|
2810
|
+
});
|
|
2811
|
+
server.tool("loop_stats", "How much has been recorded in this workspace, how much of it has been reviewed, and how many rows each training method could use right now. The 'ready' figures are upper bounds: repeated questions are folded into one row when a set is built, so the finished count can be lower.", {}, async () => {
|
|
2812
|
+
const data = await client.api("/api/loop/stats");
|
|
2813
|
+
return json(data);
|
|
2814
|
+
});
|
|
2815
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2816
|
+
/* TOOLS: Conscious Loop — automatic training */
|
|
2817
|
+
/* */
|
|
2818
|
+
/* A TRAINING RULE is a standing instruction to spend the workspace's money: */
|
|
2819
|
+
/* curate what has been reviewed, train an adapter on it, boot a candidate, */
|
|
2820
|
+
/* compare it against what serves today, and either hand back a verdict or */
|
|
2821
|
+
/* promote the winner. It is the only loop surface that spends without a */
|
|
2822
|
+
/* person in the room, which is why creating one carries a recorded consent */
|
|
2823
|
+
/* and why loop_create_training_rule refuses to be called blind. */
|
|
2824
|
+
/* */
|
|
2825
|
+
/* The wire shapes here are the frozen contract in */
|
|
2826
|
+
/* docs/loop-training-contracts.md. Money is whole cents in a field named */
|
|
2827
|
+
/* _cents; on a request an ABSENT key leaves that setting alone and a */
|
|
2828
|
+
/* PRESENT null clears it, so omit what you are not changing rather than */
|
|
2829
|
+
/* echoing the current value back. */
|
|
2830
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2831
|
+
/** One rung of a ranked GPU ladder. The provider is the neutral public brand. */
|
|
2832
|
+
const trainingGPURungSchema = z.object({
|
|
2833
|
+
gpu_type: z.string().describe("e.g. 'H100_80GB'"),
|
|
2834
|
+
gpu_count: z.number().int().min(1),
|
|
2835
|
+
provider: z.string().describe("The public provider brand, e.g. 'bios-cloud'"),
|
|
2836
|
+
region: z.string().describe("e.g. 'global'"),
|
|
2837
|
+
tier: z.string().describe("e.g. 'secure'"),
|
|
2838
|
+
});
|
|
2839
|
+
/**
|
|
2840
|
+
* The create body, minus the yes. Preflight takes exactly this, create adds
|
|
2841
|
+
* accept_terms, and update adds expected_revision — which is what the contract
|
|
2842
|
+
* says, so the three tools share one shape rather than three copies that drift.
|
|
2843
|
+
*/
|
|
2844
|
+
const trainingRuleInputShape = {
|
|
2845
|
+
workspace_id: z.string().optional().describe("The workspace the rule belongs to. Omit to use the key's own workspace."),
|
|
2846
|
+
name: z.string().optional().describe("What to call this rule, e.g. 'Support tuning'. Unique in the workspace."),
|
|
2847
|
+
build_rule_id: z.string().optional().describe("Curate from an EXISTING standing build rule (loop_list_build_rules). Give this or `build`, never both."),
|
|
2848
|
+
build: z.object({
|
|
2849
|
+
method: z.string().describe("The training method the curated set is built for, e.g. 'sft'"),
|
|
2850
|
+
spec: z.record(z.string(), z.any()).describe("The same selection loop_build_dataset takes: deployment_id, label, attributes, model, origin, holdout_percent, max_items, sample"),
|
|
2851
|
+
}).optional().describe("Create a new standing build rule for this training rule to own. Give this or `build_rule_id`, never both."),
|
|
2852
|
+
trigger: z.object({
|
|
2853
|
+
cadence: z.enum(["none", "daily", "weekly"]).optional().describe("'none' means the rule only ever fires when a person asks it to"),
|
|
2854
|
+
cadence_hour_utc: z.number().int().min(0).max(23).optional().describe("The UTC hour the rule is due. The platform adds its own per-rule jitter so ten thousand rules are not all due at once."),
|
|
2855
|
+
cadence_weekday: z.number().int().min(0).max(6).nullable().optional().describe("0 is Sunday. null DROPS the weekday, which is what a weekly rule moved to daily needs."),
|
|
2856
|
+
combinator: z.enum(["and", "or"]).optional().describe("'and' fires only when the schedule is due AND enough new rows exist; 'or' fires on either"),
|
|
2857
|
+
min_new_rows: z.number().int().min(1).nullable().optional().describe("Rows reviewed SINCE THE LAST FIRING, not the whole corpus. null removes the row floor, leaving the schedule as the only trigger."),
|
|
2858
|
+
}).optional(),
|
|
2859
|
+
serving: z.object({
|
|
2860
|
+
kind: z.enum(["serverless_slug", "deployment"]).describe("What serves this model to customers today"),
|
|
2861
|
+
name: z.string().describe("The public handle traffic arrives on. A promotion re-points THIS name at the winner, so it is also the alias name."),
|
|
2862
|
+
deployment_id: z.string().nullable().describe("The deployment behind the name when kind is 'deployment'. Pass null for a serverless slug — the key is part of the shape, so send it explicitly rather than leaving the platform to guess."),
|
|
2863
|
+
}).optional(),
|
|
2864
|
+
training: z.object({
|
|
2865
|
+
model_id: z.string().optional().describe("The base model, from the Run BiOS catalog (search_models)"),
|
|
2866
|
+
model_revision: z.string().optional().describe("Pinned once at save so a rule cannot silently start training a different set of weights. Preflight returns the revision it pinned; pass it back rather than inventing one."),
|
|
2867
|
+
training_method: z.enum(["sft", "rlhf"]).optional().describe("'rlhf' is refused until the platform enables it; preflight says so rather than the rule failing at fire time"),
|
|
2868
|
+
rlhf_type: z.enum(["dpo", "kto"]).nullable().optional().describe("null clears it, which is what moving a rule back to sft means"),
|
|
2869
|
+
train_type: z.enum(["lora", "qlora", "full"]).optional(),
|
|
2870
|
+
config: z.record(z.string(), z.any()).optional().describe("Hyperparameters, the same shape create_training_job takes"),
|
|
2871
|
+
train_gpu_priorities: z.array(trainingGPURungSchema).optional().describe("The training ladder, best rung first. The run takes the first rung in stock under the price cap."),
|
|
2872
|
+
train_max_price_hour_cents: z.number().int().min(1).optional().describe("The most this rule will pay per hour to TRAIN, in cents"),
|
|
2873
|
+
}).optional(),
|
|
2874
|
+
deploy: z.object({
|
|
2875
|
+
deploy_gpu_priorities: z.array(trainingGPURungSchema).optional().describe("The candidate's serving ladder, best rung first"),
|
|
2876
|
+
deploy_max_price_hour_cents: z.number().int().min(1).optional().describe("The most this rule will pay per hour to SERVE the candidate while it is compared, in cents"),
|
|
2877
|
+
context_length: z.number().int().min(1).optional(),
|
|
2878
|
+
quant: z.string().optional(),
|
|
2879
|
+
serving_config: z.record(z.string(), z.any()).optional(),
|
|
2880
|
+
hf_integration_id: z.string().optional(),
|
|
2881
|
+
}).optional(),
|
|
2882
|
+
money: z.object({
|
|
2883
|
+
training_ceiling_cents: z.number().int().min(1).optional().describe("The hard stop on one run's training spend, in cents"),
|
|
2884
|
+
candidate_ceiling_cents: z.number().int().min(1).optional().describe("The hard stop on the candidate's serving spend for one run, in cents"),
|
|
2885
|
+
eval_ceiling_cents: z.number().int().min(1).optional().describe("The hard stop on the comparison's model calls for one run, in cents"),
|
|
2886
|
+
monthly_ceiling_cents: z.number().int().min(1).nullable().optional().describe("Everything this rule may spend in a calendar month. null REMOVES the monthly cap; absent leaves it exactly where it stands."),
|
|
2887
|
+
eval_max_rows: z.number().int().min(1).optional().describe("How many held-out conversations the comparison scores"),
|
|
2888
|
+
}).optional(),
|
|
2889
|
+
evaluation: z.object({
|
|
2890
|
+
judge_id: z.string().nullable().optional().describe("The rubric that scores both answers (loop_list_judges). null removes the judge from this rule."),
|
|
2891
|
+
judge_model: z.string().nullable().optional().describe("Override the judge's own advisory model. null drops the override."),
|
|
2892
|
+
graders_scope: z.enum(["source", "all", "none"]).optional().describe("Which deterministic graders also run: the source's own, every grader in the workspace, or none"),
|
|
2893
|
+
min_holdout_rows: z.number().int().min(1).optional().describe("Below this the run refuses to fire rather than reporting a verdict from a handful of rows"),
|
|
2894
|
+
eval_max_tokens: z.number().int().min(1).optional(),
|
|
2895
|
+
}).optional(),
|
|
2896
|
+
promotion: z.object({
|
|
2897
|
+
auto_promote: z.boolean().optional().describe("true re-points the serving name at a winning candidate WITHOUT anyone reading the report. Say what that means before you set it."),
|
|
2898
|
+
promote_margin: z.number().optional().describe("How much better the candidate must score, as a fraction"),
|
|
2899
|
+
promote_min_win_rate: z.number().min(0).max(1).optional().describe("The share of paired conversations the candidate must win"),
|
|
2900
|
+
min_judge_agreement: z.number().min(0).max(1).optional().describe("How far the judge must already agree with this workspace's own reviewers before its verdict counts"),
|
|
2901
|
+
review_window_hours: z.number().int().min(1).optional().describe("How long a candidate waits for a person before the run expires and the candidate is retired"),
|
|
2902
|
+
keep_candidate_warm_minutes: z.number().int().min(0).optional().describe("Minutes the candidate keeps serving after a decision. Every minute is paid for."),
|
|
2903
|
+
}).optional(),
|
|
2904
|
+
enabled: z.boolean().optional().describe("false stores the rule without letting it fire"),
|
|
2905
|
+
};
|
|
2906
|
+
const acceptTermsSchema = z.object({
|
|
2907
|
+
terms_version: z.string().describe("Exactly the terms_version loop_preflight_training_rule returned. Do not type a version you have not read back to the user."),
|
|
2908
|
+
});
|
|
2909
|
+
server.tool("loop_preflight_training_rule", "Price a training rule WITHOUT creating anything. This is the first call, always: it pins the model revision, runs the training and deployment preflights, works out the worst hourly price each ladder can reach and how many hours each ceiling buys, lists any API key that cannot read deployments (such a key would create a rule whose promotions it cannot see), and returns terms_text — the sentence the member is agreeing to, with their real figures in it. Nothing is written and no money moves. Read worst_hourly_training_cents, worst_hourly_candidate_cents, eval_calls_max, the refusals and the warnings back to the user in their own words, then read terms_text back VERBATIM. NEVER state a ceiling, a price cap or an hour count this call did not return. If valid is false and refusals is EMPTY, look at unreachable: it names the peers the platform could not reach, and on such a pass both worst_hourly_* are 0 and so are both max_*_hours beside them. model_revision is empty unless training-service actually pinned a commit, so an empty one means nothing was pinned — but it does not on its own say whether the pass was refused or degraded; unreachable does. Tell the user which service is not answering and that waiting is the fix; do NOT read those zeroed figures out as if they were prices, and do not ask them to accept terms_text built from them.", trainingRuleInputShape, async (input) => json(await client.api("/api/loop/training-rules/preflight", { method: "POST", body: input })));
|
|
2910
|
+
server.tool("loop_create_training_rule", "Create the standing instruction to train, compare and possibly promote — the one loop surface that spends the workspace's money with nobody in the room. SPEND CONSENT, and it is RECORDED: accept_terms is the member's signature on the sentence preflight rendered, kept as an append-only consent row. CALL loop_preflight_training_rule FIRST. Read its estimate and its terms_text back to the user, word for word, and WAIT FOR AN EXPLICIT YES before you send accept_terms. Do not send accept_terms in the same turn you were asked to set the rule up, do not treat 'go ahead' from before the estimate as the yes, and NEVER INVENT a ceiling, a price cap, an hour count or a terms_version — every one of those figures comes from preflight or from the user, never from you. A rule created without a ceiling the user chose is a standing charge they did not agree to.", {
|
|
2911
|
+
...trainingRuleInputShape,
|
|
2912
|
+
accept_terms: acceptTermsSchema.describe("The member's yes. Send it ONLY after reading preflight's terms_text back and hearing an explicit yes. Absent is a refusal, not a default."),
|
|
2913
|
+
}, async (input) => json(await client.api("/api/loop/training-rules", { method: "POST", body: input })));
|
|
2914
|
+
server.tool("loop_list_training_rules", "The workspace's training rules, with what each last did and why. READ last_reason BACK TO THE USER when they ask why nothing trained: a rule quiet because it is waiting for rows reads exactly like one that is broken, and that field is the only thing that tells them apart. paused_reason says when a rule has stopped itself — a money-bearing edit that has not been consented to again, a ceiling that no longer buys an hour, a peer that could not be reached at save time.", {
|
|
2915
|
+
enabled: z.boolean().optional().describe("Only enabled rules, or only paused ones"),
|
|
2916
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
2917
|
+
offset: z.number().int().min(0).optional(),
|
|
2918
|
+
}, async ({ enabled, limit, offset }) => json(await client.api("/api/loop/training-rules", {
|
|
2919
|
+
params: { enabled: enabled?.toString(), limit: limit?.toString(), offset: offset?.toString() },
|
|
2920
|
+
})));
|
|
2921
|
+
server.tool("loop_get_training_rule", "One training rule in full: the consent it stands on, its recent runs, what it has spent this calendar month, and how far its judge agrees with this workspace's own reviewers. Report month_spent_cents against monthly_ceiling_cents when the user asks what this is costing them, and say so plainly if accepted_revision is behind revision — the rule is paused until somebody consents again.", { rule_id: z.string().describe("The rule's id, from loop_list_training_rules") }, async ({ rule_id }) => json(await client.api(`/api/loop/training-rules/${encodeURIComponent(rule_id)}`)));
|
|
2922
|
+
server.tool("loop_update_training_rule", "Edit or pause a training rule. Send ONLY what is changing: an absent key leaves that setting alone and a present null clears the ones that can be cleared (monthly_ceiling_cents, cadence_weekday, min_new_rows, rlhf_type, judge_id, judge_model). A MONEY-BEARING CHANGE — any ceiling, either price cap, the model, the method or the ladders — bumps the rule's revision, drops the consent and STOPS FUTURE FIRINGS until somebody accepts the new terms; the response says so as consent_required, and the honest next step is loop_preflight_training_rule again, not loop_consent_training_rule on a sentence nobody re-read. Pass expected_revision to be refused rather than overwrite an edit somebody else made.", {
|
|
2923
|
+
rule_id: z.string().describe("The rule's id, from loop_list_training_rules"),
|
|
2924
|
+
...trainingRuleInputShape,
|
|
2925
|
+
expected_revision: z.number().int().min(0).optional().describe("The revision you read. A stale value is refused with REVISION_MISMATCH instead of overwriting somebody else's edit."),
|
|
2926
|
+
accept_terms: acceptTermsSchema.optional().describe("Consent to the new terms in the same call. Same rule as create: only after the user has read them back and said yes."),
|
|
2927
|
+
}, async ({ rule_id, ...body }) => json(await client.api(`/api/loop/training-rules/${encodeURIComponent(rule_id)}`, {
|
|
2928
|
+
method: "PUT",
|
|
2929
|
+
body,
|
|
2930
|
+
})));
|
|
2931
|
+
server.tool("loop_consent_training_rule", "Accept the rule's CURRENT terms, which is what restarts a rule paused by a money-bearing edit. SPEND CONSENT: this is the member's signature, so read the terms back first — call loop_preflight_training_rule with the rule as it now stands and read its terms_text verbatim — and send this only after an explicit yes. The revision you send must be the revision they were shown; a stale one is refused rather than silently accepted.", {
|
|
2932
|
+
rule_id: z.string(),
|
|
2933
|
+
terms_version: z.string().describe("The version the user was shown, from preflight"),
|
|
2934
|
+
revision: z.number().int().min(0).describe("The rule revision the user was shown"),
|
|
2935
|
+
}, async ({ rule_id, terms_version, revision }) => json(await client.api(`/api/loop/training-rules/${encodeURIComponent(rule_id)}/consent`, {
|
|
2936
|
+
method: "POST",
|
|
2937
|
+
body: { terms_version, revision },
|
|
2938
|
+
})));
|
|
2939
|
+
server.tool("loop_run_training_rule", "Fire a rule NOW, without waiting for its cadence or its row floor. SPEND CONSENT: this starts a real training job and boots a real candidate against the rule's ceilings, so say what it will cost before you call it. The floors that protect the result still apply — too few curated or held-out rows answers NOT_ENOUGH_ROWS with the counts, an unconsented rule answers CONSENT_REQUIRED, and a rule already mid-run answers RUN_ACTIVE rather than starting a second one.", { rule_id: z.string() }, async ({ rule_id }) => json(await client.api(`/api/loop/training-rules/${encodeURIComponent(rule_id)}/run`, {
|
|
2940
|
+
method: "POST",
|
|
2941
|
+
body: {},
|
|
2942
|
+
})));
|
|
2943
|
+
server.tool("loop_delete_training_rule", "Stop a training rule for good. An active run is cancelled; runs that already finished, the checkpoints they produced and anything already promoted are untouched: this stops future firings, it does not undo past ones. The response names the run it cancelled, if any, and retained_name, the name this rule keeps. THIS CANNOT BE UNDONE and the name is spent. The deleted rule goes on holding its name while every list stops showing it, so no later rule in the workspace can be called that, and creating one under the same name is refused. To stop a rule firing for now, set enabled false with loop_update_training_rule instead. Deleting is also the only way to return the rule's curation spec to the build sweeper; switching the rule off does not.", { rule_id: z.string() }, async ({ rule_id }) => json(await client.api(`/api/loop/training-rules/${encodeURIComponent(rule_id)}`, { method: "DELETE" })));
|
|
2944
|
+
server.tool("loop_list_training_runs", "Every firing of every training rule, newest first: what state it is in, what it has spent, and what the comparison said. A run sitting in 'awaiting_review' is waiting for a PERSON — it holds a candidate that is being paid for by the minute, so surface those first when the user asks what needs attention.", {
|
|
2945
|
+
rule_id: z.string().optional().describe("Only this rule's runs"),
|
|
2946
|
+
state: z.string().optional().describe("Only runs in this state, e.g. 'awaiting_review'"),
|
|
2947
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
2948
|
+
offset: z.number().int().min(0).optional(),
|
|
2949
|
+
}, async ({ rule_id, state, limit, offset }) => json(await client.api("/api/loop/training-runs", {
|
|
2950
|
+
params: { rule_id, state, limit: limit?.toString(), offset: offset?.toString() },
|
|
2951
|
+
})));
|
|
2952
|
+
server.tool("loop_get_training_run", "One run in full, with the timeline of every state it passed through and why, links to the job, the candidate, the datasets and the comparison, and available_actions — which says what THIS reader may actually do next. Use available_actions rather than guessing from the state: a run can look promotable and not be, because the consent moved or the candidate is already gone.", { run_id: z.string() }, async ({ run_id }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}`)));
|
|
2953
|
+
server.tool("loop_promote_training_run", "Re-point the serving name at the candidate: from this moment the workspace's traffic is answered by the newly trained model. Read the comparison back to the user first (loop_get_evaluation) and get an explicit yes — this changes what real customers get, and it is only undoable for 30 days with loop_rollback_training_run. An INCONCLUSIVE comparison is refused unless force is set, and forcing one means promoting a model nothing showed to be better; say that out loud rather than setting force to get past the error.", {
|
|
2954
|
+
run_id: z.string(),
|
|
2955
|
+
expected_revision: z.number().int().min(0).optional().describe("The rule revision the decision was made against. Stale answers CONSENT_CHANGED instead of promoting under terms nobody agreed to."),
|
|
2956
|
+
force: z.boolean().optional().describe("Promote an inconclusive comparison anyway. Only ever after the user has been told the comparison did not show the candidate was better."),
|
|
2957
|
+
}, async ({ run_id, expected_revision, force }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/promote`, {
|
|
2958
|
+
method: "POST",
|
|
2959
|
+
body: { expected_revision, force },
|
|
2960
|
+
})));
|
|
2961
|
+
server.tool("loop_reject_training_run", "Retire the candidate and leave the serving name exactly where it is. This is the cheap decision: the candidate stops being paid for. The checkpoint the run produced is kept, so a rejection is not a deletion.", {
|
|
2962
|
+
run_id: z.string(),
|
|
2963
|
+
reason: z.string().optional().describe("Why, in the user's own words. It goes on the run's timeline, which is what somebody reads six weeks later."),
|
|
2964
|
+
}, async ({ run_id, reason }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/reject`, {
|
|
2965
|
+
method: "POST",
|
|
2966
|
+
body: { reason },
|
|
2967
|
+
})));
|
|
2968
|
+
server.tool("loop_rollback_training_run", "Undo a promotion: point the serving name back at what answered before it. Available for 30 days, after which the answer is ROLLBACK_WINDOW_CLOSED and the way back is a new run. Tell the user what the name will be serving once this lands — the previous target is named on the run.", {
|
|
2969
|
+
run_id: z.string(),
|
|
2970
|
+
reason: z.string().optional().describe("Why. It goes on the run's timeline."),
|
|
2971
|
+
}, async ({ run_id, reason }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/rollback`, {
|
|
2972
|
+
method: "POST",
|
|
2973
|
+
body: { reason },
|
|
2974
|
+
})));
|
|
2975
|
+
server.tool("loop_cancel_training_run", "Stop a run that is still moving. The training job is stopped, the candidate is torn down and nothing further is spent on this firing. What has already been spent is already spent — cancelling is not a refund, and saying so is more honest than letting the user discover it on the invoice.", {
|
|
2976
|
+
run_id: z.string(),
|
|
2977
|
+
reason: z.string().optional().describe("Why. It goes on the run's timeline."),
|
|
2978
|
+
}, async ({ run_id, reason }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/cancel`, {
|
|
2979
|
+
method: "POST",
|
|
2980
|
+
body: { reason },
|
|
2981
|
+
})));
|
|
2982
|
+
server.tool("loop_get_evaluation", "The comparison report behind a verdict: how many pairs were scored, the candidate's win rate, the mean scores per rubric dimension and per grader, the warnings, and the margins the verdict was measured against. Read the WARNINGS out too, not just the verdict — 'better' from a judge that rarely agrees with this workspace's reviewers, or from a handful of rows, is a number with a caveat attached, and the caveat is in that list.", { evaluation_id: z.string().describe("From the run's links, or loop_get_training_run") }, async ({ evaluation_id }) => json(await client.api(`/api/loop/evaluations/${encodeURIComponent(evaluation_id)}`)));
|
|
2983
|
+
server.tool("loop_list_evaluation_items", "The paired conversations behind the numbers: the same prompt, what the model serving today answered, what the candidate answered, and what the judge and the graders made of each. This is how a person checks a verdict instead of trusting it. Filter to winner='incumbent' to read the cases the candidate LOST, which is the honest thing to show somebody before they promote.", {
|
|
2984
|
+
evaluation_id: z.string(),
|
|
2985
|
+
winner: z.enum(["candidate", "incumbent", "tie"]).optional().describe("Only pairs with this outcome"),
|
|
2986
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum 100"),
|
|
2987
|
+
offset: z.number().int().min(0).optional(),
|
|
2988
|
+
}, async ({ evaluation_id, winner, limit, offset }) => json(await client.api(`/api/loop/evaluations/${encodeURIComponent(evaluation_id)}/items`, {
|
|
2989
|
+
params: { winner, limit: limit?.toString(), offset: offset?.toString() },
|
|
2990
|
+
})));
|
|
2991
|
+
server.tool("loop_get_judge_agreement", "How often this judge reached the same verdict as the workspace's OWN reviewers, over a window, overall and per rubric dimension. It is the answer to 'why should I believe the comparison'. When enough_pairs is false the figure is not yet evidence of anything and must be reported that way rather than as a low score.", {
|
|
2992
|
+
judge_id: z.string(),
|
|
2993
|
+
from: z.string().optional().describe("Start of the window, RFC 3339"),
|
|
2994
|
+
to: z.string().optional().describe("End of the window, RFC 3339"),
|
|
2995
|
+
}, async ({ judge_id, from, to }) => json(await client.api(`/api/loop/judges/${encodeURIComponent(judge_id)}/agreement`, {
|
|
2996
|
+
params: { from, to },
|
|
2997
|
+
})));
|
|
2998
|
+
server.tool("loop_get_agent_settings", "The workspace's agent settings: which model it uses by default, the system prompts it judges and samples with, and the monthly cap on what evaluation may spend. Read eval_monthly_cap_cents back when the user asks why a comparison stopped early.", {}, async () => json(await client.api("/api/loop/agent/settings")));
|
|
2999
|
+
server.tool("loop_update_agent_settings", "Change the agent's default model, its judge or sampler system prompt, or the monthly cap on evaluation spend. Send only what is changing: an absent key leaves that setting alone and a PRESENT NULL returns it to the platform default. Raising eval_monthly_cap_cents raises what the workspace can be charged, so treat it as SPEND CONSENT and say the new figure back before you send it.", {
|
|
3000
|
+
default_model: z.string().nullable().optional().describe("null returns to the platform default"),
|
|
3001
|
+
judge_system_prompt: z.string().nullable().optional().describe("null returns to the platform default"),
|
|
3002
|
+
sampler_system_prompt: z.string().nullable().optional().describe("null returns to the platform default"),
|
|
3003
|
+
eval_monthly_cap_cents: z.number().int().min(0).nullable().optional().describe("Whole cents. null returns to the platform default."),
|
|
3004
|
+
}, async (input) => json(await client.api("/api/loop/agent/settings", { method: "PUT", body: input })));
|
|
3005
|
+
server.tool("loop_update_build_rule", "Edit or pause a standing build rule: its row floor, its selection, or whether it runs at all. Send only what is changing. A build rule OWNED BY AN ENABLED TRAINING RULE refuses a change of spec.deployment_id with a 409, because that would silently retrain a live model on a different slice of conversations than the one its consent was priced against.", {
|
|
3006
|
+
rule_id: z.string().describe("The build rule's id, from loop_list_build_rules"),
|
|
3007
|
+
enabled: z.boolean().optional().describe("false stores the rule without running it"),
|
|
3008
|
+
min_new_rows: z.number().int().min(1).optional().describe("Rows reviewed since the last build before it fires again"),
|
|
3009
|
+
spec: z.record(z.string(), z.any()).optional().describe("The selection, same shape as loop_build_dataset"),
|
|
3010
|
+
}, async ({ rule_id, enabled, min_new_rows, spec }) => json(await client.api(`/api/loop/build-rules/${encodeURIComponent(rule_id)}`, {
|
|
3011
|
+
method: "PUT",
|
|
3012
|
+
body: { enabled, min_new_rows, spec },
|
|
3013
|
+
})));
|
|
3014
|
+
server.tool("set_inference_alias", "Point a re-pointable public handle at a deployment. An alias is a NAME customers call that can be moved to different weights without them changing anything — it is what a promotion writes, and what lets a retrained model take over traffic with no client edit. Setting one CHANGES WHAT REAL TRAFFIC REACHES, so confirm with the user first. A name that belongs to a live deployment is refused (ALIAS_NAME_IS_A_DEPLOYMENT) unless the alias is being given that deployment's own name, and a target that is not serving is refused (TARGET_NOT_SERVABLE) rather than pointing the handle at something that cannot answer.", {
|
|
3015
|
+
name: z.string().describe("The public handle, e.g. 'support-bot'"),
|
|
3016
|
+
target_inference_id: z.string().describe("The deployment the name should reach, from list_inferences"),
|
|
3017
|
+
origin: z.string().optional().describe("Who or what set it, for the audit trail"),
|
|
3018
|
+
}, async ({ name, target_inference_id, origin }) => json(await client.api(`/api/inference/aliases/${encodeURIComponent(name)}`, {
|
|
3019
|
+
method: "PUT",
|
|
3020
|
+
body: { target_inference_id, origin },
|
|
3021
|
+
})));
|
|
3022
|
+
server.tool("list_inference_aliases", "Every re-pointable handle in this workspace: what each one points at now, what it pointed at before, and whether it has taken over a deployment's own name. Read this before proposing a name — it is the only way to see that a handle a customer is already calling is about to be moved.", {}, async () => json(await client.api("/api/inference/aliases")));
|
|
3023
|
+
server.tool("delete_inference_alias", "Remove a re-pointable handle. Traffic that was arriving on that name STOPS REACHING THE DEPLOYMENT IT POINTED AT — the name goes back to whatever row owns it, which is usually nothing. Confirm with the user before calling it, and say which deployment stops being reachable by that name.", { name: z.string().describe("The handle, from list_inference_aliases") }, async ({ name }) => json(await client.api(`/api/inference/aliases/${encodeURIComponent(name)}`, { method: "DELETE" })));
|
|
2237
3024
|
return mcp;
|
|
2238
3025
|
}
|
|
2239
3026
|
//# sourceMappingURL=server.js.map
|