Skip to content

Releases: sblattj/llm-ferry

v1.37.0 — Claude subscription lanes

Choose a tag to compare

@sblattj sblattj released this 13 Sep 23:17

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.

Read the project overview

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 credential

New 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 on 127.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-model anthropic-beta string, plus the request-cloaking seam.
  • front/ferry_front.py install_claude_oauth_hook() — wraps litellm's AnthropicModelInfo.validate_environment; a claude-oauth-tagged deployment swaps its placeholder key for the live Bearer token and cloaks headers, while every other anthropic lane passes through untouched. Installed in build_app() like the ChatGPT hook.
  • lib/ferry-auth-claude.zsh — the ferry auth-claude CLI, registered as a build module and dispatch case.

Notes

  • Validated live: login → token → a cloaked /v1/messages call returned HTTP 200 on claude-haiku-4-5.
  • Refresh-trap closed: a stock litellm subscription provider trusts expires_at and 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 a transform_request wrapper) is not yet wired; the MVP cloaks headers only. Pinned as a skipped test in lib/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

Choose a tag to compare

@sblattj sblattj released this 12 Sep 23:04

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.

Read the project overview

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 serving

The 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

Choose a tag to compare

@sblattj sblattj released this 07 Sep 02:00

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).

Read the project overview

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 at github:sblattj/OpenCode-goal-plugin pre-bundles all dependencies into dist/goal-plugin.js for clean, zero-external-dependency execution.
  • Automatic Migration: Running ferry opencode (or ferry update) automatically migrates existing opencode-goal-plugin, @prevalentware/opencode-goal-plugin, and willytop8/OpenCode-goal-plugin entries to github:sblattj/OpenCode-goal-plugin.
  • Git Plugin Syntax Support: OpenCode plugin name resolution now supports github:org/repo#ref and #ref tag/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 --host

This 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 --client

Clients 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

Choose a tag to compare

@sblattj sblattj released this 07 Sep 01:41

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.

Read the project overview

What changed

  • New Goal Plugin: OpenCode integration now configures opencode-goal-plugin (published as opencode-goal-plugin on npm).
  • Automatic Migration: Running ferry opencode (or ferry update) automatically migrates existing @prevalentware/opencode-goal-plugin entries in ~/.config/opencode/opencode.json and ferry profiles (opencode-cloud.json, opencode-local.json, opencode-super.json) to opencode-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 --host

This 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 --client

Clients 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

Choose a tag to compare

@sblattj sblattj released this 06 Sep 19:50

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.

Read the project overview

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_model remains ferry/super-flash.
  • General uses medium only when the host advertises it. Older or unreachable hosts retain flash for General.
  • super-flash uses openrouter/~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-luna deployment. The same Gemini-only configuration is recommended for an international fleet; the template still provisions only domestic lanes.
  • --small-model continues to override General and Explore. --housekeeper overrides Compaction, Title, and Summary. --super sends every non-driver agent to super-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 --host

The 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 configured

Remove 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 reload

Then update each client

ferry update --client

Clients 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

Choose a tag to compare

@sblattj sblattj released this 06 Sep 19:11

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.

Read the project overview

What changed

  • A fourth cloud lane. Fleet resolution, /v1/models, and ferry fleet ls now recognize medium. 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 through chatgpt/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 opencode adds medium when the host advertises it. Existing driver, worker, and housekeeping defaults remain in place. Explicit --model medium remains 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 --host

Existing 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 medium

Host profiles can select a fleet for one session:

FERRY_FLEET=domestic opencode-cloud -m ferry/medium
FERRY_FLEET=international opencode-cloud -m ferry/medium

The international command requires the international medium route to be configured.

llm-ferry v1.28.1 — Screenshots reach the cloud lanes

Choose a tag to compare

@sblattj sblattj released this 06 Sep 02:30

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.

Read the project overview

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 opencode declares modalities: {input: [text, image, pdf], output: [text]} on the three cloud lanes (heavy, flash, super-flash). A hand-written name on 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-orch and local-sub take 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 --host

Each 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 have

Verify: 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

Choose a tag to compare

@sblattj sblattj released this 05 Sep 22:37

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

Signal Studio desktop workspace

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.heavy primary with one GPT-5.6 Sol fallback. domestic.flash uses 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-only

Stop the currently running dashboard and rerun the documented command to load Signal Studio:

ferry dash --open

The 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 up

Existing 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

Choose a tag to compare

@sblattj sblattj released this 05 Sep 05:14

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

Choose a tag to compare

@sblattj sblattj released this 04 Sep 22:17

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.