avp-cli 0.1.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.
- avp/__init__.py +31 -0
- avp/commission.py +236 -0
- avp/content.py +273 -0
- avp/data/__init__.py +0 -0
- avp/data/prices.json +21945 -0
- avp/descriptor.py +204 -0
- avp/envelope.py +108 -0
- avp/gen_ai.py +160 -0
- avp/history.py +86 -0
- avp/pricing.py +138 -0
- avp/sink.py +62 -0
- avp/trajectory.py +530 -0
- avp_cli/__init__.py +82 -0
- avp_cli/agent.py +566 -0
- avp_cli/agent_install.py +331 -0
- avp_cli/agent_manifest.py +73 -0
- avp_cli/agents.py +258 -0
- avp_cli/brand.py +46 -0
- avp_cli/broker.py +227 -0
- avp_cli/catalog/__init__.py +128 -0
- avp_cli/catalog/capitals.json +67 -0
- avp_cli/catalog/custom.json +35 -0
- avp_cli/catalog/parsebench.json +44 -0
- avp_cli/cli.py +1858 -0
- avp_cli/commission.py +144 -0
- avp_cli/config.py +250 -0
- avp_cli/console.py +51 -0
- avp_cli/environment.py +218 -0
- avp_cli/eval/__init__.py +0 -0
- avp_cli/eval/dataset.py +37 -0
- avp_cli/eval/engine.py +426 -0
- avp_cli/eval/report.py +178 -0
- avp_cli/eval/scoring.py +260 -0
- avp_cli/eval/setup.py +69 -0
- avp_cli/images.py +119 -0
- avp_cli/library.py +95 -0
- avp_cli/live.py +185 -0
- avp_cli/observability.py +128 -0
- avp_cli/onboarding.py +80 -0
- avp_cli/osb.py +347 -0
- avp_cli/paths.py +47 -0
- avp_cli/run_manifest.py +113 -0
- avp_cli/state.py +195 -0
- avp_cli/vault.py +116 -0
- avp_cli/viz.py +303 -0
- avp_cli-0.1.0.dist-info/METADATA +359 -0
- avp_cli-0.1.0.dist-info/RECORD +49 -0
- avp_cli-0.1.0.dist-info/WHEEL +4 -0
- avp_cli-0.1.0.dist-info/entry_points.txt +2 -0
avp/pricing.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Shared pricing for AVP agents.
|
|
2
|
+
|
|
3
|
+
Why this lives in `avp` core, not per-agent:
|
|
4
|
+
- Anthropic models are reachable through nearly every mainstream agent
|
|
5
|
+
SDK: the raw Anthropic SDK, the Claude Agent SDK, LangChain /
|
|
6
|
+
LangGraph, LlamaIndex, Pydantic AI, the OpenAI-compatible surfaces on
|
|
7
|
+
Bedrock and Vertex, plus the long tail of provider-agnostic
|
|
8
|
+
frameworks. Every AVP adapter that wraps one of these SDKs needs the
|
|
9
|
+
same model-price lookup; a per-adapter copy drifts the moment
|
|
10
|
+
Anthropic ships new pricing.
|
|
11
|
+
- The price table is data, not policy. Adapters load it at startup,
|
|
12
|
+
users can override via the public `PriceTable` type, and the on-wire
|
|
13
|
+
cost number is tagged with `avp.cost.source` so downstreams can tell
|
|
14
|
+
a locally-computed number from a provider-reported one (or unknown).
|
|
15
|
+
|
|
16
|
+
Default ships in `avp/data/prices.json`, mirrored from models.dev and
|
|
17
|
+
keyed by its `<provider>/<model>` id (synced by `scripts/sync-prices.py`);
|
|
18
|
+
`load_default_prices()` reads it fresh on each call. `compute_cost`
|
|
19
|
+
normalizes a wire model (plus its provider) to a key here. To override,
|
|
20
|
+
pass a `PriceTable` to your driver / translator at construction.
|
|
21
|
+
|
|
22
|
+
`COST_SOURCE_*` constants name the audit-source values:
|
|
23
|
+
- `computed`: we did the math locally from a price table
|
|
24
|
+
- `reported`: the API/SDK handed us the number directly
|
|
25
|
+
- `unknown`: no price found and no provider report (cost reported as 0.0)
|
|
26
|
+
|
|
27
|
+
The agent stamps `avp.cost.source` on each `assistant_message` event so
|
|
28
|
+
trajectory consumers can filter / weight by provenance, e.g. an audit
|
|
29
|
+
pipeline that trusts reported numbers but flags computed numbers from a
|
|
30
|
+
stale price table.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
from collections.abc import Mapping
|
|
37
|
+
from importlib import resources
|
|
38
|
+
from typing import Literal
|
|
39
|
+
|
|
40
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
41
|
+
|
|
42
|
+
CostSource = Literal["computed", "reported", "unknown"]
|
|
43
|
+
|
|
44
|
+
COST_SOURCE_COMPUTED: CostSource = "computed"
|
|
45
|
+
COST_SOURCE_REPORTED: CostSource = "reported"
|
|
46
|
+
COST_SOURCE_UNKNOWN: CostSource = "unknown"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ModelPrice(BaseModel):
|
|
50
|
+
"""Per-1M-token pricing in USD."""
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
53
|
+
|
|
54
|
+
input: float = Field(ge=0)
|
|
55
|
+
output: float = Field(ge=0)
|
|
56
|
+
cache_read: float = Field(default=0.0, ge=0)
|
|
57
|
+
cache_write: float = Field(default=0.0, ge=0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
PriceTable = Mapping[str, ModelPrice]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_default_prices() -> dict[str, ModelPrice]:
|
|
64
|
+
"""Load the bundled default price table from `avp/data/prices.json`.
|
|
65
|
+
|
|
66
|
+
Reads on each call so users who patch the file in-place get fresh
|
|
67
|
+
numbers without restarting their process. Returns a fresh dict;
|
|
68
|
+
callers can mutate without side effects.
|
|
69
|
+
"""
|
|
70
|
+
raw = resources.files("avp.data").joinpath("prices.json").read_text()
|
|
71
|
+
parsed = json.loads(raw)
|
|
72
|
+
return {
|
|
73
|
+
model: ModelPrice.model_validate(spec) for model, spec in parsed.get("models", {}).items()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resolve_price(prices: PriceTable, model: str, provider: str | None = None) -> ModelPrice | None:
|
|
78
|
+
"""Resolve a price by the model the agent put on the wire.
|
|
79
|
+
|
|
80
|
+
The bundled table is mirrored from models.dev and keyed by its
|
|
81
|
+
`<provider>/<model>` id. A wire `model` is either a slug already
|
|
82
|
+
containing a provider (`openai/gpt-4o`, used as-is) or a bare
|
|
83
|
+
provider-native string (`claude-sonnet-4-6`) qualified with
|
|
84
|
+
`provider` to form the key (`anthropic/claude-sonnet-4-6`). The
|
|
85
|
+
exact string is tried first so a custom table keyed by bare names
|
|
86
|
+
still works.
|
|
87
|
+
|
|
88
|
+
Gateway caveat: the key is the model's origin slug, so when a Commission
|
|
89
|
+
routes a model through a different storefront (`model: "openai/gpt-4o"` with
|
|
90
|
+
`provider.id: "openrouter"`), this returns the model's *list* price, not the
|
|
91
|
+
gateway's actual price (gateways add margin). Treat the result as a
|
|
92
|
+
best-effort estimate and prefer provider-reported cost when available
|
|
93
|
+
(`avp.cost.source = "reported"`).
|
|
94
|
+
"""
|
|
95
|
+
p = prices.get(model)
|
|
96
|
+
if p is not None:
|
|
97
|
+
return p
|
|
98
|
+
if provider and "/" not in model:
|
|
99
|
+
return prices.get(f"{provider}/{model}")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def compute_cost(
|
|
104
|
+
model: str,
|
|
105
|
+
*,
|
|
106
|
+
provider: str | None = None,
|
|
107
|
+
input_tokens: int,
|
|
108
|
+
output_tokens: int,
|
|
109
|
+
cache_read: int,
|
|
110
|
+
cache_write: int,
|
|
111
|
+
prices: PriceTable,
|
|
112
|
+
) -> tuple[float, CostSource]:
|
|
113
|
+
"""Compute billable USD cost from a turn's token counts.
|
|
114
|
+
|
|
115
|
+
Returns `(cost, source)` so callers can stamp `avp.cost.source` on
|
|
116
|
+
the wire alongside `avp.cost_usd`.
|
|
117
|
+
|
|
118
|
+
`provider` is the model's provider (e.g. `"anthropic"`), used to
|
|
119
|
+
resolve a bare wire model against the `<provider>/<model>`-keyed
|
|
120
|
+
table; omit it if the wire model is already provider-qualified.
|
|
121
|
+
|
|
122
|
+
`input_tokens` here is the AVP convention (cache reads INCLUDED).
|
|
123
|
+
Cache reads / writes are billed at their own per-token rates; the
|
|
124
|
+
fresh portion gets the regular input rate. If the model isn't in
|
|
125
|
+
the table, returns `(0.0, "unknown")` — caller should warn so
|
|
126
|
+
silent under-counts don't ship.
|
|
127
|
+
"""
|
|
128
|
+
p = resolve_price(prices, model, provider)
|
|
129
|
+
if p is None:
|
|
130
|
+
return 0.0, COST_SOURCE_UNKNOWN
|
|
131
|
+
fresh = max(0, input_tokens - cache_read - cache_write)
|
|
132
|
+
cost = (
|
|
133
|
+
fresh * p.input / 1_000_000
|
|
134
|
+
+ cache_read * p.cache_read / 1_000_000
|
|
135
|
+
+ cache_write * p.cache_write / 1_000_000
|
|
136
|
+
+ output_tokens * p.output / 1_000_000
|
|
137
|
+
)
|
|
138
|
+
return cost, COST_SOURCE_COMPUTED
|
avp/sink.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""avp.sink — Event-sink type and built-in NDJSON sinks.
|
|
2
|
+
|
|
3
|
+
An :data:`EventSink` is any async callable that consumes one trajectory
|
|
4
|
+
event at a time. It owns serialization and I/O (stdout, file, DB, etc.)
|
|
5
|
+
so the surrounding agent stays I/O-agnostic.
|
|
6
|
+
|
|
7
|
+
Two built-ins are provided:
|
|
8
|
+
|
|
9
|
+
- :func:`stdio_sink` writes NDJSON to stdout (local runs, examples).
|
|
10
|
+
- :func:`jsonl_sink` writes NDJSON to a file (agent implementors
|
|
11
|
+
satisfying the conformance ``run --out <path.jsonl>`` contract).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import Awaitable, Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from avp.trajectory import Event
|
|
19
|
+
|
|
20
|
+
EventSink = Callable[[Event], Awaitable[None]]
|
|
21
|
+
"""Async callable that consumes one trajectory event at a time."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _serialize(event: Event) -> str:
|
|
25
|
+
"""Serialize an event to its canonical NDJSON line (no trailing newline).
|
|
26
|
+
|
|
27
|
+
Uses ``by_alias=True`` so dotted CloudEvents / AVP wire keys
|
|
28
|
+
(``avp.correlation_id`` etc.) round-trip in their canonical form.
|
|
29
|
+
"""
|
|
30
|
+
return json.dumps(event.model_dump(by_alias=True, exclude_none=True, mode="json"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def stdio_sink(event: Event) -> None:
|
|
34
|
+
"""Built-in :data:`EventSink`: print one event as NDJSON to stdout."""
|
|
35
|
+
print(_serialize(event), flush=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def jsonl_sink(path: Path) -> EventSink:
|
|
39
|
+
"""Build an :data:`EventSink` that writes events to a JSONL file at ``path``.
|
|
40
|
+
|
|
41
|
+
The file is truncated on call so each run replaces any prior content,
|
|
42
|
+
then each event is appended in its own ``open("a") / write / close``
|
|
43
|
+
cycle. That makes progress visible to a concurrent reader (e.g. ``tail
|
|
44
|
+
-f``) without relying on OS-level buffer flushing. Used by agent
|
|
45
|
+
implementors to satisfy the conformance ``run --out <path>`` contract.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
from avp.sink import jsonl_sink
|
|
50
|
+
|
|
51
|
+
async def run(commission, out_path):
|
|
52
|
+
sink = jsonl_sink(out_path)
|
|
53
|
+
await sink(some_event)
|
|
54
|
+
...
|
|
55
|
+
"""
|
|
56
|
+
path.open("w").close() # reset file
|
|
57
|
+
|
|
58
|
+
async def sink(event: Event) -> None:
|
|
59
|
+
with path.open("a") as file:
|
|
60
|
+
file.write(_serialize(event) + "\n")
|
|
61
|
+
|
|
62
|
+
return sink
|