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
wmo/platform/transfer.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Deterministic model-bundle packing and safe unpacking for push/pull.
|
|
2
|
+
|
|
3
|
+
A pushed bundle is byte-compatible with the bundles the platform's own build
|
|
4
|
+
pipeline produces: a gzipped tarball of the model directory with
|
|
5
|
+
archive-relative member paths. Packing is an include-list — the model's
|
|
6
|
+
`config.toml`, `metrics.json`, `card.json`, `prompts/`, and `index/` — so
|
|
7
|
+
local `runs/` cost records and raw `traces/` (customer data) never leave the
|
|
8
|
+
machine.
|
|
9
|
+
|
|
10
|
+
Bundles can reach the platform's 1GB cap, so packing and unpacking are
|
|
11
|
+
file-based: bytes stream between disk and the network without ever being held
|
|
12
|
+
in memory whole.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import shutil
|
|
20
|
+
import tarfile
|
|
21
|
+
import tomllib
|
|
22
|
+
import uuid
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
|
|
27
|
+
from wmo.config.config import HarnessConfig
|
|
28
|
+
from wmo.core.types import JsonValue
|
|
29
|
+
|
|
30
|
+
_INCLUDED_FILES = ("config.toml", "metrics.json", "card.json")
|
|
31
|
+
_INCLUDED_DIRS = ("prompts", "index")
|
|
32
|
+
|
|
33
|
+
_HASH_CHUNK_BYTES = 1024 * 1024
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PackedModelBundle(BaseModel):
|
|
37
|
+
"""A packed model bundle on disk, ready for upload."""
|
|
38
|
+
|
|
39
|
+
path: Path
|
|
40
|
+
sha256: str
|
|
41
|
+
byte_size: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BundleFormatError(ValueError):
|
|
45
|
+
"""The directory or bytes are not a valid world-model bundle."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def sha256_file(path: Path) -> str:
|
|
49
|
+
"""Digest a file's contents without loading it whole."""
|
|
50
|
+
digest = hashlib.sha256()
|
|
51
|
+
with path.open("rb") as fh:
|
|
52
|
+
while chunk := fh.read(_HASH_CHUNK_BYTES):
|
|
53
|
+
digest.update(chunk)
|
|
54
|
+
return digest.hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pack_model_dir(directory: Path, dest: Path) -> PackedModelBundle:
|
|
58
|
+
"""Pack a model directory into the platform's bundle format at ``dest``.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
directory: A built model directory (must contain `config.toml`).
|
|
62
|
+
dest: Where to write the gzipped tarball (parent must exist).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The bundle file plus integrity metadata; member order is sorted so
|
|
66
|
+
identical inputs produce identical archives.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
BundleFormatError: If the directory is missing or has no config.toml.
|
|
70
|
+
"""
|
|
71
|
+
if not directory.is_dir():
|
|
72
|
+
msg = f"model directory does not exist: {directory}"
|
|
73
|
+
raise BundleFormatError(msg)
|
|
74
|
+
if not (directory / "config.toml").is_file():
|
|
75
|
+
msg = f"{directory} has no config.toml; only built world models can be pushed"
|
|
76
|
+
raise BundleFormatError(msg)
|
|
77
|
+
|
|
78
|
+
members: list[Path] = []
|
|
79
|
+
for name in _INCLUDED_FILES:
|
|
80
|
+
path = directory / name
|
|
81
|
+
if path.is_file():
|
|
82
|
+
members.append(path)
|
|
83
|
+
for name in _INCLUDED_DIRS:
|
|
84
|
+
root = directory / name
|
|
85
|
+
if root.is_dir():
|
|
86
|
+
members.extend(sorted(path for path in root.rglob("*")))
|
|
87
|
+
members.append(root)
|
|
88
|
+
|
|
89
|
+
with tarfile.open(dest, mode="w:gz") as tar:
|
|
90
|
+
for path in sorted(set(members)):
|
|
91
|
+
tar.add(path, arcname=str(path.relative_to(directory)), recursive=False)
|
|
92
|
+
return PackedModelBundle(
|
|
93
|
+
path=dest,
|
|
94
|
+
sha256=sha256_file(dest),
|
|
95
|
+
byte_size=dest.stat().st_size,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def unpack_model_bundle(source: Path, dest_dir: Path, *, force: bool = False) -> None:
|
|
100
|
+
"""Unpack a pulled bundle file into a local model directory.
|
|
101
|
+
|
|
102
|
+
Extraction happens in a temporary sibling renamed into place, so a crashed
|
|
103
|
+
unpack never leaves a half-written model that later loads as real.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
source: Downloaded gzipped tarball.
|
|
107
|
+
dest_dir: Target model directory (`.wmo/models/<name>`).
|
|
108
|
+
force: Replace an existing directory instead of refusing.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
BundleFormatError: If the file is not a readable bundle or a member
|
|
112
|
+
would escape the destination.
|
|
113
|
+
FileExistsError: If ``dest_dir`` exists and ``force`` is not set.
|
|
114
|
+
"""
|
|
115
|
+
if dest_dir.exists() and not force:
|
|
116
|
+
msg = f"{dest_dir} already exists; pass --force to replace it"
|
|
117
|
+
raise FileExistsError(msg)
|
|
118
|
+
staging_dir = dest_dir.with_name(f"{dest_dir.name}.pull-{uuid.uuid4().hex}")
|
|
119
|
+
staging_dir.mkdir(parents=True)
|
|
120
|
+
try:
|
|
121
|
+
with tarfile.open(source, mode="r:gz") as tar:
|
|
122
|
+
# The "data" filter rejects absolute paths, traversal, and special
|
|
123
|
+
# members instead of writing them.
|
|
124
|
+
tar.extractall(staging_dir, filter="data")
|
|
125
|
+
except (tarfile.TarError, OSError) as error:
|
|
126
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
127
|
+
msg = f"bundle could not be unpacked: {error}"
|
|
128
|
+
raise BundleFormatError(msg) from error
|
|
129
|
+
if dest_dir.exists():
|
|
130
|
+
shutil.rmtree(dest_dir, ignore_errors=True)
|
|
131
|
+
staging_dir.rename(dest_dir)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def extract_push_meta(directory: Path) -> dict[str, JsonValue]:
|
|
135
|
+
"""Derive the push metadata the platform stores alongside a bundle.
|
|
136
|
+
|
|
137
|
+
Parses the model's own `config.toml` (and `metrics.json` when present)
|
|
138
|
+
through wmo's typed config so the platform never reads files out of the
|
|
139
|
+
tarball.
|
|
140
|
+
"""
|
|
141
|
+
config = HarnessConfig.model_validate(
|
|
142
|
+
tomllib.loads((directory / "config.toml").read_text(encoding="utf-8"))
|
|
143
|
+
)
|
|
144
|
+
meta: dict[str, JsonValue] = {
|
|
145
|
+
"serve_provider": config.serve_provider.value,
|
|
146
|
+
"embed_provider": config.embed_provider.value,
|
|
147
|
+
"embed_dim": config.embed_dim,
|
|
148
|
+
"gepa_budget": config.gepa_budget,
|
|
149
|
+
}
|
|
150
|
+
try:
|
|
151
|
+
meta["serve_model"] = config.serve_provider_config().model
|
|
152
|
+
except ValueError:
|
|
153
|
+
# No provider block for the serve provider; the platform column stays unset.
|
|
154
|
+
pass
|
|
155
|
+
metrics_path = directory / "metrics.json"
|
|
156
|
+
if metrics_path.is_file():
|
|
157
|
+
meta["metrics"] = json.loads(metrics_path.read_text(encoding="utf-8"))
|
|
158
|
+
return meta
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Unified LLM provider layer.
|
|
2
|
+
|
|
3
|
+
One interface (`Provider`), multiple backends, one entry point (`get_provider` — or
|
|
4
|
+
`provider_or_chain`, which upgrades to the local `.wmo/fallback.toml` failover chain when present).
|
|
5
|
+
All can be verified on startup with a cheap ping. Built fresh for this repo; no external client
|
|
6
|
+
framework.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from wmo.providers.base import (
|
|
10
|
+
DEFAULT_MAX_TOKENS,
|
|
11
|
+
Completion,
|
|
12
|
+
EmbedderKind,
|
|
13
|
+
Message,
|
|
14
|
+
Provider,
|
|
15
|
+
ProviderConfig,
|
|
16
|
+
ProviderKind,
|
|
17
|
+
VerifyResult,
|
|
18
|
+
)
|
|
19
|
+
from wmo.providers.models import ProviderModel, model_types_for_provider, resolve_provider_model
|
|
20
|
+
from wmo.providers.registry import get_provider, verify_all, verify_embedder
|
|
21
|
+
from wmo.providers.waterfall import WaterfallProvider, provider_or_chain
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Provider",
|
|
25
|
+
"ProviderConfig",
|
|
26
|
+
"ProviderKind",
|
|
27
|
+
"ProviderModel",
|
|
28
|
+
"EmbedderKind",
|
|
29
|
+
"DEFAULT_MAX_TOKENS",
|
|
30
|
+
"Completion",
|
|
31
|
+
"Message",
|
|
32
|
+
"VerifyResult",
|
|
33
|
+
"get_provider",
|
|
34
|
+
"provider_or_chain",
|
|
35
|
+
"WaterfallProvider",
|
|
36
|
+
"verify_all",
|
|
37
|
+
"verify_embedder",
|
|
38
|
+
"model_types_for_provider",
|
|
39
|
+
"resolve_provider_model",
|
|
40
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Structured tool-calling translation for Bedrock Converse providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from llm_waterfall import ChatRequest, ChatResponse
|
|
9
|
+
from pydantic import JsonValue
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def converse_request(request: ChatRequest, model: str) -> dict[str, object]:
|
|
13
|
+
"""Translate the provider-neutral structured contract to Bedrock Converse."""
|
|
14
|
+
system: list[dict[str, str]] = []
|
|
15
|
+
messages: list[dict[str, object]] = []
|
|
16
|
+
|
|
17
|
+
def push(role: str, content: list[dict[str, object]]) -> None:
|
|
18
|
+
if messages and messages[-1]["role"] == role:
|
|
19
|
+
existing = cast("list[dict[str, object]]", messages[-1]["content"])
|
|
20
|
+
existing.extend(content)
|
|
21
|
+
else:
|
|
22
|
+
messages.append({"role": role, "content": content})
|
|
23
|
+
|
|
24
|
+
for message in request.messages:
|
|
25
|
+
if message.role in ("system", "developer"):
|
|
26
|
+
text = _chat_text(message.content)
|
|
27
|
+
if text:
|
|
28
|
+
system.append({"text": text})
|
|
29
|
+
continue
|
|
30
|
+
if message.role == "tool":
|
|
31
|
+
push(
|
|
32
|
+
"user",
|
|
33
|
+
[
|
|
34
|
+
{
|
|
35
|
+
"toolResult": {
|
|
36
|
+
"toolUseId": message.tool_call_id or "",
|
|
37
|
+
"content": [{"text": _chat_text(message.content)}],
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
continue
|
|
43
|
+
blocks: list[dict[str, object]] = []
|
|
44
|
+
text = _chat_text(message.content)
|
|
45
|
+
if text:
|
|
46
|
+
blocks.append({"text": text})
|
|
47
|
+
for tool_call in message.tool_calls or []:
|
|
48
|
+
try:
|
|
49
|
+
arguments = json.loads(tool_call.function.arguments)
|
|
50
|
+
except ValueError:
|
|
51
|
+
arguments = {}
|
|
52
|
+
blocks.append(
|
|
53
|
+
{
|
|
54
|
+
"toolUse": {
|
|
55
|
+
"toolUseId": tool_call.id,
|
|
56
|
+
"name": tool_call.function.name,
|
|
57
|
+
"input": arguments,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
if blocks:
|
|
62
|
+
push("assistant" if message.role == "assistant" else "user", blocks)
|
|
63
|
+
|
|
64
|
+
max_tokens = request.max_tokens or request.max_completion_tokens or 4096
|
|
65
|
+
inference: dict[str, float | int] = {"maxTokens": max_tokens}
|
|
66
|
+
if request.temperature is not None:
|
|
67
|
+
inference["temperature"] = request.temperature
|
|
68
|
+
result: dict[str, object] = {
|
|
69
|
+
"modelId": model,
|
|
70
|
+
"messages": messages,
|
|
71
|
+
"inferenceConfig": inference,
|
|
72
|
+
}
|
|
73
|
+
if system:
|
|
74
|
+
result["system"] = system
|
|
75
|
+
if request.tools:
|
|
76
|
+
tools = [
|
|
77
|
+
{
|
|
78
|
+
"toolSpec": {
|
|
79
|
+
"name": tool.function.name,
|
|
80
|
+
"description": tool.function.description,
|
|
81
|
+
"inputSchema": {"json": tool.function.parameters},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for tool in request.tools
|
|
85
|
+
]
|
|
86
|
+
tool_config: dict[str, object] = {"tools": tools}
|
|
87
|
+
choice = request.tool_choice
|
|
88
|
+
if choice == "required":
|
|
89
|
+
tool_config["toolChoice"] = {"any": {}}
|
|
90
|
+
elif isinstance(choice, dict):
|
|
91
|
+
function = choice.get("function")
|
|
92
|
+
if isinstance(function, dict) and isinstance(function.get("name"), str):
|
|
93
|
+
tool_config["toolChoice"] = {"tool": {"name": function["name"]}}
|
|
94
|
+
if choice != "none":
|
|
95
|
+
result["toolConfig"] = tool_config
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def converse_response(raw: object, model: str) -> ChatResponse:
|
|
100
|
+
"""Translate a Bedrock Converse response to the structured provider contract."""
|
|
101
|
+
response = cast("dict[str, object]", raw)
|
|
102
|
+
output = cast("dict[str, object]", response["output"])
|
|
103
|
+
message_data = cast("dict[str, object]", output["message"])
|
|
104
|
+
blocks = cast("list[dict[str, object]]", message_data["content"])
|
|
105
|
+
text = "".join(str(block["text"]) for block in blocks if "text" in block)
|
|
106
|
+
tool_calls: list[dict[str, object]] = []
|
|
107
|
+
for block in blocks:
|
|
108
|
+
use = block.get("toolUse")
|
|
109
|
+
if not isinstance(use, dict):
|
|
110
|
+
continue
|
|
111
|
+
tool_calls.append(
|
|
112
|
+
{
|
|
113
|
+
"id": str(use.get("toolUseId", "")),
|
|
114
|
+
"type": "function",
|
|
115
|
+
"function": {
|
|
116
|
+
"name": str(use.get("name", "")),
|
|
117
|
+
"arguments": json.dumps(use.get("input", {})),
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
stop_reason = str(response.get("stopReason", "end_turn"))
|
|
122
|
+
finish_reason = {
|
|
123
|
+
"tool_use": "tool_calls",
|
|
124
|
+
"max_tokens": "length",
|
|
125
|
+
"content_filtered": "content_filter",
|
|
126
|
+
"guardrail_intervened": "content_filter",
|
|
127
|
+
}.get(stop_reason, "stop")
|
|
128
|
+
message: dict[str, object] = {"role": "assistant", "content": text}
|
|
129
|
+
if tool_calls:
|
|
130
|
+
message["tool_calls"] = tool_calls
|
|
131
|
+
usage = response.get("usage")
|
|
132
|
+
usage_data = usage if isinstance(usage, dict) else {}
|
|
133
|
+
return ChatResponse.model_validate(
|
|
134
|
+
{
|
|
135
|
+
"model": model,
|
|
136
|
+
"choices": [{"index": 0, "message": message, "finish_reason": finish_reason}],
|
|
137
|
+
"usage": {
|
|
138
|
+
"prompt_tokens": usage_data.get("inputTokens", 0),
|
|
139
|
+
"completion_tokens": usage_data.get("outputTokens", 0),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _chat_text(content: JsonValue) -> str:
|
|
146
|
+
"""Flatten the text-bearing forms used by OpenAI-compatible chat messages."""
|
|
147
|
+
if isinstance(content, str):
|
|
148
|
+
return content
|
|
149
|
+
if isinstance(content, list):
|
|
150
|
+
parts: list[str] = []
|
|
151
|
+
for item in content:
|
|
152
|
+
if isinstance(item, dict) and isinstance(item.get("text"), str):
|
|
153
|
+
parts.append(item["text"])
|
|
154
|
+
return "".join(parts)
|
|
155
|
+
return "" if content is None else str(content)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Shared request mapping / response parsing for the two OpenAI-shaped backends.
|
|
2
|
+
|
|
3
|
+
`OpenAIProvider` and `AzureOpenAIProvider` differ only in how their client is constructed; the
|
|
4
|
+
chat-completion and embedding wire formats are identical, so that logic lives here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast
|
|
10
|
+
|
|
11
|
+
from llm_waterfall import ChatMaxTokensField, ChatRequest, ChatResponse
|
|
12
|
+
from openai import BadRequestError
|
|
13
|
+
|
|
14
|
+
from wmo.providers.base import Completion, Message, StreamChunk, TokenUsage
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
|
|
19
|
+
from openai.types import CreateEmbeddingResponse
|
|
20
|
+
from openai.types.chat import ChatCompletion, ChatCompletionMessageParam
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _ChatCompletions(Protocol):
|
|
24
|
+
def create(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
model: str,
|
|
28
|
+
messages: list[ChatCompletionMessageParam],
|
|
29
|
+
max_completion_tokens: int,
|
|
30
|
+
temperature: float = ...,
|
|
31
|
+
) -> ChatCompletion: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _Embeddings(Protocol):
|
|
35
|
+
def create(
|
|
36
|
+
self, *, model: str, input: list[str], dimensions: int = ...
|
|
37
|
+
) -> CreateEmbeddingResponse: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_messages(system: str, messages: list[Message]) -> list[ChatCompletionMessageParam]:
|
|
41
|
+
"""Fold the system prompt into the message list as OpenAI's leading `system` turn."""
|
|
42
|
+
out: list[dict[str, str]] = []
|
|
43
|
+
if system:
|
|
44
|
+
out.append({"role": "system", "content": system})
|
|
45
|
+
out.extend({"role": m.role, "content": m.content} for m in messages)
|
|
46
|
+
return cast("list[ChatCompletionMessageParam]", out)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def complete(
|
|
50
|
+
chat_completions: _ChatCompletions,
|
|
51
|
+
model: str,
|
|
52
|
+
system: str,
|
|
53
|
+
messages: list[Message],
|
|
54
|
+
max_tokens: int,
|
|
55
|
+
temperature: float | None = None,
|
|
56
|
+
max_tokens_field: str = "max_completion_tokens",
|
|
57
|
+
) -> Completion:
|
|
58
|
+
"""Run one chat completion and map it onto our `Completion`.
|
|
59
|
+
|
|
60
|
+
`max_tokens_field` names the output-budget parameter the deployment accepts: GPT-5.x wants
|
|
61
|
+
`max_completion_tokens`, while Azure MaaS open models (DeepSeek, Kimi) still take the
|
|
62
|
+
classic `max_tokens` (see `ProviderConfig.resolved_chat_max_tokens_field`). `temperature`
|
|
63
|
+
is sent ONLY when given: GPT 5.5's reasoning models reject non-default sampling params
|
|
64
|
+
(callers pass None), while OpenAI-compatible servers (vLLM policies) need it.
|
|
65
|
+
"""
|
|
66
|
+
# The output-budget param name is dynamic, so this call crosses the SDK boundary through
|
|
67
|
+
# the same one-line cast `stream` uses below.
|
|
68
|
+
resource = cast("Any", chat_completions)
|
|
69
|
+
base_kwargs: dict[str, Any] = {
|
|
70
|
+
"model": model,
|
|
71
|
+
"messages": to_messages(system, messages),
|
|
72
|
+
max_tokens_field: max_tokens,
|
|
73
|
+
}
|
|
74
|
+
if temperature is None:
|
|
75
|
+
response: ChatCompletion = resource.create(**base_kwargs)
|
|
76
|
+
else:
|
|
77
|
+
try:
|
|
78
|
+
response = resource.create(**base_kwargs, temperature=temperature)
|
|
79
|
+
except BadRequestError as exc:
|
|
80
|
+
# Reasoning-model deployments (GPT-5.x behind Azure/custom endpoints) reject any
|
|
81
|
+
# non-default temperature with a 400 unsupported_value. The caller can't know which
|
|
82
|
+
# models sample; degrade to the model's default rather than failing the request.
|
|
83
|
+
if "temperature" not in str(exc):
|
|
84
|
+
raise
|
|
85
|
+
response = resource.create(**base_kwargs)
|
|
86
|
+
if not response.choices:
|
|
87
|
+
# Content filtering (and some error modes) can return zero choices; surface it clearly
|
|
88
|
+
# rather than letting choices[0] raise a bare IndexError.
|
|
89
|
+
raise ValueError(f"{model} returned no choices")
|
|
90
|
+
text = response.choices[0].message.content or ""
|
|
91
|
+
usage = response.usage
|
|
92
|
+
token_usage = _chat_usage(usage) if usage is not None else TokenUsage()
|
|
93
|
+
return Completion(text=text, usage=token_usage)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def stream(
|
|
97
|
+
chat_completions: object,
|
|
98
|
+
model: str,
|
|
99
|
+
system: str,
|
|
100
|
+
messages: list[Message],
|
|
101
|
+
max_tokens: int,
|
|
102
|
+
temperature: float | None = None,
|
|
103
|
+
max_tokens_field: str = "max_completion_tokens",
|
|
104
|
+
) -> Iterator[StreamChunk]:
|
|
105
|
+
"""Stream one chat completion as `StreamChunk`s (deltas, then a terminal chunk with usage).
|
|
106
|
+
|
|
107
|
+
`stream_options.include_usage` makes the wire stream end with a usage-bearing chunk, so the
|
|
108
|
+
terminal `StreamChunk` carries real token counts instead of estimates. `temperature` and
|
|
109
|
+
`max_tokens_field` follow the same rules as `complete`.
|
|
110
|
+
"""
|
|
111
|
+
resource = cast("Any", chat_completions)
|
|
112
|
+
kwargs: dict[str, Any] = {
|
|
113
|
+
"model": model,
|
|
114
|
+
"messages": to_messages(system, messages),
|
|
115
|
+
max_tokens_field: max_tokens,
|
|
116
|
+
"stream": True,
|
|
117
|
+
"stream_options": {"include_usage": True},
|
|
118
|
+
}
|
|
119
|
+
if temperature is not None:
|
|
120
|
+
kwargs["temperature"] = temperature
|
|
121
|
+
usage = TokenUsage()
|
|
122
|
+
upstream = resource.create(**kwargs)
|
|
123
|
+
try:
|
|
124
|
+
for chunk in upstream:
|
|
125
|
+
choices = getattr(chunk, "choices", None) or []
|
|
126
|
+
if choices:
|
|
127
|
+
delta = getattr(choices[0], "delta", None)
|
|
128
|
+
text = getattr(delta, "content", None) if delta is not None else None
|
|
129
|
+
if text:
|
|
130
|
+
yield StreamChunk(delta=text)
|
|
131
|
+
chunk_usage = getattr(chunk, "usage", None)
|
|
132
|
+
if chunk_usage is not None:
|
|
133
|
+
usage = _chat_usage(chunk_usage)
|
|
134
|
+
finally:
|
|
135
|
+
# The SDK Stream holds an httpx response; without an explicit close an abandoned
|
|
136
|
+
# stream releases the connection only when the object is garbage collected.
|
|
137
|
+
close = getattr(upstream, "close", None)
|
|
138
|
+
if callable(close):
|
|
139
|
+
close()
|
|
140
|
+
yield StreamChunk(done=True, usage=usage)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _chat_usage(usage: object) -> TokenUsage:
|
|
144
|
+
"""Chat-completions usage -> TokenUsage, including the cached-prompt split when reported."""
|
|
145
|
+
details = getattr(usage, "prompt_tokens_details", None)
|
|
146
|
+
return TokenUsage(
|
|
147
|
+
input_tokens=getattr(usage, "prompt_tokens", 0) or 0,
|
|
148
|
+
output_tokens=getattr(usage, "completion_tokens", 0) or 0,
|
|
149
|
+
cached_input_tokens=(getattr(details, "cached_tokens", None) or 0) if details else 0,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def complete_chat(
|
|
154
|
+
chat_completions: object,
|
|
155
|
+
model: str,
|
|
156
|
+
request: ChatRequest,
|
|
157
|
+
*,
|
|
158
|
+
max_tokens_field: ChatMaxTokensField,
|
|
159
|
+
) -> ChatResponse:
|
|
160
|
+
"""Run a validated structured request against an OpenAI-compatible SDK resource."""
|
|
161
|
+
# ChatRequest validates the stable tool-calling core before this SDK boundary. The OpenAI
|
|
162
|
+
# package models its evolving request surface as a large TypedDict union, so the narrow cast
|
|
163
|
+
# preserves forward-compatible extra fields without leaking Any into the public contract.
|
|
164
|
+
resource = cast("Any", chat_completions)
|
|
165
|
+
response = resource.create(**request.provider_payload(model, max_tokens_field=max_tokens_field))
|
|
166
|
+
return ChatResponse.model_validate(response.model_dump(mode="json"))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def embed(
|
|
170
|
+
embeddings: _Embeddings, model: str, texts: list[str], dim: int | None = None
|
|
171
|
+
) -> list[list[float]]:
|
|
172
|
+
"""Embed `texts` against `model` (an OpenAI model id, or an Azure embedding deployment).
|
|
173
|
+
|
|
174
|
+
`dim`, when set, requests a specific output dimension via the `dimensions` param (supported by
|
|
175
|
+
text-embedding-3-* and their Azure deployments) so the index and query vectors match.
|
|
176
|
+
"""
|
|
177
|
+
response = (
|
|
178
|
+
embeddings.create(model=model, input=texts, dimensions=dim)
|
|
179
|
+
if dim is not None
|
|
180
|
+
else embeddings.create(model=model, input=texts)
|
|
181
|
+
)
|
|
182
|
+
return [item.embedding for item in response.data]
|