Skip to content

feat: TODO batch — OpenRouter cost reconciliation, safer cleanup manifest, pinned harness tests, docs#12

Merged
tardigrde merged 7 commits into
mainfrom
fix/todo-batch-2
Jun 11, 2026
Merged

feat: TODO batch — OpenRouter cost reconciliation, safer cleanup manifest, pinned harness tests, docs#12
tardigrde merged 7 commits into
mainfrom
fix/todo-batch-2

Conversation

@tardigrde

@tardigrde tardigrde commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Works through the remaining TODO.md items (everything except the subagent-evals generalization, which is deferred).

feat(harness): reconcile claude-code cost against OpenRouter pricing

  • The claude CLI prices runs at Anthropic list prices regardless of endpoint; cost_usd was wrong whenever claude-code was routed through OpenRouter.
  • The OpenRouter generation API would be exact but needs per-request generation ids the CLI doesn't expose, so cost is recomputed from the run's token counts × OpenRouter's published per-model rates (public models endpoint, fetched once per process, failure-safe).
  • timing.json gains cost_usd_source (cli / openrouter-pricing / cli-unreconciled) and cost_usd_cli (the CLI's original estimate), plus cache_creation_tokens so cache-write charges are included.

fix(runner): only record eval-created open PRs in cleanup manifest

  • The PR snapshot uses gh pr list --state all, so the manifest could record PRs merged/closed during the run — and even PRs someone else opened mid-run, which cleanup would then close with --delete-branch.
  • Now a new PR number is recorded only when the PR is still OPEN and its head branch was pushed by this run. Old snapshots without state/headRefName keep the previous behavior.

test(harness)

  • Full argv pinned for claude-code, opencode, and codex build_command (was codex-only, flag-presence-only).
  • _build_cleanup_entry covered for merged/closed/external/pre-existing/number-less PRs.

docs / examples

  • pyproject authors filled with the real maintainer.
  • README: python -m agent_skill_eval usage, cost reporting + reconciliation semantics, multi-agent comparison walkthrough (how to read per-agent with/without-skill deltas), stale codex invocation row fixed.
  • examples/commit-push-pr/evals/files/sample_change.py expanded from two print lines into a realistic user-directory fixture matching what the eval prompts claim about it (login fix + search feature).

local cleanup (not in the diff)

Test plan

  • make test — 260 passed (includes free e2e tier)
  • make lint — clean
  • make test-live — 7 passed (all real harnesses, zero skipped grades); details in comment

🤖 Generated with Claude Code

tardigrde and others added 6 commits June 11, 2026 16:56
The claude CLI prices runs at Anthropic list prices regardless of the
endpoint it talks to, so cost_usd was wrong whenever the harness routed
claude-code through OpenRouter. The generation API would give exact
billed amounts but needs per-request ids the CLI does not expose;
instead, recompute from the run's token counts and OpenRouter's
published per-model rates (fetched once per process from the public
models endpoint).

timing.json now records cost_usd_source ("cli", "openrouter-pricing",
or "cli-unreconciled" when the model cannot be mapped to an OpenRouter
slug) and keeps the CLI's original estimate in cost_usd_cli. Cache
creation tokens are now captured too so the reconciled cost includes
cache-write charges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build_command was only pinned for codex (and only the trust-check
flag). Pin the full argv for all three real harnesses so a flag
regression fails loudly, and cover cache_creation_input_tokens parsing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PR snapshot is taken with `gh pr list --state all`, so
_build_cleanup_entry could record PRs that were merged or closed during
the run, and even PRs someone else opened on the source repo mid-run —
cleanup would then try to close a stranger's PR with --delete-branch.

Record a new PR number only when the PR is still OPEN and its head
branch is one this run pushed. Snapshots from before state/headRefName
were captured keep the old behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mparison

- pyproject authors: replace the placeholder with the real maintainer
- README: document running the CLI as `python -m agent_skill_eval`
- README: fix the stale codex invocation row (--full-auto was replaced
  by --sandbox workspace-write --skip-git-repo-check)
- README: document cost_usd semantics and the OpenRouter reconciliation
  (cost_usd_source / cost_usd_cli in timing.json)
- README: add a multi-agent comparison walkthrough showing how to read
  per-agent with/without-skill deltas from the report and benchmark.json

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two-line print fixture gave agents nothing plausible to commit. The
new fixture is a small user-directory module whose contents match what
the eval prompts claim about it: a login fix (email normalization in
authenticate) and a new search feature (search_users).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local-only cleanup also performed (not visible in the diff): stale
merged local branches deleted, both .claude/worktrees copies of the old
skill_eval module tree removed, stale untracked HANDOFF.md deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces cost reconciliation for Claude models routed through OpenRouter, captures cache creation tokens, and refines PR cleanup logic to exclude merged, closed, or external PRs. It also expands documentation, adds realistic test fixtures, and introduces comprehensive unit tests. Feedback on the changes highlights a concurrency issue in the newly added OpenRouter pricing fetcher, suggesting the use of a thread lock, a custom User-Agent to prevent CDN blocking, and defensive type-checking on the API response.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/agent_skill_eval/openrouter.py Outdated
Comment on lines +12 to +48
from __future__ import annotations

import json
import re
import urllib.request
from typing import Optional

from agent_skill_eval.models import TimingData

OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"
_FETCH_TIMEOUT_SECONDS = 10

# model id -> pricing dict, populated once per process. The models endpoint
# is public (no API key) but we still never want it on a hot path.
_pricing_cache: Optional[dict[str, dict]] = None


def fetch_openrouter_pricing() -> dict[str, dict]:
"""Return ``{model_id: pricing}`` from the OpenRouter models endpoint.

Cached for the process lifetime. Raises on network/parse errors; callers
that must not fail (the harness) wrap this in try/except.
"""
global _pricing_cache
if _pricing_cache is not None:
return _pricing_cache

with urllib.request.urlopen(OPENROUTER_MODELS_URL, timeout=_FETCH_TIMEOUT_SECONDS) as resp:
data = json.loads(resp.read().decode("utf-8"))

pricing: dict[str, dict] = {}
for model in data.get("data", []):
model_id = model.get("id")
if model_id and isinstance(model.get("pricing"), dict):
pricing[model_id] = model["pricing"]
_pricing_cache = pricing
return pricing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fetch_openrouter_pricing function is called concurrently by multiple threads when evaluations are run in parallel. Without synchronization, multiple threads can concurrently trigger redundant HTTP requests to the OpenRouter API when _pricing_cache is initially None.

Additionally, public APIs protected by Cloudflare or other CDNs often block the default Python urllib User-Agent with a 403 Forbidden error. It is safer to specify a custom User-Agent header. Finally, we should defensively verify that the parsed JSON data is a dictionary before calling .get() to avoid potential AttributeError exceptions if the API returns an unexpected format.

from __future__ import annotations

import json
import re
import threading
import urllib.request
from typing import Optional

from agent_skill_eval.models import TimingData

OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"
_FETCH_TIMEOUT_SECONDS = 10

# model id -> pricing dict, populated once per process. The models endpoint
# is public (no API key) but we still never want it on a hot path.
_pricing_cache: Optional[dict[str, dict]] = None
_pricing_lock = threading.Lock()


def fetch_openrouter_pricing() -> dict[str, dict]:
    """Return {model_id: pricing} from the OpenRouter models endpoint.

    Cached for the process lifetime. Raises on network/parse errors; callers
    that must not fail (the harness) wrap this in try/except.
    """
    global _pricing_cache
    if _pricing_cache is not None:
        return _pricing_cache

    with _pricing_lock:
        if _pricing_cache is not None:
            return _pricing_cache

        req = urllib.request.Request(
            OPENROUTER_MODELS_URL,
            headers={"User-Agent": "agent-skill-eval/0.4.1"},
        )
        with urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT_SECONDS) as resp:
            data = json.loads(resp.read().decode("utf-8"))

        pricing: dict[str, dict] = {}
        if isinstance(data, dict):
            for model in data.get("data", []):
                model_id = model.get("id")
                if model_id and isinstance(model.get("pricing"), dict):
                    pricing[model_id] = model["pricing"]
        _pricing_cache = pricing
        return pricing

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d40feba: added a lock around the cache fill, a real User-Agent header (version-derived rather than hardcoded), and isinstance checks on the response shape.

