Releases: sblattj/llm-ferry
Release list
v1.37.0 — Claude subscription lanes
llm-ferry v1.37.0 — Claude subscription lanes
ferry can now serve claude-* traffic from a Claude Pro/Max subscription instead of metered Anthropic billing — the Claude-side twin of the existing ChatGPT-subscription lane. One browser login, then the front door refreshes and cloaks the credential for you.
What's new
ferry auth-claude login # browser PKCE login -> ~/.config/ferry/claude/auth.json (0600)
ferry auth-claude status # account, expiry, VALID/EXPIRED (never prints tokens)
ferry auth-claude refresh # force a refresh; persists the rotated refresh token
ferry auth-claude logout # remove the stored credentialNew OAuth deployments in the route config (claude-opus / claude-sonnet / claude-haiku, model prefix claude-oauth/), each with a metered OpenRouter fallback hop so an exhausted or refresh-failed lane degrades to pay-per-token instead of erroring the client.
How it works
front/ferry_claude_oauth.py— the credential engine: PKCE authorize URL, one-shot localhost callback on127.0.0.1:54545, code exchange, refresh with rotation persisted single-writer, token stored at mode 0600.front/ferry_claude_provider.py— builds the Claude Code-identical header set and the per-modelanthropic-betastring, plus the request-cloaking seam.front/ferry_front.pyinstall_claude_oauth_hook()— wraps litellm'sAnthropicModelInfo.validate_environment; aclaude-oauth-tagged deployment swaps its placeholder key for the live Bearer token and cloaks headers, while every other anthropic lane passes through untouched. Installed inbuild_app()like the ChatGPT hook.lib/ferry-auth-claude.zsh— theferry auth-claudeCLI, registered as a build module and dispatch case.
Notes
- Validated live: login → token → a cloaked
/v1/messagescall returned HTTP 200 onclaude-haiku-4-5. - Refresh-trap closed: a stock litellm subscription provider trusts
expires_atand never refreshes on a server-side 401. Ferry's engine refreshes proactively and on any 401, and persists the rotated refresh token so a concurrent refresh can't orphan the credential. - Deferred: the body cloak (
metadata.user_id+ system billing block via atransform_requestwrapper) is not yet wired; the MVP cloaks headers only. Pinned as a skipped test inlib/ferry-claude-front.test.py. - Design doc:
docs/TODO-claude-subscription-oauth.md.
Tests
Five new/updated suites (oauth engine, provider, front hook, auth-claude zsh, claude regression). Full python suite 29/29, dashui 92 checks, build.zsh --check in sync.
v1.35.0 — a dedicated schematron door
llm-ferry v1.35.0 — a dedicated schematron door
ferry up --schematron serves ONLY the HTML→JSON extraction lane, on its own port (8094 by default), so a scraper workload can run alongside — or instead of — the main seven-lane stack without either door disturbing the other.
What's new
# host
ferry up --schematron [--port P] # extraction lane alone on its own door (default 8094)
ferry down --port 8094 # stop ONLY that door — :8090 keeps servingThe door is served from a filtered copy of the live route config, regenerated on every launch into the deterministic ~/.config/ferry/litellm-schematron.yaml: exactly the schematron deployment, its {"schematron": []} fallback entry, and the shared master_key / drop_params / num_retries / request_timeout / callbacks settings. Nothing else — no other lane, no fallback hops — is callable through it. The filter runs under a real YAML parser (the host python3 when it has PyYAML, else the litellm tool venv's own interpreter), never a text slicer, and is written atomically.
CDP_EXTRACT_BASE_URL=http://127.0.0.1:8094/v1 points cdp-toolkit's extract_page at the door.
Why a second door
The lane already existed on :8090 (v1.34.x added it to the stack). But a scraper and an agent pool have different restart rhythms: ferry reload/ferry up on the main door would drop extraction traffic mid-run, and a wedged extractor would tempt a full ferry down that kills every agent. :8094 gives each workload its own lifecycle. The port is claimed for ferry (the relay refuses to hand it to a client exposure) and the launch refuses — rather than killing — any foreign process found holding it.
ferry down --port P is new alongside: the only previous way to stop one door was stopping them all.
Tests
lib/ferry-serve.test.py grew TestSchematronDoor (port default + override, relay reservation, flag parsing with -p winning in either order, the refuse-don't-reap port check, deterministic config path, liveliness readiness wait, log reset) and TestSchematronFilter, which executes the real embedded extractor from the built ferry against the shipped template and loads the result with PyYAML: exactly one deployment (openrouter/inference-net/schematron-v2-turbo, temperature 0), no other lane or hop leaking in, master_key preserved, fallbacks trimmed to [{"schematron": []}], and a lane-less config erroring rather than serving an empty endpoint.
Verified against a live host: the door came up on :8094 beside the running :8090 stack, answered /v1/models with only schematron, completed a real json_schema-pinned extraction through OpenRouter, and ferry down --port 8094 retired it while :8090 kept answering throughout.
v1.29.3: Bundled Standalone OpenCode Goal Plugin
llm-ferry v1.29.3 — Bundled Standalone OpenCode Goal Plugin
Switches OpenCode's standard goal plugin to github:sblattj/OpenCode-goal-plugin (v0.9.1), providing a self-contained, pre-bundled distribution that fixes runtime dependency failures (Cannot find package 'zod') during OpenCode plugin loading. Automatically migrates previous plugin entries (opencode-goal-plugin, @prevalentware/opencode-goal-plugin, and willytop8/OpenCode-goal-plugin).
What changed
- Pre-Bundled Standalone Goal Plugin: OpenCode loads plugins directly via Bun/git without running sub-dependency
npm install. The unbundled upstream package failed at runtime with missing imports (zod). The fork atgithub:sblattj/OpenCode-goal-pluginpre-bundles all dependencies intodist/goal-plugin.jsfor clean, zero-external-dependency execution. - Automatic Migration: Running
ferry opencode(orferry update) automatically migrates existingopencode-goal-plugin,@prevalentware/opencode-goal-plugin, andwillytop8/OpenCode-goal-pluginentries togithub:sblattj/OpenCode-goal-plugin. - Git Plugin Syntax Support: OpenCode plugin name resolution now supports
github:org/repo#refand#reftag/branch specifiers without duplicating entries on successive runs. - Local Fork & Case-Insensitive Path Matching: Preserves local developer forks pointing to custom goal plugin directories or bundles on case-insensitive filesystems (macOS APFS).
Update instructions
1. Update the host first
From your host llm-ferry repository:
git pull --ff-only
ferry update --hostThis rebuilds the global ferry binary, relinks it, and updates local OpenCode profiles (~/.config/ferry/opencode-*.json and default opencode.json).
2. Update each client
On each client laptop:
ferry update --clientClients fetch the updated ferry binary from the host and regenerate their local OpenCode profiles, migrating the goal plugin automatically.
v1.29.2 — OpenCode Goal Plugin Upgrade
llm-ferry v1.29.2 — OpenCode Goal Plugin Upgrade
Upgrades OpenCode's standard goal plugin to opencode-goal-plugin (v0.9.0+) for durable, guarded goal workflows, and automatically migrates existing configurations away from @prevalentware/opencode-goal-plugin.
What changed
- New Goal Plugin: OpenCode integration now configures
opencode-goal-plugin(published asopencode-goal-pluginon npm). - Automatic Migration: Running
ferry opencode(orferry update) automatically migrates existing@prevalentware/opencode-goal-pluginentries in~/.config/opencode/opencode.jsonand ferry profiles (opencode-cloud.json,opencode-local.json,opencode-super.json) toopencode-goal-plugin. - Version Pinning Support: Fixes version-pinned package name parsing for unscoped packages (
pkg@version), ensuring pinned entries count as present rather than triggering duplicate installs. - Local Fork Preservation: Local filesystem paths pointing to custom builds (
.../opencode-goal-plugin/...) remain recognized and are preserved without injecting upstream packages.
Update instructions
1. Update the host first
From your host llm-ferry repository:
git pull --ff-only
ferry update --hostThis rebuilds the global ferry binary, relinks it, and updates local OpenCode profiles (~/.config/ferry/opencode-*.json and default opencode.json).
2. Update each client
On each client laptop:
ferry update --clientClients fetch the updated ferry binary from the host and regenerate their local OpenCode profiles, migrating the goal plugin automatically.
v1.29.1 — Gemini compaction and agent defaults
llm-ferry v1.29.1 — Gemini compaction and agent defaults
OpenCode now assigns substantive delegated work to medium, while compaction, titles, and summaries use super-flash. The route template keeps super-flash on OpenRouter's ~google/gemini-flash-latest with no model fallback.
What changed
| OpenCode agent | Cloud lane |
|---|---|
| Build, Plan | heavy |
| General | medium |
| Explore | flash |
| Compaction, Title, Summary | super-flash |
- The global model remains
ferry/heavy;small_modelremainsferry/super-flash. - General uses
mediumonly when the host advertises it. Older or unreachable hosts retainflashfor General. super-flashusesopenrouter/~google/gemini-flash-latest, minimal reasoning, and throughput-based OpenRouter provider routing. Its empty fallback list prevents a Gemini failure from switching to Luna, GLM, or GPT. Existing same-model retries still apply; an exhausted Gemini request returns an error.- The template removes its obsolete
domestic.super-flash-lunadeployment. The same Gemini-only configuration is recommended for an international fleet; the template still provisions only domestic lanes. --small-modelcontinues to override General and Explore.--housekeeperoverrides Compaction, Title, and Summary.--supersends every non-driver agent tosuper-flash; GPU profiles retain their existing local models.- CLI lane descriptions, examples, and regression tests match the current defaults.
Update the host first
From the existing host checkout:
git pull --ff-only
ferry update --hostThe host update rebuilds and relinks the CLI, reloads the proxy and file-sharing service, and refreshes OpenCode profiles. GPU models stay running unless --full is supplied.
Existing ~/.config/ferry/litellm.yaml configurations are preserved. To adopt the Gemini-only policy, edit the existing domestic.super-flash and, if configured, international.super-flash deployments to use openrouter/~google/gemini-flash-latest. Copy the OpenRouter parameters from the template, preserving each deployment's distinct ID and public: true flag. Set their entries in the existing router_settings.fallbacks list to:
- domestic.super-flash: []
- international.super-flash: [] # only if this fleet is configuredRemove obsolete fallback deployments only after confirming no other chain references them. Keep unrelated deployments and fallback entries. Do not configure global or wildcard model fallbacks for these lanes. After editing the routes:
ferry reloadThen update each client
ferry update --clientClients fetch the CLI and regenerate their configured OpenCode profiles from the host, so the host must be updated and reachable first. A separate ferry opencode run is unnecessary after a successful client update. Start a new opencode-cloud session to use the refreshed agent defaults.
v1.29.0 — Medium lanes
llm-ferry v1.29.0 — Medium lanes
medium adds a selectable tier for implementation, debugging, and code review between the heavy driver and the high-volume flash worker. Bare medium follows the caller's selected fleet; domestic.medium and international.medium select a configured fleet explicitly.
What changed
- A fourth cloud lane. Fleet resolution,
/v1/models, andferry fleet lsnow recognizemedium. The catalogue includes public medium lanes and keeps their fallback deployments hidden. - Domestic default: GPT-5.6 Terra at
xhigh. The route template uses the existing Codex/ChatGPT subscription throughchatgpt/responses/gpt-5.6-terra, with the same model and effort through OpenRouter as its fallback. Subscription deployments share account limits; OpenRouter fallback usage is billed separately. - International recipe: GLM-5.3 at
high. Hosts with an international fleet can add GLM-5.3 on the Z.ai coding subscription, with thinking enabled, plus a same-model OpenRouter fallback. The template includes a commented deployment and fallback recipe; it does not provision a complete international fleet automatically. - OpenCode picker support.
ferry opencodeaddsmediumwhen the host advertises it. Existing driver, worker, and housekeeping defaults remain in place. Explicit--model mediumremains available, with a clear warning if the host does not serve it. - Compatibility with existing fleets. Fleets without medium continue serving their existing lanes. The shared OpenCode medium entry is text-only because the recommended international GLM-5.3 backend is text-only, even though domestic Terra accepts images.
Upgrade
On the host, from an existing checkout:
git pull --ff-only
ferry update --hostExisting route configurations are preserved. Add the medium deployment and its fallback from litellm-route-example.yaml to the fleets you serve, then run ferry reload. A fresh domestic configuration includes medium by default.
On an OpenCode client, run ferry update to fetch the current CLI and regenerate its configuration. If the CLI is already current, ferry opencode refreshes the picker. Choose ferry/medium, or pin the driving role with:
ferry opencode --model mediumHost profiles can select a fleet for one session:
FERRY_FLEET=domestic opencode-cloud -m ferry/medium
FERRY_FLEET=international opencode-cloud -m ferry/mediumThe international command requires the international medium route to be configured.
llm-ferry v1.28.1 — Screenshots reach the cloud lanes
llm-ferry v1.28.1 — Screenshots reach the cloud lanes
A patch release. opencode clients wired by ferry opencode could not get an image or PDF to any cloud lane: the model replied that it "can't read the screenshot" even though the lane behind it reads images fine. The fix is one declaration in the generated provider config; nothing on the host changes.
What was wrong
opencode decides per model whether an attachment may be sent, from the provider entry's modalities.input. A custom provider such as ferry gets no fallback from models.dev, so a lane declared without modalities has capabilities.input.image == false. opencode still reads the pasted screenshot with its Read tool, then replaces the image with the text ERROR: Cannot read "x.png" (this model does not support image input). Inform the user. before the request leaves the laptop. The model reports what it was told.
The host was never at fault. The ferry front and LiteLLM's Chat→Responses bridge pass user image_url parts, tool-result images, Anthropic image blocks, and file PDFs through to GPT-6 Astra and the Gemini worker lanes unchanged. Every shape was probed against the live front with a no-attachment control, and the client-side swap was confirmed by capturing the request bytes on the wire.
What changed
ferry opencodedeclaresmodalities: {input: [text, image, pdf], output: [text]}on the three cloud lanes (heavy,flash,super-flash). A hand-writtennameon a lane still survives; the rest of each lane entry is rebuilt on every run, so a config generated before this release is upgraded by a plain re-run.- The GPU pair stays text-only. The mlx servers behind
local-orchandlocal-subtake no image input; declaring one would ship bytes they reject. - Regression tests cover both the cloud declaration and the local-lane omission, plus the upgrade of a pre-1.28.1 config.
Upgrade
Host, from an existing checkout:
git pull --ff-only
ferry update --hostEach opencode client then needs its config regenerated. Either of these does it:
ferry update # re-pulls the CLI from the host and re-applies the takeover
ferry opencode # or just re-run the takeover with the CLI you already haveVerify: the lane entries in ~/.config/opencode/opencode.json under provider.ferry.models now carry modalities, and a pasted screenshot on ferry/heavy is described rather than refused.
v1.28.0 — Signal Studio
llm-ferry v1.28.0 — Signal Studio
Signal Studio turns ferry dash into a responsive workspace for building, reviewing, and observing fallback routes. Search the configured model library, drag a model into any eligible slot, reorder fallbacks within a lane, or copy one to another lane without removing it from the source. Click, keyboard, and touch controls provide the same editing path on desktop, iPad-sized, and phone layouts, with horizontal route positions preserved while the dashboard refreshes.
Read the project overview · Open the Signal Studio guide
The screenshots use synthetic demonstration data. The displayed models, traffic, latency, and token values are examples rather than benchmarks or production activity.
What’s new
- A route editor built for real workflows. Insert, remove, reorder, and copy fallback hops with drag and drop or accessible button controls. Primaries stay pinned during ordinary edits; promoting a fallback uses a separate preview and confirmation flow that swaps backend definitions while preserving route names and chain positions.
- Reviewable, recoverable changes. Undo and discard support draft iteration. Preview validates the complete lane map and shows the YAML diff; any later edit invalidates that review and disables Apply until the new draft is previewed. Apply validates again and saves a timestamped snapshot before writing.
- Responsive controls and reliable scrolling. The desktop canvas becomes a horizontal model strip and touch-sized controls at tablet widths, then a compact phone layout with a 2×2 action area. Each route remains horizontally scrollable, and its position survives refreshes and view changes.
- Accurate request timing and usage. The live feed now records requested streaming mode, time to first visible text, duration through the final response body, completion state, and provider-reported input, output, reasoning, and cached-input tokens. Interrupted responses are marked incomplete, reasoning remains a subset of output, explicit zero stays distinct from unknown, and older events without the new fields continue to display honestly. Median latency and byte throughput now use measured full-response duration.
- Claude Code compatibility on ChatGPT-backed lanes. Anthropic system blocks are translated to developer-role input only at the ChatGPT Responses provider boundary. Prompt order and content remain intact, tool messages and other providers are unaffected, and the adapter covers both primary and fallback deployments.
- Updated domestic routing template. The checked-in example now uses GPT-6 Astra as the
domestic.heavyprimary with one GPT-5.6 Sol fallback.domestic.flashuses GPT-5.6 Luna followed by Gemini Flash Latest and GPT-5.6 Terra. These are repository defaults; existing local configurations are not silently rewritten by this release.
Upgrade
From an existing checkout:
git pull --ff-onlyStop the currently running dashboard and rerun the documented command to load Signal Studio:
ferry dash --openThe new timing and usage fields are recorded by the inference front process. Restart an already running inference stack with the event tap enabled before expecting newly handled requests to contain them:
FERRY_EVENTS=on ferry upExisting event records are not backfilled; fields absent from older records remain unknown (—). Passive dashboard observation does not make inference calls. Test backends is the explicit action that calls providers and may spend tokens.
v1.27.0: the front door auto-complies tool schemas
The front door now auto-complies tool schemas. A tool schema shape a provider is known to reject silently is patched before the request goes upstream, on every inference request, whichever lane or fallback hop serves it. The events record still names each finding under schema_warnings, now with "fixed": true per entry.
The one rule today is array_without_items → items: {}. Gemini function declarations reject an array property with no items, and through OpenRouter the request hangs with no response headers until the deployment timeout, so every such call burned the primary and fell to the chatgpt hop. Verified live on 2026-09-04: the identical request with items: {} is served by Gemini directly with zero fallbacks.
New rules are one detect + one fix in lib/ferry_events.py (SCHEMA_RULES / SCHEMA_FIXES).
Behavior change: the request body is now buffered on every inference request, where it used to be skipped with no fleets configured and FERRY_EVENTS off. Chat bodies are not streamed uploads.
v1.26.0: fleets — several complete routing sets behind the same lane names
v1.26.0: fleets — several complete routing sets behind the same lane names
Fleets. One litellm.yaml now carries several complete routing sets (domestic.heavy, international.heavy, ...) and a client keeps sending the bare lane name (heavy, flash, super-flash). The front door on :8090 rewrites the bare name to the caller's fleet, in this precedence: an explicit <fleet>.<lane> prefix, then the X-Ferry-Fleet request header, then the caller's sticky choice in ~/.config/ferry/fleets.json, then the host default. Callers are identified by X-Ferry-Client (which ferry opencode and ferry claude now write into every generated config), else host on loopback, else the peer IP. An unknown fleet is a 400 with the list of known ones. Fallback chains never cross a fleet, and /v1/models shows the caller only the bare lanes of their own fleet.
CLI. ferry fleet ls (every fleet, its three drivers, key status, who is on what), ferry fleet show, ferry fleet use <fleet> (sticky, no reload), ferry fleet use <fleet> --default on the host, ferry fleet use --clear. FERRY_FLEET=<fleet> pins one invocation; the pin is a request header, so ferry fleet show does not display it. The control plane is GET/POST /v1/ferry/fleet.
Shipped host config. domestic = US only: GPT-5.6 Sol on the ChatGPT subscription for heavy (no chain), OpenRouter Gemini Flash for flash and super-flash with a ChatGPT-bridge hop and an OpenRouter GPT hop behind each. international = cheapest first and never the subscription: Kimi K3 for heavy with GLM 5.3 and GPT hops, GLM 5.3 Flash for flash (thinking on) and super-flash (thinking off) with Gemini and GPT hops. local-orch and local-sub are shared. The bundled litellm-route-example.yaml shows the fleet-prefixed shape.
Docs and CI. README gains a Fleets section; the demo config, both route-editing skills and the client config example use the current lane names. The prxref review workflow now gives each chunk 180 s (PRXREF_LLM_TIMEOUT, overridable as a repo variable) after two of seven chunks timed out at the 45 s default on PR #2.
Update. Host: ferry update --host. Clients: ferry update --client, then ferry fleet ls and ferry fleet use <fleet> to pick. A client that has not re-run its reset sends no identity header and is treated as host on loopback, or by its IP over the LAN.
