whileai 0.54__tar.gz → 0.55__tar.gz
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.
- {whileai-0.54/whileai.egg-info → whileai-0.55}/PKG-INFO +1 -1
- {whileai-0.54 → whileai-0.55}/pyproject.toml +1 -1
- {whileai-0.54 → whileai-0.55}/whileai/simulations/__init__.py +4 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/export.py +7 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/agents.py +34 -2
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/grading.py +11 -2
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/judging.py +42 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/training.py +58 -6
- {whileai-0.54 → whileai-0.55/whileai.egg-info}/PKG-INFO +1 -1
- {whileai-0.54 → whileai-0.55}/LICENSE +0 -0
- {whileai-0.54 → whileai-0.55}/MANIFEST.in +0 -0
- {whileai-0.54 → whileai-0.55}/README.md +0 -0
- {whileai-0.54 → whileai-0.55}/setup.cfg +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/__init__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/_env.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/auth.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/cli.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/ingest.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/py.typed +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/__main__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/data.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/environment.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/adapters.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/diversity.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/generator.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/scenarios.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/platform.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/traces.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/monitor.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/py.typed +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/run/config.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/run/engine.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/run/rows.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/run/spec.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/schema.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/schemas/row-v1.json +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/delta.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/grade_llm.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/hygiene.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/optimize.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/passat.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/preflight.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/privileged.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/stats.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/style.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/simulation.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai/simulations/world/sandbox.py +0 -0
- {whileai-0.54 → whileai-0.55}/whileai.egg-info/SOURCES.txt +0 -0
- {whileai-0.54 → whileai-0.55}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.54 → whileai-0.55}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.54 → whileai-0.55}/whileai.egg-info/requires.txt +0 -0
- {whileai-0.54 → whileai-0.55}/whileai.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.55"
|
|
8
8
|
description = "While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -175,6 +175,7 @@ from .training import (
|
|
|
175
175
|
TrainingRun,
|
|
176
176
|
attach_delta,
|
|
177
177
|
attach_holdout,
|
|
178
|
+
delete_model,
|
|
178
179
|
delete_run,
|
|
179
180
|
get_run,
|
|
180
181
|
list_runs,
|
|
@@ -183,6 +184,7 @@ from .training import (
|
|
|
183
184
|
serve,
|
|
184
185
|
train,
|
|
185
186
|
training_run,
|
|
187
|
+
unserve,
|
|
186
188
|
)
|
|
187
189
|
from .verify import Verifier, verifier
|
|
188
190
|
from .world.sandbox import MockEnvironment
|
|
@@ -253,6 +255,7 @@ __all__ = [
|
|
|
253
255
|
"dedupe_groups",
|
|
254
256
|
"delete_dataset",
|
|
255
257
|
"delete_empty_datasets",
|
|
258
|
+
"delete_model",
|
|
256
259
|
"delete_run",
|
|
257
260
|
"delta_report",
|
|
258
261
|
"dimensions_from_traces",
|
|
@@ -382,6 +385,7 @@ __all__ = [
|
|
|
382
385
|
"trim_unanimous_groups",
|
|
383
386
|
"ungrounded_arguments",
|
|
384
387
|
"unpublish",
|
|
388
|
+
"unserve",
|
|
385
389
|
"update_dataset",
|
|
386
390
|
"validate",
|
|
387
391
|
"verifier",
|
|
@@ -734,6 +734,7 @@ export_dataset = export_training
|
|
|
734
734
|
_PAIR_KEYS = (
|
|
735
735
|
"tie",
|
|
736
736
|
"pairwise",
|
|
737
|
+
"first_turn_differs",
|
|
737
738
|
"chosen_score",
|
|
738
739
|
"rejected_score",
|
|
739
740
|
"margin",
|
|
@@ -853,6 +854,12 @@ def export_preference(
|
|
|
853
854
|
length_note = length_confound_warning(chosen_longer, len(deltas))
|
|
854
855
|
if length_note:
|
|
855
856
|
report["warnings"] = [length_note]
|
|
857
|
+
identical = sum(1 for r in out_rows if r.get("first_turn_differs") is False)
|
|
858
|
+
if identical:
|
|
859
|
+
from .score.judging import first_turn_note
|
|
860
|
+
|
|
861
|
+
report["first_turn_identical"] = identical
|
|
862
|
+
report.setdefault("warnings", []).append(first_turn_note(identical, len(out_rows)))
|
|
856
863
|
margins = [r["margin"] for r in out_rows if isinstance(r.get("margin"), (int, float))]
|
|
857
864
|
if margins:
|
|
858
865
|
report["mean_margin"] = round(sum(margins) / len(margins), 4)
|
|
@@ -10,7 +10,7 @@ import os
|
|
|
10
10
|
import re
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
|
-
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Callable, Mapping
|
|
14
14
|
from typing import Any
|
|
15
15
|
from urllib.parse import urlparse
|
|
16
16
|
|
|
@@ -531,6 +531,7 @@ def complete(
|
|
|
531
531
|
timeout: float = 60,
|
|
532
532
|
n: int = 1,
|
|
533
533
|
logprobs: bool | str = False,
|
|
534
|
+
extra: Mapping[str, Any] | None = None,
|
|
534
535
|
) -> dict:
|
|
535
536
|
"""POST /chat/completions. Reuses a thread-local keep-alive connection.
|
|
536
537
|
|
|
@@ -577,6 +578,8 @@ def complete(
|
|
|
577
578
|
if tools:
|
|
578
579
|
payload["tools"] = _wire_tools(tools)
|
|
579
580
|
payload.update(_request_extras(base_url, model))
|
|
581
|
+
if extra:
|
|
582
|
+
payload.update(dict(extra))
|
|
580
583
|
headers = {"Content-Type": "application/json", "Connection": "keep-alive"}
|
|
581
584
|
if key:
|
|
582
585
|
headers["Authorization"] = f"Bearer {key}"
|
|
@@ -730,8 +733,21 @@ def _strip_tool_markup(text: str) -> str:
|
|
|
730
733
|
return cleaned.strip()
|
|
731
734
|
|
|
732
735
|
|
|
736
|
+
_THINK_BLOCK = re.compile(r"<think>.*?</think>\s*", re.S | re.I)
|
|
737
|
+
_THINK_OPEN = re.compile(r"<think>.*\Z", re.S | re.I)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _strip_think(text: str) -> str:
|
|
741
|
+
"""Drop a thinking model's reasoning markup. A closed block goes whole;
|
|
742
|
+
an unclosed ``<think>`` (the token cap landed inside it) goes to the
|
|
743
|
+
end. What is left is the reply, which is what a grader, a marker and
|
|
744
|
+
the next turn's history should see (#264)."""
|
|
745
|
+
text = _THINK_BLOCK.sub("", text)
|
|
746
|
+
return _THINK_OPEN.sub("", text)
|
|
747
|
+
|
|
748
|
+
|
|
733
749
|
def _spoken_text(reply: dict) -> str:
|
|
734
|
-
return _strip_tool_markup(str(reply.get("content") or ""))
|
|
750
|
+
return _strip_tool_markup(_strip_think(str(reply.get("content") or "")))
|
|
735
751
|
|
|
736
752
|
|
|
737
753
|
def _calls_from_reply(reply: dict) -> tuple[list[dict], dict]:
|
|
@@ -1391,9 +1407,23 @@ def local_model(
|
|
|
1391
1407
|
timeout: float = 60,
|
|
1392
1408
|
max_tokens: int | None = None,
|
|
1393
1409
|
user_model: str | None = None,
|
|
1410
|
+
thinking: bool | None = None,
|
|
1394
1411
|
) -> Callable:
|
|
1412
|
+
"""An agent that talks to an OpenAI-compatible endpoint (a served
|
|
1413
|
+
adapter, a local vLLM, any chat server) for ``simulate(agent=...)``.
|
|
1414
|
+
|
|
1415
|
+
``thinking`` is for reasoning bases such as Qwen3: ``False`` sends
|
|
1416
|
+
``chat_template_kwargs={"enable_thinking": False}`` so the reply is
|
|
1417
|
+
the answer, not the reasoning, the way the hosted Qwen path already
|
|
1418
|
+
does; ``True`` asks for it; ``None`` (the default) sends nothing and
|
|
1419
|
+
leaves the server's default. Either way ``<think>`` markup never
|
|
1420
|
+
reaches ``step["text"]`` or ``final_text``.
|
|
1421
|
+
"""
|
|
1395
1422
|
local = threading.local()
|
|
1396
1423
|
plans = fault_plans if fault_plans is not None else {}
|
|
1424
|
+
extras: dict[str, Any] | None = (
|
|
1425
|
+
None if thinking is None else {"chat_template_kwargs": {"enable_thinking": bool(thinking)}}
|
|
1426
|
+
)
|
|
1397
1427
|
# The simulated user's model. None means the agent's own model plays
|
|
1398
1428
|
# the user (the default); a backend spec moves that role to another
|
|
1399
1429
|
# model, with the key resolved for that endpoint.
|
|
@@ -1438,6 +1468,7 @@ def local_model(
|
|
|
1438
1468
|
temperature=temperature,
|
|
1439
1469
|
timeout=timeout,
|
|
1440
1470
|
max_tokens=120,
|
|
1471
|
+
extra=extras,
|
|
1441
1472
|
)
|
|
1442
1473
|
opener_text = (_spoken_text(greet) or "").strip()
|
|
1443
1474
|
if opener_text:
|
|
@@ -1477,6 +1508,7 @@ def local_model(
|
|
|
1477
1508
|
timeout=timeout,
|
|
1478
1509
|
max_tokens=reply_budget(max_tokens),
|
|
1479
1510
|
logprobs=logprobs,
|
|
1511
|
+
extra=extras,
|
|
1480
1512
|
)
|
|
1481
1513
|
calls, assistant = _calls_from_reply(reply)
|
|
1482
1514
|
# One agent turn, one set of sampling facts, on its first step.
|
|
@@ -40,6 +40,15 @@ _FAULT_ALIASES = {
|
|
|
40
40
|
"entity already acted on": "already_done",
|
|
41
41
|
}
|
|
42
42
|
_CLEAN_FAULT = {"", "ok", "created", "deleted", "success", "exit_0", "no fault", "clean"}
|
|
43
|
+
#: Statuses a tool result can carry that mean the call failed. A ``status``
|
|
44
|
+
#: key with any other value (``paid``, ``open``, ``completed``) is the
|
|
45
|
+
#: tool's own vocabulary, not a fault (#261).
|
|
46
|
+
_KNOWN_FAULTS = (
|
|
47
|
+
_BAD_STATUS
|
|
48
|
+
| set(_FAULT_ALIASES)
|
|
49
|
+
| set(_FAULT_ALIASES.values())
|
|
50
|
+
| {"stale", "malformed", "already_done", "not_found", "deny", "garbled"}
|
|
51
|
+
)
|
|
43
52
|
_HTTP_FAIL = re.compile(r"^[45]\d\d$")
|
|
44
53
|
_INFRA_STUB = re.compile(
|
|
45
54
|
r"^<agent error:|returned\s+[45]\d\d\b|^(https?://\S+\s+)?[45]\d\d(\s|$)", re.I
|
|
@@ -426,10 +435,10 @@ def _fault_from_result(result) -> str:
|
|
|
426
435
|
if status in {"already_done", "already_acted_on"} or reason == "already_acted_on":
|
|
427
436
|
return "already_done"
|
|
428
437
|
name = normalize_fault_name(status)
|
|
429
|
-
if name:
|
|
438
|
+
if name and (name in _KNOWN_FAULTS or status in _KNOWN_FAULTS):
|
|
430
439
|
return name
|
|
431
440
|
if _step_faulted(result):
|
|
432
|
-
return
|
|
441
|
+
return name or "error"
|
|
433
442
|
return ""
|
|
434
443
|
|
|
435
444
|
|
|
@@ -563,6 +563,42 @@ def length_confound_warning(chosen_longer: int, n: int) -> str | None:
|
|
|
563
563
|
return None
|
|
564
564
|
|
|
565
565
|
|
|
566
|
+
def _first_turn(row: dict) -> str:
|
|
567
|
+
"""What the policy emitted first, read the way the hosted DPO trainer
|
|
568
|
+
reads it: the first tool step as its call, else the first assistant
|
|
569
|
+
text, else ``final_text``."""
|
|
570
|
+
steps = [s for s in (row.get("steps") or []) if isinstance(s, dict)]
|
|
571
|
+
for step in steps:
|
|
572
|
+
if step.get("tool"):
|
|
573
|
+
args = step.get("arguments")
|
|
574
|
+
if args is None:
|
|
575
|
+
args = step.get("args")
|
|
576
|
+
return json.dumps({"name": step["tool"], "arguments": args or {}}, sort_keys=True)
|
|
577
|
+
for step in steps:
|
|
578
|
+
if str(step.get("text") or "").strip():
|
|
579
|
+
return str(step["text"]).strip()
|
|
580
|
+
for message in row.get("messages") or []:
|
|
581
|
+
if isinstance(message, dict) and message.get("role") == "assistant":
|
|
582
|
+
text = str(message.get("content") or "").strip()
|
|
583
|
+
if text:
|
|
584
|
+
return text
|
|
585
|
+
return str(row.get("final_text") or "").strip()
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def first_turn_note(identical: int, n: int) -> str:
|
|
589
|
+
"""The warning for pairs whose first assistant turns read the same."""
|
|
590
|
+
if not identical:
|
|
591
|
+
return ""
|
|
592
|
+
left = n - identical
|
|
593
|
+
return (
|
|
594
|
+
f"{identical}/{n} pairs have identical first assistant turns (same opening tool call "
|
|
595
|
+
"or line); their contrast is later in the rollout. The hosted DPO trainer compares "
|
|
596
|
+
f"first turns only and will drop them, leaving {left} (it needs at least 8). Keep "
|
|
597
|
+
"[p for p in pairs if p['first_turn_differs']] to see what it will train on, or "
|
|
598
|
+
"export_preference(pairs) for a trainer that reads whole conversations."
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
566
602
|
def build_preference_pairs(
|
|
567
603
|
rows: Sequence[dict],
|
|
568
604
|
*,
|
|
@@ -651,6 +687,7 @@ def build_preference_pairs(
|
|
|
651
687
|
"rejected_model": r_model,
|
|
652
688
|
"same_policy": (c_model == r_model) if c_model and r_model else None,
|
|
653
689
|
"length_delta": reply_length(chosen) - reply_length(rejected),
|
|
690
|
+
"first_turn_differs": _first_turn(chosen) != _first_turn(rejected),
|
|
654
691
|
"chosen_reason": str(chosen.get("reason") or ""),
|
|
655
692
|
"rejected_reason": str(rejected.get("reason") or ""),
|
|
656
693
|
"rejected_failure_class": rejected.get("failure_class"),
|
|
@@ -673,10 +710,13 @@ def build_preference_pairs(
|
|
|
673
710
|
for p in pairs
|
|
674
711
|
if p["chosen_score"] not in (0.0, 1.0) or p["rejected_score"] not in (0.0, 1.0)
|
|
675
712
|
)
|
|
713
|
+
identical = sum(1 for p in pairs if not p["first_turn_differs"])
|
|
676
714
|
warnings: list[str] = []
|
|
677
715
|
length_note = length_confound_warning(chosen_longer, n)
|
|
678
716
|
if length_note:
|
|
679
717
|
warnings.append(length_note)
|
|
718
|
+
if identical:
|
|
719
|
+
warnings.append(first_turn_note(identical, n))
|
|
680
720
|
if mixed_policy:
|
|
681
721
|
warnings.append(
|
|
682
722
|
f"{mixed_policy}/{n} pairs mix policies (chosen and rejected from different "
|
|
@@ -694,6 +734,8 @@ def build_preference_pairs(
|
|
|
694
734
|
"min_margin": min_margin,
|
|
695
735
|
"mean_margin": round(sum(p["margin"] for p in pairs) / n, 4) if n else None,
|
|
696
736
|
"partial_score_pairs": partial,
|
|
737
|
+
"first_turn_identical": identical,
|
|
738
|
+
"trainer_pairs": n - identical,
|
|
697
739
|
"same_policy_pairs": same_policy,
|
|
698
740
|
"mixed_policy_pairs": mixed_policy,
|
|
699
741
|
"eval_sourced": eval_pairs,
|
|
@@ -474,6 +474,11 @@ class TrainingRun:
|
|
|
474
474
|
else:
|
|
475
475
|
self.finish("done")
|
|
476
476
|
|
|
477
|
+
@property
|
|
478
|
+
def id(self) -> str:
|
|
479
|
+
"""The run id, the handle ``get_run``, ``serve`` and ``delete_run`` take."""
|
|
480
|
+
return self.run_id
|
|
481
|
+
|
|
477
482
|
def __repr__(self) -> str:
|
|
478
483
|
return (
|
|
479
484
|
f"TrainingRun({self.run_id!r}, {self.name!r}, status={self.status!r}, step={self.step})"
|
|
@@ -825,11 +830,40 @@ def models(*, api_key: str | None = None) -> list[dict[str, Any]]:
|
|
|
825
830
|
"""The account's hosted models: ``name``, ``baseModel``, ``adapter``,
|
|
826
831
|
``adapterRunId``, ``version``, ``endpoint`` (an OpenAI-compatible
|
|
827
832
|
base URL; send the account key as the bearer and ``name`` as the
|
|
828
|
-
model).
|
|
833
|
+
model).
|
|
834
|
+
|
|
835
|
+
A row here is a registry entry, not a running GPU: the endpoint
|
|
836
|
+
behind it idles to zero on its own and an unused model costs nothing.
|
|
837
|
+
The row stays until ``unserve(name)`` removes it; serving the same
|
|
838
|
+
name again bumps its ``version`` rather than adding a row."""
|
|
829
839
|
out = _call("GET", "/models", api_key)
|
|
830
840
|
return list(out.get("models") or []) if isinstance(out, dict) else []
|
|
831
841
|
|
|
832
842
|
|
|
843
|
+
def unserve(
|
|
844
|
+
name: str,
|
|
845
|
+
*,
|
|
846
|
+
api_key: str | None = None,
|
|
847
|
+
transport: Callable[..., Any] | None = None,
|
|
848
|
+
) -> dict[str, Any]:
|
|
849
|
+
"""Stop hosting ``name``: removes the model row from the account, so
|
|
850
|
+
``models()`` no longer lists it and its endpoint stops answering for
|
|
851
|
+
that name. The inverse of ``serve``, the way ``delete_dataset`` is the
|
|
852
|
+
inverse of ``push``. The adapter weights and the training run stay;
|
|
853
|
+
``serve`` the run again to bring it back (at version 1).
|
|
854
|
+
Returns ``{"name": ..., "deleted": True}``."""
|
|
855
|
+
call = transport or _call
|
|
856
|
+
key = str(name).strip().lower()
|
|
857
|
+
if not key:
|
|
858
|
+
raise ValueError("unserve: name is the hosted model's name, as models() lists it")
|
|
859
|
+
out = call("DELETE", f"/models/{key}", api_key)
|
|
860
|
+
return dict(out) if isinstance(out, dict) else {"name": key, "deleted": True}
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
#: Same call, the other spelling: symmetric with ``delete_dataset``.
|
|
864
|
+
delete_model = unserve
|
|
865
|
+
|
|
866
|
+
|
|
833
867
|
def serve(
|
|
834
868
|
name: str,
|
|
835
869
|
run: TrainingRun | str | None = None,
|
|
@@ -842,18 +876,36 @@ def serve(
|
|
|
842
876
|
row; ``endpoint`` is the OpenAI-compatible base URL and ``name`` the
|
|
843
877
|
model id to send. Posting an existing name bumps ``version``.
|
|
844
878
|
|
|
845
|
-
``run`` is a ``TrainingRun
|
|
846
|
-
|
|
847
|
-
serves the bare base
|
|
879
|
+
``run`` is a ``TrainingRun``, the record ``get_run`` returns, or the
|
|
880
|
+
run id; the adapter and base model come from the run record unless
|
|
881
|
+
``base_model`` is given. No ``run`` serves the bare base
|
|
882
|
+
(``base_model`` required). ``unserve`` is the inverse.
|
|
848
883
|
"""
|
|
849
884
|
call = transport or _call
|
|
850
885
|
adapter: str | None = None
|
|
851
886
|
base = base_model
|
|
887
|
+
run_id: str | None
|
|
852
888
|
if isinstance(run, TrainingRun):
|
|
853
889
|
adapter = run.adapter
|
|
854
|
-
run_id
|
|
890
|
+
run_id = run.run_id
|
|
891
|
+
elif isinstance(run, dict):
|
|
892
|
+
# The record ``get_run`` returns. Reading the id here is what lets
|
|
893
|
+
# train in one process and serve in the next compose (#262).
|
|
894
|
+
run_id = str(run.get("runId") or run.get("run_id") or run.get("id") or "").strip() or None
|
|
895
|
+
if not run_id:
|
|
896
|
+
raise TypeError(
|
|
897
|
+
"run is a dict with no runId; pass the record wai.get_run(run_id) returns, "
|
|
898
|
+
"a TrainingRun, or the run id string"
|
|
899
|
+
)
|
|
900
|
+
adapter = str(run.get("adapter") or "").strip() or None
|
|
901
|
+
base = base or run.get("baseModel") or run.get("base_model")
|
|
902
|
+
elif run is None or isinstance(run, str):
|
|
903
|
+
run_id = (str(run).strip() or None) if run else None
|
|
855
904
|
else:
|
|
856
|
-
|
|
905
|
+
raise TypeError(
|
|
906
|
+
"run must be a TrainingRun, the record wai.get_run(run_id) returns, or the run "
|
|
907
|
+
f"id string; got {type(run).__name__}"
|
|
908
|
+
)
|
|
857
909
|
if run_id and (adapter is None or base is None):
|
|
858
910
|
meta = call("GET", f"/runs/{run_id}", api_key)
|
|
859
911
|
meta = meta if isinstance(meta, dict) else {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|