Guard the pricing cache with a lock (eval threads run concurrently and
could fire redundant fetches), send a real User-Agent (CDN-fronted APIs
commonly 403 urllib's default), and type-check the response shape
before indexing into it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tardigrde

Copy link
Copy Markdown
Owner Author

make test-live results (run at d40feba's parent, before the review-feedback hardening commit which only touches fetch robustness):

tests/test_e2e.py::TestLiveAgentsE2E — 7 passed, 3 deselected in 236.21s

All three real harnesses (claude-code via OpenRouter, opencode, codex) completed with real output, zero skipped grades, and each fully passed at least one positive eval.

The new reconciliation path fired live — timing.json from the run:

{
  "cached_tokens": 61288,
  "cache_creation_tokens": 13918,
  "cost_usd": 0.0255923,
  "cost_usd_source": "openrouter-pricing",
  "cost_usd_cli": 0.025592300000000002
}

(The reconciled value equals the CLI estimate here because OpenRouter currently prices Anthropic models at Anthropic list price — the two columns will diverge whenever that stops being true, which is the point of recording both.)

🤖 Generated with Claude Code

@tardigrde tardigrde merged commit f6ed822 into main Jun 11, 2026
7 checks passed
@tardigrde tardigrde deleted the fix/todo-batch-2 branch June 11, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant