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.
Files changed (127) hide show
  1. coder_eval/.gitattributes +2 -0
  2. coder_eval/__init__.py +3 -0
  3. coder_eval/agent.py +302 -0
  4. coder_eval/agents/__init__.py +41 -0
  5. coder_eval/agents/_logging.py +89 -0
  6. coder_eval/agents/antigravity_agent.py +811 -0
  7. coder_eval/agents/claude_code_agent.py +1701 -0
  8. coder_eval/agents/codex_agent.py +2055 -0
  9. coder_eval/agents/noop_agent.py +100 -0
  10. coder_eval/agents/registry.py +163 -0
  11. coder_eval/agents/watchdog.py +116 -0
  12. coder_eval/analysis.py +88 -0
  13. coder_eval/cli/__init__.py +87 -0
  14. coder_eval/cli/aggregate_command.py +153 -0
  15. coder_eval/cli/console.py +7 -0
  16. coder_eval/cli/evaluate_command.py +164 -0
  17. coder_eval/cli/plan_command.py +152 -0
  18. coder_eval/cli/report_command.py +121 -0
  19. coder_eval/cli/run_command.py +686 -0
  20. coder_eval/cli/run_helpers.py +137 -0
  21. coder_eval/cli/run_task_internal_command.py +210 -0
  22. coder_eval/cli/utils.py +37 -0
  23. coder_eval/config.py +212 -0
  24. coder_eval/criteria/__init__.py +163 -0
  25. coder_eval/criteria/_classification_aggregate.py +106 -0
  26. coder_eval/criteria/agent_judge.py +425 -0
  27. coder_eval/criteria/base.py +248 -0
  28. coder_eval/criteria/classification_match.py +107 -0
  29. coder_eval/criteria/command_executed.py +181 -0
  30. coder_eval/criteria/commands_efficiency.py +73 -0
  31. coder_eval/criteria/file_check.py +119 -0
  32. coder_eval/criteria/file_contains.py +85 -0
  33. coder_eval/criteria/file_exists.py +45 -0
  34. coder_eval/criteria/file_matches_regex.py +86 -0
  35. coder_eval/criteria/json_check.py +184 -0
  36. coder_eval/criteria/llm_judge.py +260 -0
  37. coder_eval/criteria/reference_comparison.py +130 -0
  38. coder_eval/criteria/run_command.py +220 -0
  39. coder_eval/criteria/skill_triggered.py +111 -0
  40. coder_eval/criteria/uipath_eval.py +223 -0
  41. coder_eval/errors/__init__.py +26 -0
  42. coder_eval/errors/agent.py +26 -0
  43. coder_eval/errors/budget.py +28 -0
  44. coder_eval/errors/categories.py +205 -0
  45. coder_eval/errors/categorization.py +240 -0
  46. coder_eval/errors/executor.py +124 -0
  47. coder_eval/errors/judge.py +12 -0
  48. coder_eval/errors/retry.py +211 -0
  49. coder_eval/errors/timeout.py +63 -0
  50. coder_eval/evaluation/__init__.py +10 -0
  51. coder_eval/evaluation/checker.py +260 -0
  52. coder_eval/evaluation/judge_anthropic.py +55 -0
  53. coder_eval/evaluation/judge_bedrock.py +114 -0
  54. coder_eval/evaluation/judge_context.py +497 -0
  55. coder_eval/evaluation/judge_models.py +53 -0
  56. coder_eval/evaluation/judge_persistence.py +291 -0
  57. coder_eval/evaluation/judge_usage.py +50 -0
  58. coder_eval/evaluation/sub_agent.py +231 -0
  59. coder_eval/evaluation/summaries.py +48 -0
  60. coder_eval/evaluation/verdict_tool.py +213 -0
  61. coder_eval/formatting.py +191 -0
  62. coder_eval/isolation/__init__.py +15 -0
  63. coder_eval/isolation/docker_runner.py +1253 -0
  64. coder_eval/logging_config.py +399 -0
  65. coder_eval/models/__init__.py +337 -0
  66. coder_eval/models/agent_config.py +373 -0
  67. coder_eval/models/container_paths.py +26 -0
  68. coder_eval/models/criteria.py +942 -0
  69. coder_eval/models/enums.py +121 -0
  70. coder_eval/models/experiment.py +314 -0
  71. coder_eval/models/judge.py +90 -0
  72. coder_eval/models/judge_defaults.py +11 -0
  73. coder_eval/models/limits.py +89 -0
  74. coder_eval/models/merge_strategy.py +132 -0
  75. coder_eval/models/mutations.py +95 -0
  76. coder_eval/models/results.py +787 -0
  77. coder_eval/models/routing.py +160 -0
  78. coder_eval/models/sandbox.py +371 -0
  79. coder_eval/models/tasks.py +631 -0
  80. coder_eval/models/telemetry.py +454 -0
  81. coder_eval/models/templates.py +108 -0
  82. coder_eval/orchestration/__init__.py +13 -0
  83. coder_eval/orchestration/batch.py +690 -0
  84. coder_eval/orchestration/config.py +111 -0
  85. coder_eval/orchestration/config_merge.py +431 -0
  86. coder_eval/orchestration/evaluation.py +94 -0
  87. coder_eval/orchestration/experiment.py +837 -0
  88. coder_eval/orchestration/overrides.py +206 -0
  89. coder_eval/orchestration/task_loader.py +437 -0
  90. coder_eval/orchestrator.py +2078 -0
  91. coder_eval/path_utils.py +79 -0
  92. coder_eval/plugins.py +81 -0
  93. coder_eval/pricing.py +169 -0
  94. coder_eval/py.typed +0 -0
  95. coder_eval/reports.py +1066 -0
  96. coder_eval/reports_experiment.py +761 -0
  97. coder_eval/reports_html.py +1659 -0
  98. coder_eval/reports_stats.py +250 -0
  99. coder_eval/resources/__init__.py +137 -0
  100. coder_eval/resources/default_experiment.yaml +85 -0
  101. coder_eval/resources/default_ignore_patterns.yaml +60 -0
  102. coder_eval/resources/tags.yaml +55 -0
  103. coder_eval/sandbox.py +1127 -0
  104. coder_eval/scoring/__init__.py +11 -0
  105. coder_eval/scoring/ast_similarity.py +38 -0
  106. coder_eval/scoring/complexity.py +115 -0
  107. coder_eval/scoring/quality.py +154 -0
  108. coder_eval/scoring/signature_similarity.py +57 -0
  109. coder_eval/scoring/similarity.py +103 -0
  110. coder_eval/scoring/token_similarity.py +44 -0
  111. coder_eval/simulation/__init__.py +27 -0
  112. coder_eval/simulation/termination.py +88 -0
  113. coder_eval/simulation/user_simulator.py +324 -0
  114. coder_eval/streaming/__init__.py +44 -0
  115. coder_eval/streaming/callbacks.py +57 -0
  116. coder_eval/streaming/collector.py +193 -0
  117. coder_eval/streaming/events.py +198 -0
  118. coder_eval/streaming/renderers.py +248 -0
  119. coder_eval/streaming/wire.py +115 -0
  120. coder_eval/telemetry.py +407 -0
  121. coder_eval/utils.py +517 -0
  122. coder_eval-0.8.2.dist-info/METADATA +242 -0
  123. coder_eval-0.8.2.dist-info/RECORD +127 -0
  124. coder_eval-0.8.2.dist-info/WHEEL +4 -0
  125. coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
  126. coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
  127. coder_eval-0.8.2.dist-info/licenses/NOTICE +18 -0
@@ -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