✨ New Features
- feat(providers): add Claude Fable 5 (
claude-fable-5) — wires the new flagship model across the full pipeline:ccandkiroprovider registries (1M context, 128k output), pricing at $15/$75 per 1M tokens, model spec (adaptive thinking, vision, tool use), fast mode, 1M-context beta header, fallback chain (claude-fable-5 → claude-opus-4-8 → claude-opus-4-7 → claude-sonnet-4-6), and cost data. (#3524 — thanks @ggiak) - feat(resilience): add global provider cooldown tracking to prevent combo re-walking — after a provider fails in a combo request, subsequent requests skip it for a configurable exponential backoff (default 5s min, 5min max, doubling per failure), reducing wasted time on known-failing providers. Configurable and opt-out via Settings → Resilience. (#3556 — thanks @pizzav-xyz)
- feat(resilience): expose provider breaker degradation threshold setting — the consecutive-failure count before a provider enters the DEGRADED state is now configurable in Settings → Resilience alongside the existing open/half-open thresholds. (#3535 — thanks @rdself)
🔧 Bug Fixes
-
fix(translator): scope the Gemini
thoughtSignaturebypass to the Antigravity/CLI path and unwrap array-shaped Gemini error bodies — signature-less historical tool calls on Antigravity/CLI are emitted as native parts carrying theskip_thought_signature_validatorsentinel (preventing upstream 400s), while the standard Gemini direct path keeps its existing text/context representation untouched. (#3560 — thanks @oyi77 and @Six7Day via #3414) -
fix(routing): combo model substitution no longer forwards a client
thinking:{type:"disabled"}to a target model that rejects it — when a combo/route swaps the upstream model (e.g.claude-opus-4-8→claude-fable-5), OmniRoute now strips the now-invalidthinking.type:"disabled"for models flaggedrejectsThinkingDisabled(Fable 5 defaults to adaptive and rejects it), preventing the upstream 400 that silently broke Claude Code's internal title/name-generation calls. Models that acceptdisabled(opus/sonnet) are untouched. (#3554) -
fix(usage): the budget dashboard can now save a budget with some limit fields left empty and clear all limits —
setBudgetSchemaused.positive()(rejecting the0the form sends for blank fields) plus a superRefine requiring at least one limit> 0, so saving with one field filled 400'd and clearing all limits was impossible. Limits now accept0(= "no limit for this period"; enforcement only kicks in above 0) and the cross-field minimum was removed; negatives are still rejected. (#3537) -
fix(gamification): badge-unlock events no longer re-fire on every request — the "already unlocked?" guard used
getBadges(), which INNER-JOINsbadge_definitions(empty until seeded), so it always reported "not earned" and re-emittedevents.badge_unlockedper request. Added ahasBadge()helper that readsuser_badgesdirectly, so dedup is correct regardless of whether definitions are seeded. (#3472) -
fix(routing): the
automodel keyword now works on the Codex/v1/responsespath —resolveResponsesApiModelrewrote the bareautokeyword tocodex/auto, which ChatGPT rejects (The 'auto' model is not supported when using Codex with a ChatGPT account).auto(OmniRoute's zero-config auto-routing keyword) now passes through untouched so combo routing handles it. (#3509) -
fix(cli-tools): saving the OpenCode/CLI tool config no longer 400s in cloud mode — every CLI tool card posts
apiKey: null(the real key is resolved server-side fromkeyId), butguideSettingsSaveSchemausedz.string().optional(), which rejectsnull. The schema now normalizesnull→undefined, so the save succeeds and thekeyId/default path is used. (#3552) -
fix(catalog): PublicAI is no longer miscatalogued as keyless/free — it requires an API key (registry
authType:"apikey"; signup grants a one-time credit, then it bills). The three PublicAI models moved fromfreeType:"keyless"(which could pick them into the no-auth pool and dispatch with noAuthorizationheader) to"one-time-initial", and the provider'shasFreeflag is nowfalse— matchingfreeTierCatalog.ts, which already excluded publicai. (#3558) -
fix(gemini-web): a missing Playwright Chromium browser no longer loops and trips the provider breaker — when the browser binary is not installed,
chromium.launch()threw an error surfaced as a retryable 500, so accountFallback marked the account unavailable and retry-looped. It is now classified as a host/config problem and returns 503 with an actionable message (npx playwright install chromium) and theX-Omni-Fallback-Hint: connection_cooldownheader, which skips the provider circuit breaker and applies a short non-exponential cooldown. (#3516) -
fix(proxy): the SOCKS5 proxy option now follows the runtime
ENABLE_SOCKS5_PROXYenv instead of the build-timeNEXT_PUBLIC_ENABLE_SOCKS5_PROXY— Next.js inlinesNEXT_PUBLIC_*at build time, so a prebuilt Docker image ignored a runtime setting and the SOCKS5 type stayed hidden. The proxy modal now readssocks5EnabledfromGET /api/settings/proxies(server-sideENABLE_SOCKS5_PROXY), with the build-time value kept only as a static-deploy fallback. (#3508) -
fix(playground): the playground model selector now lists models from custom-endpoint (OpenAI/Anthropic-compatible) providers — it filtered
/v1/modelsby the provider's connection id, but the catalog emits compatible-provider models under the node's custom prefix (prefix/model), so the list came up empty ("None"/"-"). The selector now filters by the node prefix (exposed additively asmodelPrefixon provider options; the connection id is unchanged, so translator send/translate and connection lookups are unaffected). (#3505) -
fix(usage): the Kiro quota card no longer renders a blank when the account returns no usage breakdown —
getKiroUsagereturnedquotas:{}for a successful GetUsageLimits response without ausageBreakdownList(observed with some AWS IAM / Builder ID accounts), which the dashboard showed as an unexplained empty card. It now returns an informative message (surfaced via the card's connection-message path). (#3506) -
fix(security): route raw
err.messagethroughsanitizeErrorMessage()in five web executors (adapta-web,deepseek-web,perplexity-web,qoder,veoaifree-web) and the embeddings + search handlers (Hard Rule #12) — these built error response bodies from the raw upstream/exception message, which could leak internal detail. (#3494, #3495) -
fix(dashboard): correct two dashboard fetches that hit non-existent routes (404) —
CustomHostsManagercalled/api/tools/traffic-inspector/custom-hosts(the real route is/hosts), andFeatureFlagsGrid's post-restart liveness probe called/api/health(the real lightweight endpoint is/api/health/ping). (#3486, #3487) -
chore(providers): remove the dead
krutrimregistry entry — it was half-registered (present inproviderRegistry.tswith a baseUrl + one model, but absent fromproviders.ts, with no executor/translator/OAuth), so it was never selectable. Dropped itsProviderIconentry and theKNOWN_REGISTRY_ONLYexception. (#3483) -
docs(api): fix the agent-bridge per-agent state route in
openapi.yamlandAGENTBRIDGE.md— both documented/api/tools/agent-bridge/agents/{id}/state, which has no route; corrected to the real per-agent/api/tools/agent-bridge/agents/{id}(global state remains/api/tools/agent-bridge/state). (#3489) -
docs(api): correct
API_REFERENCE.mdendpoints that documented non-existent routes — skills (PUT /api/skills/[id],POST/GET /api/skills/executions), plugins ([id]→[name],activate/deactivate), ACP (DELETE/POST /api/acp/agentsvia?id/{action:"refresh"}), cache (DELETE /api/cache/reasoning,/api/cache/entries), and removed the fabricated/api/admin/circuit-breaker,/api/admin/rate-limits, and/api/system-info(admin only exposes/concurrency). (#3497) -
fix(executor): strip provider prefix from versioned built-in tool model field — Anthropic rejects
tools[N].model: "cc/claude-opus-4-8"from Claude Code'sadvisor_20260301and similar versioned built-in tools; the native Claude OAuth execute path now strips any provider prefix frommodelon tools whose name matchesname_YYYYMMDD. (#3532 — thanks @ggiak) -
fix(dashboard): handle DEGRADED and unknown provider breaker states on the Runtime page — an unrecognised breaker state (e.g. DEGRADED) caused a crash because the styling map had no entry for it; now falls back to a neutral style so the page never throws on unknown states. (#3533 — thanks @rdself)
-
fix(usage): make opencode-go quota fetcher fail-open instead of throwing 500 — the quota API rejects chat API keys with a JSON-401 body even though the same key works for chat; previously this threw and crashed the dashboard with a red error banner. It now returns an informative message and keeps rendering like other connection-message cases. (#3522 — thanks @wilsonicdev)
-
fix(translator): map the Codex
local_shelltool type —local_shellwas absent from the translator's tool-type map, causing it to fall through as an unknown type; it is now forwarded correctly to the upstream. (#3534 — thanks @kamaka) -
fix(images): prefer bare combo names over built-in image model aliases — a user combo named
gpt-image-2can now shadow the native OpenAI alias so image requests route through the combo; provider-qualified IDs likeopenai/gpt-image-2still resolve via the built-in path. (#3527 — thanks @AveryanAlex) -
fix(translator): fix OpenAI→Gemini translation of historical tool calls — tool results from earlier turns were being converted to text, causing Gemini to pattern-match the response as prose rather than structured content; they now use the native Gemini
functionResponsepart format. (#3569 — thanks @hartmark) -
fix(plugins): forward plugin lifecycle hooks (
onInstall,onActivate,onDeactivate,onUninstall) via IPC and wraponDeactivate/onUninstallin try/catch so a buggy plugin handler can no longer brick teardown; also removes redundantRegExp()wrappers inaccountFallback.tsand fixes indentation inrequestLogger.ts. (#3562 — thanks @oyi77) -
fix(auto-update): use a stable PROJECT_ROOT walker instead of frozen
process.cwd()—resolveProjectRootnow walks up from__dirnameto find the nearest directory containingpackage.jsonor.git(bounded at 16 levels), preventing ENOENT errors when the working directory is not the project root. (#3561 — thanks @oyi77 / @ViFigueiredo via #3423) -
fix(resilience): expose
providerCooldowninGET /api/resilienceand accept it inPATCH— PR #3556 added the global provider cooldown tracker to the settings model andResilienceTabUI, but the API route never returned the field (causing a crash when the tab loaded) andupdateResilienceSchema(.strict()) rejected PATCH bodies containing it with 400.providerCooldownSettingsSchemais now wired into the Zod schema, returned in the GET response, and merged in the PATCH handler. Caught during v3.8.20 VPS homologation (Hard Rule #18 — 5 TDD tests). (#3590 — thanks @diegosouzapw)
🙌 Contributors
Thanks to all who contributed to this release:
@ggiak · @pizzav-xyz · @rdself · @hartmark · @oyi77 · @Six7Day · @ViFigueiredo · @wilsonicdev · @kamaka · @AveryanAlex · @diegosouzapw
What's Changed
- Release v3.8.20 by @diegosouzapw in #3547
- fix(resilience): expose providerCooldown in GET+PATCH /api/resilience (v3.8.20 post-deploy fix) by @diegosouzapw in #3591
Full Changelog: v3.8.19...v3.8.20