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 forautoare ranked per request under a configurable strategy (balanced,cheapest,fastest,quality; defaultbalanced, 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;cheapestuses blended 0.3·input + 0.7·output pricing;quality/fastest/balancedrank on the recorded attempt signals (#257) with strict two-axis tiering. Request Logs record the resolved model next to the requested one and thex-janus-resolved-modelheader 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 theanalytics_savings_baselinesetting), 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 reusecompute_costover per-model token totals; subscription and unpriced traffic is excluded and reported separately, never counted as savings. New publicGET /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 newattempt_signalstable (7-day retention). Fallback-masked failures now count against the failing account. Data collection only; ranking arrives withmodel="auto". (#187) - Claude OAuth in Key Inventory — Connect now accepts Claude Code's
.credentials.json(and baresk-ant-oat…tokens) asclaude_oauthinventory 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
cursorapi_type was an unfinished scaffold (it posted OpenAI chat completions toapi2.cursor.shinstead of Cursor's native protocol). The executor, driver, catalog entry and Providers page option are gone, and creating acursorprovider now returns a 400. Existingcursorprovider 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_typehas 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_TOKENandJANUS_ALLOW_INSECURE_DEV_KEYfrom.env. Before, setting the key in.envhad 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-catalogmodel_count/visible_model_countand toggleable counts), every model of a selected provider, or a capped, provider-grouped search (match_total,truncated), with unusedcapabilities/selectedfields dropped.meta.paginationis gone fromstate/models;GET /dashboard/api/v2/modelsis 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 newPOST /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, 9routerproviderConnectionsentries,workos:tokens; refresh token kept), alongside Antigravity and Kiro JSON. Claude Code.credentials.jsonis recognized and rejected with a pointer to the Claude OAuth provider.
Changed¶
/dashboard/inventory/addand/dashboard/inventory/importredirect 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; genericsk-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_inflightreplaces the 50-eventlivering; dropsby_model,combos,global_budget,base_url);GET /dashboard/api/v2/modelsis paginated (defaultlimit=200, max 2000,limit=0rejected) 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-providermodels,combos,quota_warnings, and the staticrotation_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_dayssetting (default 365, clamp 7-3650) prunes the usage table via a throttled, fail-safe prune on the record-usage write path;upstream_key_historygains achanged_atindex and 180-day retention;get_unpriced_modelsis 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 treatsstatus=0rows 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_countandskipped_count, and dashboard imports land directly inpending_validationinstead of briefly keeping the exported routable status. (#202) - "Recheck all" keeps manual-review state — unidentified and
validation_pausedcredentials are no longer reset topending_validationwith 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 forprovider_id/key_idvalues outside[A-Za-z0-9._-]instead of interpolating them intoContent-Disposition;GET /dashboard/api/exportomits provider API keys unless?include_secrets=trueis passed (the Settings "Export secrets" button opts in, and each secret export is logged without the secrets); v2 state responses sendX-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 areprobe_inconclusiverather than invalid; antigravity onboarding polling is bounded toFETCH_TIMEOUTso it cannot monopolize a recheck slot. (#205)
[3.24.0] - 2026-09-29¶
Added¶
- Optional detailed gateway health checks —
GET /v1/health?details=truereports 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
--prunefor 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_defaultsseeding 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()(bareurlopenwithout url_guard), deadif limit elsebranches in the dashboard state API, and the unusedaccountStale/VALUE_STALE_MSdeclarations in the inventory keys page.streaming/translator.py(translate_stream) was removed outright — it duplicated the production cross-format path (_streaming_generatorinapi/routes.py), whichdocs/architecture.mdnow 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=/dashboardand markedSecureon HTTPS requests, and can no longer authenticate gateway/v1/*requests.POST /dashboard/loginis throttled per client IP (5 failures per 5 minutes → 429; success clears the counter), and all mutating dashboard routes reject requests whoseOriginheader 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
Hostheader 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 upstream502. 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_exhaustedalert 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 dashboardCLI 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_endpointcatalog 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-Afterreset time. - CLI and dashboard budget management —
janus keys create/update --absolute-budgetandjanus budgets set --absoluteconfigure 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
ClientDisconnectfromrequest.json()and uvicorn logged a full ASGI ERROR traceback on every abort. Format entry points now treat that as a quiet204client 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_labelvalues 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 localoffsetupdates 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_presetsheld the same object asproviders.catalog(the source read"catalog": catalog, "catalog_presets": catalog), duplicating 33,640 B or 25% of that payload, while the dashboard readdata.catalog_presets ?? data.catalogand only ever used one.inventory.top_keyswas 18,579 B, 47.9% of the inventory payload, referenced nowhere in the frontend, and cost aget_top_keys_per_providerdatabase 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.ymlanddocker.ymlboth depend on. The matrix covers 3.11 and 3.12 (the classifiers advertised 3.12 while CI tested only 3.11).pytest-covwas installed but unused; coverage is now reported, uploaded, summarised, and gated at 80% against a measured 83%.scripts/is linted and type-checked. Publishing runstwine 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 asGET /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/modelsso the list route still wins, and uses a:pathconverter 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
:latesttag was rejected by/api/show— the handler matched catalog names exactly, soopenai/gpt-5returned 200 whileopenai/gpt-5:latestreturned 404. Ollama treats an untagged reference as:latestand clients normalise to that form, so a model that could be chatted with could not be described; since Ollama clients call/api/showbefore 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 asmodel:v2still 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 emptyallowed_modelsparsed toNoneand wiped the stored allowlist. An edit now updatesallowed_modelsonly 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 nofont-sizeand 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 renderedUS$0.00in every non-US locale; "Spend today" renderedUS$26.4929because onemoney()helper served both aggregates and per-MTok rates (now split intomoney()at 2dp andrate()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 staleeditingand couldPUTto a deleted id; the selected Models group could never be collapsed; Leaderboard renderedNaN%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,100dvhfor 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_remainmatched only(?:9|10)px, so six 8px declarations, smaller than what the test was written to ban, passed it. The pattern is corrected, widened tolib/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 asworkos:<jwt>and refreshed via WorkOS (/user_management/authenticate);_validate_cline_keyprobes 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 freez-ai/glm-5.3-freetier on every key. Registered as prefixtr; only the free model is exposed (allowed_models) because the full keys advertise quota-locked SOTA models.
Fixed¶
requires_responsesover-application — the v3.11.0 promotion of Responses-only models to/v1/responsesnow applies only to the OpenAI gateway (target.prefix == "openai"). Otheropenai_compatgateways (Cline, gorouter, TokenRouter) servegpt-6-astravia/chat/completionsand were returning 404.insufficient_user_quotaquota classification — TokenRouter's unfunded-account 403 is now classified asno_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-astrastill intermittently returned an empty 200 to coding-agent clients for two reasons. (1) The OpenAI chat adapter only readmax_tokens, so coding agents / OpenAI SDK 1.x that sendmax_completion_tokensnever forwarded their token budget to the Responsesmax_output_tokensfield; without an explicit budget, reasoning could exhaust the default and the model returned empty.OpenAIAdapter.parse_requestnow acceptsmax_completion_tokensas a fallback (max_tokensstill 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 rejectstools+reasoning_efforton/v1/chat/completionsfor these models and requires/v1/responses. Janus now promotes the per-attempt wire format toopenai_responsesfor anyopenai-format model that sets the newrequires_responsescapability (gpt-6-astratoday, plus thegpt-6-astra*pattern), so a/chat/completionsclient gets a Responses payload built and posted to{base}/responses, then emitted back as chat.OpenAICompatProviderselects the upstream endpoint from the payload shape (input→/responses,messages→/chat/completions), and the thinking layer remaps the chatreasoning_effortscalar onto the Responsesreasoning: {"effort": …}field. The decision is per-request and gated on the model capability, so the same shared OpenAI keys keep using/chat/completionsfor 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-activedescendantcombobox/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, reflectingrequire_api_keywith a copyable runnablecurlcommand 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/snapshotfetch, 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-statelivepayload are removed, request events carry a monotonicseqwith 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/healthcontract 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-validatedproviderfilter inmeta.queryfollowing 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, andgrok-4-6). (#142, contributed by @hett-patell)
Fixed¶
- Codex account-plan restriction rotates accounts — free/Go ChatGPT
accounts reject
gpt-5.6-solwith 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_modelsrow even though most collapse to the same provider/model pair (audited database: ~116,500 rows for ~1,200 distinct pairs), andlist_model_ids_for_keysdeduplicated with an O(n²) list-membership check per key.list_catalog_modelsnow reads through the newlist_distinct_discovered_models, which collapses rows in SQL with a singleGROUP BYover packed(created_at, id, field)values, and the per-key helper usesSELECT DISTINCT. Result cardinality now scales with distinct provider/model pairs; on a seeded audit-sized database (116,000 rows → 40 distinct pairs)list_catalog_modelsdrops 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 otherhttpx.RequestErrorsubclasses now rotate to the next account with a network cooldown instead of surfacing as an unhandled 500. Previously onlyTimeoutExceptionandConnectErrorwere 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 viais_200_wrapped_errorand trigger fallback instead of being parsed as empty successes. (#104)
[3.4.0] - 2026-09-07¶
Fixed¶
- Cache-token accounting across adapters —
compute_costno longer double-bills cached tokens. It charged the full input rate onUsage.input_tokensand 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_tokensis the total prompt input andcache_creation_input_tokens/cache_read_input_tokensare subsets of it.compute_costbills only the uncached remaindermax(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
usagetable, which always reported 100% success. A newrequest_outcomestable 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 onusage(success-only rows); success rate, spend-summary request totals, usage stats, and leaderboard now read fromrequest_outcomesvia aggregated joins. Historicalusagerows are backfilled once (marker inPRAGMA 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_countersledger on every attempt instead of being re-seeded from success-onlyusagerows. 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 —
/dashboardand 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/uicovers 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
/dashboardHTMX interface remains available while/dashboard/uibecomes 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
httpxfingerprints
[2.1.4] - 2026-08-14¶
Fixed¶
- Codex usage-limit cooldowns — 429
usage_limit_reachedbodies (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_overloadedevents 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.RetryInforetry 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-afterandx-ratelimit-resetheaders are parsed alongsideRetry-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-typeheaders and surfacetoolUseEvent,reasoningContentEvent,messageStopEvent, andmetricsEventin both stream and JSON paths; strip<thinking>blocks while keepingcodeEventcontent raw; map stop reasons to OpenAI finish reasons; remove unavailableclaude-sonnet-4-thinkingfrom default models - Kiro vision — preserve inline
data:/ base64 image parts when translating canonical requests to Kiro's native envelope - Antigravity duplicate finish reason —
GeminiStreamParsernow 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
ListAvailableModelsdiscovery; 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 aninventory_providersrow 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_archivedstate: archived keys are hidden by default, excluded from routing/counts/recheck-all, and restorable under a newarchivedstatus 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
providerConnectionsarrays into Key Inventory; select Codex (ChatGPT); validate via OAuth refresh; expand into multi-accountcodex/<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, prefixollama) 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/modelsis 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_catalogtable: 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 orjanus 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 recomputeusage.costfor $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
fusionmode (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 viacombo_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_modelson providers (dashboard field, comma-separated,fnmatchglobs likeclaude-opus-*supported; empty = all models). Blocked models are rejected at routing time (400) and hidden from/v1/modelsand Ollama/api/tags. Lets you expose only selected models from a provider (e.g. onlyclaude-opus-4-7from Anthropic) - Request log "User" column — request logs now record and display which client API key made each call (
client_key_labelfor 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/ultralevels (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
503with aRetry-Afterheader derived from the earliest cooldown expiry instead of a generic400 - 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) andallowed_models(exact IDs orprefix/*wildcards; empty/unset = all models). Disallowed models return403witherror.type = model_not_allowed;GET /v1/modelsis 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/showand/api/generate—POST /api/showreturns stub model metadata for routable models;POST /api/generateaccepts a barepromptand remaps chat responses to Ollama's completion shape.GET /api/tagsnow respects the API key model allowlist (parity withGET /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_statushelper 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), andthinkingpassthrough. 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 theusagetable 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_copilotwith 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 endpointsPOST /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/compressbefore 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_logstable, 64 KB body truncation, last 500 requests kept). New dashboard page/dashboard/request-logswith 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) translatesinputitems, flat tool definitions,function_call/function_call_outputround-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 withgemini/ - 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 theusagetable 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) andinventory/catalog.py(29 inventory providers) now derive from it, along with thegoogle↔gemini/dashscope↔qwenid 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
qwenrouting prefix (theqwen→dashscopeid bridge was missing, so those keys were silently ignored by routing)
Changed¶
AGENTS.mdnow correctly documents that account cooldowns persist to SQLite (storage/cooldowns.py) instead of being in-memory only- CI now runs
mkdocs build --strictso 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_keydefaults 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
keysarray 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/.januson 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
chownfallback
[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_labelon 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-checkedon 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 --hostdefault remains127.0.0.1; bind0.0.0.0explicitly 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-keyheader and?key=query param - Docker image published to
ghcr.io/amanverasia/janusvia CI (multi-arch: amd64 + arm64)
Fixed¶
- Cost was recorded as $0 for vendor-prefixed models —
PricingRegistrynow matches names likeopenai/gpt-4o-miniagainst builtin/override keys - Startup crash when migrating
upstream_keysrows (init_dbread tuple rows by string key)
Changed¶
- Dockerfile copies
README.mdandLICENSEso 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 increate_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