Skip to content

Releases: ihsan-ramadhan/bansos-router

v0.5.0

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 20 Oct 05:43

What's Changed

A release about living with the daemon day to day: it can start at login, each agent can be set up with only the models you pick, DeepSeek tool calls stop failing, and a second bansos start no longer leaves two daemons fighting over the same state.

New Features & Enhancements

  • Start at login (bansos autostart on|off|status):
    • Linux uses a systemd user service, macOS a LaunchAgent, Windows a script in the Startup folder. On Linux and macOS a crashed daemon is restarted, while bansos stop still stops it for good.
    • A daemon that is already running is left alone and the service takes over at the next login. off never stops the running daemon.
    • The same switch is in the dashboard's Settings tab. bansos doctor and the Health check show it, and flag it when the recorded command points at a path that no longer exists.
  • Pick models per agent:
    • bansos setup <agent> --models a,b and --upstream zen,kilo write only the models you picked.
    • Agent Setup in the dashboard has a checklist per model and per upstream. A fully checked upstream keeps covering models that a later catalog refresh adds.
    • Each agent gets the picker that matches how it takes models: a list, tiers (Claude Code), a single model, or discovery (9router, OMP).

Bug Fixes

  • DeepSeek tool calls failed with 400 The reasoning_content in the thinking mode must be passed back to the API: the router stripped reasoning_content from earlier turns. It is now kept on chat requests, and thinking from Claude Code (Anthropic) and Codex (Responses) is sent back as reasoning_content, or an empty string when there was none. Every model in the catalog was checked to accept the field.
  • bansos doctor and the dashboard could hang the daemon (#22): 9router's data.sqlite was read as text. It is now queried as SQLite, and the JSONC reader is linear, so a 1 MB config takes about 40 ms instead of 11 s.
  • Two daemons after a manual start: bansos start --bg, which the pi/OMP extension also runs, started a second daemon on the next port when one was already up, splitting state.json and the activity log between them. Every start path now checks /healthz first and prints bansosd already running at .... An explicit different --port still starts a second one.
  • llm7 never went through the relay (#23): the allowlist held https://llm7.io, but llm7 is called at https://api.llm7.io, so its requests went direct without any error.
  • Restart: no more 409 after the port auto-bumps, a restart right after another one is no longer dropped, and bansosd --bg reads the port from config instead of pinning it.
  • Strict mode leaves blocked upstreams out of /v1/models, setup and the startup banner. Pinning a blocked model is rejected with 400 by the API; the CLI warns.
  • Secret redaction no longer reads an already-redacted marker plus the next line as a live credential when a nested JSON string contains escaped newlines.
  • Windows: daemons are found with PowerShell Get-CimInstance, since wmic is gone in Windows 11 24H2.

Behavior Changes

  • bansos start, bansos start --bg and bansosd exit 0 with a message when a daemon already answers on the configured port.
  • With the relay enabled, llm7 traffic now goes through it. A self-hosted relay must allow https://api.llm7.io, otherwise llm7 requests fail with the relay's error instead of going direct.
  • In strict mode /v1/models lists only allowed upstreams; ?all=1 returns everything.

Quality

  • The suite runs against a temporary BANSOS_DIR and no longer writes to ~/.bansos (#24). Earlier test runs left fixture events in ~/.bansos/activity.json under the models mock/zen:free, strict-origin and zen-origin; they can be deleted.

Contributors

Full Changelog: v0.4.1...v0.5.0

v0.4.1

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 18 Oct 10:13

What's Changed

A security and dashboard release: strict mode no longer locks up a session when a credential shows up, normal mode stops blocking pasted keys, and the daemon can now be configured, checked and debugged from the web dashboard.

New Features & Enhancements

  • Strict mode redacts secrets instead of blocking (src/security/secret-guard.ts):
    • API keys, PATs, private keys and credential assignments in a request bound for an external upstream are replaced with [REDACTED:<type>] before it is sent. The rest of the message still goes through.
    • Blocking kept the secret in the session history, so every later request in that session failed with 422, compaction included. Redaction keeps the session usable while the secret still never leaves.
    • Tool-call arguments (JSON inside a string) are redacted structurally, so the nested JSON stays valid, and a truncated PEM key is cut at the key body without eating the text after it. If anything is still detected after redaction, the request is blocked (fail closed).
    • Set "dlp": "block" to keep the previous 422 behavior.
  • Dashboard Settings, Health and Logs:
    • Settings: security mode, secret handling (redact/block), allowed providers, cross-provider failover, catalog refresh, port and bind. Security changes apply immediately, other changes are flagged for restart, and there is a restart button.
    • Health check: the same checks as bansos doctor (daemon version, config validity, security in force, each coding agent's setup state).
    • Logs: the daemon log, live, with a filter, pause and copy.
    • Agent Setup: apply or undo a harness config directly from the UI.
  • Dashboard shows the security actually in force:
    • The Models card shows real per-provider counts (Zen 12 · Kilo 15 · …). In strict mode it counts only allowed providers and flags the rest as blocked.
    • Models outside the strict allowlist get a blocked badge in the model table, are sorted last, and are skipped by Ping / Ping All.
    • The Uptime card shows the failover / strict state, and /bansos/status now reports providers and security.dlp.
  • Config diagnostics:
    • security.mode is case-insensitive ("Strict" works).
    • An unrecognized mode/dlp value, a security that is not an object, or an invalid config.json is flagged in the daemon log, bansos doctor and the Health check. These cases used to fall back to normal mode silently.

Bug Fixes

  • Normal mode blocked pasted credentials: since a96dfe8 the secret guard ran in every mode, so any request whose history held a key-like string got HTTP 422 in normal (default) mode. As documented, it now runs in strict mode only.
  • The normal-mode test never exercised an external upstream: it ran against a loopback mock that is exempt from the guard. It now asserts that the body reaches an external upstream unchanged.

Behavior Changes

  • Normal mode (the default) no longer scans or blocks secrets. Use "mode": "strict" if you want them kept from leaving.
  • Strict mode redacts by default. Set "dlp": "block" for the previous behavior.
  • A broken config file is reported but never overwritten from the dashboard, and strict mode cannot be enabled while the daemon is bound to a non-loopback address.

Contributors

Full Changelog: v0.4.0...v0.4.1

v0.4.0

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 17 Oct 10:56

What's Changed

A reliability release: every model and every harness (pi, OMP, opencode, Claude Code, Codex CLI) was re-tested end to end with tool calls, and the bugs that turned up are fixed here.

New Features & Enhancements

  • Zen tool bridge (src/protocols/tool-bridge.ts):
    • The Zen gate injects read / shell tools. When a model called one of those instead of the harness's own tool, bansos used to hide the call and the turn ended empty. That call is now re-emitted as the caller's equivalent tool (Bash, bash, exec_command, Read, read_file, …), with arguments mapped from the target's JSON schema. Harnesses without a reader (Codex) get read as a quoted sed -n over their shell tool.
    • The mapping fails safe: if a required field cannot be filled, the call stays hidden as before. The model is shown its original call when the history comes back, so it does not loop.
    • Zen models in Codex, OMP, and Claude Code now finish file + shell tasks that previously ended empty.
  • Capability-aware routing:
    • Models that cannot take tools (from Kilo supported_parameters / LLM7 capabilities.tools) or images are skipped for requests that need them. /v1/models now reports supports_tools.
    • Upstream errors such as does not support tools or No endpoints found that support tool use trigger failover when it is enabled. When failover is off, the upstream error is returned as-is: tools are never silently dropped.
  • pi / OMP extension:
    • Model names carry their upstream (Big Pickle (Zen), Qwen3.5 9B (OVH)).
    • The live catalog is cached in ~/.bansos/pi-models.json and used when the daemon is unreachable. The bundled fallback list has been refreshed (it still listed retired models).
    • Vision models now accept image input. reasoning comes from the daemon instead of being guessed from the model id.

Bug Fixes

  • **/v1/responses streams never returned tool calls (Codex CLI coeam encoder ignored delta.tool_calls. It now emits function_call items with function_call_arguments.* events, one item per output index.
  • Injected Zen tools leaked: the Zen transform pushed read / shell into the caller's own tools array. As a result they counted as caller tools (Codex logged unsupported call: read) and were sent on to whichever upstto.
  • Relay could be repointed by any website (CSRF): POST /bansos/relay accepted cross-site text/plain requests. POSTs with a cross-site Origin are now rejected with 403.
  • Errors inside HTTP 200 streams were swallowed: Zen's Streamid Kilo/OpenRouter error frames (sent after keepalives) became emptyreplies. They now fail over, or surface as a 502, an Anthropic errorevent, orresponse.failed`.
  • bansos setup codex / jcode wrote invalid TOML: root keys left model_provider pointing at a deleted table. Root keys arenow tagged # bansos-router, and a value you already had is restored on undo.
  • opencode v2 rejected streams ("content after the finish reason"): the reasoning fold was sent after the finish frame, even after tool calls.
  • Non-ASCII text corrupted in streams: multibyte characters split across chunks became �. CRLF-delimited events no longer wait for the stream to end.
  • Anthropic /v1/messages:
    • input_tokens was always 0, so Claude Code's auto-compact never triggered.
    • Content blocks overlapped (text after a tool call went into a c
    • Separate text blocks were glued together, and is_error on too
    • 429 was reported as invalid_request_error instead of rate_limit_error.
  • /v1/responses input:
    • Images were inlined as base64 text.
    • Replayed reasoning items returned 400.
    • Parallel function calls were split into separate assistant messages.
  • Empty upstream streams produced event orders the Anthropic and OpenAI SDKs reject.
  • Reasoning shown as the final answer after a model called a to
  • Setup reruns:
    • Stale models piled up in opencode / openclaw / continue, and Goose duplicated its list on every run.
    • --undo continue removed nothing.
    • The JSONC parser rewrote string contents in user configs.
  • Rate-limit hints that only appear in the body (LLM7 `Retry afooldown.
  • OMP rejected requests with 400 unknown field "__bansosToolAliases".

Behavior Changes

  • Cross-site browser POSTs to the daemon return 403. Native clients send no Origin header, and the dashboard is same-origin, so neither is affected.
  • bansos setup codex / jcode tag root keys with # bansos-router and keep your previous value as a # bansos-router-prev: comment.
  • Re-running bansos setup replaces the bansos model list instead

Contributors

Full Changelog: v0.3.3...v0.4.0

v0.3.3

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 17 Oct 05:40

What's Changed

New Features & Enhancements

  • OVHcloud AI Endpoints upstream (src/upstreams/ovh.ts):
    • New keyless upstream adding ovh/Qwen3.5-397B-A17B, Qwen3.5-9B, Qwen3.6-27B, Qwen3.8-27B, Meta-Llama-3_3-70B-Instruct, Mistral-Small-3.2-24B-Instruct-2506, and gpt-oss-20b / gpt-oss-120b.
  • OMP (Oh My Pi) harness support (bansos setup omp, #21):
    • Writes a bansos provider into ~/.omp/agent/models.yml via a YAML-block merge that nests it under providers: at the sibling indent, creating the key when absent. Reversible with bansos setup omp --undo.
    • Also loadable as the pi extension (drop extensions/pi/dist/index.js into ~/.omp/agent/extensions/): registers a bansosr provider and auto-manages the daemon. See README for both paths.

Bug Fixes

  • vLLM thinking controls rejected by strict upstreams (src/protocols/openai-chat.ts):
    • OMP sends preserve_thinking / chat_template_kwargs / enable_thinking, which strict upstreams (OVHcloud) reject with 400 "extra arguments". The daemon now strips these from the outbound body.
  • Paid pro-tier models listed as free (src/upstreams/llm7.ts):
    • LLM7 flags tier pro with usage_based_only: false, and those 402 at the gateway. They are now excluded from the catalog regardless of the usage flag.
  • Empty HTTP 200 when an upstream overloads (src/daemon/server.ts):
    • Some upstreams (Kilo → Nvidia overloaded) answer 200 with an error envelope and no choices, which surfaced as an empty reply. The daemon now treats that as a transient failure and fails over.

Contributors

Full Changelog: v0.3.2...v0.3.3

v0.3.2

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 15 Oct 06:10

What's Changed

Enhancements

  • OpenCode Zen v2 client realignment (src/upstreams/zen.ts):
    • Rebuilt the spoofed client identity to match OpenCode's current desktop client: updated User-Agent (opencode/1.18.31), refreshed x-opencode-* headers, and session/request-id generators that follow OpenCode's ses_/request-id format.
    • Split the endpoints the upstream talks to: inference now goes to /inference/v1 while the catalog is still read from /zen/v1/models.
    • The spoofed User-Agent can be overridden with the BANSOS_OPENCODE_UA env var, so the client version can be bumped without a code change when OpenCode moves again.
    • Reworked the chat and responses request-body transforms and refreshed the pinned free-model seed.

Contributors

Full Changelog: v0.3.1...v0.3.2

v0.3.1

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 22 Sep 14:41

What's Changed

New Features & Enhancements

  • Dynamic Zen Catalog from models.dev (src/upstreams/zen.ts):
    • Build the Zen catalog from the intersection of the models.dev opencode index and Zen's live /v1/models listing, instead of filtering a hardcoded seed.
    • New free Zen models now enter the catalog automatically; models that turn paid or get marked deprecated drop out without a code change.
    • Model facts (name, reasoning, context window, output limit, modalities) are read from models.dev. A field the index omits falls back to the matching ZEN_MODELS entry before the generic default.
    • The seed is no longer the master list. It now only supplies wireApi, fallback metadata, and the ids probed keyless when Zen's listing omits them.
    • A models.dev outage degrades the pass instead of wiping the catalog: a rejected fetch (DNS failure, refused connection, timeout) is handled them.
    • A models.dev outage degrades the pass instead of wiping the catalog: a rejected fetch (DNS failure, refused connection, timeout) is handled the same as a non-ok status.
  • PDF Input on the Responses Wire (src/protocols/responses-upstream.ts):
    • Chat file parts are translated to input_file on the way out, keeping file_id / file_data / filename.
    • pdf is advertised only for responses-wire models and filtered out for chat-wire ones, because Zen answers HTTP 500 for a file part on /chat/completions./chat/completions.
  • Injected Tool Filtering (src/daemon/server.ts):
    • The spoofed read / shell tool definitions required by Zen's free tier are stripped from both streaming and folded responses when the caller never declared them.
    • finish_reason is rewritten from tool_calls to stop when every call in the reply was injected, so downstream harnesses do not wait on a tool result they never asked for.
    • The Zen responses wire rejects tool_choice: "none", so it is filtered outbound.

Bug Fixes

  • Failover could pick a model smaller than the origin (src/daemon/server.ts):
    • Candidates were ranked by absolute context distance, so a 190k model beat a 256k one for a 200k origin. A near-full-context request that failed over then hit a context-length 400. Candidates that cover the origin's context are now preferred as a group; the 90% tolerance band is only reached for when nothing larger exists.
  • A seeded model that turned paid stayed in the catalog (src/upstreams/zen.ts):
    • The free/paid filter was bypassed for seeded ids, so a model flipping from free to paid kept being offered and failed with 401/402 at the gateway. Paid ids are now probed like deprecated ones.
  • A models.dev outage wiped the Zen catalog (src/upstreams/zen.ts):
    • The index fetch shared a Promise.all with Zen's listing, so a rejection (not just a non-ok status) aborted the whole pass and left Zen contributing zero models.
    • Unread response bodies on degraded refresh paths are now cancelled instead of held open until GC.
  • Published limits were lost when models.dev omitted them (src/upstreams/zen.ts):
    • Muse Spark fell back to a generic 128k / 16k default, which dropped it out of Nemotron's failover band and shifted default-model ranking.

Tests & Quality

  • Added unit tests for Zen catalog assembly (test/zen-catalog.test.ts), injected tool filtering (test/tool-calls.test.ts), PDF passthrough (test/responses-upstream.test.ts), and failover ranking (test/failover.test.ts).
  • Test suite is now 160 passing (was 127).
  • Dependencies: @types/node to 26.5.1, lucide-preact to 1.45.0, and vite to 8.3.0 (#12), plus @types/node in extensions/pi (#11). All dev-only; the published package still ships zero runtime dependencies.

Contributors

Full Changelog: v0.3.0...v0.3.1

v0.3.0

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 18 Sep 06:12

What's Changed

Enhancements

  • OpenCode Zen v2 Upstream Spoofing & Gateway Bypass:
    • Update spoofed client identity to OpenCode v2.0.5 (opencode/latest/2.0.5/cli with authorization: Bearer public).
    • Implement official OpenCode descending TypeID generator (ses_ + 48-bit inverted timestamp hex + Base62 random suffix).
    • Inject required session affinity and tracing headers (x-session-affinity, x-session-id, x-opencode-project, b3, traceparent).
    • Bypass free-tier gate payload restrictions: automatically inject core tool definitions (read and shell) with tool_choice: "none" for plain text completions.
    • Introduce transformRequestBody hook in Upstream interface for upstream-specific payload mutations.
    • Transparently aggregate SSE stream chunks into standard JSON responses when downstream clients request non-streaming completions (stream: false).

v0.2.6

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 12 Sep 08:32

What's Changed

Bug Fixes

  • Reported in #10: a 403 from an upstream could wedge the router. 401/403 was treated as a terminal error, so failover stopped dead and the request returned the rejection even when healthy models were available. Because Muse Spark is the default model, a deployment where OpenCode Zen refuses the egress (Cloudflare can route through a region its providers block, answering 403 This service is not available in your region) hit the same wall on every request. Now a refusal fails over to another model and parks the refused one for 30 minutes, so later requests skip it entirely. Retrying cannot help a policy refusal, but it is not permanent either: egress routing changes on its own. Send x-bansos-no-failover: 1 when you want the model's own status back.
  • The upstream's own error message is now logged. A rejection used to record only status=403, which told you nothing about why. The upstream's explanation is logged alongside it (redacted in strict mode, and scanned for credentials as every log field is).
  • The Web UI playground showed nothing on the Anthropic and Responses protocols. Its SSE reader only accepted frames beginning with data:, but those two protocols lead with an event: line, so every frame was discarded before parsing. Chat Completions was unaffected, which is why the daemon logs showed tokens flowing while the screen stayed empty. A second bug sat underneath: the Responses extractor looked for three event names the encoder never emits.
  • A reasoning-only model returned an empty reply on the Anthropic wire. The encoder emitted a thinking block and no text block at all, so Claude Code received a message with nothing to display. It now promotes the reasoning to the answer, matching what the Chat and Responses paths already did.

Behavior Changes

  • Reasoning no longer leaks into the answer. The Chat path used to rewrite a reasoning-only delta into content and drop reasoning_content entirely, and the Responses encoder sent reasoning as output_text.delta. A model that thinks first and answers second therefore had its internal monologue prepended to its reply on both wires. Reasoning now passes through in its own field (reasoning_content for Chat, response.reasoning_summary_text.delta for Responses), and is promoted to the answer only at the end of the stream, and only when the model produced no content of its own.

    The trade: for a model that never emits content, a client that ignores reasoning now sees the text arrive in one piece when the stream closes, instead of streaming from the first token. Clients that render reasoning (the playground, Claude Code, opencode) are unaffected. All three wire protocols now produce identical content for the same upstream stream, which is covered by a test.

Contributors

Full Changelog: v0.2.5...v0.2.6

v0.2.5

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 09 Sep 02:21

What's Changed

New Features & Enhancements

  • Muse Spark, through a second upstream wire: Muse answers only on /v1/responses and returns HTTP 500 on chat completions, so a model can now carry wireApi: "responses". The daemon translates chat to Responses on the way out and back again on the way in, streaming and tool calls included, so chat, Anthropic, and Responses clients all keep their usual format. Adds muse-spark-1.3-contributor-free and muse-spark-1.2-contributor-free.
  • Muse Spark 1.3 is the new default model for bansos setup. Capacity ranking alone could not select it (its nominal context is smaller than Inkling's), so there is now an explicit shortlist that wins whenever the model is alive.
  • Rate-limited models are parked: a model that answers 429 goes on cooldown, so the next request starts on a fallback instead of spending another round trip to learn the same thing. Retry-After sets the duration when the upstream sends one, otherwise a minute, capped at fifteen. Parked models stay listed in /v1/models and are still used when nothing else qualifies.
  • Catalog refresh runs in parallel: the three upstreams are queried together instead of one after another. Measured 3.5s to 0.85s on a live daemon.
  • bansos stop is scoped by default — a behavior change. It now stops the daemon recorded in state.json instead of every bansos daemon on the machine, so a daemon started elsewhere is left alone. Use bansos stop --all for the old behavior.

Bug Fixes

  • Four dead models removed, each confirmed with a live request rather than by absence from a listing: laguna-s-2.1-free (401 not supported), gpt-oss (400 unavailable), and minimax/minimax-m3:free / minimax/minimax-m2.7:free (404 on the Kilo gateway). inclusionai/ling-3.0-flash-sante:free is seeded in their place.
  • muse-spark-1.2-contributor-free is back. v0.2.4 listed it as retired, but it was healthy the whole time: the health check had been probing it on the wrong wire.
  • Responses-wire streaming could take the daemon down: an upstream reset arrived as an unhandled stream error, which Node turns into an uncaught exception. The upstream leg now goes through stream.pipeline, which also releases the upstream socket when a client disconnects.
  • Failover log named the wrong upstream: the "fallback used" warning read fromUpstream after it had already advanced to the fallback, so a Zen model was reported as coming from Kilo.

Contributors

Full Changelog: v0.2.4...v0.2.5

v0.2.4

Choose a tag to compare

@ihsan-ramadhan ihsan-ramadhan released this 05 Sep 08:23

What's Changed

New Features & Enhancements

  • Always-on network hardening: SSRF blocklist for internal IP ranges (LAN, loopback, link-local/metadata, CGNAT), DNS-rebinding guard on the Host header, loopback-only CORS instead of a wildcard, plus nosniff/DENY/CSP headers. Active in normal mode, not just strict.
  • Zero runtime dependencies: dropped smol-toml and yaml; the CLI now installs with no transitive runtime packages.
  • Log rotation: bansosd.log rotates at 5 MB keeping 2 backups, so a long-running daemon never grows one unbounded file.
  • Reasoning fallback on streams: upstreams that stream only reasoning now get it folded into content, so clients never receive an empty response.
  • Client disconnect aborts upstream work instead of letting the request finish in the background.
  • CLI follows the live port: setup, ping, doctor, relay, status, and models read the bound port from state.json, so they still work after an auto-bump.
  • Catalog synced with live upstreams: added MiniMax M2.7, Ling 3.0 Flash Fin, Dots 3 Note Preview, Inkling, and Inkling Small; retired tencent/hy3:free, DeepSeek-V4-Flash-0731, gemini-3.1-flash-lite, and muse-spark-1.2-contributor-free.
  • Activity events now include the HTTP status code, shown in the Web UI and bansos logs --activity.

Bug Fixes

  • Duplicate lines in bansos logs: the --bg child wrote the log file twice. Completes the partial fix from v0.2.3.
  • bansos start --bg printed the wrong port: state.json was written after the first catalog refresh, past the 5s window the parent waits. It is now published as soon as the socket is listening.
  • Refused bind was silent and exited 0: under strict mode a non-loopback bind failed with an empty log and a success exit code. The error now reaches bansosd.log and the exit code is non-zero.
  • pi extension was pinned to port 17070: it ignored state.json, so after an auto-bump it registered a provider pointing at a dead port and silently fell back to a stale model list. It now resolves the live host and port.
  • pi extension killed every daemon on quit: it ran bansos stop, which stops every daemon on the machine. It now stops only the one it started.

Contributors

Full Changelog: v0.2.3...v0.2.4