Repository navigation
fix(novelty): routing estimate and permanent-error stop - #709
Conversation
…ntel#677) Signed-off-by: Anton Volkov <antonwolfy@gmail.com> intel#676: `novelty routing --estimate-only` sized the selection prompt at chars / 4 and a reply at 65 tokens, which priced a measured 35-skill run about 1.55x below its bill. The input is still counted locally, now at SELECTION_CHARS_PER_TOKEN = 2.6 characters per token, and the flat reply allowance is 150 tokens, inside the measured 114 to 193. The estimate JSON carries chars_per_input_token, the stderr line names both allowances and says the old figures were about 1.55x low, and the docs say the figure is an estimate, not a bound. The estimate code moves to novelty/routing_estimate.py so routing.py stays under its growth baseline. intel#677: the per-call catch of the routing loop recorded every exception as one more failed row, so a missing credential ran all 270 calls. A permanent error (dokimi.llm.is_retryable is False) now stops the loop at that call through the path the spend cap uses: the rows made so far are kept, the result carries cap_stop and a partial block, and the message holds the error text and the call it stopped at. A refusal (ContentPolicyViolationError), an unparseable reply and a retryable error that ran out of retries are still rows, and the loop goes on. The exit code is unchanged.
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 13 selected item(s). CI at Review details
|
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at b979089. The estimate change (#676) is right and the docs, the printed line and the tests follow it. The stop at a permanent error (#677) works for an auth error, but it also stops the run on errors that belong to one query; details are in the line comments.
Agreed with @exai-bot on the query-local rejection that now aborts the run; I added more cases in its thread.
What I ran, at b979089:
make ci-local --offline: 20 passed, 1 failed, 19 skipped; tests and coverage of changed lines pass. The failure isschema_compat: it compares against the currentmain, where #633 widenednovelty.v1.jsonafter this branch's base. This PR does not touch schemas; a rebase clears it.- A stub client failing call 2 of 3:
IndexError,BadRequestError,CliCallError(retryable=False)andRuntimeErroreach stop the run; on the base each is one failed row. all_vs_allwith 2 skills x 2 queries and an auth error on call 4: the message andpartial.calls_donedisagree.
Checked, no issues
- Allowances: 2.6 chars per token matches the issue's numbers (5,126 x 4 / 7,949 = 2.58); 150 sits inside the measured 114 to 193; the single and all-vs-all estimates and the USD estimate use the same constants.
- Stop path: a permanent stop always leaves the failing call unmade, so
partialis always written and the command exits non-zero, in both modes; later skills in all-vs-all are not called. - Docs and changelog: the fragment is under Fixed and says what changed;
skill-novelty.mdmatches the code. The new files have the SPDX header, and the new tests fail on the base.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles the review points on the permanent-error stop (intel#677). Only an authentication, permission or not-found error, or a no-credentials message, stops the run now; any other error that is_retryable does not know (a context-length rejection, an IndexError, a non-retryable CLI error, a bare RuntimeError) is one failed row again, so the later queries are still asked. The tests that had been reworded to say "connection" are back as they were. The stop message no longer counts the calls of the current skill only (wrong in --all-vs-all); the exit line already gives calls_done of calls_planned. partial_record takes a reason, and a permanent stop is stored with "permanent_error" in routing and all-vs-all instead of "spend_cap". The predicate moves to novelty/routing_stop.py to keep routing.py under its size cap. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
@vlad-perevezentsev (#709 (review)): The review's points are answered in the threads 4563405051, 4563405365 and 4563405377. The |
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #709 +/- ##
=======================================
Coverage 95.73% 95.73%
=======================================
Files 408 410 +2
Lines 54644 54667 +23
Branches 9404 9407 +3
=======================================
+ Hits 52311 52338 +27
+ Misses 1442 1438 -4
Partials 891 891
🚀 New features to boost your workflow:
|
napetrov
left a comment
There was a problem hiding this comment.
Change class: not computed (no receipt profile for this repo, or receipt.py failed)
Verdict: comment. No blocking. Two questions on the run-wide stop; the estimate change checks out (150 tokens is inside the measured 114 to 193). Head a4bef1c, merge base d61e7db, lane L3.
Ran: the run-wide and estimate probes on head (the pass's probes, with the catchall probe for the pre-existing case). Full pytest was not run here: pytest-timeout and pytest-socket are not installed in this sandbox.
CI: 0 failing Actions jobs on head. The timeouts in tests/test_audit_test_quality.py are the PR body's author-host report; CI on head shows no failure, so not chased.
Raised before:
- @exai-bot T1 (a query-local rejection aborts the run): fixed on head; only run-wide errors stop, the rest are rows.
- @vlad-perevezentsev T2 (the stop message counts one skill's calls): answered; the exit line counts the whole run, per the PR body. Not re-probed.
- @vlad-perevezentsev T3 (stop stored as spend_cap): fixed on head; the stop stores permanent_error.
- @vlad-perevezentsev R2 (estimate change): answered; the estimate and the docs follow it.
Coverage: checked is_run_wide and the stop on head (including the catch-all case), the estimate constants and the printed line. Not looked at: other Python versions, a real provider, the CLI-backed path.
Design: the UnsupportedParamsError scope is the author's call; a question, not a blocker. Follow-up issues: none.
vchamarthi
left a comment
There was a problem hiding this comment.
Reviewed head a4bef1c. 1 must fix, 1 nit to fix before merge. Details are in the line comments. Adds a routing stop at permanent provider errors and recalibrates the selection estimate. The stop misses two no-credentials cases (a claude-cli login error and a catch-all error class), and the rest of the change holds.
Verified
- Changed tests: The three changed test files pass on the head, 87 tests.
- Tests pin the stop: On main the new estimate and stop test file does not collect, because
routing_stopandrouting_estimatedo not exist there. - Other failures: The three failing tests in
tests/test_repo_compare_interval.pyfail on main as well, so they are not from this change. - Merge: The branch merges onto main with no conflict.
- Stop is tested: Replacing
if is_run_wide(exc):in_ask_oncewithif False:fails 6 of the changed tests. - Empty error message: An
AuthenticationErrorwith an empty message stops the run withpermanent error: AuthenticationError.
Must fix:
dokimi/dokimi/novelty/routing_stop.py:12:claude-cliwith no login (Not logged in) does not stop the run, so every call still makes a failed row.
Fix before merge:
dokimi/dokimi/novelty/routing_stop.py:17:APIConnectionError("Unable to locate credentials")is judged transient, so the run keeps calling and writing failed rows.
Not verified
- Whether
UnsupportedParamsErrorshould stop the run. Add it to_RUN_WIDE_ERRORSwith a test, or state in the comment why a rejected parameter is left to a failed row. - Whether a
claude-cliusage-limit reply should stop the run. Decide it, then match the message inis_run_widewith a test, or record in the comment why it is left out.
|
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…stops routing CliCallError gains run_wide, set where llm_cli already knows every later call fails alike; is_run_wide reads it, as is_retryable reads retryable. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Every call sends the same parameters, so UnsupportedParamsError fails each of them alike. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
A skill routing run whose every call failed exited 0 with status ok before this PR. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Pushed 4 commits (
On the merged tree: full suite (19955 passed), ruff, mypy, bandit, |
xaleryb
left a comment
There was a problem hiding this comment.
Re-checked at b81e6e8 (merge of main plus our three commits): a claude-cli login, usage-limit, unknown-model or missing-binary error and an UnsupportedParamsError now stop the run at the first call, a 400, non-JSON reply or timeout stay one failed row, and the changelog says a skill routing run whose every call failed exits 1; the full suite (19955 passed), ruff, mypy, bandit and the docs and growth gates pass, and CI is green. Left for the author: the body's exit-code sentence, and APIConnectionError('Unable to locate credentials') counted as transient as on main.
…lasses is permanent (#751) Settles the open review threads of #709 (r4569209131, r4568069518): `is_run_wide(APIConnectionError("Unable to locate credentials"))` was False because `is_retryable` judged the catch-all class transient, so `model_routing` kept calling and writing failed rows. `unable to locate credentials` joins `_NON_RETRYABLE_MESSAGES` in `llm.py`, so `is_retryable` is False for it under `APIConnectionError`, `APIError` and `InternalServerError`, and `is_run_wide` is True. `routing_stop._NO_CREDENTIALS` was a copy plus that one entry and is dropped. litellm's Bedrock mapping leaves other botocore credential errors as a bare `APIConnectionError`, so the messages of `NoAuthTokenError`, `PartialCredentialsError`, `ProfileNotFound` and `UnauthorizedSSOTokenError` are added too. A retry never supplies credentials, so the retry wrappers (`LLM.ask` and the other `is_retryable` callers) now fail at the first attempt instead of after 2 + 4 + 8 s of backoff. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Summary
novelty routinghas two problems, both indokimi/novelty/routing.py.main--estimate-onlyon a 35-skill catalog, 90 queries, 3 repeats (270 calls)--model claude-haiku-5-5 --provider amazon-bedrockwith no Bedrock credentialsUnable to locate credentials, then exit 1For #676 the taken fix option is the third plus a cheap part of the first: the input stays a local count (no new network call, so
--estimate-onlystays offline) at a calibrated 2.6 characters per token, and the reply allowance is one flat 150 tokens, inside the measured 114 to 193, because the repo has no per-model table. A provider token counter would need a network call or a per-provider tokenizer, so it is not used. For #677 the permanent error stops the loop through the path the spend cap uses.Closes #676
Closes #677
Part of #631:
novelty analyze's estimate has the same class of error and is not touched here.The same per-call catch in the atoms, probe and eval-arm code is tracked in #708.
PR #595 renames the routing CLI flags and edits
novelty/routing.py, so it may conflict textually with this one; the CLI spelling is untouched here.Changes
novelty/routing_estimate.py(new):SELECTION_CHARS_PER_TOKEN = 2.6andSELECTION_REPLY_TOKENS = 150, with their source (issue novelty routing --estimate-only prices about 1.55x below the bill: chars/4 input and a fixed 65-token reply #676), andselection_prompt_tokens/selection_estimatemoved there fromrouting.py, which was at its growth baseline. The estimate now also carrieschars_per_input_token.commands/novelty_routing_report.py: the stderr line of--estimate-onlynames both allowances and says the old figures were about 1.55x below measured bills.docs/skill-novelty.md: the estimate paragraph says it is an estimate, not a bound, and gives both allowances; a new paragraph describes the permanent-error stop.novelty/routing_stop.py(new):is_run_wideis true for an authentication, permission or not-found error, or a no-credentials message, thatdokimi.llm.is_retryablecalls permanent.novelty/routing.py_ask_once: whenis_run_wideholds for the exception, the call has no row and the run stops.model_routingputs the error text incap_stop, setsstop_reasonto"permanent_error"and writes thepartialblock, as for the cap. Every other error is one failed row and the run goes on: a refusal, a request too long for the model (BadRequestError), anIndexError, a non-retryable CLI error, a bareRuntimeError, an unparseable reply, a retryable error that ran out of retries. The exit code is unchanged (1).novelty/noise.pypartial_recordtakes areason(default"spend_cap"); routing and all-vs-all store"permanent_error"for this stop. The message is the error text alone: the exit line givescalls_doneofcalls_planned, which in all-vs-all counts the whole run.Risk
--estimate-onlyfigures rise: input by about 1.5x, output by about 2.3x. An authentication, permission or not-found error, or a no-credentials error, now stops a routing run at its first occurrence instead of failing every call; a provider that words a run-wide failure otherwise (a plainRuntimeError, say) still gets one failed row per call, as onmain. The partial record'sreasonis"permanent_error"for this stop, so a script that readsreasondoes not take it for a--max-usdstop.Testing
On a Linux review host, at
a4bef1c(onmaind61e7db):make ci-local: 31 passed, 1 failed, 13 skipped (docker smoke, the other-Python and Intel matrix legs, conda-forge, fuzz, semgrep, deploy-only jobs). The one failure istest / Run tests: 19118 passed, 19 skipped, and 2 errors that areTimeout (>60.0s) from pytest-timeoutintests/test_audit_test_quality.py(test_a_fixed_finding_leaves_the_baseline_too,test_no_new_test_that_cannot_fail), files this PR does not touch; that file run alone passes (172 passed in 5s), and a first run had the same timeout in the first of them with 19119 passed. Theschema_compatstep passes on the merged tree.tests/test_novelty_routing_estimate_and_stop.py: the estimate equals the prompt's characters over 2.6 (pinned by value), the printed line names both allowances, an authentication error on the third call makes exactly 3 calls and keeps 2 rows, one on the first call makes one call, and a refusal and a transient error are rows while the loop goes on. Againstmain's code 3 of the 5 fail (the other two cover the new estimate module, copied in); with the permanent-error check disabled 2 fail, with the ratio set to 4 three fail.tests/test_novelty_routing_estimate_and_stop.py): a permission, not-found or "Missing credentials" error on call 2 stops the run after 2 calls;BadRequestError(context length),IndexError, aretryable = FalseCLI error and a bareRuntimeErroron call 1 are one failed row and all 3 queries are asked; inall_vs_all(2 skills x 2 queries, auth error on call 4) the partial is 3 of 4 calls,reasonpermanent_error, messagepermanent error: bad key. Withis_run_widealways true 9 tests of the two routing files fail; withpartial_recordignoring the reason 5 fail; withall_vs_allnot passing the reason the all-vs-all test fails.fix/routing-lexical-rows,docs/heading-query-artifactonmainc8e707c:pytest -n auto19082 passed, 19 skipped,mypyclean,ruff checkclean.Revert
A plain revert restores the chars / 4 and 65-token estimate and the record-every-failure loop. Nothing is stored.
Checklist
pytestpassesruff,mypy, and schema checks passAGENTS.md.github/.licenserc.yaml)