Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • janus bench — benchmarks every routable model through a running gateway: streams a fixed deterministic prompt set per model, measures status/TTFT/tokens-per-sec client-side, estimates cost from the local pricing registry, and writes a Markdown report (per-model aggregates + per-request detail). Requests go through the gateway, so the server's attempt signals and usage record themselves as a side effect; a plan-and-confirm gate runs before anything is sent. (#262)
  • Auto quality overrides + preview — operators can pin per-model quality scores (0–1) that win over measured signals; the Routing tab's Auto-routing panel shows the full scoring breakdown (tier, score, blended cost, error rate, TPS, TTFT, samples) and manages the overrides. New public GET /v1/quality/auto-preview?strategy= returns the same ranked trace the request path uses — no DRY drift. (#187)
  • model="auto" routing — requests asking for auto are ranked per request under a configurable strategy (balanced, cheapest, fastest, quality; default balanced, settable from the Routing tab) and the top-5 chain becomes the fallback try-order through the existing cooldown/demotion pipeline. Candidates are priced, routable models filtered by the key's allowlist and request capabilities (tools, vision); version siblings (-001/date suffixes) are deduped; cheapest uses blended 0.3·input + 0.7·output pricing; quality/fastest/balanced rank on the recorded attempt signals (#257) with strict two-axis tiering. Request Logs record the resolved model next to the requested one and the x-janus-resolved-model header reports it per response. The Routing tab shows the strategy control and the chain auto would pick right now. (#183)
  • Savings vs baseline model — the Analytics page now shows what its window's priced traffic would have cost on a baseline model (default gpt-4o, overridable per request and via the analytics_savings_baseline setting), how much was actually spent, and a per-model breakdown; Home gains a "Saved today" tile on the same configured reporting day as budget "today". Counterfactual costs reuse compute_cost over per-model token totals; subscription and unpriced traffic is excluded and reported separately, never counted as savings. New public GET /v1/analytics/savings?baseline=&days= endpoint. (#237)
  • "Models you can't reach" — a new Routing tab lists catalog, discovered, and configured models that Janus can't route, with the reason (no provider, provider disabled, no active credential, model not enabled), grouped by provider with a Connect button that opens Connect with that provider preselected. Models whose every account is cooling down are shown separately as "Reachable soon". (#238)
  • Per-attempt latency and reliability signals — every upstream attempt now records time-to-first-token, output tokens/sec, and its outcome (ok / error / client_error / aborted) per model and account in a new attempt_signals table (7-day retention). Fallback-masked failures now count against the failing account. Data collection only; ranking arrives with model="auto". (#187)
  • Claude OAuth in Key Inventory — Connect now accepts Claude Code's .credentials.json (and bare sk-ant-oat… tokens) as claude_oauth inventory credentials, validates them against /api/oauth/usage, and probes 5h / weekly / per-model weekly windows for the keys table and routing soft-ordering. Existing Providers-page Claude OAuth keys move to the new inventory provider on startup. (#251)
  • Account-value probes for OAuth inventory accounts — Codex (ChatGPT wham/usage: 5h/weekly/monthly windows by declared duration, plan type, reset credits), Kiro (GetUsageLimits: monthly allowance, free trial, overage flag; region taken only from an allowlisted profile ARN/region) and Antigravity (retrieveUserQuotaSummary: Gemini/Claude 5h and weekly buckets). Probes use the stored access token only and never consume a refresh token; an expired token or a 401/403 from the usage endpoint marks the probe unavailable without changing the key's status. Account emails and user ids in usage responses are discarded. (#189)

Removed

  • Cursor upstream provider — the cursor api_type was an unfinished scaffold (it posted OpenAI chat completions to api2.cursor.sh instead of Cursor's native protocol). The executor, driver, catalog entry and Providers page option are gone, and creating a cursor provider now returns a 400. Existing cursor provider rows are disabled on startup, not deleted, and their mirrored inventory keys are revoked. Using the Cursor IDE as a client against Janus's OpenAI-compatible endpoint is unaffected. (#251)

Changed

  • Unsupported api_types no longer break provider reload — a provider row or YAML seed entry whose api_type has no driver is skipped with a warning instead of aborting the whole routing rebuild.

Fixed

  • docker compose passes the encryption settings through — the compose file now forwards INVENTORY_ENCRYPTION_KEY, INVENTORY_ENCRYPTION_PREVIOUS_KEY, INVENTORY_PUSH_TOKEN and JANUS_ALLOW_INSECURE_DEV_KEY from .env. Before, setting the key in .env had no effect, so deployments with stored credentials hit the startup encryption check (#249) and crash-looped.
  • Refreshed OAuth tokens are saved — Claude, Codex, Kiro and Antigravity executors now write refreshed credentials back to their inventory row (compare-and-swap) and re-read the stored credential before refreshing, and validators refresh only expired tokens. Rotating refresh tokens no longer break after a restart or a validator run. (#251)
  • Models page no longer paginates a provider-grouped view — ?offset=… pages cut providers mid-list, so groups opened collapsed, per-provider counts and the rail described only the current slice, and "All off" could hide a whole provider based on 25 loaded rows. The Models state section now ships a per-provider overview (whole-catalog model_count/visible_model_count and toggleable counts), every model of a selected provider, or a capped, provider-grouped search (match_total, truncated), with unused capabilities/selected fields dropped. meta.pagination is gone from state/models; GET /dashboard/api/v2/models is unchanged. (#245)

[4.0.0] - 2026-09-29

Added

  • Dashboard-first navigation — the sidebar is now six hubs (Home, Connect, Inventory, Routing, Usage, Settings) with section tabs; every page keeps its own URL. Home gains a first-run checklist and a "Your endpoint" card.
  • Connect (/dashboard/ui/connect) — paste keys or drop login files, see a masked per-provider preview (new / already stored / rejected), then import with routing provisioned. Backed by the new POST /dashboard/api/inventory/preview, which shares classification with import, never writes, never returns secrets, and has its own rate limiter. Restore backup moves to /dashboard/ui/connect/restore.
  • More credential formats on Auto — Codex CLI ~/.codex/auth.json, Cline exports ("provider": "cline" JSON, 9router providerConnections entries, workos: tokens; refresh token kept), alongside Antigravity and Kiro JSON. Claude Code .credentials.json is recognized and rejected with a pointer to the Claude OAuth provider.

Changed

  • /dashboard/inventory/add and /dashboard/inventory/import redirect to Connect and Restore backup.
  • The Restore backup "Export current inventory" button downloads via POST, as required since #197 (the old GET link returned 405).

Fixed

  • Preview and import could disagree on the provider — distinctive key prefixes (gsk_, nvapi-, sk-ant-, sk-proj-, …) are now authoritative on both paths; generic sk- keys are identified by probing on import.
  • Auto-detect assigned unknown keys to providers that accept any key — Venice, Vultr, SambaNova, Novita, NanoGPT, Synthetic, ZenMux, Kilo, Parallel and local Ollama return 200 to any key on their health endpoint. Detection now skips a provider that also accepts a random canary key.

[3.26.1] - 2026-09-29

  • Security (#197): inventory credential exports (bulk export and per-key agent JSON) are POST-only with query-param auth rejected; unauthenticated dashboard API GETs return 401 instead of a login redirect reserved for HTML requests.
  • Inventory robustness (#223): provider base_url edits propagate to mirrored keys; scheduler interval clamped to >=1h with a startup check; account-value co-waiters fall back to stored state on leader failure and NaN/inf are rejected; push ingest errors sanitized; masked list/detail GETs send no-store; partial 422 submits invalidate the alert cache; persisted error notes are URL-stripped and truncated; LIKE search escapes wildcards; bulk delete chunks host parameters; leaderboard and client breakdown group by key id.

[3.26.0] - 2026-09-29

Fixed

  • In-flight guards on all mutating dashboard forms — double-clicks can no longer double-submit: modal submits, row actions, toggles, and catalog-sync buttons are disabled while their action is pending, and row-action pages track in-flight work per id. The Keys page can no longer orphan a created key behind a second submit. (#209)

Changed

  • Trimmed dashboard state payloads — the overview section now carries only rendered fields (live_inflight replaces the 50-event live ring; drops by_model, combos, global_budget, base_url); GET /dashboard/api/v2/models is paginated (default limit=200, max 2000, limit=0 rejected) instead of shipping the full ~1.1 MB catalog; the pricing state's unpriced list is capped at the top 25 models by tokens; routing state drops unread per-provider models, combos, quota_warnings, and the static rotation_note. (#216)

[3.25.1] - 2026-09-29

Fixed

  • Dashboard mutation routes validate settings, providers, quotas, and combos; missing records return 404 and validation errors use safe JSON responses. (#207)

Changed

  • Centralize package version metadata and streamline CI checks and release validation. (#220)

[3.25.0] - 2026-09-29

Fixed

  • Inventory Inspect modal can no longer reveal or re-prioritize the wrong credential — the detail fetch carries a generation token so a late response from a previously-inspected row is discarded; reveal and priority-save capture the id up front and drop their results if the inspected credential changed mid-request. (#208)
  • Bound unbounded growth — new server_usage_retention_days setting (default 365, clamp 7-3650) prunes the usage table via a throttled, fail-safe prune on the record-usage write path; upstream_key_history gains a changed_at index and 180-day retention; get_unpriced_models is a 60s TTL cache invalidated by pricing reloads and cost backfills instead of recomputing on every ~5s alert tick; cooldown reads are read-only, with expired-row pruning moved to throttled write paths; the legacy outcomes backfill treats status=0 rows as 200. (#215)

[3.24.2] - 2026-09-29

Fixed

  • Inventory import is atomic and idempotent — every row is validated before anything is written and all inserts share one transaction, so an invalid or failing row leaves zero partial rows (the 422 names the offending row). Rows whose credential already exists (non-revoked) or repeats earlier in the file are skipped, so re-uploading an export creates no duplicates. The response adds duplicate_count and skipped_count, and dashboard imports land directly in pending_validation instead of briefly keeping the exported routable status. (#202)
  • "Recheck all" keeps manual-review state — unidentified and validation_paused credentials are no longer reset to pending_validation with their error note wiped; the reset uses the same eligibility filter as the checker. (#203)

[3.24.1] - 2026-09-29

Security

  • Dashboard/inventory hardening batch — the inventory push token is compared in constant time; masked and detail upstream-key projections no longer ship the SHA-256 key_hash; inventory export and per-key JSON downloads return 422 for provider_id/key_id values outside [A-Za-z0-9._-] instead of interpolating them into Content-Disposition; GET /dashboard/api/export omits provider API keys unless ?include_secrets=true is passed (the Settings "Export secrets" button opts in, and each secret export is logged without the secrets); v2 state responses send X-Content-Type-Options: nosniff; rejected ingest entries echo the standard masked key instead of the first 8 characters. (#199)

Fixed

  • Healthy inventory keys no longer invalidated by upstream noise — non-integer x-ratelimit-* headers (1,000, 500.0, unlimited) are parsed leniently instead of flipping a key to invalid; Kiro 5xx responses, timeouts, and network errors are probe_inconclusive rather than invalid; antigravity onboarding polling is bounded to FETCH_TIMEOUT so it cannot monopolize a recheck slot. (#205)

[3.24.0] - 2026-09-29

Added

  • Optional detailed gateway health checks — GET /v1/health?details=true reports database reachability, configured provider count, inventory scheduler state, and the age of the latest inventory check while preserving the default lightweight health response.

Fixed

  • Settings controls recover after a failed save — toggles, selects, and inputs return to their saved value when a settings update is rejected. (#117)

[3.23.0] - 2026-09-29

Changed

  • Tooling robustness — the dashboard bundle staleness gate catches file↔directory flips and content differences behind equal size+mtime; the model-catalog generator strips comments string-aware (string values such as URLs are no longer truncated) and validates the parsed catalog schema; the OpenRouter pricing seeder skips and logs unparseable rates instead of silently writing $0 overrides and gains --prune for stale overrides; the migration smoke script no longer leaks temp dirs; the dashboard state size test asserts real response statuses and uses a 5-second wall-clock budget to stop CI flakes. (#221)
  • Dashboard settings state no longer writes on read — the settings state GET skips the 15-upsert ensure_server_defaults seeding when defaults already exist (response unchanged), and the custom-model migration early-exits when its triggers are already in place instead of re-running a full-scan + DDL rebuild on every startup. (#222)

Removed

  • Dead code batch — deleted the uncalled SubmitRateLimiter.prune(), is_sticky_client_key_routing_enabled() (13 upserts per read), fetch_export_payload() (bare urlopen without url_guard), dead if limit else branches in the dashboard state API, and the unused accountStale/VALUE_STALE_MS declarations in the inventory keys page. streaming/translator.py (translate_stream) was removed outright — it duplicated the production cross-format path (_streaming_generator in api/routes.py), which docs/architecture.md now describes accurately. (#222)

[3.22.1] - 2026-09-29

Changed

  • Bounded SQLite concurrency and connection reuse — enable WAL and a five-second busy timeout, reuse a small per-database, per-event-loop connection pool, and close pooled connections during application shutdown.
  • Dashboard response compression no longer blocks the event loop — large JSON state responses are gzip-compressed in a worker thread.

[3.22.0] - 2026-09-29

Security

  • Dashboard session auth hardened — the session cookie is now scoped to Path=/dashboard and marked Secure on HTTPS requests, and can no longer authenticate gateway /v1/* requests. POST /dashboard/login is throttled per client IP (5 failures per 5 minutes → 429; success clears the counter), and all mutating dashboard routes reject requests whose Origin header is present and mismatches the request origin, as a CSRF backstop. (#196)
  • URL-guard SSRF hardening — resolver failures in the dashboard URL guard now reject the URL instead of allowing it; inventory probe/test connections resolve and validate every redirect hop and pin each connection to the validated address (original Host header and TLS SNI preserved), closing the DNS-rebinding window; Authorization/credential headers are stripped on cross-origin redirects and redirects to disallowed targets are refused. (#198)

[3.21.0] - 2026-09-29

Fixed

  • Aborted client streams no longer lose usage, cost, and request rows — when a client disconnects mid-stream, terminal bookkeeping (usage row with any observable tokens, cost, request log, outcome) now runs in tracked background tasks that survive task-group cancellation, and the outcome is recorded as 499 (client closed request) instead of a misleading upstream 502. Happy-path streaming behavior, retries, and cooldown semantics are unchanged.
  • Request Logs shows the issuing client API key — the table gains an "API key" column (key name, falling back to the key label, then an em dash) and the Inspect modal shows the same field, so operators can attribute requests, errors, and spend to specific keys without exporting logs.

[3.20.0] - 2026-09-28

Added

  • Per-provider account-value probes — a dedicated probe layer now queries each provider's own billing/usage endpoint and normalizes the result into credits (USD-converted) and usage windows with reset times. Supported: OpenRouter (/key), Zhipu/Z.AI + BigModel coding-plan quota, DeepSeek and Moonshot balances, MiniMax coding-plan remains, and Venice billing. Results persist on each upstream key with fetch/check timestamps, a 600-second TTL, per-key single-flight refresh, and last-good preservation on transient failures.
  • Dashboard usage surfacing — the Inventory keys table renders per-key quota-window bars (danger-tinted at ≥90%), the key detail modal gains an "Account usage" section with a Refresh usage action, the Inventory overview credit pools show each provider's worst usage window, and a new inventory:quota_exhausted alert fires when any active account crosses 90% of a usage window.
  • POST /dashboard/api/inventory/keys/{id}/account-value/refresh — authenticated on-demand probe endpoint; probes also run automatically after every successful key validation (manual test or scheduled recheck).
  • janus dashboard CLI command — prints the Cloudline URL and opens it in the browser.

Changed

  • Dashboard-first positioning — the README and docs now present the Cloudline dashboard as the primary interface for operating Janus, with the CLI documented as the scripting surface. Credit fetching inside key validation was replaced by the probe layer (the credit_check_endpoint catalog field is no longer consulted).

Fixed

  • Key validation no longer fails or slows when a provider's billing endpoint is unavailable; probe errors are recorded on the key and surfaced in the dashboard without affecting routability.

[3.19.1] - 2026-09-23

Fixed

  • Set budget always shows the absolute-limit option — the Budgets dialog previously hid it behind the default global scope. The field is now visible immediately, with guidance to select a specific API key to enable it. Global scope is labeled "Global gateway (daily only)", and an empty key list provides a link to create an API key.
  • Budget scope-switch regression coverage verifies that per-key absolute limits can be saved, while switching back to global scope clears and disables the lifetime input and excludes it from the global-budget submission.

[3.19.0] - 2026-09-23

Added

  • Absolute budgets for individual Janus API keys — set a lifetime USD spending limit that never resets and includes all previously recorded spending. Use it on its own or alongside a daily budget; reaching either limit blocks new requests. Absolute-limit rejections return HTTP 429 without a misleading Retry-After reset time.
  • CLI and dashboard budget management — janus keys create/update --absolute-budget and janus budgets set --absolute configure lifetime caps. Independent clear options remove one cap without changing the other. The Keys and Budgets pages expose both limits, lifetime spending, edit controls, and warning/exceeded status.
  • Backward-compatible budget migration and regression coverage — existing daily budgets, row IDs, timestamps, and usage history are preserved. Tests cover historical usage, restarts, independent keys, concurrent edits, streaming/non-streaming metering, validation, CLI behavior, and dashboard forms.

Fixed

  • API-key budget editing now loads existing limits from the typed budget state, validates amounts before mutating keys, and supports explicitly clearing limits. Budget validation returns structured errors instead of HTML fragments.
  • Key editor checkbox layout no longer applies text-field sizing to the "Clear model restrictions" checkbox.

[3.18.2] - 2026-09-16

Fixed

  • Models pages stay fixed-size again — group-complete pagination could put an entire large provider on one page (hundreds of models). Pages are back to a flat model slice; the UI still hides empty off-page provider cards so the "No models cached yet" orphan from #167 does not return. (#179)

[3.18.1] - 2026-09-16

Fixed

  • Quiet mid-body client disconnects on gateway JSON entry points — hanging up while the request body was still being read raised ClientDisconnect from request.json() and uvicorn logged a full ASGI ERROR traceback on every abort. Format entry points now treat that as a quiet 204 client abort. (#166, #178)
  • Models catalog pagination no longer orphans provider groups — pages expand to complete provider prefixes and the UI stops rendering empty "No models cached yet" cards for providers that simply land on another page. (#167, #178)
  • Global dashboard alerts collapse and can be dismissed for the session — production-size databases stacked four full banners on every route; the strip now collapses to an "N alerts" summary with per-alert session dismiss. (#168, #178)
  • Inventory encryption banner no longer contradicts itself when credentials exist but none are encrypted yet. (#169, #178)
  • Routing pool masks email-shaped account labels instead of printing raw inventory key_label values on the high-traffic Routing page. (#170, #178)
  • Browser regression suite works against production-size databases — search derives a live catalog model id, pricing CRUD polls and always cleans up regression-override-delete-me, and pagination waits for SPA hydration. (#171, #178)
  • Inventory All keys Apply no longer collapses the table to one row, and the pager footer stays in sync after Next — number() treats missing query params as the fallback, and local offset updates on navigate. (#173, #177, #178)
  • Inventory credential Copy uses the clipboard icon (Archive gets an archive glyph). (#174, #178)
  • Inventory provider filter no longer lists identical Ollama Cloud options twice — colliding display names are disambiguated, and gateway-only catalog synthesis skips duplicate base URLs. (#175, #178)
  • Request log Inspect shows structured fields with request/response bodies behind an explicit "Show bodies" control instead of dumping the full JSON envelope. (#176, #178)

Changed

  • Dashboard visual polish for production-size data — truncated provider / allowlist / combo overflow, wrapping Tools curl snippets, an Unpriced pricing tab, human-readable Settings descriptions, labeled inventory row actions, and hiding the empty Providers "Needs attention" card. (#172, #178)

[3.18.0] - 2026-09-15

Changed

  • Models and routing state responses are paginated server-side — compression (v3.15.0) and duplicate-removal (v3.16.0) shrank the transfer, but the server still built and serialised the whole payload and the browser still parsed it. The models section shipped the entire model catalog (~1,140 rows on a production-like fixture, ~800 KB) and the routing section flattened ~347 routing accounts into the overview even though the pool panel renders ten.

Both now page server-side, following the pricing precedent: limit/offset/ search query params and the same meta.pagination shape. _models_data slices the in-memory catalog (provider + model-id search) and returns visible_total/model_total aggregates so the stat cards no longer need the full list; GET /api/v2/models still ships the whole catalog for the management endpoint. _routing_data flattens the per-provider account lists server-side, strips them from the overview (slim provider summaries plus total_accounts/ready_accounts aggregates), and ships one paginated pool page plus the small cooling-down subset whole. A shared Pagination.svelte is reused across the pricing, models, and routing pages.

Measured on a real-size fixture (1,140 models / 293 routable accounts): models ~800 KB → ~17 KB raw, routing ~150 KB → ~37 KB raw. The dead get_flow analytics helper (no application caller) is removed. (#111, #164)

Added

  • Browser regression suite, CI gate, and component tests — the dashboard had an opt-in route smoke that was never in CI and covered only render + API-key-leak, and no component tests. The state-contract suite already made a backend field rename fail CI; this closes the remaining gap.

scripts/browser_regression.py drives a real Chromium against a running Janus instance: every route renders, no secret (sk-janus-{32hex} / AKIA{16}) lands in the DOM, back/forward navigation, server-side filters, pagination, a reversible pricing-override CRUD mutation, and empty-state rendering. A browser job in .github/workflows/checks.yml generates an isolated two-provider / 70-model config, boots a server, creates a dashboard key, and runs the suite on every push/PR.

dashboard-ui gains vitest + @testing-library/svelte + jsdom with a Pagination.svelte component test; npm test runs inside scripts/build_dashboard_ui.py --check. A separate vitest.config.ts forces the Svelte 5 browser resolve condition for tests without leaking into the production build. (#110, #165)

[3.17.0] - 2026-09-13

Changed

  • The pricing catalog is paginated server-side — it was 828,927 B across 4,828 rows on a production deployment, 98.8% of the pricing payload, and the page rendered every row, so the browser built 4,828 DOM nodes with no way to page or filter. Against a real 4,873-row catalog the payload is now about 21,000 B for one page and 25 DOM rows.

list_catalog is unchanged and still returns the whole table, because the pricing registry needs it to resolve model rates; paging is a separate list_catalog_page(limit, offset, search) so the rate-lookup path is untouched. Only the catalog is paged -- builtin (4,874 B) and overrides (699 B) are small and still arrive whole. The section reuses the limit/offset/search query parameters the state endpoint already accepts and returns the same meta.pagination shape as request-logs, leaderboard and inventory-keys. Search filters in SQL rather than the browser and resets the offset so a filtered result starts at page one. (#111, #162)

[3.16.0] - 2026-09-13

Changed

  • Dashboard state payloads no longer carry duplicated or unread sections — compression (v3.15.0) shrank the transfer but not the work at either end: the server still builds and serialises every byte, and the browser still parses it. Measured on a production deployment with real data, providers.catalog_presets held the same object as providers.catalog (the source read "catalog": catalog, "catalog_presets": catalog), duplicating 33,640 B or 25% of that payload, while the dashboard read data.catalog_presets ?? data.catalog and only ever used one. inventory.top_keys was 18,579 B, 47.9% of the inventory payload, referenced nowhere in the frontend, and cost a get_top_keys_per_provider database query on every inventory page load. Both are removed: roughly 52 KB per affected page load and one fewer query.

pricing.unpriced looked like a third candidate but is asserted by two integration tests, so it encodes intended behaviour and was left alone. A new payload-shape guard covers both regressions plus a general check that no state section serves two top-level keys holding identical values. (#111, #160)

[3.15.0] - 2026-09-11

Added

  • Response compression for dashboard state payloads — sections were served uncompressed, and a live deployment running uvicorn with no reverse proxy had nothing else compressing them. Measured against real data (25 providers, 937 upstream credentials, 4,096 pricing rows): models 1,196,941 B to 34,397 B (97% smaller), pricing 826,915 to 53,357, providers 126,777 to 14,654, routing 167,342 to 25,195. The main dashboard JS chunk drops 228,398 to 72,416 since the same middleware covers mounted static files.

Janus streams across 21 call sites, and compressing a stream buffers it. A content-type blocklist would hold until something streamed an unlisted type, so the rule is structural instead: only a response that already declared a content-length is compressed, and a streamed response never does. text/event-stream is excluded explicitly as a second layer, and Vary: Accept-Encoding is set so caches stay correct. Verified in production after deploy: SSE live usage and a real streaming completion both arrive as plaintext chunked frames, while a non-streaming completion round-trips correctly through gzip. (#111, #157)

Changed

  • CI gates publishing, tests both supported Python versions, and reports coverage — a tag previously triggered publishing independently of CI; the v3.14.0 release reached PyPI 2m21s before CI finished. Checks now live in a reusable workflow that publish.yml and docker.yml both depend on. The matrix covers 3.11 and 3.12 (the classifiers advertised 3.12 while CI tested only 3.11). pytest-cov was installed but unused; coverage is now reported, uploaded, summarised, and gated at 80% against a measured 83%. scripts/ is linted and type-checked. Publishing runs twine check --strict, asserts the built wheel actually contains the dashboard bundle, and verifies the git tag matches the packaged version. (#122, #158)

[3.14.0] - 2026-09-11

Added

  • GET /v1/models/{id} — OpenAI's "Retrieve model" endpoint was never registered, so clients fetching a single model received a router-level 404 (observed in a live deployment as GET /v1/models/deepseek/deepseek-v4-flash-vision-exp). It reuses the list endpoint's visibility logic through a shared helper so the two cannot drift, is registered after /models so the list route still wins, and uses a :path converter because Janus model ids are namespaced. "Absent" and "not entitled" both return 404, so a restricted key cannot use it to probe which models exist. (#154)

Fixed

  • Ollama's implicit :latest tag was rejected by /api/show — the handler matched catalog names exactly, so openai/gpt-5 returned 200 while openai/gpt-5:latest returned 404. Ollama treats an untagged reference as :latest and clients normalise to that form, so a model that could be chatted with could not be described; since Ollama clients call /api/show before a chat to read capabilities, this blocked them outright. The fallback applies only to :latest, only after an exact match fails, and searches the already-allowlist-filtered entries, so it cannot widen what a restricted key sees. An unknown tag such as model:v2 still 404s. (#154)
  • API key model restrictions were cleared on a blank edit — the Keys form always sends models_field="1" while editing, and the update route treated that marker as an instruction to apply model changes, so an empty allowed_models parsed to None and wiped the stored allowlist. An edit now updates allowed_models only when the submitted field is non-blank; the dedicated "Clear model restrictions" checkbox remains the only way to remove it. (#153)
  • Dashboard UI defects — a frontend audit found and fixed ~30 issues, ten of them reproduced in a browser against a running gateway and re-verified after the fix. Visible defects: a stray close button rendered in the sidebar at every viewport width (.icon-button { display: grid } outranked .mobile-close { display: none } at equal specificity); the time-range <select> rendered as unstyled browser chrome; all three Response-health bars drew identical teal because each row's severity tone was computed and never rendered; Settings labels rendered larger (16px) than the section headings above them (14px) because they set no font-size and fell back to the default; Settings used system-blue OS checkboxes while Models and Token savers used pill switches; opening the mobile nav and widening the viewport left a stray unstyled button in normal flow; currency rendered US$0.00 in every non-US locale; "Spend today" rendered US$26.4929 because one money() helper served both aggregates and per-MTok rates (now split into money() at 2dp and rate() at 4dp); an empty chart drew a flat line pinned to the baseline that read as real data at zero; and the "Gateway readiness" checklist never disappeared on a fully configured gateway. Behavioural fixes: the command palette's arrow keys and Escape were dead whenever a result had focus (the panel stopped keydown propagation), and it now also traps Tab and restores focus on close; request-log detail errors rendered unstyled because their class was defined only in another component's scoped block; concurrent log inspections could resolve out of order and show the wrong row; the Combos empty-state create button kept a stale editing and could PUT to a deleted id; the selected Models group could never be collapsed; Leaderboard rendered NaN% for non-numeric input; the API-key daily budget never prefilled when editing; Budgets and Combos submit handlers left unhandled rejections; Settings number inputs saved out-of-range values without validation; modals and the palette dismissed on a drag that started inside and ended on the backdrop; toasts were not dismissible, uncapped, and re-announced the whole stack; Ctrl/Cmd-click could not open nav links in a new tab and alert links bypassed the SPA router; and the identity chip was itself a logout button, so a stray click ended the session. Also: keyed table rows, 100dvh for mobile viewports, modal height accounting for its header, scrolling tab strips, and per-instance DOM ids for modal labels and chart gradients. (#155)
  • A legibility guard that did not guard — test_no_sub_11px_font_sizes_remain matched only (?:9|10)px, so six 8px declarations, smaller than what the test was written to ban, passed it. The pattern is corrected, widened to lib/components, and verified to fail when 8px is reintroduced. (#155)

[3.13.0] - 2026-09-10

Added

  • Cline provider (api.cline.bot/api/v1) — WorkOS-OAuth OpenAI-compatible gateway exposing ~436 models (incl. openai/gpt-6-astra, anthropic/claude-opus-5, google/gemini-3.8-flash). Credentials are stored as workos:<jwt> and refreshed via WorkOS (/user_management/authenticate); _validate_cline_key probes identity, refreshes on 401, and persists the rotated tokens.
  • TokenRouter provider (api.tokenrouter.com/v1) — static-key OpenAI-compatible aggregator (PaleBlueDot upstream) with a genuinely free z-ai/glm-5.3-free tier on every key. Registered as prefix tr; only the free model is exposed (allowed_models) because the full keys advertise quota-locked SOTA models.

Fixed

  • requires_responses over-application — the v3.11.0 promotion of Responses-only models to /v1/responses now applies only to the OpenAI gateway (target.prefix == "openai"). Other openai_compat gateways (Cline, gorouter, TokenRouter) serve gpt-6-astra via /chat/completions and were returning 404.
  • insufficient_user_quota quota classification — TokenRouter's unfunded-account 403 is now classified as no_quota (a billing state) rather than an auth failure, so it does not burn the key's health score. (#152)

[3.12.0] - 2026-09-08

Fixed

  • Empty (void) gpt-6-astra responses — after routing to /v1/responses (v3.11.0), gpt-6-astra still intermittently returned an empty 200 to coding-agent clients for two reasons. (1) The OpenAI chat adapter only read max_tokens, so coding agents / OpenAI SDK 1.x that send max_completion_tokens never forwarded their token budget to the Responses max_output_tokens field; without an explicit budget, reasoning could exhaust the default and the model returned empty. OpenAIAdapter.parse_request now accepts max_completion_tokens as a fallback (max_tokens still wins). (2) Some OpenAI-compat relays return a well-formed 200 with empty output (no content, no tool call) for tool-bearing requests; Janus forwarded that as a successful empty answer, so agents saw a call that silently did nothing. Janus now treats such void completions as failures: non-stream responses with no text and no tool call cool the account (server_error) and fall back to the next account; streaming requests watch for client-visible output (text/tool-call events) and, if the stream ends with none, cool the account for the next turn (a committed 200 can't be retried mid-stream). (#151, PR #151)

[3.11.0] - 2026-09-08

Fixed

  • Responses-only OpenAI models with function tools — calls to openai/gpt-6-astra (and other Responses-only models) that include function tools failed with HTTP 400 from OpenAI, which rejects tools + reasoning_effort on /v1/chat/completions for these models and requires /v1/responses. Janus now promotes the per-attempt wire format to openai_responses for any openai-format model that sets the new requires_responses capability (gpt-6-astra today, plus the gpt-6-astra* pattern), so a /chat/completions client gets a Responses payload built and posted to {base}/responses, then emitted back as chat. OpenAICompatProvider selects the upstream endpoint from the payload shape (input → /responses, messages → /chat/completions), and the thinking layer remaps the chat reasoning_effort scalar onto the Responses reasoning: {"effort": …} field. The decision is per-request and gated on the model capability, so the same shared OpenAI keys keep using /chat/completions for normal models. (#149, PR #150)

[3.10.0] - 2026-09-08

Added

  • Dashboard accessibility and operator quality-of-life pass — all 9–10 px table/status/field labels raised to an 11 px floor (80 declarations); DataTable omits the Actions column when a page provides no actions slot; the command palette supports ArrowUp/ArrowDown/Home/End selection with aria-activedescendant combobox/listbox semantics and a visually distinct active item; API error toasts parse structured JSON bodies (detail/error/message/reason) instead of showing raw JSON; the Leaderboard gains a visible 7/30/90/365-day time-range control; and Tools examples are authentication-aware, reflecting require_api_key with a copyable runnable curl command using $JANUS_API_KEY (never an actual key). Route-level page chunking was considered and deliberately deferred to be revisited with #111's payload budgets. (#116, PR #147)

[3.9.0] - 2026-09-08

Fixed

  • Single live-usage initialization path — the Usage view received its initial live snapshot three times (embedded in the usage section state, via a separate /dashboard/api/usage/snapshot fetch, and again from the SSE stream's connect-time snapshot), and events published between SSE subscription and snapshot emission could render twice. The SSE snapshot on connect is now the only initialization path: the duplicate REST endpoint and the section-state live payload are removed, request events carry a monotonic seq with a matching snapshot watermark so clients discard already-folded events (subscribe/snapshot race, reconnect replays), and the recent-activity ring stays mounted across transient disconnects. (#115, PR #144)
  • Representative provider connection tests — the Providers page Test action is available for every executor type displayed by the UI, selects a model available to the credential under test instead of blindly using the first configured model, supports per-account and aggregate scopes, performs URL safety validation without blocking the event loop while preserving SSRF protections, and returns sanitized, actionable failure detail. (#112, PR #145)
  • State-backed dashboard health and identity — the shell no longer hardcodes "System online"/"Administrator / JA" and the Overview page no longer labels providers "Operational" without data. A new /dashboard/api/v2/health contract exposes DB reachability, provider counts, scheduler status, cooldown counts, and last inventory check age; the shell renders online/degraded/offline states with a visually distinct stale indicator, and the identity chip shows the authenticated key's stored label (never credential material). The Overview provider-health chip reflects provider and cooldown state. (#119, PR #148)

[3.8.0] - 2026-09-07

Fixed

  • Provider-to-model navigation is SPA-native with deep links — the provider page's internal links (Manage models, inventory add/import and key management) used raw anchors that reloaded the whole dashboard, and the Models page ignored the ?provider= query parameter its own links produced, always opening on "All providers". Internal links now use the shared SPA navigation action (links opened from inside the provider modal close it first), the selected model provider group is tracked in the URL so Back/Forward restores it and direct links to /dashboard/ui/models?provider=<prefix> open the intended group (unknown values degrade gracefully to All providers), and the models state endpoint echoes a length-validated provider filter in meta.query following the established dashboard query contract. (#113, PR #143)

[3.7.0] - 2026-09-07

Added

  • SeekAI gateway — SeekAI is now available as an OpenAI-compatible gateway provider in the catalog (default models include claude-opus-4-8, claude-sonnet-5, gpt-5.6-sol, gemini-3-6-flash, deepseek-v4-pro, and grok-4-6). (#142, contributed by @hett-patell)

Fixed

  • Codex account-plan restriction rotates accounts — free/Go ChatGPT accounts reject gpt-5.6-sol with a 400 "not supported when using Codex with a ChatGPT account". That marker is now classified as a provider miss so the request rotates to the next (Plus/Team) account instead of surfacing the 400 to the client. (#142, contributed by @hett-patell)
  • Committed dashboard bundle matches sources again — the v3.6.0 release bumped the package version without regenerating the committed SvelteKit bundle, so the shipped wheel carried dashboard assets built from 3.5.0 sources and the bundle-consistency CI check failed on main. The bundle is now regenerated from the release sources as part of the release process.

[3.6.0] - 2026-09-07

Changed

  • Discovered-model reloads scale with distinct models, not key×model rows — provider reload and the model catalog read every per-key upstream_models row even though most collapse to the same provider/model pair (audited database: ~116,500 rows for ~1,200 distinct pairs), and list_model_ids_for_keys deduplicated with an O(n²) list-membership check per key. list_catalog_models now reads through the new list_distinct_discovered_models, which collapses rows in SQL with a single GROUP BY over packed (created_at, id, field) values, and the per-key helper uses SELECT DISTINCT. Result cardinality now scales with distinct provider/model pairs; on a seeded audit-sized database (116,000 rows → 40 distinct pairs) list_catalog_models drops from ~5.6 s to ~0.15 s (~37×). Metadata precedence is specified: each field (display_name, context_window, max_output_tokens, capabilities) resolves to the most recent non-NULL observation, older non-NULL values backfill fields newer observations lack, and pairs keep the legacy earliest-observation order so reload output stays deterministic. (#114)

[3.5.0] - 2026-09-07

Fixed

  • Streaming and transport failures in fallback — ReadError, WriteError, RemoteProtocolError, and other httpx.RequestError subclasses now rotate to the next account with a network cooldown instead of surfacing as an unhandled 500. Previously only TimeoutException and ConnectError were caught. Mid-stream failures are recorded as an accurate terminal 502 (never a false 200) and cool the account for future requests, while client cancellation (CancelledError) is deliberately left uncaught so it never penalises an account. Explicit quota/error envelopes returned with HTTP 200 (e.g. {"error": "quota exceeded"}) are now detected via is_200_wrapped_error and trigger fallback instead of being parsed as empty successes. (#104)

[3.4.0] - 2026-09-07

Fixed

  • Cache-token accounting across adapters — compute_cost no longer double-bills cached tokens. It charged the full input rate on Usage.input_tokens and then added cache subsets at their own rates, so OpenAI Chat/Responses and Gemini cached tokens were billed twice (full input rate plus the cache-read rate), while Anthropic dropped its cache fields entirely so cached tokens got no cache discount. A single invariant is now enforced at the adapter boundary: Usage.input_tokens is the total prompt input and cache_creation_input_tokens / cache_read_input_tokens are subsets of it. compute_cost bills only the uncached remainder max(input - cache_creation - cache_read, 0) at the full input rate, plus each cache subset at its own rate. The Anthropic adapter folds its disjoint usage buckets into a total at parse time (and reconstructs the disjoint form on emit); the OpenAI Chat and Gemini adapters now parse and round-trip their cached-token fields in non-stream, stream, and emit paths. Historical cost rows are not backfilled — pre-fix costs for cache-bearing requests are approximate. (#105)

[3.3.0] - 2026-09-07

Fixed

  • Truthful request analytics — the dashboard success rate is no longer derived from the success-only usage table, which always reported 100% success. A new request_outcomes table records a terminal status on every request path: upstream success (streaming and non-streaming), fallback-exhausted 503s, all-accounts-cooled 503s, budget blocks, parse and allowlist rejections, non-fallback upstream errors, and mid-stream interruptions. Attempts and duration are captured so request counts match reality even when fallback retried several accounts. Token and cost sums stay on usage (success-only rows); success rate, spend-summary request totals, usage stats, and leaderboard now read from request_outcomes via aggregated joins. Historical usage rows are backfilled once (marker in PRAGMA user_version) so counts stay continuous across the upgrade. (#103)

[3.2.0] - 2026-09-07

Fixed

  • Routing attempt counters survive reloads and restarts — RPD and subscription request/token quota counters now persist to a new attempt_counters ledger on every attempt instead of being re-seeded from success-only usage rows. A provider reload merges adopted in-memory counts with the ledger (max per current window) rather than overwriting them, so failed attempts no longer restore consumed quota headroom or vanish on a process restart. Window-keyed rows expire with the window; stale rows are pruned after 35 days. Per-account RPD and per-provider-row quota scopes stay distinct. (#101)

[3.1.0] - 2026-08-26

Added

  • Unified provider control plane — provider setup, credential inventory, live model discovery, custom model registration, visibility controls, and routing state now share one catalog-backed workflow inspired by OpenCodeX while retaining Janus's multi-user gateway architecture
  • Model catalog management — the Cloudline dashboard provides a dedicated Models workspace for searching 1,000+ discovered models, enabling or hiding catalog entries, and adding custom models without restarting the gateway
  • Provider runtime drivers — provider-specific authentication, headers, endpoint behavior, model normalization, and capability metadata are resolved through reusable drivers and durable routing snapshots

Changed

  • Cloudline is the sole dashboard — /dashboard and former page URLs redirect to /dashboard/ui; the legacy server-rendered HTMX interface, partial endpoints, templates, scripts, stylesheets, and vendored browser dependencies have been removed
  • Inventory-backed routing — shared upstream credentials and discovered model availability feed provider state, routing expansion, health reporting, cooldown behavior, and the public model lists without exposing decrypted credentials

Fixed

  • Provider reload stability — invalid credentials, partial upstream model responses, duplicate provider prefixes, disabled models, and transient catalog failures no longer destabilize the active routing snapshot

[3.0.1] - 2026-08-26

Fixed

  • Inventory key validation — automatic checks pause after three consecutive failures instead of retrying indefinitely, while Test and Recheck actions resume validation with a fresh attempt counter

[3.0.0] - 2026-08-25

Added

  • Cloudline dashboard — a responsive SvelteKit 2, Svelte 5, and TypeScript control plane at /dashboard/ui covers all 18 operational views with deep links, mobile navigation, light/dark themes, command search, live usage, interactive routing visibility, and one-time secret actions
  • Dashboard state API — authenticated, non-cacheable JSON endpoints under /dashboard/api/v2/state/{section} provide stable contracts for modular present and future control-plane clients
  • Reproducible frontend packaging — the dashboard build, exact lockfile, local static bundle, wheel/sdist contents, Docker image, and CI are checked together without runtime CDN dependencies

Changed

  • Modular dashboard architecture — Cloudline is split into reusable components and independent feature pages so new routing, inventory, analytics, and future peer-topology modules can be added without rebuilding a monolithic server-rendered interface
  • Progressive dashboard migration — the existing /dashboard HTMX interface remains available while /dashboard/ui becomes the modern application surface on the same FastAPI backend
  • Inventory mutations — add, import, encryption, and credential-management workflows support safe JSON responses for Cloudline while retaining their legacy HTML/HTMX behavior

Security

  • Credential-safe browser contracts — state responses recursively strip secrets, explicit reveal actions remain non-cacheable and time-limited, untrusted values use safe DOM rendering, and all Cloudline routes honor Janus's API-key-only dashboard authentication policy

[2.2.2] - 2026-08-25

Fixed

  • API-key copying — the dashboard now provides a working one-time full-key copy action after key creation, labels stored-key actions as prefix copies, and falls back when the modern Clipboard API is unavailable or rejected

[2.2.1] - 2026-08-25

Security

  • API-key-only dashboard login — dashboard authentication now applies to localhost and remote clients alike; username/password login and the loopback bypass are removed, DB keys require can_login=true, and legacy dashboard credential settings are purged during initialization

Fixed

  • Dashboard access editing — dynamically loaded API-key edit forms are initialized before submission, so disabling a key's dashboard access persists instead of navigating with form data in the URL

[2.2.0] - 2026-08-25

Security

  • Credential-safe inventory views — ordinary dashboard responses no longer embed decrypted upstream credentials; authenticated users must explicitly reveal or copy a credential, revealed values are served with no-store headers, and the browser clears them after 30 seconds
  • Dashboard DOM rendering — toast messages, live activity, and provider verification results now use safe text/DOM construction instead of interpreting dynamic values as HTML

Added

  • Reporting timezone — Today's Spend, daily budget enforcement, analytics, alerts, and retry windows share a configurable IANA timezone with daylight-saving-aware UTC query bounds
  • Self-hosted dashboard assets — pinned Tailwind, HTMX, Chart.js, D3, and d3-sankey assets ship with licenses, checksums, and a reproducible updater, removing the runtime CDN dependency

Fixed

  • Budget validation — malformed amounts and invalid key selections return actionable validation responses instead of server errors
  • Inventory history quality — no-op status transitions are no longer recorded or displayed, while historical balance snapshots appear as unit-neutral credits

[2.1.5] - 2026-08-17

Added

  • Gorouter provider — gorouter (https://gorouter.app/v1) is registered in the provider catalog as an OpenAI-compatible gateway with its Claude models (claude-opus-5-thinking, claude-opus-5, claude-opus-4-8, claude-opus-4-8-thinking); gorouter keys are validated with a real chat probe

Fixed

  • Cloudflare-protected upstreams — outbound requests (key validation/probes, dashboard "fetch models", and runtime OpenAI-compatible calls) now send a browser-like User-Agent by default, so gateways behind Cloudflare bot protection (gorouter / new-api style) no longer return a 403 to plain httpx fingerprints

[2.1.4] - 2026-08-14

Fixed

  • Codex usage-limit cooldowns — 429 usage_limit_reached bodies (resets_at / resets_in_seconds) now set a precise per-model cooldown instead of a blind exponential backoff, so exhausted ChatGPT accounts are not hammered on every request
  • Codex in-stream errors — "Selected model is at capacity" and server_is_overloaded events inside 200-OK SSE streams surface as a routable 503 (rotating to the next account) instead of a broken client stream
  • Gemini/Antigravity rate-limit resets — google.rpc.RetryInfo retry delays and "retry in Ns" / "reset after XhYmZs" message phrases in 429 bodies are honored as cooldown durations
  • GitHub Copilot monthly limits — 402 premium-request exhaustion cools the account down toward the next UTC month instead of retrying every five minutes
  • Kiro account rotation — bare 400 "Improperly formed request." (CodeWhisperer's quota/account signal), "request not allowed", and "no credentials" errors rotate to the next account instead of failing the request
  • Rate-limit headers — x-ratelimit-reset-after and x-ratelimit-reset headers are parsed alongside Retry-After

Added

  • Fallback observability — the "All providers exhausted" 503 detail and request log now list every attempted account (account: status), and each fallback is logged at WARNING level

[2.1.3] - 2026-08-14

Fixed

  • Kiro EventStream completeness — decode AWS :event-type headers and surface toolUseEvent, reasoningContentEvent, messageStopEvent, and metricsEvent in both stream and JSON paths; strip <thinking> blocks while keeping codeEvent content raw; map stop reasons to OpenAI finish reasons; remove unavailable claude-sonnet-4-thinking from default models
  • Kiro vision — preserve inline data: / base64 image parts when translating canonical requests to Kiro's native envelope
  • Antigravity duplicate finish reason — GeminiStreamParser now emits each stop reason once and still reports late-arriving usage metadata

[2.1.2] - 2026-08-14

Fixed

  • Kiro agent compatibility — native request translation now preserves Pi system prompts, tools, tool results, and multi-turn history; AWS EventStream responses are converted to correctly framed OpenAI SSE/JSON, with auth-aware Kiro/CodeWhisperer/Amazon Q endpoint selection and AWS/social credential refresh
  • Kiro inventory and model discovery — Kiro credentials can be imported, natively validated, and used for live ListAvailableModels discovery; inventory-backed gateways try alternate active accounts when an otherwise valid account cannot list models
  • Antigravity agent compatibility — Cloud Code response envelopes are unwrapped, imported OAuth credentials refresh reliably, complex Pi tool schemas are reduced to Antigravity's supported Gemini subset, and tool call/result IDs and names survive multi-turn Claude requests

[2.1.1] - 2026-08-07

Fixed

  • Custom provider keys now appear in Key Inventory — keys added via the Providers page are mirrored into upstream_keys (the canonical routable store) on create/update/delete, with a one-time backfill on startup for existing/seeded providers. Custom providers with arbitrary prefixes get an inventory_providers row so the key is searchable and visible; known-catalog prefixes (openai, anthropic, …) map to their existing inventory id. Mirrored keys go through the normal background validation probe. Closes #71

[2.1.0] - 2026-08-07

Added

  • Key Inventory "Test" button — per-key synchronous test that sends a minimal "hi" chat completion (reusing validate_key's models + usability probe) and shows the result inline (pass/fail + the actual error message), non-persisting. Complements the existing background Recheck action
  • Bulk-select + soft archive on Key Inventory — checkbox column with select-all-on-page, a bulk-action toolbar (Archive / Restore / Recheck / Delete) that survives HTMX partial swaps, and a "Select all N matching" affordance across filtered pages. Archive is a new reversible is_archived state: archived keys are hidden by default, excluded from routing/counts/recheck-all, and restorable under a new archived status filter without losing their validation status

[2.0.0] - 2026-07-28

Added

  • Cooldown override controls — Settings/CLI toggle server_cooldowns_enabled (default on) to disable account cooldown enforcement and marking; Routing Clear all cooldowns wipes active memory + SQLite timers without changing the toggle

[1.8.0] - 2026-07-28

Added

  • Codex inventory credentials — paste Janus OAuth JSON, 9router Codex connection objects, or providerConnections arrays into Key Inventory; select Codex (ChatGPT); validate via OAuth refresh; expand into multi-account codex/<model> routing while keeping Providers-page paste working

[1.7.0] - 2026-07-28

Added

  • Ollama Cloud provider — first-class catalog entry (openai_compat, https://ollama.com/v1, prefix ollama) with dashboard Fetch Models / Test Connection and inventory-backed multi-key routing
  • Ollama Cloud inventory validation — keys are validated via an authenticated one-token chat probe (gpt-oss:20b) because /v1/models is public without a key; routable keys auto-provision the gateway provider

[1.6.0] - 2026-07-27

Added

  • Dashboard control room — Overview redesigned as an at-a-glance status hub: system health strip, attention panel with prioritized actionable alerts, setup checklist for fresh installs, and a live activity widget polling /dashboard/api/usage/snapshot
  • Centralized dashboard alerts — collect_dashboard_alerts() aggregates budget warnings, quota exhaustion, routing cooldowns, unhealthy inventory keys, unpriced models, and setup gaps; global banner on all dashboard pages
  • Routing live state — Routing page highlights the next-available account per prefix, HTMX-refreshes every 8s, ticks cooldown timers, and surfaces sticky routing pins via FallbackHandler.routing_snapshot()
  • Analytics trust flags — banner and breakdown badges when models have token usage but no pricing entry
  • Toast notifications — replaces browser alert() on Providers/Combos HTMX failures; global error handler in the dashboard shell
  • Missing provider logos — SVG assets for Qwen, OpenCode, GitHub Copilot, and custom providers
  • Live pricing catalog — Janus now syncs real model pricing from LiteLLM's community price table (~3k models) and OpenRouter's models API into a local pricing_catalog table: on startup when stale, on a 24h schedule (PRICING_SYNC_INTERVAL_HOURS, PRICING_SYNC_ENABLED), and on demand via the Pricing tab's "Sync now" button or janus pricing sync. Fail-open: a failed fetch keeps the last-synced catalog. Pricing resolution is layered: user override > synced catalog > builtin, with prefix matching per layer (an override always wins when it matches)
  • Pricing tab upgrades — sync status line (model count + last-synced age, amber when stale), per-row source badges (override / catalog / builtin), searchable collapsed catalog section, and an "Unpriced models seen recently" table with one-click override prefill
  • janus pricing backfill [--days N] [--dry-run] — retroactively recompute usage.cost for $0-cost rows (including cache-only rows) using current pricing; prints how much today's measured spend increased (budgets enforce against it)

Fixed

  • Subscription/OAuth providers no longer accrue phantom cost — Copilot/Kiro/Cursor/Codex/Claude-sub/Antigravity/MiMo/OpenCode usage is recorded with $0 cost even when the synced catalog knows the bare model id; backfill and the unpriced-models table exclude subscription rows. Previously copilot/gpt-4o traffic was billed at API list price into budget enforcement
  • Leaderboard nav highlight — sidebar active state now marks the Leaderboard page correctly

[1.5.0] - 2026-07-10

Added

  • Fusion combo strategy — combos can now run in fusion mode (9router port): the request fans out to all combo members in parallel (tools stripped, non-streaming, tool history flattened), a quorum + straggler-grace collector gathers answers, and a judge model (configurable via combo_fusion_judge, defaults to the first panel member) synthesizes one authoritative answer from anonymized [Source N] responses. The judge keeps the client's stream flag and tools and rides the normal fallback machinery. Panel usage is recorded per model. Judge is validated before any panel spend, with fallback to the first answering panel model. Tuning: combo_fusion_min_panel, combo_fusion_straggler_grace_s, combo_fusion_hard_timeout_s
  • Per-provider model allowlist — new optional allowed_models on providers (dashboard field, comma-separated, fnmatch globs like claude-opus-* supported; empty = all models). Blocked models are rejected at routing time (400) and hidden from /v1/models and Ollama /api/tags. Lets you expose only selected models from a provider (e.g. only claude-opus-4-7 from Anthropic)
  • Request log "User" column — request logs now record and display which client API key made each call (client_key_label for config keys, key #<id> for DB-issued keys, "—" for anonymous)
  • Per-saver savings metrics — the token-saver pipeline measures request size before/after each saver, logs shrinkage, and shows cumulative per-saver savings ("saved X KB across N requests, since restart") on the Token Savers page. Stats survive dashboard-triggered saver reloads
  • Caveman levels — the Caveman saver now has lite / full / ultra levels (dashboard select, saver_caveman_level), with 9router's safety boundaries (security warnings, irreversible confirmations, and multi-step instructions are always written normally; code/paths/commands/errors/URLs never abbreviated)
  • Combo Routing settings UI — Settings page section for combo_strategy (fallback / round_robin / fusion), combo_sticky_limit, and fusion tuning, with server-side validation (whitelisted strategy, finite bounded numbers)
  • 503 + Retry-After on exhausted cooldowns — when every account for a model is cooling down, Janus now returns 503 with a Retry-After header derived from the earliest cooldown expiry instead of a generic 400
  • Body-text rate-limit detection — upstream error bodies are scanned for rate-limit markers ("rate limit", "too many requests", "quota exceeded", "capacity", "overloaded", "resource exhausted"); a disguised rate limit (e.g. HTTP 400 with "quota exceeded") now cools the account down with rate-limit backoff and falls back to the next account. Handles dict, list, string, and other JSON body shapes
  • RTK filter upgrade — ported 9router's compression filter set: git-log, git-status, grep (per-file cap, non-matching lines preserved), find (per-dir cap, path-only detection), tree, and build-output filters with priority auto-detection, plus line-based head+tail smart truncation (120 head / 60 tail above 250 lines). Compression gate raised to 500 bytes minimum, 10 MiB raw cap

  • API key scopes — DB-managed keys support can_login (default on; opt out for API-only keys that cannot open the dashboard) and allowed_models (exact IDs or prefix/* wildcards; empty/unset = all models). Disallowed models return 403 with error.type = model_not_allowed; GET /v1/models is filtered the same way. Optional daily budget can be set on key create/update via CLI (--daily-budget) or the dashboard Keys page. Spec: docs/superpowers/specs/2026-07-09-api-key-scopes-design.md

  • Ollama /api/show and /api/generate — POST /api/show returns stub model metadata for routable models; POST /api/generate accepts a bare prompt and remaps chat responses to Ollama's completion shape. GET /api/tags now respects the API key model allowlist (parity with GET /v1/models)
  • Request log retention setting — server_request_log_retention (default 500, clamp 50–5000) replaces the hardcoded row cap; configurable from Settings
  • Request logs pagination — dashboard request-logs table supports Prev/Next paging via HTMX partial
  • Quota UX round 2 — shared quota_status helper drives ≥80% amber banners on provider cards, quota fields on the Routing page, and an 8s poll refresh for provider usage bars
  • Ollama-compatible endpoints (POST /api/chat, GET /api/tags, GET /api/version) — tools that only speak Ollama can now route through Janus. Full format adapter with NDJSON streaming (Ollama defaults to streaming), tool-call round-trips (positional call-id assignment), images, options (num_predict/temperature/top_p/stop), and thinking passthrough. Phase 8.6 — completes the 9router feature-parity plan
  • Subscription quota tracking — per-provider quota windows (5h, daily, weekly, monthly, all UTC) with a request or token limit, configurable in the provider Add/Edit forms. Provider cards show a usage bar + reset countdown; exhausted providers are deprioritized in fallback ordering (soft enforcement — never blocked). Counters are shared across a provider's inventory accounts and seeded from the usage table on startup/reload. Phase 8.5 of the 9router parity plan
  • GitHub Copilot OAuth provider — first subscription/OAuth provider (Phase 8.4 of the 9router parity plan). New api_type: github_copilot with device-code login from the dashboard ("Connect GitHub Account" in Add Provider), automatic exchange of the long-lived GitHub OAuth token for short-lived Copilot session tokens (refreshed before expiry behind a single-flight lock), OpenAI-compatible routing (copilot/gpt-4o, ...), Fetch Models and Test Connection support. New dashboard endpoints POST /dashboard/api/oauth/copilot/{start,poll}
  • Headroom token saver — optional integration with an external Headroom compression proxy: when enabled, conversations are sent through POST {url}/v1/compress before any other saver and before routing. Fail-open (Headroom being down never breaks a request). Toggle + URL field on the Token Savers dashboard page (saver_headroom_enabled, saver_headroom_url). Phase 8.3 of the 9router parity plan
  • Request logging / debug mode — opt-in capture of full request/response bodies to SQLite (request_logs table, 64 KB body truncation, last 500 requests kept). New dashboard page /dashboard/request-logs with per-request JSON detail, export, and clear; toggle in Settings (server_request_logging, off by default). Streaming requests and all-providers-exhausted failures are captured too. Phase 8.2 of the 9router parity plan
  • OpenAI Responses API endpoint (POST /v1/responses) — Codex CLI and other Responses-API clients now work natively. New bidirectional format adapter (formats/openai_responses.py) translates input items, flat tool definitions, function_call/function_call_output round-trips, max_output_tokens, reasoning.effort, and streaming via named SSE events (response.created … response.completed). Phase 8.1 of the 9router feature-parity plan (docs/superpowers/specs/2026-07-05-phase8-9router-parity.md)

Fixed

  • Combo round-robin rotation now honors combo_sticky_limit (stay on a combo member for N requests before advancing); previously the limit was accepted but ignored
  • Non-JSON upstream error bodies (HTML gateway pages, plain text) no longer raise during error handling in the Anthropic/Gemini/OpenAI-compat providers
  • HTTP 402 responses are classified as payment errors, are fallback-eligible, and cool the account for 5 minutes
  • Bare model names on the Gemini-native endpoint (/v1beta/models/gemini-2.5-flash:generateContent) are auto-prefixed with gemini/
  • Fusion panel tasks are cancelled and awaited if the client disconnects mid-panel (no orphaned upstream spend)
  • Request logging coverage — debug mode now captures pre-routing rejections (budget exceeded, model not allowed), non-fallback upstream errors, and empty-stream failures (not only successes and the final 503)

Changed

  • Docs — README feature list and Client Setup cover Phase 8 surfaces (Responses/Codex config.toml, Ollama, Gemini CLI, Copilot OAuth, quotas, request logging, Headroom); docs index provider count aligned to 29

[1.2.0] - 2026-07-05

Added

  • Rate-limit-aware routing — inventory key rate limits (rate_limit_rpm, rate_limit_rpd) now feed the fallback handler: accounts at/over their per-minute or per-day request quota are deprioritized (moved to the end of the try-order) during rotation. Daily counts are seeded from the usage table on startup and provider reload
  • Unified provider catalog src/janus/catalog.py — single source of truth for provider metadata; dashboard/catalog.py (14 gateway providers) and inventory/catalog.py (29 inventory providers) now derive from it, along with the google↔gemini / dashscope↔qwen id bridges

Fixed

  • Key Inventory: provider/status/search filters no longer reset after a few seconds while validation is in progress (stale auto-refresh poll could overwrite the filtered table and re-arm itself with the old filter). Poll and user actions are now coordinated with hx-sync, and the toolbar buttons + filter form stay in sync with the current view.
  • DashScope (Qwen) inventory keys are now picked up for the qwen routing prefix (the qwen→dashscope id bridge was missing, so those keys were silently ignored by routing)

Changed

  • AGENTS.md now correctly documents that account cooldowns persist to SQLite (storage/cooldowns.py) instead of being in-memory only
  • CI now runs mkdocs build --strict so docs breakage fails the main pipeline

[1.1.0] - 2026-07-03

Added

  • Routing dashboard page (/dashboard/routing) — visualizes provider account try-order, priority, credits, cooldowns, and combo fallback chains
  • Inventory key priority column, sort, and editable priority in key detail modal

Changed

  • require_api_key defaults to on (config, CLI template, DB seed)
  • Disabling Require API Key in Settings requires two confirmation dialogs

[1.0.0] - 2026-07-03

First stable release. Janus is a local-first AI routing gateway with multi-provider fallback, key inventory, analytics, budgets, and a full dashboard.

Added

  • Key Inventory import page documents expected JSON format (wrapped keys array or bare array, field reference table)

Fixed

  • Dashboard sidebar stays fixed while main content scrolls; Sign out is always visible

[0.3.2] - 2026-07-03

Added

  • Docker entrypoint auto-fixes bind-mount permissions for /home/janus/.janus on startup

Fixed

  • Settings Require API Key control is now a toggle (same pattern as Token Savers) and persists correctly

Changed

  • Deployment docs: document Docker volume UID mismatch and manual chown fallback

[0.3.1] - 2026-07-03

Added

  • Key Inventory bulk-add preview with auto-provision of routing providers when adding upstream keys
  • Shared inventory tab navigation across all Key Inventory pages
  • Token Savers partial polling so toggle state syncs across devices/tabs
  • DeepSeek/Moonshot credit balances normalized from CNY to USD (INVENTORY_CNY_USD_RATE, default 0.138)
  • Analytics per-client API key labels in the breakdown (stores client_key_label on usage rows)

Fixed

  • Token saver toggles not persisting — HTMX now sends the correct checked value on save
  • Token saver toggle visuals (plain CSS instead of broken Tailwind peer-checked on CDN)
  • Providers dashboard 500 when inventory key stats were added to the template
  • Provider edit/test/fetch-models uses inventory keys when the provider has no direct API key
  • Analytics key breakdown showing — for named client keys
  • Tool Setup copy button alignment on /dashboard/tools

Changed

  • janus serve --host default remains 127.0.0.1; bind 0.0.0.0 explicitly for LAN access
  • Saver settings use DB defaults via ensure_saver_defaults() (RTK on by default when unset)

[0.3.0] - 2026-07-03

Added

  • Analytics Details view with a Sankey request-flow diagram (API Key → Model → Provider) and a Tokens/Cost/Requests metric toggle
  • Costs↔Tokens toggle on the analytics spend chart, plus finer time ranges (24h/7D/30D/60D/90D)
  • scripts/seed_openrouter_pricing.py — seeds per-model pricing overrides from the OpenRouter catalog
  • Gemini inbound client endpoint — Gemini-native tools can now talk to Janus directly via POST /v1beta/models/{prefix/model}:generateContent (and :streamGenerateContent)
  • Dashboard Test-Connection button — probes an upstream provider with a 1-token request and reports status + latency
  • Dashboard Export Config — download the current DB state (providers, combos, pricing) as a YAML file
  • Dashboard Reset-to-Defaults — clear DB tables and re-seed from config.yaml (danger zone on Settings page)
  • Cooldown state now persists across server restarts via SQLite
  • API key auth now accepts Gemini-style x-goog-api-key header and ?key= query param
  • Docker image published to ghcr.io/amanverasia/janus via CI (multi-arch: amd64 + arm64)

Fixed

  • Cost was recorded as $0 for vendor-prefixed models — PricingRegistry now matches names like openai/gpt-4o-mini against builtin/override keys
  • Startup crash when migrating upstream_keys rows (init_db read tuple rows by string key)

Changed

  • Dockerfile copies README.md and LICENSE so the hatchling build succeeds in CI
  • Dashboard mutations no longer wipe in-memory fallback cooldowns

[0.2.2] - 2026-06-25

Fixed

  • Provider logos now visible on dark background (white variant + CSS filter)
  • API key preserved when editing a provider (leave blank to keep current key)

[0.2.1] - 2026-06-25

Added

  • Fetch Models button in provider Add/Edit modals — auto-populates models from upstream API
  • Official provider logos via Simple Icons CDN in catalog gallery and provider cards
  • Root URL / now redirects to /dashboard

[0.2.0] - 2026-06-25

Added

  • Full dashboard CRUD: providers, combos, token savers, pricing overrides, and settings now manageable from the web UI
  • Provider catalog with 14 known providers (OpenAI, Anthropic, Gemini, Groq, DeepSeek, OpenRouter, etc.) — add providers with just an API key
  • DB-driven config: providers, combos, savers, and pricing stored in SQLite (YAML config file is now a one-time seed)
  • Hot-reload: dashboard changes take effect immediately without server restart
  • Combo editor with drag-and-drop model reordering (Sortable.js)
  • Token savers toggle page (RTK, Caveman, Ponytail) with Ponytail level selector
  • CLI Tool Setup page: copy-paste env vars for Claude Code, Codex, Cursor, Cline
  • Pricing page: view builtin model prices + add/edit/delete custom overrides
  • Settings page: toggle require_api_key from UI, view server config
  • Grouped sidebar navigation (Monitor / Manage / Access / System) with 13 pages
  • GPLv3 license, PyPI packaging (OIDC trusted publisher), MkDocs Material docs site

Changed

  • Config YAML is now a seed file — loaded once on first startup, then SQLite DB is source of truth
  • _build_provider() is called during lifespan/reload, not in create_app()
  • _ensure_db() now seeds config and reloads all state (for ASGITransport test compat)

[0.1.0] - 2026-06-25

Added

  • Core routing gateway with canonical intermediate model translation
  • Three format adapters: OpenAI, Anthropic, Gemini
  • Four provider executors: openai_compat, anthropic, gemini, opencode_free
  • SSE streaming translation between all supported formats
  • Multi-account fallback routing with cooldowns (429→60s, 5xx→30s, auth→300s, network→15s)
  • Named combos: ordered model sequences for automatic fallback chains
  • Token savers: RTK compression (default ON), Caveman terse prompt, Ponytail lazy-dev prompt (3 levels)
  • SQLite persistence: API keys (SHA256-hashed), usage tracking, budget storage
  • Pricing engine: 28 builtin model prices, YAML-overridable, progressive prefix matching
  • Budget enforcement: per-key and global daily limits with warn (80%) and block (100%) thresholds
  • Analytics: cost tracking, spend trends, success rates, per-model/provider/key breakdowns
  • HTMX dashboard with 7 pages: Overview, Providers, Combos, API Keys, Usage, Analytics, Budgets
  • CLI: serve, config-init, config-path, keys (create/list/revoke), usage (stats/cost/by-key), budgets (list/set/delete), pricing (list/show)
  • Docker support: multi-stage build, docker-compose with volume persistence
  • GitHub Actions CI: ruff check + format, mypy --strict, pytest
  • Connection pooling: shared httpx.AsyncClient per provider (100 connections, 20 keepalive)
  • Provider caching in app.state.providers keyed by config.id