coder-eval 0.8.2__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.
- coder_eval/.gitattributes +2 -0
- coder_eval/__init__.py +3 -0
- coder_eval/agent.py +302 -0
- coder_eval/agents/__init__.py +41 -0
- coder_eval/agents/_logging.py +89 -0
- coder_eval/agents/antigravity_agent.py +811 -0
- coder_eval/agents/claude_code_agent.py +1701 -0
- coder_eval/agents/codex_agent.py +2055 -0
- coder_eval/agents/noop_agent.py +100 -0
- coder_eval/agents/registry.py +163 -0
- coder_eval/agents/watchdog.py +116 -0
- coder_eval/analysis.py +88 -0
- coder_eval/cli/__init__.py +87 -0
- coder_eval/cli/aggregate_command.py +153 -0
- coder_eval/cli/console.py +7 -0
- coder_eval/cli/evaluate_command.py +164 -0
- coder_eval/cli/plan_command.py +152 -0
- coder_eval/cli/report_command.py +121 -0
- coder_eval/cli/run_command.py +686 -0
- coder_eval/cli/run_helpers.py +137 -0
- coder_eval/cli/run_task_internal_command.py +210 -0
- coder_eval/cli/utils.py +37 -0
- coder_eval/config.py +212 -0
- coder_eval/criteria/__init__.py +163 -0
- coder_eval/criteria/_classification_aggregate.py +106 -0
- coder_eval/criteria/agent_judge.py +425 -0
- coder_eval/criteria/base.py +248 -0
- coder_eval/criteria/classification_match.py +107 -0
- coder_eval/criteria/command_executed.py +181 -0
- coder_eval/criteria/commands_efficiency.py +73 -0
- coder_eval/criteria/file_check.py +119 -0
- coder_eval/criteria/file_contains.py +85 -0
- coder_eval/criteria/file_exists.py +45 -0
- coder_eval/criteria/file_matches_regex.py +86 -0
- coder_eval/criteria/json_check.py +184 -0
- coder_eval/criteria/llm_judge.py +260 -0
- coder_eval/criteria/reference_comparison.py +130 -0
- coder_eval/criteria/run_command.py +220 -0
- coder_eval/criteria/skill_triggered.py +111 -0
- coder_eval/criteria/uipath_eval.py +223 -0
- coder_eval/errors/__init__.py +26 -0
- coder_eval/errors/agent.py +26 -0
- coder_eval/errors/budget.py +28 -0
- coder_eval/errors/categories.py +205 -0
- coder_eval/errors/categorization.py +240 -0
- coder_eval/errors/executor.py +124 -0
- coder_eval/errors/judge.py +12 -0
- coder_eval/errors/retry.py +211 -0
- coder_eval/errors/timeout.py +63 -0
- coder_eval/evaluation/__init__.py +10 -0
- coder_eval/evaluation/checker.py +260 -0
- coder_eval/evaluation/judge_anthropic.py +55 -0
- coder_eval/evaluation/judge_bedrock.py +114 -0
- coder_eval/evaluation/judge_context.py +497 -0
- coder_eval/evaluation/judge_models.py +53 -0
- coder_eval/evaluation/judge_persistence.py +291 -0
- coder_eval/evaluation/judge_usage.py +50 -0
- coder_eval/evaluation/sub_agent.py +231 -0
- coder_eval/evaluation/summaries.py +48 -0
- coder_eval/evaluation/verdict_tool.py +213 -0
- coder_eval/formatting.py +191 -0
- coder_eval/isolation/__init__.py +15 -0
- coder_eval/isolation/docker_runner.py +1253 -0
- coder_eval/logging_config.py +399 -0
- coder_eval/models/__init__.py +337 -0
- coder_eval/models/agent_config.py +373 -0
- coder_eval/models/container_paths.py +26 -0
- coder_eval/models/criteria.py +942 -0
- coder_eval/models/enums.py +121 -0
- coder_eval/models/experiment.py +314 -0
- coder_eval/models/judge.py +90 -0
- coder_eval/models/judge_defaults.py +11 -0
- coder_eval/models/limits.py +89 -0
- coder_eval/models/merge_strategy.py +132 -0
- coder_eval/models/mutations.py +95 -0
- coder_eval/models/results.py +787 -0
- coder_eval/models/routing.py +160 -0
- coder_eval/models/sandbox.py +371 -0
- coder_eval/models/tasks.py +631 -0
- coder_eval/models/telemetry.py +454 -0
- coder_eval/models/templates.py +108 -0
- coder_eval/orchestration/__init__.py +13 -0
- coder_eval/orchestration/batch.py +690 -0
- coder_eval/orchestration/config.py +111 -0
- coder_eval/orchestration/config_merge.py +431 -0
- coder_eval/orchestration/evaluation.py +94 -0
- coder_eval/orchestration/experiment.py +837 -0
- coder_eval/orchestration/overrides.py +206 -0
- coder_eval/orchestration/task_loader.py +437 -0
- coder_eval/orchestrator.py +2078 -0
- coder_eval/path_utils.py +79 -0
- coder_eval/plugins.py +81 -0
- coder_eval/pricing.py +169 -0
- coder_eval/py.typed +0 -0
- coder_eval/reports.py +1066 -0
- coder_eval/reports_experiment.py +761 -0
- coder_eval/reports_html.py +1659 -0
- coder_eval/reports_stats.py +250 -0
- coder_eval/resources/__init__.py +137 -0
- coder_eval/resources/default_experiment.yaml +85 -0
- coder_eval/resources/default_ignore_patterns.yaml +60 -0
- coder_eval/resources/tags.yaml +55 -0
- coder_eval/sandbox.py +1127 -0
- coder_eval/scoring/__init__.py +11 -0
- coder_eval/scoring/ast_similarity.py +38 -0
- coder_eval/scoring/complexity.py +115 -0
- coder_eval/scoring/quality.py +154 -0
- coder_eval/scoring/signature_similarity.py +57 -0
- coder_eval/scoring/similarity.py +103 -0
- coder_eval/scoring/token_similarity.py +44 -0
- coder_eval/simulation/__init__.py +27 -0
- coder_eval/simulation/termination.py +88 -0
- coder_eval/simulation/user_simulator.py +324 -0
- coder_eval/streaming/__init__.py +44 -0
- coder_eval/streaming/callbacks.py +57 -0
- coder_eval/streaming/collector.py +193 -0
- coder_eval/streaming/events.py +198 -0
- coder_eval/streaming/renderers.py +248 -0
- coder_eval/streaming/wire.py +115 -0
- coder_eval/telemetry.py +407 -0
- coder_eval/utils.py +517 -0
- coder_eval-0.8.2.dist-info/METADATA +242 -0
- coder_eval-0.8.2.dist-info/RECORD +127 -0
- coder_eval-0.8.2.dist-info/WHEEL +4 -0
- coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
- coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
- coder_eval-0.8.2.dist-info/licenses/NOTICE +18 -0
coder_eval/path_utils.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Path utilities for run directory management."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
TASK_LOG_FILENAME = "task.log"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def task_log_path(run_dir: Path) -> Path:
|
|
12
|
+
"""Per-task log file path inside a task run directory."""
|
|
13
|
+
return run_dir / TASK_LOG_FILENAME
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_run_id() -> str:
|
|
17
|
+
"""Generate filesystem-safe timestamp: YYYY-MM-DD_HH-MM-SS."""
|
|
18
|
+
return datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def replicate_subdir_name(replicate_index: int) -> str:
|
|
22
|
+
"""Two-digit, zero-padded directory name for a replicate (``'00'``, ``'01'``, ...).
|
|
23
|
+
|
|
24
|
+
Two-digit padding caps unique replicate names at 100 (indices 0-99); if a
|
|
25
|
+
follow-up PR ever needs >=100 replicates, widen the padding here.
|
|
26
|
+
"""
|
|
27
|
+
return f"{replicate_index:02d}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_task_run_dir(
|
|
31
|
+
run_dir: Path,
|
|
32
|
+
variant_id: str,
|
|
33
|
+
task_id: str,
|
|
34
|
+
replicate_index: int = 0,
|
|
35
|
+
) -> Path:
|
|
36
|
+
"""Build the per-task run dir: ``<run_dir>/<variant_id>/<task_id>/<NN>/``."""
|
|
37
|
+
return run_dir / variant_id / task_id / replicate_subdir_name(replicate_index)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_task_log_id(variant_id: str, task_id: str, replicate_index: int = 0) -> str:
|
|
41
|
+
"""Canonical ``<variant_id>/<task_id>/<NN>`` identifier used by:
|
|
42
|
+
- Orchestrator ``_log_task_id`` (console/file log tag, streaming events)
|
|
43
|
+
- Batch ``stream_label``
|
|
44
|
+
- CLI tqdm progress-bar postfix
|
|
45
|
+
|
|
46
|
+
Shape mirrors ``build_task_run_dir`` (same three segments, same NN padding
|
|
47
|
+
via ``replicate_subdir_name``) so log tags and on-disk paths stay in
|
|
48
|
+
lockstep. Callers MUST use this helper rather than hand-rolling the
|
|
49
|
+
f-string so future format changes (e.g., NN → NNN) touch exactly one
|
|
50
|
+
place.
|
|
51
|
+
"""
|
|
52
|
+
return f"{variant_id}/{task_id}/{replicate_subdir_name(replicate_index)}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_latest_symlink(runs_base: Path, run_id: str) -> None:
|
|
56
|
+
"""Create/update 'latest' symlink to current run.
|
|
57
|
+
|
|
58
|
+
Gracefully handles Windows where symlinks may fail.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
runs_base: Base directory containing all runs (e.g., "runs/")
|
|
62
|
+
run_id: ID of the current run (e.g., "2025-10-09_15-30-45")
|
|
63
|
+
"""
|
|
64
|
+
latest_link = runs_base / "latest"
|
|
65
|
+
# Use relative path for symlink target (just the run_id directory name)
|
|
66
|
+
# This ensures the symlink works correctly when both are in the same directory
|
|
67
|
+
target = Path(run_id)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Remove existing symlink/file
|
|
71
|
+
if latest_link.exists() or latest_link.is_symlink():
|
|
72
|
+
latest_link.unlink()
|
|
73
|
+
|
|
74
|
+
# Create symlink with relative path
|
|
75
|
+
latest_link.symlink_to(target, target_is_directory=True)
|
|
76
|
+
except (OSError, NotImplementedError):
|
|
77
|
+
# Windows may not support symlinks, skip gracefully
|
|
78
|
+
if platform.system() != "Windows":
|
|
79
|
+
raise
|
coder_eval/plugins.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Plugin discovery for bring-your-own-agent (BYOA) extensions.
|
|
2
|
+
|
|
3
|
+
External packages extend coder-eval by declaring a Python entry point in the
|
|
4
|
+
``coder_eval.plugins`` group whose target is a ``register(registry)`` callable::
|
|
5
|
+
|
|
6
|
+
[project.entry-points."coder_eval.plugins"]
|
|
7
|
+
my_plugin = "my_pkg.plugin:register"
|
|
8
|
+
|
|
9
|
+
At startup :func:`load_plugins` scans every installed distribution for that
|
|
10
|
+
group, imports each target, and calls it with the :class:`AgentRegistry` so the
|
|
11
|
+
plugin can add its agent kinds. coder-eval registers its own built-in agents
|
|
12
|
+
through the *same* group (the ``coder_eval`` entry point ->
|
|
13
|
+
:func:`coder_eval.agents.register_builtins`), so the discovery path is exercised
|
|
14
|
+
by core itself and cannot silently rot.
|
|
15
|
+
|
|
16
|
+
Discovery is idempotent and re-entrancy-safe (the ``_loaded`` flag is set before
|
|
17
|
+
the scan, so a plugin that imports back into coder-eval during its own
|
|
18
|
+
registration does not recurse). A plugin whose ``register`` raises is logged and
|
|
19
|
+
skipped — one broken plugin never aborts startup.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
PLUGIN_ENTRY_POINT_GROUP = "coder_eval.plugins"
|
|
30
|
+
|
|
31
|
+
# coder-eval's own built-in agents register through this same entry point. A
|
|
32
|
+
# failure registering it is a real breakage (empty registry), NOT a skippable
|
|
33
|
+
# third-party plugin error — so it is fatal rather than logged-and-skipped.
|
|
34
|
+
BUILTIN_PLUGIN_NAME = "coder_eval"
|
|
35
|
+
|
|
36
|
+
_loaded = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_plugins(*, force: bool = False) -> None:
|
|
40
|
+
"""Discover and run every ``coder_eval.plugins`` entry point's ``register`` hook.
|
|
41
|
+
|
|
42
|
+
Idempotent: a second call is a no-op unless ``force=True``. The ``_loaded``
|
|
43
|
+
flag is set *before* iterating so a plugin re-entering via
|
|
44
|
+
:func:`ensure_plugins_loaded` during its own import does not recurse.
|
|
45
|
+
"""
|
|
46
|
+
global _loaded
|
|
47
|
+
if _loaded and not force:
|
|
48
|
+
return
|
|
49
|
+
_loaded = True
|
|
50
|
+
|
|
51
|
+
from importlib.metadata import entry_points
|
|
52
|
+
|
|
53
|
+
from coder_eval.agents.registry import AgentRegistry
|
|
54
|
+
|
|
55
|
+
for ep in entry_points(group=PLUGIN_ENTRY_POINT_GROUP):
|
|
56
|
+
try:
|
|
57
|
+
register = ep.load()
|
|
58
|
+
register(AgentRegistry)
|
|
59
|
+
except Exception:
|
|
60
|
+
# Built-in registration failing leaves the registry empty and would
|
|
61
|
+
# surface later as a misleading "No agent registered for 'claude-code'".
|
|
62
|
+
# Keep it fatal so the real (import/registration) cause fails loudly.
|
|
63
|
+
if ep.name == BUILTIN_PLUGIN_NAME:
|
|
64
|
+
# Clear the flag so a caller that catches and retries re-runs the
|
|
65
|
+
# scan instead of getting a no-op against an empty registry.
|
|
66
|
+
_loaded = False
|
|
67
|
+
raise
|
|
68
|
+
logger.exception("Failed to load coder_eval plugin %r (%s); skipping", ep.name, ep.value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ensure_plugins_loaded() -> None:
|
|
72
|
+
"""Run :func:`load_plugins` once if it has not already run.
|
|
73
|
+
|
|
74
|
+
Safety-net for entry paths that do not go through CLI init (direct library
|
|
75
|
+
use, tests): registry consumers call this before reading the registry so
|
|
76
|
+
registration is always populated. ``create_agent`` calls it today; the
|
|
77
|
+
config-dispatch consumers (``parse_agent_config`` and the agent-root config
|
|
78
|
+
merge) wire it in once they become registry-driven.
|
|
79
|
+
"""
|
|
80
|
+
if not _loaded:
|
|
81
|
+
load_plugins()
|
coder_eval/pricing.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Model pricing for cost calculation.
|
|
2
|
+
|
|
3
|
+
Anthropic/OpenAI built-in rates; plugins contribute additional rates via
|
|
4
|
+
``register_pricing()``. Prices are per million tokens (MTok).
|
|
5
|
+
Source: https://www.anthropic.com/pricing
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ModelPricing:
|
|
13
|
+
"""Pricing for a single model (per million tokens)."""
|
|
14
|
+
|
|
15
|
+
input_per_mtok: float
|
|
16
|
+
output_per_mtok: float
|
|
17
|
+
cache_write_per_mtok: float # prompt caching write
|
|
18
|
+
cache_read_per_mtok: float # prompt caching read
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Official Anthropic pricing as of 2025
|
|
22
|
+
# Key: CLI model name (before gateway mapping)
|
|
23
|
+
_PRICING: dict[str, ModelPricing] = {
|
|
24
|
+
# Claude 4.8 / 4.7 / 4.6 / 4.5 / 4 Opus
|
|
25
|
+
"claude-opus-4-8": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
26
|
+
"claude-opus-4-7": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
27
|
+
"claude-opus-4-6": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
28
|
+
"claude-opus-4-6-20250514": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
29
|
+
"claude-opus-4-5-20251101": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
30
|
+
"claude-opus-4-20250514": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
31
|
+
# Claude 4.6 / 4.5 / 4 Sonnet
|
|
32
|
+
"claude-sonnet-4-6": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
33
|
+
"claude-sonnet-4-6-20250514": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
34
|
+
"claude-sonnet-4-5-20250929": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
35
|
+
"claude-sonnet-4-20250514": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
36
|
+
# Claude 4.5 Haiku
|
|
37
|
+
"claude-haiku-4-5-20251001": ModelPricing(0.80, 4.0, 1.0, 0.08),
|
|
38
|
+
# Claude 3.7 Sonnet
|
|
39
|
+
"claude-3-7-sonnet-20250219": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
40
|
+
# Claude 3.5 Sonnet
|
|
41
|
+
"claude-3-5-sonnet-20241022": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
42
|
+
"claude-3-5-sonnet-20240620": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
43
|
+
# Claude 3 Opus
|
|
44
|
+
"claude-3-opus-20240229": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
45
|
+
# Claude 3 Sonnet
|
|
46
|
+
"claude-3-sonnet-20240229": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
47
|
+
# Claude 3 Haiku
|
|
48
|
+
"claude-3-haiku-20240307": ModelPricing(0.25, 1.25, 0.30, 0.03),
|
|
49
|
+
# OpenAI GPT-5 / Codex (direct or via Azure OpenAI). Released 2025-09-23.
|
|
50
|
+
# Source: https://openai.com/api/pricing (gpt-5-codex: $1.25/M in,
|
|
51
|
+
# $0.125/M cached in, $10/M out). OpenAI does not bill cache writes
|
|
52
|
+
# separately, so cache_write == input rate.
|
|
53
|
+
"gpt-5-codex": ModelPricing(1.25, 10.0, 1.25, 0.125),
|
|
54
|
+
"gpt-5": ModelPricing(1.25, 10.0, 1.25, 0.125),
|
|
55
|
+
# gpt-5.3-codex (2026-02-24): $1.75/M input, $0.175/M cached, $14/M output.
|
|
56
|
+
"gpt-5.3-codex": ModelPricing(1.75, 14.0, 1.75, 0.175),
|
|
57
|
+
# gpt-5.4 (2026-03-05): $2.50/M input, $0.25/M cached, $15/M output.
|
|
58
|
+
"gpt-5.4": ModelPricing(2.5, 15.0, 2.5, 0.25),
|
|
59
|
+
# gpt-5.5: $5/M input, $0.50/M cached, $30/M output.
|
|
60
|
+
# CAVEAT: this flat rate does NOT model gpt-5.5's long-context surcharge
|
|
61
|
+
# (2x input / 1.5x output once a session exceeds 272K input tokens), so cost
|
|
62
|
+
# for very-large-context runs reads low. Fine for typical eval tasks; revisit
|
|
63
|
+
# if benchmarking large-context Codex runs.
|
|
64
|
+
"gpt-5.5": ModelPricing(5.0, 30.0, 5.0, 0.50),
|
|
65
|
+
# Google Gemini (AntigravityAgent, via the Gemini Developer API). Per-MTok
|
|
66
|
+
# rates from ai.google.dev/gemini-api/docs/pricing (2026). Gemini bills no
|
|
67
|
+
# separate cache-WRITE fee, so cache_write == input (the agent maps
|
|
68
|
+
# cache_creation_tokens to 0, so this value is effectively unused); cache_read
|
|
69
|
+
# is the cached-input rate (~10% of input). CAVEAT: Pro's >200K-token tier is
|
|
70
|
+
# higher ($4/$18); this flat rate reads low for very-large-context runs — fine
|
|
71
|
+
# for typical eval tasks. Keyed on the bare model id in agent.model.
|
|
72
|
+
"gemini-3-pro-preview": ModelPricing(2.0, 12.0, 2.0, 0.20),
|
|
73
|
+
"gemini-3.1-pro-preview": ModelPricing(2.0, 12.0, 2.0, 0.20),
|
|
74
|
+
"gemini-3.1-pro-preview-customtools": ModelPricing(2.0, 12.0, 2.0, 0.20),
|
|
75
|
+
"gemini-3.5-flash": ModelPricing(1.5, 9.0, 1.5, 0.15),
|
|
76
|
+
"gemini-3-flash-preview": ModelPricing(1.5, 9.0, 1.5, 0.15),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Plugin-contributed rates (e.g. coder_eval_uipath registers UiPath models).
|
|
81
|
+
# Merged over the built-in table at lookup time.
|
|
82
|
+
_REGISTERED_PRICING: dict[str, ModelPricing] = {}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _lookup_rate(key: str) -> ModelPricing | None:
|
|
86
|
+
"""Resolve a (normalized) pricing key: plugin overlay first, then built-ins.
|
|
87
|
+
|
|
88
|
+
Uses ``is not None`` rather than truthiness so a later ``__bool__`` on
|
|
89
|
+
``ModelPricing`` (or a type change) can't make a falsy-but-present rate fall
|
|
90
|
+
through to the built-in table.
|
|
91
|
+
"""
|
|
92
|
+
registered = _REGISTERED_PRICING.get(key)
|
|
93
|
+
return registered if registered is not None else _PRICING.get(key)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def register_pricing(rates: dict[str, ModelPricing]) -> None:
|
|
97
|
+
"""Merge plugin model rates into the pricing table.
|
|
98
|
+
|
|
99
|
+
Re-registering an identical rate for an existing key is idempotent; a
|
|
100
|
+
*conflicting* rate raises (reproducibility — mirrors AgentRegistry's
|
|
101
|
+
anti-shadow rule, so load order can't change a model's price).
|
|
102
|
+
|
|
103
|
+
All-or-nothing: every key is validated against the existing table before
|
|
104
|
+
any is committed, so a conflict on a later key in a multi-model batch does
|
|
105
|
+
not leave earlier keys half-registered.
|
|
106
|
+
"""
|
|
107
|
+
for key, rate in rates.items():
|
|
108
|
+
existing = _lookup_rate(key)
|
|
109
|
+
if existing is not None and existing != rate:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"pricing for {key!r} already registered as {existing}; refusing to shadow with {rate}. "
|
|
112
|
+
+ "A built-in or another plugin already prices this model — check plugin load order "
|
|
113
|
+
+ "(two plugins must not register conflicting rates for the same model id)."
|
|
114
|
+
)
|
|
115
|
+
_REGISTERED_PRICING.update(rates)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Bedrock cross-region inference-profile prefixes (mirrors
|
|
119
|
+
# models.routing._BEDROCK_KNOWN_PREFIXES). A Bedrock route qualifies a bare
|
|
120
|
+
# alias into e.g. ``eu.anthropic.claude-opus-4-8``; the pricing table is keyed
|
|
121
|
+
# on the bare alias, so we strip these back off before the lookup.
|
|
122
|
+
_BEDROCK_REGION_PREFIXES: tuple[str, ...] = ("eu.", "us.", "apac.", "global.")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _normalize_model(model: str) -> str:
|
|
126
|
+
"""Strip Bedrock region + vendor qualifiers back to the bare pricing key.
|
|
127
|
+
|
|
128
|
+
``eu.anthropic.claude-opus-4-8`` → ``claude-opus-4-8``. Idempotent on
|
|
129
|
+
already-bare aliases (``claude-opus-4-8`` / ``gpt-5-codex`` pass through),
|
|
130
|
+
so it is safe to apply unconditionally for every route.
|
|
131
|
+
"""
|
|
132
|
+
model = model.strip()
|
|
133
|
+
for prefix in _BEDROCK_REGION_PREFIXES:
|
|
134
|
+
if model.startswith(prefix):
|
|
135
|
+
model = model[len(prefix) :]
|
|
136
|
+
break
|
|
137
|
+
if model.startswith("anthropic."):
|
|
138
|
+
model = model[len("anthropic.") :]
|
|
139
|
+
return model
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def calculate_cost(
|
|
143
|
+
model: str,
|
|
144
|
+
uncached_input_tokens: int,
|
|
145
|
+
output_tokens: int,
|
|
146
|
+
cache_creation_tokens: int = 0,
|
|
147
|
+
cache_read_tokens: int = 0,
|
|
148
|
+
) -> float | None:
|
|
149
|
+
"""Calculate cost in USD for the given token usage.
|
|
150
|
+
|
|
151
|
+
``uncached_input_tokens`` is the fresh slice billed at the input rate — pass
|
|
152
|
+
``TokenUsage.uncached_input_tokens``, NOT ``input_tokens`` (the derived total,
|
|
153
|
+
which already includes the cache buckets and would double-count them).
|
|
154
|
+
|
|
155
|
+
The model id is normalized (Bedrock region/vendor prefixes stripped) so a
|
|
156
|
+
qualified inference-profile id like ``eu.anthropic.claude-opus-4-8`` prices
|
|
157
|
+
the same as the bare ``claude-opus-4-8``. Returns None if the (normalized)
|
|
158
|
+
model is not in the pricing table.
|
|
159
|
+
"""
|
|
160
|
+
pricing = _lookup_rate(_normalize_model(model))
|
|
161
|
+
if pricing is None:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
uncached_input_tokens * pricing.input_per_mtok
|
|
166
|
+
+ output_tokens * pricing.output_per_mtok
|
|
167
|
+
+ cache_creation_tokens * pricing.cache_write_per_mtok
|
|
168
|
+
+ cache_read_tokens * pricing.cache_read_per_mtok
|
|
169
|
+
) / 1_000_000
|
coder_eval/py.typed
ADDED
|
File without changes
|