Repository navigation
feat(novelty): usage and cost block for routing --model #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
09f2ce7
14a7e93
714ce0d
d00eb15
54eb79c
f6df38a
d2fd404
a5b007e
54f4ca8
21a1e0d
2594a80
f4df39a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ### Added | ||
|
|
||
| - **Usage and cost for `novelty routing --model`** — a model run now writes a `cost` block into `routing.json` (calls, input and output tokens, cache read and write tokens, and `usd`, which is `null` when any call this run paid for had no known price) and prints the same totals on stdout, so a routing measurement states what it cost. The cache token totals are also added to the per-role cost summary, so `novelty analyze` reports them in its `cost` block. Calls replayed from the response cache are counted in `replayed_calls` and left out of the token and `usd` totals, and `usd` is also `null` when a call returned no usage. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -509,6 +509,110 @@ def test_catalog_competitors_with_name_in_competitors_excludes_it_from_catalog(t | |
| assert set(names) == {"xpu", "cache"}, f"Expected xpu and cache, got {names}" | ||
|
|
||
|
|
||
| def _stub_provider(monkeypatch, usage): | ||
| """Replace the provider call the --model path makes; every reply picks card 1.""" | ||
| from dokimi import llm | ||
|
|
||
| def stub(prompt, model, **_kwargs): | ||
| return '{"pick": 1}', usage | ||
|
|
||
| monkeypatch.setattr(llm, "llm_call_with_usage", stub) | ||
|
|
||
|
|
||
| def _model_run(tmp_path): | ||
| queries = tmp_path / "q.json" | ||
| queries.write_text(json.dumps(["profile a binary", "tune a slow query"]), encoding="utf-8") | ||
| out = tmp_path / "routing.json" | ||
| args = _parse("novelty", "routing", | ||
| "--artifact", _skill(tmp_path, "a", "profile a binary with perf"), | ||
| "--competitors", _skill(tmp_path, "b", "tune a database"), | ||
| "--queries", str(queries), "--model", "selector-m", "--out", str(out)) | ||
| args.func(args) | ||
| return json.loads(out.read_text(encoding="utf-8")) | ||
|
|
||
|
|
||
| def test_a_model_run_records_its_calls_tokens_and_cost(tmp_path, monkeypatch, capsys): | ||
| from dokimi.metrics.usage import UsageRecord | ||
|
|
||
| _stub_provider(monkeypatch, UsageRecord(prompt_tokens=100, completion_tokens=5, | ||
| cache_read_tokens=60, cache_write_tokens=30, | ||
| cost_usd=0.002)) | ||
| payload = _model_run(tmp_path) | ||
| assert payload["cost"]["llm_calls"] == 2 | ||
| assert payload["cost"]["input_tokens"] == 200 | ||
| assert payload["cost"]["output_tokens"] == 10 | ||
| assert payload["cost"]["cache_read_tokens"] == 120 | ||
| assert payload["cost"]["cache_write_tokens"] == 60 | ||
| assert payload["cost"]["usd"] == 0.004 | ||
| assert ("cost 2 calls, 200 input / 10 output tokens, cache read 120 / write 60, " | ||
| "$0.0040") in (out := capsys.readouterr().out) | ||
| assert "replayed" not in out | ||
|
|
||
|
|
||
| def test_a_model_run_with_an_unpriced_call_reports_no_usd_figure(tmp_path, monkeypatch, capsys): | ||
| from dokimi.metrics.usage import UsageRecord | ||
|
|
||
| _stub_provider(monkeypatch, UsageRecord(prompt_tokens=10, completion_tokens=1)) | ||
| payload = _model_run(tmp_path) | ||
| assert payload["cost"]["usd"] is None # one unpriced call must not be read as free | ||
| assert "USD not known: a call had no price or no usage" in capsys.readouterr().out | ||
|
|
||
|
|
||
| def test_a_lexical_run_spends_nothing_and_has_no_cost_block(tmp_path): | ||
| queries = tmp_path / "q.json" | ||
| queries.write_text(json.dumps(["profile a binary"]), encoding="utf-8") | ||
| out = tmp_path / "routing.json" | ||
| args = _parse("novelty", "routing", | ||
| "--artifact", _skill(tmp_path, "a", "profile a binary with perf"), | ||
| "--competitors", _skill(tmp_path, "b", "tune a database"), | ||
| "--queries", str(queries), "--out", str(out)) | ||
| args.func(args) | ||
| assert "cost" not in json.loads(out.read_text(encoding="utf-8")) | ||
|
|
||
|
|
||
| def test_a_model_run_whose_every_call_fails_prints_no_price_blame(tmp_path, monkeypatch, capsys): | ||
| from dokimi import llm | ||
|
|
||
| def failing(prompt, model, **_kwargs): | ||
| raise RuntimeError("provider refused") | ||
|
|
||
| monkeypatch.setattr(llm, "llm_call_with_usage", failing) | ||
| payload = _model_run(tmp_path) | ||
| assert payload["cost"]["llm_calls"] == 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing pins Mutation check: removing Suggestion: add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pinned in 2594a80: |
||
| assert payload["cost"]["usd"] is None # no calls: nothing known, not "free" | ||
| out = capsys.readouterr().out | ||
| assert "cost 0 calls\n" in out | ||
| assert "USD not known" not in out | ||
|
|
||
|
|
||
| def test_a_call_with_no_usage_makes_the_total_unknown_not_the_priced_subset( | ||
| tmp_path, monkeypatch, capsys): | ||
| from dokimi import llm | ||
| from dokimi.metrics.usage import UsageRecord | ||
|
|
||
| replies = iter([None, UsageRecord(prompt_tokens=10, completion_tokens=1, cost_usd=0.001)]) | ||
| monkeypatch.setattr(llm, "llm_call_with_usage", | ||
| lambda prompt, model, **_kw: ('{"pick": 1}', next(replies))) | ||
| payload = _model_run(tmp_path) | ||
| assert payload["cost"]["llm_calls"] == 2 | ||
| assert payload["cost"]["usd"] is None | ||
| assert "$0.0010" not in capsys.readouterr().out | ||
|
|
||
|
|
||
| def test_replayed_calls_are_counted_apart_and_not_billed(tmp_path, monkeypatch, capsys): | ||
| from dokimi.metrics.usage import UsageRecord | ||
|
|
||
| _stub_provider(monkeypatch, UsageRecord(prompt_tokens=100, completion_tokens=5, | ||
| cache_read_tokens=60, cache_write_tokens=30, | ||
| cost_usd=0.002, replayed=True)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The replay test never gives the replayed rows cache tokens, so cache totals on replayed rows are unpinned. The Mutation check: summing Suggestion: build the record with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 2594a80: |
||
| payload = _model_run(tmp_path) | ||
| cost = payload["cost"] | ||
| assert (cost["llm_calls"], cost["replayed_calls"]) == (2, 2) | ||
| assert (cost["input_tokens"], cost["output_tokens"], cost["usd"]) == (0, 0, 0.0) | ||
| assert (cost["cache_read_tokens"], cost["cache_write_tokens"]) == (0, 0) | ||
| assert "2 replayed from the cache and not counted" in capsys.readouterr().out | ||
|
|
||
|
|
||
| def test_headings_inside_code_only_refuse_the_run_and_say_why(tmp_path): | ||
| out = tmp_path / "routing.json" | ||
| args = _parse("novelty", "routing", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.