Route deepseek-v4-flash to Fireworks when DeepSeek API is unhealthy - #745
Route deepseek-v4-flash to Fireworks when DeepSeek API is unhealthy#745jahooma wants to merge 6753 commits into
Conversation
Co-authored-by: James Grugett <[email protected]>
This reverts commit 13805ac.
[email protected] split its package exports into separate `import` and `require` conditions. In bun --compile binaries, require.resolve from init-node.ts now returns the build-time absolute path of tree-sitter.cjs, which doesn't exist on user machines, causing freebuff to crash on startup with "Cannot find module .../tree-sitter.cjs". Fix by embedding tree-sitter.wasm into the binary via Bun's \`import ... with { type: 'file' }\` and stashing the resulting bunfs path in process.env so all copies of init-node.ts (including the SDK pre-built bundle's inlined copy) can pick it up.
This reverts commit d4d2c0a.
The pre-init module legitimately needs to set process.env so the embedded wasm path reaches every copy of init-node.ts (the SDK's pre-built bundle inlines its own copy).
- cli/src/pre-init/tree-sitter-wasm.ts: silence TS error for the bun-only
`with { type: 'file' }` import (TS resolves the .wasm via the package's
exports map and has no loader for binary assets).
- cli/src/__tests__/integration-tmux.test.ts: explicitly clear
FREEBUFF_MODE from the tmux global env before running. A prior freebuff
build or `bun run dev:freebuff` in the same tmux server leaves it set,
which made the help-output test see the freebuff CLI variant (no
`--agent` flag) instead of codebuff.
- web/jest.config.cjs: fix react/react-dom moduleNameMapper paths — they
pointed at `web/node_modules/react` but bun hoists react to the
workspace root.
- web/jest.setup.js: polyfill TextEncoder/TextDecoder, ReadableStream,
Request/Response/Headers/fetch from Node + undici. JSDOM lacks these
globals, and undici (loaded transitively via `next/server`) needs them
at module-load time.
Mirrors the existing input on freebuff-release.yml so prod releases can be built from a specific commit while still bumping version on latest main. Used to roll back to a known-good commit when main is broken. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The previous fix (6f223bf) embedded the wasm path on process.env and let init-node.ts resolve it via fs.existsSync. That worked on Linux/mac but kept crashing on Windows: bun --compile reports the embedded asset as `B:\~BUN\root\tree-sitter.wasm`, and fs.existsSync returns false for that path even though fs.readFileSync succeeds. resolveTreeSitterWasm fell through every branch and threw "Internal error: tree-sitter.wasm not found". Read the bytes once in pre-init via fs.readFileSync (which works on Windows bunfs) and stash them on globalThis. init-node.ts now passes them straight to Parser.init({ wasmBinary }), bypassing locateFile and filesystem path resolution entirely. globalThis is the cross-bundle channel: the SDK pre-built bundle inlines its own copy of init-node.ts, so a module-level variable in this package isn't visible to the singleton initialized via the SDK. The path-based fallback is preserved for external SDK consumers that don't pre-load. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Both --version smoke tests passed on Windows even though the binary
crashed for users: commander exits the process synchronously, before the
Parser.init promise has a chance to reject. Three changes to close the
gap:
- cli/scripts/smoke-binary.ts: portable script that spawns the binary,
lets it run for 5s, kills it, and asserts the captured stdout/stderr
doesn't contain earlyFatalHandler markers ("Fatal error during
startup", "Internal error: tree-sitter.wasm not found", unhandled
rejections, missing modules). Wired into the release-build smoke step
for every platform and into the freebuff-e2e build smoke step.
- freebuff/e2e/tests/startup.e2e.test.ts: wait for "Pick a model to
start" to render instead of just non-empty output. The model selector
only appears once the binary survived module init (Parser.init
included), the auth/session API call returned, and the React tree
mounted, so a half-rendered crash splash no longer satisfies the
assertion.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The previous smoke test and e2e test both checked for known error markers
("Fatal error during startup", etc.). That misses anything we didn't
think to add — novel error messages, silent crashes, hangs, segfaults
that produce no stderr.
Switch both to a positive signal: assert the binary actually rendered a
known boot screen. If something goes wrong we don't anticipate, the
boot text never appears and the test fails with a clear "binary never
reached a known boot screen" diagnostic. Negative pattern matches stay
for clearer error messages on regressions of bugs we've already seen.
- cli/scripts/smoke-binary.ts: gate pass/fail on at least one of N boot
signals appearing in stdout/stderr (chat surface header, login modal,
freebuff queue states, freebuff country-block screen, chat input
prompt). Verified locally: passes on real binaries, fails on a stub
that hangs without rendering.
- freebuff/e2e/tests/startup.e2e.test.ts: wait for the FREEBUFF ASCII
logo's F+R crossbar pattern (`█████╗ ██████╔╝`). The logo renders
for every valid boot state — including the country-block screen that
GitHub Actions runners hit because their egress is flagged as
anonymized network — so this assertion survives the geo gate that
was tripping the previous "Pick a model to start" wait.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Freebuff 0.0.62 still crashed on Windows with the same "Internal error:
tree-sitter.wasm not found" — surfaced this time through the late
renderer-cleanup handler ("Unhandled rejection: error: ...") instead of
the early one, so it appeared *after* the logo had rendered. CI Windows
smoke passed because the rejection fires past the 5s kill timer (after
React mounts and the renderer is up), and even when it does fire, the
boot screen has already matched our positive signal.
Root cause: the previous fix's `fs.readFileSync(treeSitterWasmPath)` of
the bunfs path silently fails on Windows for some user environments,
its catch block falls through, globalThis stays unset, and init-node
then hits the broken path-based fallback. CI Windows happened to pass
fs.readFileSync — user Windows didn't.
Bypass the filesystem entirely: bake the wasm bytes into the JS source
as a base64 string literal that bun --compile bundles into the binary's
text segment. No runtime fs read, no path normalization, no platform
quirks.
- cli/src/pre-init/tree-sitter-wasm-bytes.ts: committed stub with empty
base64. Dev mode and unit tests see this and fall through to
code-map's path-based resolution (which works locally because
node_modules/web-tree-sitter/tree-sitter.wasm exists).
- cli/scripts/build-binary.ts: overwrites the stub with the real bytes
before `bun build --compile`, restores it after. `process.on('exit',
restore)` is a backstop so a crash mid-build doesn't leave a multi-MB
diff in the working tree.
- cli/src/pre-init/tree-sitter-wasm.ts: drop the `with { type: 'file' }`
+ readFileSync path, decode the embedded base64 directly.
- cli/scripts/smoke-binary.ts: bump the run window from 5s to 10s and
match the late-handler form ("Unhandled rejection:" / "Uncaught
exception:") in addition to the early one. The 0.0.62 regression
fired *after* the boot screen rendered, so a positive boot signal
alone isn't enough — we need to keep watching for fatal markers
through the full window.
Verified locally: full bun --compile build embeds 205KB of wasm as
274KB of base64, stub is restored after build (and after a simulated
mid-build crash via the exit handler), binary boots cleanly to the
chat surface with no wasm errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-authored-by: James Grugett <[email protected]>
Co-authored-by: James Grugett <[email protected]>
Co-authored-by: James Grugett <[email protected]>
Co-authored-by: James Grugett <[email protected]>
Adds Fireworks as a transparent fallback for deepseek-v4-flash, gated by
a passive circuit breaker so we only divert when the official DeepSeek
API actually misbehaves.
- New deepseek-health.ts circuit breaker: 3 failures in 60s opens the
circuit for 5 min; the next request after expiry probes DeepSeek again
and resets on success. No background polling — every user request is
itself the probe.
- Tighter 60s headersTimeout for the Flash undici agent so dead-API
requests fail fast (the existing 30-min default is kept for reasoning
models on v4-pro).
- handleDeepSeek{Stream,NonStream} now wrap the fetch call so network
errors, timeouts, and 5xx/408/429 responses feed the breaker; 2xx
resets it.
- _post.ts routes to Fireworks when the circuit is open and adds inline
pre-stream failover so the first user to hit an outage also gets a
Fireworks response instead of an error.
- Adds accounts/fireworks/models/deepseek-v4-flash to the Fireworks
model + pricing maps.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
commented
Aug 20, 2026
|
This is a well-structured change — a passive circuit breaker with sensible failure/window/cooldown parameters, tests for the classifier and breaker state transitions, and a reasonable fallback wiring in Since this is a member submission, the substance looks portable: the circuit-breaker design (in-process state, no polling, request-as-probe) and the tightened No action needed here other than closing as out-of-scope for this repo; happy to review a version of this diff scoped to non- |
1cfcf33 to
e47f537
Compare
commented
Aug 31, 2026
PR checks failedA couple of things need fixing before this can be reviewed. This PR touches paths the public mirror does not accept.
Backend, database, billing and deployment code is not part of Edit the PR and this check re-runs automatically. |
Summary
deepseek-v4-flash(accounts/fireworks/models/deepseek-v4-flash).deepseek-health.ts): 3 failures / 60s window opens the circuit for 5 min, then the next request probes DeepSeek and resets on success. No background polling — every user request is the probe, so all pods converge naturally.headersTimeoutfor the Flash undici agent so dead-API requests fail fast instead of hanging on the existing 30-min default (kept for reasoning models on v4-pro)._post.tsroutes to Fireworks when the circuit is open, plus inline pre-stream failover so the first user to hit an outage also gets a Fireworks response instead of an error.FIREWORKS_PRICING_MAP: 0.14 / 0.03 / 0.28 per M tokens (input / cached / output).How it works
createDeepSeekRequestTrackedwraps the DeepSeek fetch. Network errors, timeouts, 5xx/408/429 →recordDeepSeekFailure(). 2xx →recordDeepSeekSuccess()(clears state).recentFailures.length >= 3within the 60s window,openUntil = now + 5min._post.tscallsshouldBypassDeepSeek(model)and, if true, setsuseDeepSeek = falseso the existing Fireworks branch picks up the same model id (now inFIREWORKS_MODEL_MAP).Test plan
deepseek-health.test.ts, all passing).fireworks-deploymentandfireworks-healthtest suites still pass (75 llm-api tests total).bun run typecheckclean for changed files (pre-existing SDK errors unrelated).deepseek/deepseek-v4-flashcalls succeed via DeepSeek when healthy and via Fireworks once the breaker opens (induce by temporarily pointingDEEPSEEK_BASE_URLat a sink, or watch real outage logs).🤖 Generated with Claude Code