world-model-optimizer 0.2.0__py3-none-any.whl
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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Serve-side trace access: read a model's local traces, or fetch them from the Hugging Face Hub.
|
|
2
|
+
|
|
3
|
+
The raw trace corpus (`traces.otel.jsonl`) is large and need not be committed. When it is present
|
|
4
|
+
locally it is used directly; otherwise, if the model's card declares a `traces_hf` source, the
|
|
5
|
+
backend streams it from the Hub's public resolve URL (no auth, no client-side Hub API) into the
|
|
6
|
+
model directory, reporting byte progress the website can poll. A local copy always supersedes the
|
|
7
|
+
Hub. Recorded traces are grouped by task into replayable scenarios for the Explore-traces tab.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
from wmo.config.card import TracesSource
|
|
21
|
+
from wmo.core.types import Action
|
|
22
|
+
from wmo.ingest import get_adapter
|
|
23
|
+
|
|
24
|
+
TRACES_FILENAME = "traces.otel.jsonl"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_url(source: TracesSource) -> str:
|
|
28
|
+
"""The public Hub resolve URL for a traces source (works unauthenticated for public repos)."""
|
|
29
|
+
prefix = "datasets/" if source.kind == "dataset" else ""
|
|
30
|
+
return f"https://huggingface.co/{prefix}{source.repo}/resolve/{source.revision}/{source.path}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def local_traces_path(model_dir: Path) -> Path | None:
|
|
34
|
+
"""The traces file for a model, if present: a downloaded copy, else the example sibling."""
|
|
35
|
+
downloaded = model_dir / TRACES_FILENAME
|
|
36
|
+
if downloaded.is_file():
|
|
37
|
+
return downloaded
|
|
38
|
+
# examples/<task>/traces.otel.jsonl sits two levels above examples/<task>/models/<name>/.
|
|
39
|
+
sibling = model_dir.parent.parent / TRACES_FILENAME
|
|
40
|
+
if sibling.is_file():
|
|
41
|
+
return sibling
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _action_label(action: Action) -> str:
|
|
46
|
+
"""Format an action in the wmo-play grammar (matches the web index generator)."""
|
|
47
|
+
if action.kind.value == "tool_call":
|
|
48
|
+
return f"{action.name} {action.arguments}" if action.arguments else (action.name or "")
|
|
49
|
+
return f"say {action.content or ''}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ScenarioStep(BaseModel):
|
|
53
|
+
action: Action
|
|
54
|
+
action_label: str
|
|
55
|
+
observation: str
|
|
56
|
+
is_error: bool
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TraceScenario(BaseModel):
|
|
60
|
+
id: str
|
|
61
|
+
label: str
|
|
62
|
+
task: str | None
|
|
63
|
+
steps: list[ScenarioStep]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _clip(text: str, limit: int) -> str:
|
|
67
|
+
flat = " ".join(text.split())
|
|
68
|
+
return flat if len(flat) <= limit else flat[: limit - 1] + "…"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def scenarios_from_traces(
|
|
72
|
+
path: Path, *, max_scenarios: int = 6, max_steps: int = 10
|
|
73
|
+
) -> list[TraceScenario]:
|
|
74
|
+
"""Normalize a traces.otel.jsonl into a bounded list of replayable scenarios (one per trace)."""
|
|
75
|
+
traces = get_adapter("otel-genai").from_file(str(path))
|
|
76
|
+
out: list[TraceScenario] = []
|
|
77
|
+
for i, trace in enumerate(traces):
|
|
78
|
+
steps: list[ScenarioStep] = []
|
|
79
|
+
for step in trace.steps:
|
|
80
|
+
if step.observation.content is None:
|
|
81
|
+
continue
|
|
82
|
+
steps.append(
|
|
83
|
+
ScenarioStep(
|
|
84
|
+
action=step.action,
|
|
85
|
+
action_label=_clip(_action_label(step.action), 100),
|
|
86
|
+
observation=step.observation.content,
|
|
87
|
+
is_error=step.observation.is_error,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
if len(steps) >= max_steps:
|
|
91
|
+
break
|
|
92
|
+
if not steps:
|
|
93
|
+
continue
|
|
94
|
+
task = trace.steps[0].task if trace.steps else None
|
|
95
|
+
out.append(
|
|
96
|
+
TraceScenario(id=f"t{i}", label=_scenario_label(task, i), task=task, steps=steps)
|
|
97
|
+
)
|
|
98
|
+
if len(out) >= max_scenarios:
|
|
99
|
+
break
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _scenario_label(task: str | None, index: int) -> str:
|
|
104
|
+
if not task:
|
|
105
|
+
return f"Scenario {index + 1}"
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
parsed = json.loads(task)
|
|
110
|
+
text = parsed.get("reason_for_call") or parsed.get("task_instructions") or task
|
|
111
|
+
except (json.JSONDecodeError, AttributeError):
|
|
112
|
+
text = task
|
|
113
|
+
return _clip(text, 68)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class DownloadStatus(StrEnum):
|
|
117
|
+
RUNNING = "running"
|
|
118
|
+
DONE = "done"
|
|
119
|
+
FAILED = "failed"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DownloadProgress(BaseModel):
|
|
123
|
+
status: DownloadStatus
|
|
124
|
+
downloaded: int = 0
|
|
125
|
+
total: int | None = None # bytes, when the server reports Content-Length
|
|
126
|
+
error: str | None = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _DownloadState:
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
self.progress = DownloadProgress(status=DownloadStatus.RUNNING)
|
|
132
|
+
self.lock = threading.Lock()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TracesDownloader:
|
|
136
|
+
"""Runs Hub trace downloads on background threads, one in flight per model name."""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self, *, fetch: Callable[[str, Path, Callable[[int, int | None], None]], None] | None = None
|
|
140
|
+
) -> None:
|
|
141
|
+
self._fetch = fetch or _stream_to_file
|
|
142
|
+
self._states: dict[str, _DownloadState] = {}
|
|
143
|
+
self._lock = threading.Lock()
|
|
144
|
+
|
|
145
|
+
def start(self, name: str, url: str, dest: Path) -> None:
|
|
146
|
+
with self._lock:
|
|
147
|
+
existing = self._states.get(name)
|
|
148
|
+
if existing and existing.progress.status is DownloadStatus.RUNNING:
|
|
149
|
+
return # already downloading; the client just polls
|
|
150
|
+
self._states[name] = _DownloadState()
|
|
151
|
+
thread = threading.Thread(
|
|
152
|
+
target=self._run, args=(name, url, dest), name=f"wmo-traces-{name}", daemon=True
|
|
153
|
+
)
|
|
154
|
+
thread.start()
|
|
155
|
+
|
|
156
|
+
def _run(self, name: str, url: str, dest: Path) -> None:
|
|
157
|
+
state = self._states[name]
|
|
158
|
+
|
|
159
|
+
def on_progress(downloaded: int, total: int | None) -> None:
|
|
160
|
+
with state.lock:
|
|
161
|
+
state.progress.downloaded = downloaded
|
|
162
|
+
state.progress.total = total
|
|
163
|
+
|
|
164
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
tmp = dest.with_suffix(dest.suffix + ".part")
|
|
166
|
+
try:
|
|
167
|
+
self._fetch(url, tmp, on_progress)
|
|
168
|
+
tmp.replace(dest) # atomic: a partial download never looks complete
|
|
169
|
+
except Exception as exc: # noqa: BLE001 - report any failure to the client
|
|
170
|
+
tmp.unlink(missing_ok=True)
|
|
171
|
+
with state.lock:
|
|
172
|
+
state.progress.status = DownloadStatus.FAILED
|
|
173
|
+
state.progress.error = str(exc)
|
|
174
|
+
return
|
|
175
|
+
with state.lock:
|
|
176
|
+
state.progress.status = DownloadStatus.DONE
|
|
177
|
+
|
|
178
|
+
def progress(self, name: str) -> DownloadProgress | None:
|
|
179
|
+
state = self._states.get(name)
|
|
180
|
+
if state is None:
|
|
181
|
+
return None
|
|
182
|
+
with state.lock:
|
|
183
|
+
return state.progress.model_copy()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _stream_to_file(url: str, dest: Path, on_progress: Callable[[int, int | None], None]) -> None:
|
|
187
|
+
"""Stream a public Hub file to disk in chunks, following the CDN redirect."""
|
|
188
|
+
with httpx.stream("GET", url, follow_redirects=True, timeout=60.0) as resp:
|
|
189
|
+
resp.raise_for_status()
|
|
190
|
+
raw = resp.headers.get("content-length")
|
|
191
|
+
total = int(raw) if raw and raw.isdigit() else None
|
|
192
|
+
downloaded = 0
|
|
193
|
+
with dest.open("wb") as fh:
|
|
194
|
+
for chunk in resp.iter_bytes(1024 * 1024):
|
|
195
|
+
fh.write(chunk)
|
|
196
|
+
downloaded += len(chunk)
|
|
197
|
+
on_progress(downloaded, total)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TracesResponse(BaseModel):
|
|
201
|
+
"""What the Explore-traces tab needs: local scenarios if present, else a Hub download offer."""
|
|
202
|
+
|
|
203
|
+
source: str # "local" | "hub" | "none"
|
|
204
|
+
downloadable: bool
|
|
205
|
+
scenarios: list[TraceScenario] = Field(default_factory=list)
|
|
206
|
+
download: DownloadProgress | None = None
|
wmo/telemetry.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Best-effort anonymous usage telemetry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from atexit import register
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from posthog import Posthog
|
|
13
|
+
|
|
14
|
+
from wmo.config import ARTIFACT_DIR
|
|
15
|
+
from wmo.config.settings import ensure_telemetry_anonymous_id, load_settings
|
|
16
|
+
from wmo.engine.reporting import BuildReporter
|
|
17
|
+
from wmo.tracking import RunRecord
|
|
18
|
+
|
|
19
|
+
POSTHOG_PROJECT_API_KEY = "phc_rPFfCufWpxyctR7duEZTTXovP4k5kbHqSqzd4Z4MQJdL"
|
|
20
|
+
POSTHOG_HOST = "https://us.i.posthog.com"
|
|
21
|
+
|
|
22
|
+
TelemetryValue = str | int | float | bool | None
|
|
23
|
+
TelemetryProperties = dict[str, TelemetryValue]
|
|
24
|
+
|
|
25
|
+
_FALSE_VALUES = {"0", "false", "off", "no"}
|
|
26
|
+
_TRUE_VALUES = {"1", "true", "on", "yes"}
|
|
27
|
+
_CLIENTS: dict[tuple[str, str], Posthog] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class BuildTelemetryStats:
|
|
32
|
+
input_trace_count: int = 0
|
|
33
|
+
input_step_count: int = 0
|
|
34
|
+
train_trace_count: int = 0
|
|
35
|
+
val_trace_count: int = 0
|
|
36
|
+
heldout_trace_count: int = 0
|
|
37
|
+
indexed_step_count: int = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TelemetryBuildReporter:
|
|
41
|
+
def __init__(self, inner: BuildReporter, stats: BuildTelemetryStats) -> None:
|
|
42
|
+
self._inner = inner
|
|
43
|
+
self._stats = stats
|
|
44
|
+
|
|
45
|
+
def ingest_done(self, traces: int, steps: int) -> None:
|
|
46
|
+
self._stats.input_trace_count = traces
|
|
47
|
+
self._stats.input_step_count = steps
|
|
48
|
+
self._inner.ingest_done(traces, steps)
|
|
49
|
+
|
|
50
|
+
def split_done(self, train: int, val: int, test: int) -> None:
|
|
51
|
+
self._stats.train_trace_count = train
|
|
52
|
+
self._stats.val_trace_count = val
|
|
53
|
+
self._stats.heldout_trace_count = test
|
|
54
|
+
self._inner.split_done(train, val, test)
|
|
55
|
+
|
|
56
|
+
def index_done(self, steps: int) -> None:
|
|
57
|
+
self._stats.indexed_step_count = steps
|
|
58
|
+
self._inner.index_done(steps)
|
|
59
|
+
|
|
60
|
+
def optimize_start(self, budget: int) -> None:
|
|
61
|
+
self._inner.optimize_start(budget)
|
|
62
|
+
|
|
63
|
+
def activity(self, line: str) -> None:
|
|
64
|
+
self._inner.activity(line)
|
|
65
|
+
|
|
66
|
+
def rollout(self, done: int, budget: int, score: float | None) -> None:
|
|
67
|
+
self._inner.rollout(done, budget, score)
|
|
68
|
+
|
|
69
|
+
def optimize_done(self, held_out_accuracy: float, frontier_size: int, rollouts: int) -> None:
|
|
70
|
+
self._inner.optimize_done(held_out_accuracy, frontier_size, rollouts)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def capture(
|
|
74
|
+
event: str,
|
|
75
|
+
properties: TelemetryProperties | None = None,
|
|
76
|
+
*,
|
|
77
|
+
root: str | Path = ARTIFACT_DIR,
|
|
78
|
+
) -> bool:
|
|
79
|
+
"""Send one anonymous metadata-only event. Returns False when skipped or failed."""
|
|
80
|
+
if not _enabled(root):
|
|
81
|
+
return False
|
|
82
|
+
api_key = os.getenv("WMO_POSTHOG_PROJECT_API_KEY", POSTHOG_PROJECT_API_KEY).strip()
|
|
83
|
+
if not api_key:
|
|
84
|
+
return False
|
|
85
|
+
host = os.getenv("WMO_POSTHOG_HOST", POSTHOG_HOST).rstrip("/")
|
|
86
|
+
try:
|
|
87
|
+
distinct_id = ensure_telemetry_anonymous_id(root)
|
|
88
|
+
event_properties: TelemetryProperties = {
|
|
89
|
+
"$process_person_profile": False,
|
|
90
|
+
"wmo_version": _wmo_version(),
|
|
91
|
+
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}",
|
|
92
|
+
**(properties or {}),
|
|
93
|
+
}
|
|
94
|
+
# Never log prompts, traces, actions, observations, paths, models, credentials, or text.
|
|
95
|
+
message_id = _posthog_client(api_key, host).capture(
|
|
96
|
+
event,
|
|
97
|
+
distinct_id=distinct_id,
|
|
98
|
+
properties=event_properties,
|
|
99
|
+
)
|
|
100
|
+
return message_id is not None
|
|
101
|
+
except (OSError, ValueError):
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def capture_build_completed(
|
|
106
|
+
*,
|
|
107
|
+
stats: BuildTelemetryStats,
|
|
108
|
+
gepa_budget: int,
|
|
109
|
+
rollouts_used: int,
|
|
110
|
+
frontier_size: int,
|
|
111
|
+
record: RunRecord,
|
|
112
|
+
root: str | Path,
|
|
113
|
+
) -> None:
|
|
114
|
+
capture(
|
|
115
|
+
"wmo build completed",
|
|
116
|
+
{
|
|
117
|
+
"success": True,
|
|
118
|
+
"input_trace_count": stats.input_trace_count,
|
|
119
|
+
"input_step_count": stats.input_step_count,
|
|
120
|
+
"train_trace_count": stats.train_trace_count,
|
|
121
|
+
"val_trace_count": stats.val_trace_count,
|
|
122
|
+
"heldout_trace_count": stats.heldout_trace_count,
|
|
123
|
+
"indexed_step_count": stats.indexed_step_count,
|
|
124
|
+
"gepa_budget": gepa_budget,
|
|
125
|
+
"rollouts_used": rollouts_used,
|
|
126
|
+
"frontier_size": frontier_size,
|
|
127
|
+
"duration_seconds": round(record.duration_seconds, 3),
|
|
128
|
+
"llm_call_count": record.total.calls,
|
|
129
|
+
"input_tokens": record.total.input_tokens,
|
|
130
|
+
"output_tokens": record.total.output_tokens,
|
|
131
|
+
"cost_usd": round(record.total.cost_usd, 6),
|
|
132
|
+
},
|
|
133
|
+
root=root,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def capture_eval_completed(
|
|
138
|
+
*,
|
|
139
|
+
mode: str,
|
|
140
|
+
file_count: int,
|
|
141
|
+
scored_step_count: int,
|
|
142
|
+
rag_enabled: bool,
|
|
143
|
+
sample_turns: str,
|
|
144
|
+
train_split: float,
|
|
145
|
+
top_k: int,
|
|
146
|
+
root: str | Path,
|
|
147
|
+
) -> None:
|
|
148
|
+
capture(
|
|
149
|
+
"wmo eval completed",
|
|
150
|
+
{
|
|
151
|
+
"success": True,
|
|
152
|
+
"eval_mode": mode,
|
|
153
|
+
"file_count": file_count,
|
|
154
|
+
"scored_step_count": scored_step_count,
|
|
155
|
+
"rag_enabled": rag_enabled,
|
|
156
|
+
"sample_turns": sample_turns,
|
|
157
|
+
"train_split": train_split,
|
|
158
|
+
"top_k": top_k,
|
|
159
|
+
},
|
|
160
|
+
root=root,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def settings_root_from_results_root(results_root: str) -> Path:
|
|
165
|
+
path = Path(results_root)
|
|
166
|
+
return path.parent if path.name == "evals" else Path(ARTIFACT_DIR)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _enabled(root: str | Path) -> bool:
|
|
170
|
+
if _env_truthy("DO_NOT_TRACK"):
|
|
171
|
+
return False
|
|
172
|
+
env = os.getenv("WMO_TELEMETRY")
|
|
173
|
+
if env is not None:
|
|
174
|
+
return env.strip().lower() in _TRUE_VALUES
|
|
175
|
+
if os.getenv("PYTEST_CURRENT_TEST"):
|
|
176
|
+
return False
|
|
177
|
+
try:
|
|
178
|
+
return load_settings(root).telemetry.enabled
|
|
179
|
+
except (OSError, ValueError):
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _env_truthy(name: str) -> bool:
|
|
184
|
+
value = os.getenv(name)
|
|
185
|
+
if value is None:
|
|
186
|
+
return False
|
|
187
|
+
normalized = value.strip().lower()
|
|
188
|
+
if normalized in _FALSE_VALUES:
|
|
189
|
+
return False
|
|
190
|
+
return bool(normalized)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _wmo_version() -> str:
|
|
194
|
+
try:
|
|
195
|
+
return version("world-model-optimizer")
|
|
196
|
+
except PackageNotFoundError:
|
|
197
|
+
return "unknown"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _posthog_client(api_key: str, host: str) -> Posthog:
|
|
201
|
+
key = (api_key, host)
|
|
202
|
+
client = _CLIENTS.get(key)
|
|
203
|
+
if client is None:
|
|
204
|
+
client = Posthog(
|
|
205
|
+
api_key,
|
|
206
|
+
host=host,
|
|
207
|
+
flush_interval=1.0,
|
|
208
|
+
max_retries=1,
|
|
209
|
+
timeout=0.5,
|
|
210
|
+
)
|
|
211
|
+
_CLIENTS[key] = client
|
|
212
|
+
register(client.shutdown)
|
|
213
|
+
return client
|
wmo/tracking/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Run tracking: time + cost + tokens across the harness lifecycle.
|
|
2
|
+
|
|
3
|
+
Instrument at the provider boundary (`MeteredProvider`) so the world model, GEPA, and the judge are
|
|
4
|
+
all metered without changes to those modules. `RunTracker` aggregates `UsageEvent`s into
|
|
5
|
+
`UsageTotals` (priced via `wmo.tracking.pricing`) plus a wall-clock duration from an injectable
|
|
6
|
+
`Clock`; `RunRecord`s persist under `.wmo/runs/`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from wmo.tracking.clock import Clock, SystemClock
|
|
10
|
+
from wmo.tracking.metered import MeteredProvider, classify_build_call
|
|
11
|
+
from wmo.tracking.pricing import ModelPrice, cost_usd, price_for
|
|
12
|
+
from wmo.tracking.store import load_runs, save_run
|
|
13
|
+
from wmo.tracking.tracker import (
|
|
14
|
+
Phase,
|
|
15
|
+
RunRecord,
|
|
16
|
+
RunTracker,
|
|
17
|
+
UsageEvent,
|
|
18
|
+
UsageTotals,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Clock",
|
|
23
|
+
"SystemClock",
|
|
24
|
+
"MeteredProvider",
|
|
25
|
+
"classify_build_call",
|
|
26
|
+
"ModelPrice",
|
|
27
|
+
"cost_usd",
|
|
28
|
+
"price_for",
|
|
29
|
+
"load_runs",
|
|
30
|
+
"save_run",
|
|
31
|
+
"Phase",
|
|
32
|
+
"RunRecord",
|
|
33
|
+
"RunTracker",
|
|
34
|
+
"UsageEvent",
|
|
35
|
+
"UsageTotals",
|
|
36
|
+
]
|
wmo/tracking/clock.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Injectable clock so run-tracking durations are deterministic in tests.
|
|
2
|
+
|
|
3
|
+
Production uses `SystemClock` (real `time.monotonic`). Tests pass a `FakeClock` with scripted ticks
|
|
4
|
+
so cost/time assertions don't depend on wall-clock timing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class Clock(Protocol):
|
|
15
|
+
def monotonic(self) -> float:
|
|
16
|
+
"""Seconds from an arbitrary epoch; only differences are meaningful (for durations)."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SystemClock:
|
|
21
|
+
"""Default clock backed by `time.monotonic` (monotonic, unaffected by wall-clock changes)."""
|
|
22
|
+
|
|
23
|
+
def monotonic(self) -> float:
|
|
24
|
+
return time.monotonic()
|
wmo/tracking/metered.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""`MeteredProvider`: a Provider wrapper that records every call onto a RunTracker.
|
|
2
|
+
|
|
3
|
+
Instrumenting at the provider boundary means the optimizer, the judge, and the world model are all
|
|
4
|
+
metered without any of them knowing about tracking — we don't edit `gepa.py` or the judge. The
|
|
5
|
+
wrapper forwards `complete`/`embed`/`verify`/`config` to the wrapped provider unchanged and records
|
|
6
|
+
a `UsageEvent` per call.
|
|
7
|
+
|
|
8
|
+
Phase attribution: build and serve share one provider, and within build the *same* provider serves
|
|
9
|
+
GEPA rollouts, GEPA reflection, and the judge. We tell them apart by the system prompt each path
|
|
10
|
+
uses (a stable, boundary-visible signal), defaulting `complete` to whatever base phase the wrapper
|
|
11
|
+
was constructed with. Callers that want exact control can pass their own `classify`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Callable, Iterator
|
|
17
|
+
|
|
18
|
+
from wmo.optimize.judge import JUDGE_MARKER
|
|
19
|
+
from wmo.providers.base import (
|
|
20
|
+
DEFAULT_MAX_TOKENS,
|
|
21
|
+
Completion,
|
|
22
|
+
Message,
|
|
23
|
+
Provider,
|
|
24
|
+
ProviderConfig,
|
|
25
|
+
StreamChunk,
|
|
26
|
+
StreamingProvider,
|
|
27
|
+
TokenUsage,
|
|
28
|
+
VerifyResult,
|
|
29
|
+
)
|
|
30
|
+
from wmo.tracking.tracker import Phase, RunTracker
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def classify_build_call(system: str) -> Phase:
|
|
34
|
+
"""Default phase classifier for a build-time `complete`: judge vs GEPA (rollout/reflection).
|
|
35
|
+
|
|
36
|
+
The judge is recognized by `JUDGE_MARKER` (owned by judge.py, so the prompt and this classifier
|
|
37
|
+
can't drift). Everything else during build — GEPA rollouts (env-sim) and reflection — is GEPA.
|
|
38
|
+
"""
|
|
39
|
+
if JUDGE_MARKER in system:
|
|
40
|
+
return Phase.JUDGE
|
|
41
|
+
return Phase.GEPA
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MeteredProvider:
|
|
45
|
+
"""Wraps a `Provider`, recording token usage + cost per call onto a `RunTracker`.
|
|
46
|
+
|
|
47
|
+
`base_phase` is the phase for `complete` calls when no `classify` is given (e.g. `Phase.SERVE`
|
|
48
|
+
for the live world model). For build, pass `classify=classify_build_call` to split judge from
|
|
49
|
+
GEPA.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
provider: Provider,
|
|
55
|
+
tracker: RunTracker,
|
|
56
|
+
*,
|
|
57
|
+
base_phase: Phase = Phase.OTHER,
|
|
58
|
+
classify: Callable[[str], Phase] | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self._provider = provider
|
|
61
|
+
self._tracker = tracker
|
|
62
|
+
self._base_phase = base_phase
|
|
63
|
+
self._classify = classify
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def config(self) -> ProviderConfig:
|
|
67
|
+
return self._provider.config
|
|
68
|
+
|
|
69
|
+
def complete(
|
|
70
|
+
self,
|
|
71
|
+
system: str,
|
|
72
|
+
messages: list[Message],
|
|
73
|
+
*,
|
|
74
|
+
temperature: float = 0.7,
|
|
75
|
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
76
|
+
) -> Completion:
|
|
77
|
+
completion = self._provider.complete(
|
|
78
|
+
system, messages, temperature=temperature, max_tokens=max_tokens
|
|
79
|
+
)
|
|
80
|
+
phase = self._classify(system) if self._classify is not None else self._base_phase
|
|
81
|
+
# Prefer the model the completion says actually served (failover chains set it); the
|
|
82
|
+
# configured model otherwise (Completion.model validates non-empty, so `or` is exact).
|
|
83
|
+
# Pricing a failed-over call at the primary's rate would silently mis-report cost.
|
|
84
|
+
model = completion.model or self._provider.config.model
|
|
85
|
+
self._tracker.record(phase, model, completion.usage)
|
|
86
|
+
return completion
|
|
87
|
+
|
|
88
|
+
def stream(
|
|
89
|
+
self,
|
|
90
|
+
system: str,
|
|
91
|
+
messages: list[Message],
|
|
92
|
+
*,
|
|
93
|
+
temperature: float = 0.7,
|
|
94
|
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
95
|
+
) -> Iterator[StreamChunk]:
|
|
96
|
+
"""Forward a native stream, recording usage from the terminal chunk.
|
|
97
|
+
|
|
98
|
+
A stream abandoned before its terminal chunk records nothing even though the provider
|
|
99
|
+
billed the partial generation; serving owns closing streams it starts.
|
|
100
|
+
"""
|
|
101
|
+
if not isinstance(self._provider, StreamingProvider):
|
|
102
|
+
raise TypeError(
|
|
103
|
+
f"{type(self._provider).__name__} does not implement StreamingProvider; "
|
|
104
|
+
"stream() is only available for native backends"
|
|
105
|
+
)
|
|
106
|
+
phase = self._classify(system) if self._classify is not None else self._base_phase
|
|
107
|
+
for chunk in self._provider.stream(
|
|
108
|
+
system, messages, temperature=temperature, max_tokens=max_tokens
|
|
109
|
+
):
|
|
110
|
+
if chunk.done and chunk.usage is not None:
|
|
111
|
+
model = chunk.model or self._provider.config.model
|
|
112
|
+
self._tracker.record(phase, model, chunk.usage)
|
|
113
|
+
yield chunk
|
|
114
|
+
|
|
115
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
116
|
+
# Embeddings carry no token usage from our providers; record a zero-usage event for the
|
|
117
|
+
# call count so EMBED shows up in the breakdown. Attribute it to the embeddings model
|
|
118
|
+
# (`embed_model`), not the completion model, so any future embed pricing is keyed right.
|
|
119
|
+
vectors = self._provider.embed(texts)
|
|
120
|
+
embed_model = self._provider.config.embed_model or self._provider.config.model
|
|
121
|
+
self._tracker.record(Phase.EMBED, embed_model, TokenUsage())
|
|
122
|
+
return vectors
|
|
123
|
+
|
|
124
|
+
def verify(self) -> VerifyResult:
|
|
125
|
+
return self._provider.verify()
|