feat(classify): cascading fallback instead of a dumb static guess #34
Reference in New Issue
Block a user
Delete Branch "feat/classifier-fallback-cascade"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
When the local classifier failed, the router jumped straight to a fixed
tier/category. Cheap and predictable — but it discards two signals the router
already holds, and stamps unrelated work with
general_chat.The cascade
local -> stale session cache -> session history -> optional cloud -> staticSteps 2 and 3 are free and local. Step 4 costs money and is disabled by
default, so this is zero-cost until someone configures
cloud_fallback.Three things bound step 4's cost:
outage buys one cloud attempt per window across all requests, not one per
request;
_account_level_refusalsites: anout-of-credit account makes a cloud classification a guaranteed wasted
request, so the step is skipped;
session_cache.put()gate now admitsclassifier_cloud, bounding asession to one cloud call per staleness window rather than one per turn.
session_stale/session_historystay out — borrowed answers must notrenew a staleness clock they never earned.
_session_history_lookupfilters degraded sources, so one outage's guesscannot propagate through every later turn of a session.
The part the review bought: attribution
A fallback-classified request is recorded as
general_chat— which is afully scored category (13 proficiency rows live) and the configured
fallback_category. Without a guard,POST /outcomeon mislabelled outagetraffic folds into
proficiency(model, general_chat)and drags real scorestoward whatever was flowing while the classifier was down.
report_outcomenow resolvesclassification_sourceand passesmodel_attributable.feedback.pyis untouched — its existingAND model_attributable = 1already implements keep-the-record,don't-steer-routing.
Attributable:
classifier,cached,override,classifier_cloud. Not:fallback,session_stale,session_history. The asymmetry is deliberate —a degraded source is good enough to route one visibly-flagged request, but a
proficiency score is consulted by every future request.
Fails open on an unknown decision row; an over-applied exclusion already
starved feedback once (
client_capped).Two deviations from the plan, both forced by the code
session_keytravels in a ContextVar, not threaded throughdispatch -> route -> classify. The kwarg broke 78 tests that stubroutewith a lambda — that is the signal a signature change here is apublic API change.
logs.pyalready carries the trace id this way.session_cache.classify_onereturns a plain dict. The plan wanted it insession_cacheand that module import-free; returning a dispatcher typebreaks the second.
Verification
local_energy.enabledboth true and false.proves that step ran.
config/config.local.yamlbyte-identical (cc3d4c2a…)..omo/.⚠️ Merge-order note
PR #33 adds a tripwire that fails on any unregistered warning class. This
PR adds one (
classifier_degradation_warning). Whichever merges second turnsthe other red — that is the tripwire working. Fix is one line:
Do not disable the test.