fix: factory accounting, retry, and fallback contracts#36
fix: factory accounting, retry, and fallback contracts#36stackbilt-admin merged 1 commit intomainfrom
Conversation
stackbilt-admin
left a comment
There was a problem hiding this comment.
CodeBeast Review — PR #36 (fix: factory accounting, retry, and fallback contracts)
Verdict: APPROVE with 2 observations
What's good
-
Ledger wiring fix (#31) — Passing ledger to
CostTrackerat construction and tracking spend whenledgerORcostOptimizationis configured is the correct fix. Previously ledger-only consumers silently lost all cost tracking. TheCreditLedgerintegration test at the bottom validates the full pipe: factory → response → accumulator → rate limit counters. -
enableRetries: false(#32) — ThemaxRetries: 0injection whenenableRetries === false && providerConfig.maxRetries === undefinedis clean. Preserves explicit per-provider overrides. Good thatupdateConfigre-initialises providers whenenableRetrieschanges — prevents stale provider instances holding onto old retry counts. -
Tool-capable model validation (#33) — Moving from "silently strip tools" to "throw ConfigurationError" is a better contract. Silent stripping masked bugs where a caller expected tool output and got freeform text instead. The Groq and Cerebras implementations are consistent.
-
Fallback routing (#34) —
getFallbackDecision()→applyFallbackDecision()is a solid refactor. TheproviderModelsmap +requestForProvider()helper ensures the fallback-specified model actually reaches the provider. The splice logic inapplyFallbackDecisioncorrectly:- Skips if target === failed provider
- Skips if target already tried (index ≤ currentIndex)
- Deduplicates if target appears later in the chain
- Inserts at
nextIndexso it's the immediate next attempt
-
isLedgerLimited()extraction — Consolidates the 4-dimension rate-limit check into one method. Minor but removes duplication.
Observations (non-blocking)
O-1: Custom fallback rules evaluated before error-type checks. In getFallbackDecision(), custom rules are evaluated after the AuthenticationError/ConfigurationError gates but before the CircuitBreaker/RateLimit/ServerError catches. This means a custom rule with condition: 'error' will match on a RateLimitError and potentially override the default fallback behavior (which would have returned { shouldFallback: true } without specifying a provider). This is probably the desired behavior — custom rules are more specific — but it's a subtle ordering dependency worth a comment.
O-2: KV race on concurrent consume. The existing checkHourlyRateLimit (not changed in this PR, just context) does read-then-write on KV without atomic increment. Two concurrent requests can both read count=99, both write 100, and both pass a limit of 100. This is pre-existing and out of scope for this PR — KV doesn't support atomic increments — but worth noting for the #102 anonymous quota work that just landed in edge-auth (same pattern, same limitation, acceptable for the use case).
Tests
- Factory tests cover all 4 issues with targeted assertions
- Mock provider
beforeEachreset is thorough (prevents inter-test leakage) - Cerebras + Groq tool validation tests correctly assert
ConfigurationError+mockFetchnot called
Ship it.
🤖 Reviewed by CodeBeast (0xDEBT666F)
Summary
Fixes four contract bugs in the factory orchestration layer:
CreditLedgerinto spend tracking: factory now passes ledger toCostTrackerat construction and records costs wheneverledgerorcostOptimizationis configured (previously onlycostOptimization).enableRetries: whenenableRetries === falseand a provider has no explicitmaxRetries, the factory now injectsmaxRetries: 0so the flag actually takes effect.updateConfigre-initialises providers whenenableRetrieschanges.rpm,rpd,tpm,tpd) via extractedisLedgerLimited().fallbackProvider/fallbackModelin fallback rules:shouldFallback()is replaced bygetFallbackDecision()which returns routing info.applyFallbackDecision()splices the target provider into the chain with the correct model override.Also adds
requestForProvider()helper so fallback-routed requests carry the rule-specified model, and expands factory + provider test coverage (346 insertions).Closes #31, closes #32, closes #33, closes #34
Test plan
npm test— 184 tests pass across 11 suitesnpm run typecheck— cleancostOptimization: false, ledger: <CreditLedger>enableRetries: falseprevents provider-level retriesfallbackProvider/fallbackModelroute correctly🤖 Generated with Claude Code