feat(plugins): transform_api_error_classification hook so provider plugins own their error quirks - #58524
Conversation
|
Nice seam — provider plugins owning their own error quirks makes a lot of sense, and it should genuinely shrink that backlog class. 👍 One small correction to the list though: #58366 (mine) isn't an error-classification patch — it doesn't touch agent/error_classifier.py. It adds a recommended:free sentinel to fallback_providers so the chain tracks the Portal's rotating free model (model resolution at chain-build time, not error handling at request time). Different code path, so it wouldn't be absorbed by this hook — just flagging so it doesn't get swept up in a batch cleanup if this lands. |
|
Good flag, you're right. #58366 is chain-build-time model resolution, not request-time error classification. Different layer, wouldn't be absorbed by this hook. Pulled it from the PR description so it doesn't end up in anyone's batch-cleanup list. Took a second look at the rest of the citations while I was in there and struck #58474 too. That one is credential-pool resolution, also not classifier territory. The solid citations for this class are #58451, #58355, and the tool-use-404 piece of #58502. Description is updated. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for proposing a concrete plugin seam and isolating malformed callback results.
Problems
- The bundled OpenRouter example is now stale. Current main already matches the exact phrase in
agent/error_classifier.py:295and returnsmodel_not_found,retryable=False, andshould_fallback=Truein the 404 path atagent/error_classifier.py:929-934(commit2bb11adb4976169f2434ce72a6185cb189d83079). Consequently, the new baseline assertion intests/test_classify_api_error_hook.py:48is no longer true. Please remove or replace that provider-specific demo; third-party integrations also belong in standalone plugin repos under the repository policy. VALID_HOOKSis also the shell-hook allow-list (agent/shell_hooks.py:311-336). But_parse_response()atagent/shell_hooks.py:566-620does not preserve{\"reason\": ...}directives for this event, so a configured shell hook can register successfully while its classification is ignored. Support the shell response shape with tests, or make this Python-plugin-only.- Please document this behavior-changing hook in
website/docs/user-guide/features/hooks.md; its current reference says onlypre_tool_callandpre_llm_callaffect behavior (:374-397).
Suggested changes
- Retain generic hook tests using a synthetic unclaimed provider error, and add the shell-path or explicit exclusion plus public contract documentation.
Automated hermes-sweeper review.
| monkeypatch.setattr(plugins_mod, "_plugin_manager", plugins_mod.PluginManager()) | ||
|
|
||
| result = _classify_tool_use_404() | ||
| # Documents the gap the demo plugin closes: a generic 404 with no |
There was a problem hiding this comment.
This baseline is stale on current main: commit 2bb11adb4976169f2434ce72a6185cb189d83079 added this exact phrase to _MODEL_NOT_FOUND_PATTERNS, and the 404 path now returns model_not_found with retryable=False and fallback. Replace the demo premise with an unclaimed synthetic provider error.
| # First valid result (registration order) wins. Invalid dicts and | ||
| # unknown reasons are skipped; exceptions are isolated — a broken | ||
| # plugin can never break error classification. | ||
| "classify_api_error", |
There was a problem hiding this comment.
Adding this to VALID_HOOKS also enables hooks.classify_api_error shell hooks, but agent/shell_hooks.py:_parse_response() only preserves pre-tool, pre-verify, or context responses. A shell hook returning the documented {reason: ...} directive is silently ignored; either implement that response path with tests or keep this event out of the shell-hook allow-list.
c66959b to
5a2a0e7
Compare
|
All three addressed in 5a2a0e759 (rebased onto current main).
Suites: tests/test_classify_api_error_hook.py 15 passed, tests/agent/test_shell_hooks.py and tests/agent/test_error_classifier.py green, 263 total across the three touched files. |
5a2a0e7 to
16447f3
Compare
|
The dispatch wording landed in #75861 ( Since we don't have push access to this branch, want to grab the conforming pass yourself now that you can see the exact wording, or would you rather I open a PR against your fork's branch (or drop a suggested diff here) for you to pull in? Either works — just don't want to duplicate effort if you're already planning to touch it. |
|
Grabbed it myself, thanks for the pointer. 774329fac aligns all three spots (the VALID_HOOKS comment, the helper docstring, and the hooks.md section) to run-all-then-pick-first, adds the Privacy flag on error_message and error_body, and states the cold-path trigger explicitly. Wording only, no behavior change, 33/33 tests still green. One note: I didn't hard-link docs/plugins/hook-taxonomy.md since #75861 hasn't merged yet. Happy to add the link once it lands. |
The plugin system's authoring contract currently lives only in code comments (VALID_HOOKS block, PluginContext docstrings, middleware.py). With the plugin-interface expansion underway, third-party authors need a single reference. This adds docs/plugins.md covering: - Quick start (manifest + register(ctx), enable, verify) - All four discovery sources, name-collision precedence, path-derived keys - plugin.yaml field reference and the five plugin kinds with their kind/source-aware loading rules (auto-load, deferred platform, opt-in) - plugins.enabled/disabled/entries config semantics - Full PluginContext surface: tools (incl. the allow_tool_override trust gate), slash + CLI commands, all 23 hooks with per-hook return contracts, the 4 middleware kinds with chaining/next_call semantics, backend provider registries and their config selectors, platforms, Slack actions, auxiliary tasks, skills, ctx.llm/inject_message/ dispatch_tool/profile_name - State & concurrency (plugins/plugin_utils primitives) - Distribution via `hermes plugins install` (all URL forms, after-install.md, manifest_version) - Debugging (HERMES_PLUGINS_DEBUG) and common gotchas - "Converting a waiting PR into a plugin" mapping table Every claim was cross-checked against hermes_cli/plugins.py, hermes_cli/middleware.py, tools/registry.py, and shipped plugins (spotify, google_meet, disk-cleanup); hook/middleware/method names in the doc are verified to exist by script. The classify_api_error entry is marked pending PR NousResearch#58524. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
|
I'll grab the conforming pass myself, thanks for checking rather than duplicating. Since the code already does run-all-then-pick-first, this should mostly be docstring work: the Privacy: line on error_body and error_message, the cold-path note, and pointing the hook's VALID_HOOKS entry at hook-taxonomy.md. I'll flag it on #75861 if any of the wording doesn't survive contact with the code. |
6398b12 to
8a24479
Compare
|
Applied the #64231 verdict conditions in 8a24479a4. The hook id is now transform_api_error_classification everywhere: VALID_HOOKS, the dispatch helper, the shell-hook exclusion, tests, and both hooks.md spots. Dispatch stays run-all-then-pick-first with the first valid result in registration order winning, and per the #64714 skipped-transform rule a valid-but-losing classification now logs a runtime warning instead of being silently shadowed, with a test covering both the warning and the no-conflict quiet path. Rebased onto current main (715d26c). 14/14 on the hook suite, 134/134 across the hook, shell-hook, and error-classifier suites, ruff clean. If the #64714 chaining resolution lands with different transform semantics I'll conform to it. |
…t failed TUI/desktop error frames often showed a generic "request failed" while the classified detail (provider, model, base_url, HTTP status, failure_reason, fallback) only reached agent.log. Add _classify_turn_error_message and use it in _fail_inflight_turn, message.complete error payloads, and compute_host turn.error frames. Composes with NousResearch#56720 turn_failed and NousResearch#58524 classify_api_error (NousResearch#64182 item 3). Observer-side framing only — no delivery-path mutation.
…n error quirks
Adds a plugin seam at the top of agent/error_classifier.classify_api_error()
(step 0, before the built-in pipeline) so model-provider plugins can classify
their provider's error quirks without patching core:
- New "classify_api_error" entry in VALID_HOOKS. Callbacks receive the parsed
error context (provider, model, status_code, error_type, error_code,
error_message, error_body, error, approx_tokens, context_length,
num_messages), self-scope on `provider`, and return None to pass or a dict
{"reason": "<FailoverReason name>", ...optional recovery-hint overrides}.
- get_plugin_error_classification() helper mirrors
get_pre_tool_call_block_message(): first valid result wins, invalid dicts
and unknown reasons are skipped, callback exceptions are isolated — a
broken plugin can never break classification. Zero behavior change when no
plugin claims the error (all 179 existing classifier tests pass untouched).
- Bundled reference plugin `openrouter-tool-use-404` (opt-in, like all
bundled standalone plugins) re-implements PR NousResearch#58451: OpenRouter's
"No endpoints found that support tool use" 404 carries no
_MODEL_NOT_FOUND_PATTERNS signal, so it classifies as unknown/retryable
and the retry loop burns 3-5 attempts on a deterministic rejection.
The plugin classifies it as model_not_found (retryable=False,
should_fallback=True) so the fast-fallback path fires immediately —
demonstrating a waiting core PR converted to a publishable plugin.
Motivation: ~10 open PRs are single-provider error-classification patches
(NousResearch#58451, NousResearch#58355, NousResearch#58502, NousResearch#58474, NousResearch#58366, ...). This hook turns that whole
class of contribution into plugin territory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
…review Rebased onto current main, where the OpenRouter tool-use 404 is now handled natively (the bundled demo's exact reason to exist), so the demo plugin is removed per the standalone-repo policy and every test now uses a synthetic unclaimed error (fake provider, neutral message, no status code) that no present or future built-in rule can claim. classify_api_error is now explicitly Python-plugin-only: VALID_HOOKS doubles as the shell-hook allow-list, but the shell response parser has no channel for the classification directive, so shell registrations are refused at config parse with a warning instead of being silently ignored (new SHELL_UNSUPPORTED_HOOKS set + regression test). The hook is documented in the hooks reference as the third behavior-changing hook, with the full kwargs contract, return shape, and the Python-only note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
…ating-hook taxonomy The taxonomy write-up for NousResearch#64231 names the Shape B contract run-all-then-pick-first: every registered callback runs with failures isolated, then the first valid result in registration order wins. Align the hook comment, helper docstring, and hooks.md section with that wording, add the Privacy flag on error_message/error_body, and state the cold-path trigger explicitly. Wording only, no behavior change.
The dispatch semantics, privacy note, and cold-path property were already documented at the VALID_HOOKS entry and the dispatch helper; this adds the explicit reference to the first-valid-wins shape in docs/plugins/hook-taxonomy.md (landing via NousResearch#75861) and the cold-path note on the helper docstring, per the contract review on NousResearch#64231.
…ousResearch#64231 verdict Applies the batch-disposition SALVAGE conditions from NousResearch#64231: the hook id moves to the taxonomy transform-family name, and run-all-then-pick-first dispatch now logs a runtime warning when a valid-but-losing classification is skipped (the NousResearch#64714 skipped-transform rule). Chaining semantics are stated explicitly at the VALID_HOOKS entry, the dispatch helper docstring, and the hooks.md catalog row and detail section.
8a24479 to
6200d46
Compare
|
Merged via #85231 (rebase-merge, your commits preserved as author) with a trim commit on top per review: hook test suite reduced to core coverage and hooks.md section condensed. All #64231 verdict conditions retained. Thanks @webdevtodayjason! |
What
Responding to the lead dev's call for plugin-interface expansion ideas — this is the first concrete seam, shipped as code instead of a suggestion: a
transform_api_error_classificationplugin hook, consulted at the top ofclassify_api_error()before the built-in pipeline, so model-provider plugins can own their provider's error-classification quirks without core patches."transform_api_error_classification"entry inVALID_HOOKS(contract documented inline, same style aspre_verify/pre_gateway_dispatch). Callbacks receive the parsed error context (provider,model,status_code,error_type,error_code,error_message,error_body,error,approx_tokens,context_length,num_messages), self-scope onprovider, and returnNoneto pass or{"reason": "<FailoverReason name>", ...optional recovery-hint overrides}.get_plugin_error_classification()mirrorsget_pre_tool_call_block_message(): first valid result wins, invalid dicts/unknown reasons are skipped, exceptions are isolated — a broken plugin can never break classification.tests/agent/test_error_classifier.pypass untouched.Why this seam
A large slice of the open-PR backlog is single-provider error-classification patches: #58451, #58355, and the tool-use-404 piece of #58502, plus the class they represent. Each one is an
if provider == X-shaped edit toagent/error_classifier.pywaiting on review. (Two earlier citations were struck after review feedback: #58366 is chain-build-time model resolution and #58474 is credential-pool resolution; different layers, not absorbed by this hook.) With this hook, that entire contribution class becomes plugin territory — publishable today, no merge required.Review updates (July 15)
Per the sweeper review: rebased onto current main, where the OpenRouter tool-use 404 is now handled natively, so the bundled reference plugin (whose whole point was that gap) is removed per the standalone-repo policy for vendor integrations. All hook tests now use a synthetic unclaimed error (fake provider, neutral message, no status code) that no present or future built-in rule can claim, so the fixtures cannot go stale the way the OpenRouter one did.
The hook is also now explicitly Python-plugin-only: VALID_HOOKS doubles as the shell-hook allow-list, and the shell response parser has no channel for the classification directive, so a shell registration is refused at config parse with a warning (new SHELL_UNSUPPORTED_HOOKS set) instead of being silently ignored. The hook is documented in the hooks reference as the third behavior-changing hook with its full kwargs contract.
Review updates (August 12)
Per the #64231 batch-disposition verdict (SALVAGE): the hook id is renamed to
transform_api_error_classification, the taxonomy transform-family name, across VALID_HOOKS, the dispatch helper, the shell-hook exclusion, tests, and the hooks.md catalog. Chaining semantics are now explicit per the #64714 rule: run-all-then-pick-first, first valid result in registration order wins, and every valid-but-losing result logs a runtime warning instead of being silently shadowed. Rebased onto current main (715d26c).Testing
tests/test_transform_api_error_classification_hook.py: baseline fall-through, plugin-wins, override-of-builtin, enum/string reasons, dataclass-default hints, invalid-reason/non-dict skipping, first-valid-wins, skipped-result runtime warning, helper-exception isolation, hook kwargs contract, message/error_context sanitization, and a synthetic plugin self-scoping and running end-to-end through the realinvoke_hookpath (no mocks).tests/agent/test_error_classifier.py: 75 passed, untouched by this branch.PluginManagerdiscovery +plugins.enabledconfig: plugin loads, hook registers, the 404 classifies asmodel_not_found, and unclaimed errors (e.g. 429 →rate_limit) are untouched.ruff checkclean on all touched files.Taxonomy classification (for the #64231 batch review)
error_body/error_messagefields can carry user content or raw provider error dumps (the Delivered-text persistence surfaces bypass PII/redaction hooks (rich-sent index, delivery mirror, API error dumps) #58248 class). Plugins should self-scope onproviderand treat these fields as sensitive.🤖 Generated with Claude Code
https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM