更新日志
版本发布说明与历史记录。
Changelog
All notable changes to krouter will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[2.6.1] - 2026-07-05
Fixed
- Streaming usage from MiniMax-style Anthropic-compatible endpoints is no longer lost: the SSE usage parser read input/cache tokens only from
message_start(the official Anthropic shape), but MiniMax sends placeholder zeros there and the real cumulative usage — includinginput_tokensandcache_read_input_tokens— in the finalmessage_delta. Every streaming MiniMax request was therefore recorded as 0 input / $0, understating cost, budget and savings. Usage frommessage_startandmessage_deltais now merged per message with non-zero delta fields overriding; sincemessage_deltausage is cumulative per the Anthropic spec, this also stops periodic-delta implementations from being double-counted. - Cost lookup now finds provider-prefixed catalog entries: agents send bare model ids (
MiniMax-M3) but LiteLLM catalogs non-flagship vendors only under<provider>/<model>(minimax/MiniMax-M3), so the exact-match lookup priced such requests at $0 even when tokens were counted.CostFornow falls back to the provider-qualified key, translating krouter adapter names to LiteLLM's where they differ (fireworks → fireworks_ai). krouter logsshows raw token counts below 1K instead of rounding a 37-token request down to "0K", which had masked the zero-usage bug above.
[2.6.0] - 2026-07-05
Changed
krouter logsnow reads liketail: entries print oldest→newest so the latest request sits at the bottom (it used to be first), and the new-f/--followflag streams requests live as they complete — snapshot first, then the daemon's SSE feed, subscribed before the snapshot fetch so nothing is missed and deduplicated by id so nothing prints twice.-n/--lines(default 50) is unchanged.
Fixed
- OpenClaw takeover no longer crash-loops the gateway on OpenClaw ≥2026.6.9: connect used to stash each provider's original endpoint in a
_krouterOriginalBaseUrlfield insideopenclaw.json(and sub-agentmodels.json), relying on OpenClaw ignoring unknown fields. Since 2026.6.9models.providers.*is strictly validated and any unknown field fails the whole config ("Invalid input"), so the gateway refused to start after takeover. Originals are now stored in krouter's own~/.kinthai/openclaw-restore.json; disconnect restores from it (legacy in-file sidecars are still honored, and reconnecting — e.g. after upgrading krouter — strips them from the config, self-healing already-broken installs). An endpoint the user has meanwhile re-pointed by hand is never clobbered on disconnect. krouter install(CLI / headless) now actually registers connected agents with the daemon: only the GUI wizard wrotepending-agents.json, so a CLI install leftapp_settingsempty — the inheritance scan never ran, no endpoints were known, and every request through the rewritten agent configs was misrouted (e.g. MiniMax models sent to Anthropic → 502, DeepSeek to OpenAI → 401). The CLI now writes the same pending file for each successfully connected agent, connects agents before starting the service so a fresh install routes correctly from the first request, and the daemon additionally consumes pending selections on its 1-minute rescan tick so registration never waits for a restart.
[2.5.0] - 2026-06-11
Added
- Editable config paths are now discoverable in the Apps page: each app's scanned config path (e.g.
~/.pi/agent/models.json) is a clickable control with an always-visible "Edit" label and a hover affordance, instead of a faint 12px pencil icon that was easy to miss. Users whose agent isn't installed at the default location can click the path, correct it, and hit "Save & rescan" — no hunting required.
Changed
- Providers page only shows a protocol badge once a provider is configured: the protocol badge (and the Protocol detail row) used to appear on every card, including unconfigured ones — wrongly implying a provider is pinned to that single protocol. krouter routes by whatever protocol you actually configure and refuses none, so unconfigured providers no longer assert a protocol; configured providers still show the protocol they're actually in use with.
Changed
- Inherited credentials never touch disk anymore: API keys and OAuth tokens that the agent scanner reads from your AI app configs (OpenClaw, Codex, …) are no longer written to SQLite. They now live only in the daemon's memory, repopulated from your agent's own config files at startup, on the 1-minute periodic rescan, and on manual rescans — so rotating a key in your agent config is picked up within a minute and krouter never holds a stale copy. Migration 022 drops the
inherited_endpoints.api_keycolumn, scrubsoauth_tokenvalues out ofextras_json, and VACUUMs the database so the old secret bytes are physically erased from the file, not just logically deleted. You can verify with any SQLite browser:~/.kinthai/data.dbcontains no credentials, period.
Fixed
- Free models now win the Saver cheapest-model scan: the routing engine's pricing lookup used a bare
float64where0meant both "unknown model" and "free model", so a genuinely $0-priced model (e.g.:freecatalog entries) could never be selected — contradicting the documented "a $0 price falls out as cheapest automatically" behavior.PricingSource.InputCostPerTokennow returns(cost, ok); free models rank normally and unknown ones stay unranked. - Two cross-protocol model-name leaks: (1) Saver's multimodal branch hardcoded
claude-sonnet-4-5for any protocol, sending an Anthropic model id to OpenAI-protocol providers on image requests; it now honors the requested model outside the Anthropic protocol. (2) Quality's Opus-24h-cap downgrade rewrote even OpenAI-protocol requests toclaude-sonnet-4-6; the downgrade is now Anthropic-only. - Circuit breaker now sees mid-fallback failures: a provider that returned 5xx (or a network error) and was abandoned for a fallback never had the failure recorded — only the final attempt's outcome was logged — so a consistently broken provider with a working fallback never tripped the 3-failure breaker and every request probed it first. Abandoned attempts now record their failure.
GET /v1/modelsno longer forwards third-party keys to Anthropic: the endpoint unconditionally proxied toapi.anthropic.comwith all request headers, including API keys belonging to OpenAI-protocol providers. Requests without Anthropic-style credentials (x-api-key, or ask-ant-Bearer token) are now answered locally from the registry's OpenAI-protocol model list; nothing is forwarded.- Session stickiness no longer breaks on provider registration order: the bound-provider liveness check resolved the provider via "first registry entry supporting the model", so a session bound to a later-registered provider was wrongly treated as unbound and lost its prompt cache. The provider is now looked up by name.
- Failed responses no longer pollute session state: non-streaming 4xx/5xx responses used to bind the session's sticky target and bump its counters; only 2xx responses update sessions now.
- Image payloads no longer inflate the token estimate:
InputTokenEstcounted base64 image bytes as text (~330k "tokens" per MB), misclassifying every image request as complex and distorting cost estimates. Image block payload size is now excluded. - Anthropic-tier fallback checks for a configured key before offering a lower tier, consistent with the rest of selection (#47).
- Codex scanner no longer resolves
env_keythrough the daemon environment: krouter does not read environment variables for credentials (privacy); the lookup also could not see user-shell exports, so it only ever misfired. The request's own key is forwarded as-is instead. - Hung upstreams now fail fast: outbound transports set a 60s
ResponseHeaderTimeout(streaming bodies remain unbounded), so a provider that accepts the connection but never responds triggers fallback instead of holding the request open indefinitely. - Inherited keys now resolve across vendor name aliases: a provider you configured in your agent under its natural vendor name (e.g.
dashscopein OpenClaw) is now matched to krouter's adapter name (qwen) when resolving the inherited API key. Previously the lookup was an exact string match, so such a provider showedconfigured: falseon the Providers page and couldn't be routed to — even though the credential was present — unless you renamed it in your agent config to match krouter's name. Known aliases (dashscope→qwen,together_ai→together,fireworks_ai→fireworks) are now collapsed at lookup time; the original name is still stored as scanned. - Cold-start
500 database erroron the management API: the SQLite DSN used mattn/go-sqlite3-style parameters (_journal_mode,_busy_timeout) that the actual driver (modernc.org/sqlite) silently ignores, so the database ran withjournal_mode=delete(writers block readers) andbusy_timeout=0(no wait on contention) instead of the intended WAL + 5s timeout. During the first few seconds after startup — while the embedded pricing seed (2745 models) is written — concurrent reads/writes hitSQLITE_BUSY, so endpoints like/internal/logs,/internal/models, and/internal/apps/configuredbriefly returned500. The DSN now uses modernc's_pragma=syntax so WAL and the busy timeout actually take effect; a cold-start stress test that previously produced 52500s now produces zero.
Changed (internal)
- Routing decisions read quota state once per request and the daemon-side quota source caches it for 3s, removing several per-request SQL aggregations from the hot path. Request-record persistence moved from one goroutine per request to a single writer goroutine fed by a bounded queue, serialising SQLite writes. Agent scanners and the upgrade-apply goroutine are now panic-isolated. The MiniMax in-memory OAuth token cache expires after 30 minutes. App-config connect diffs mask key/token/secret values before leaving the daemon.
For contributors
- Integration auth tests were updated to the Origin-based CSRF model (foreign Origin → 403, valid token bypasses the guard), replacing the obsolete token-required assertions. The wine/linux integration suites now live under
tests/in the repo (previously workspace-local);tests/wine/run_tests.shwas brought up to date with the current auth model, UI path, and preset endpoint, and twoset -ebugs that aborted the suite early were fixed.
[2.4.10] - 2026-05-27
Changed
- Pricing data pipeline: LiteLLM → krouter CDN → daemon: the daemon no longer fetches directly from LiteLLM's raw GitHub JSON. A new daily GitHub Action (
update-litellm-prices.yml) fetches from LiteLLM, merges with the local supplement file (data/token_prices_ext.json), and commitsdata/token_prices.jsonto the repo in a stable krouter-owned format. The daemon fetches fromkrouter.kinthai.ai/data/token_prices.json(primary) withraw.githubusercontent.comas fallback — the same CDN + GitHub Raw pattern used by subscription pricing. This isolates all krouter daemons from LiteLLM format changes: a format break now only affects the daily Action and is fixed with one commit. The file is also embedded viago:embedso freshly-installed daemons have full pricing data offline without waiting for a network sync.
Added
data/token_prices_ext.json: manually-maintained supplement for models absent from LiteLLM. Initial entries:claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,deepseek-coder,MiniMax-M2.7,MiniMax-M2.7-highspeed. Prices sourced from respective vendor pricing pages.
[2.4.8] - 2026-05-27
Fixed
- Anthropic streaming token counts always 0 for opus-4 and newer models (#72): when a client (e.g. OpenClaw) sends
Accept-Encoding: gzip, krouter was forwarding it to Anthropic, which then returned a gzip-compressed SSE stream. Because Go's Transport only auto-decompresses when it added theAccept-Encodingheader itself, the SSE capture buffer received raw compressed bytes — causingparseAnthropicSSEUsageto return 0 for all token buckets. The Anthropic and OpenAI adapters now stripAccept-Encodingbefore forwarding, so Go's Transport manages compression negotiation and transparently decompresses both streaming and non-streaming responses.
[2.4.7] - 2026-05-27
Fixed
- Quality preset no longer over-upgrades simple requests to opus (#65):
ComplexityScorepreviously treatedHasTools=trueand system-prompt keywords (e.g. "debug", "refactor") as complexity signals, which caused every Claude Code / Cursor request to score as "complex" and get upgraded to opus — bypassing the MiniMax subscription benefit. Those signals are removed; complexity now depends only onHasImagesandInputTokenEst(>10 k tokens → complex, 4 k–10 k → medium). - Apply Update button no longer stays stuck on "Restarting…" (#71): after the daemon restarts from a self-update, the About page now polls
/internal/statusevery 1.5 s until the version changes (or a 2-minute timeout), then resets the mutation state and re-runs the update check so the UI reflects the new version instead of staying frozen.
[2.4.6] - 2026-05-26
Changed
- Renamed "agent" → "app" throughout: the word "agent" was ambiguous — it referred to both the AI applications (openclaw / claude-code / cursor / codex) that are krouter's direct clients, and to named configuration profiles inside those applications. The canonical term is now "app" at every layer: DB columns (
agent_settings→app_settings,agent_id→app_id,requests.agent→requests.app), Go types and function names, HTTP routes (/internal/agents/*→/internal/apps/*), SSE / JSON field names, the web dashboard ("Agents" page → "Apps" page), and i18n keys (agents.*→apps.*). TheSubAgentSectioncomponent is removed — individual agent profiles inside an app are not observable from HTTP traffic and are not shown in the UI.
Added
- Key-hint routing channels on the Apps page: the
requeststable gains akey_hintcolumn (last 4 characters of the api_key extracted fromx-api-keyorAuthorization: Bearerheaders). The Apps page now shows the distinct routing channels used by each app — each channel corresponds to one api_key suffix, its request count, and its cost.NULLfor pre-migration rows;""for requests with no api_key. Migrations 018 and 019.
[2.4.5] - 2026-05-26
Changed
- DecisionCard header redesigned: savings pill, routing summary line, and app chip moved into the card header with a shadow separator between the latest card and the history list.
[2.4.4] - 2026-05-26
Added
- Passthrough preset: new fourth routing mode (
passthrough) that forwards the exact model the client requested without any substitution. The routing engine resolves which provider serves the model and passes through unchanged; quota downgrade and cache-sticky routing both skip passthrough. Migration 017 adds arouting_presetcolumn to therequeststable; all fourlogRequestcall sites record the active preset. The Dashboard KPI strip relabels "requests" as "routes" and adds a per-preset breakdown row (saver / balanced / quality / passthrough) showing request count, savings USD, and savings percentage.
Removed
- Dashboard "Recent Requests" section: the Router page already surfaces this data in a better format; the duplicated table on the Dashboard was removed.
[2.4.3] - 2026-05-26
Changed
- Router page redesigned: routing history is now a scrollable list of inline cards with infinite scroll (replaces the fixed-height table). A "Logs" button in the header links to the full searchable archive. Token breakdown split into 4 labelled rows (input / output / cache-read / cache-write).
[2.4.2] - 2026-05-26
Added
- Cache read/write pricing in routing decision card:
cache_read_per_mtokandcache_write_per_mtokappear in the Request section of each decision card. Rows are hidden for models without prompt-caching support (e.g. DeepSeek, MiniMax), so non-Anthropic cards are unaffected. - Per-request cache hit rate in response card: observed hit rate (
cache_read / total) is displayed alongside the token breakdown in the expanded card. - Gzip-compressed upstream response support: krouter now handles
Content-Encoding: gzipresponses by forwarding the original compressed bytes to the client unchanged while decompressing a separate in-memory copy for token parsing.
Fixed
- Cache hit rate math corrected:
ObservedHitRate(this turn:cache_read / total) andPredictedHitRate(next-turn estimate:(cache_read + cache_write) / total) are now distinct. Sticky routing uses the predicted rate; the decision reason string shows the observed rate.OutputSharedenominator now correctly includesCacheWriteTokens. - Transparent gzip passthrough: the
decodeResponseBodyhelper that was strippingContent-Encodingbefore forwarding is replaced bydecompressForParsing, which reads a separate in-memory copy for token counting while leaving the original bytes and headers intact for the client.
Removed
X-Krouter-ProviderandX-Krouter-Modelresponse headers: these headers were injected into every forwarded response, modifying transparent passthrough silently. Routing information is available via/internal/logsinstead.
[2.4.1] - 2026-05-26
Added
- Cache-write tokens surfaced in all UI pages:
cache_write_tokensis now tracked end-to-end from database to the browser. Logs CSV export gains acache_write_tokenscolumn; the Providers page shows a "Lifetime cache-write" row when non-zero; the Agents page token-total column now sums all four token buckets (fresh input + output + cached + cache-write); the RoutingDecision card appends "N cache-write" to the token breakdown when cache writes occurred. SSErequest_completedevents carry the field so live views update without a page refresh. Translations added for both English and Chinese.
[2.4.0] - 2026-05-26
Added
- Cache-aware sticky routing (Phase 3): the routing engine now tracks per-session token-bucket stats (fresh input / cache-read / cache-write / output) and short-circuits to the session's bound (provider, model) when the observed cache hit rate exceeds the mathematical breakeven threshold
p* = (1 − 1.25 × P_candidate / P_bound) / 0.9. Switching model mid-session requires paying a 25% write surcharge on a cold cache; sticky routing fires only when staying put is mathematically cheaper. Active for Saver and Balanced presets; Quality preset is unchanged. Eight decision rules cover output-share guard (>30% falls through), bound-provider liveness, cheapest-alternative scan, and breakeven comparison. - Session tracking infrastructure (Phase 2):
MemSessionStore(in-memory, 75-minute TTL, background eviction) accumulates cache hit rates across turns in the same agent conversation. Session key is a 16-char SHA-256 fingerprint of(api_key + system_prompt + tool_names + first_user_message)— stable across turns, distinct across agents and users. Data feeds Phase 3 routing decisions without being persisted to disk. - Session-aware savings annotation:
enrichDecisionnow computes the savings percentage using the actual session cache hit rate (keptCost = price × tokens × (hitRate×0.1 + (1−hitRate))) instead of a bare per-token comparison that ignored cache state. Falls back to the conservative cache-cold estimate on the first turn of a new session.
Fixed
- Cache write cost was under-charged by 25%:
pricing.CostForapplied the standard input rate tocache_creation_input_tokens; Anthropic charges a 1.25× surcharge on cache writes. Fixed with a newcacheWriteTokensparameter; all call sites updated. - OpenAI
cached_tokenswere never parsed: the OpenAI and DeepSeek response parsers readprompt_tokensas the full input count without subtractingcached_tokens/prompt_cache_hit_tokens, so cache-read tokens were billed at the full input rate instead of 10%. Both JSON and SSE parsers now extract the two cached-token fields and adjustInputTokensto fresh-only. cached_tokenswere always 0 in the database: none of the fourlogRequestcall sites populatedCachedTokensfrom the parse result. All four sites now carry bothCachedTokensandCacheWriteTokensinto the DB row.- Savings percentage was computed from wrong baseline:
enrichDecisioncompared the routed model's cost against the requested model's cost without accounting for cache state, producing numbers that could be badly wrong for long sessions with high hit rates. The display was removed in Phase 1 and restored in Phase 3 with the session-aware formula.
Changed
storage.RequestRecordgains aCacheWriteTokens intfield; migration016adds thecache_write_tokenscolumn (nullable, default 0 — safe on existing databases).pricing.CostForandpricing.BaselineCostForsignatures gaincacheWriteTokens intas the last parameter. All internal call sites updated; external callers (none outside this repo) must add the parameter.
[2.3.14] - 2026-05-26
Fixed
- MiniMax requests no longer 401 when re-routed via the subscription shortcut (#63): the MiniMax adapter forwarded the inbound auth header as-is, so a request the engine re-routed here carrying another provider's credential (e.g. an OpenClaw
claudesub-agent sending an Anthropicx-api-key) reached MiniMax without its OAuth Bearer and was rejected. The adapter now injectsAuthorization: Bearer <token>(resolved frominherited_endpoints.extras_json— scanned fromauth-profiles.json— falling back to the in-memory request-header cache) and drops the stalex-api-key. When no token is resolvable it falls back to transparent passthrough, so a directminimax-portalrequest is unaffected. - Budget-blocked requests are written to the dashboards (#66): the
ErrBudgetExceededpath returned429without logging, so budget-blocked requests vanished from Router/Logs and per-provider stats (the #52 fix only covered the fallback-exhausted502path). Both handlers now write a durable429log row before responding. - No more negative "savings" for unpriced models (#64): after #53 (a
$0baseline for models absent from the pricing catalog), the per-request savings banner renderedbaseline − costas a negative number. The daemon now omits the baseline from the live event when it is0(matching/internal/logs), and the UI renders the savings banner only when the baseline is positive — an unpriced model shows no banner instead of a misleading loss.
[2.3.13] - 2026-05-26
Changed
- Application attribution is now decided at connect time, not guessed from headers (#49): connecting an app bakes its id into every base URL krouter writes — the origin (
scheme://host:port) is replaced withhttp://127.0.0.1:8402/a/<appid>while the original path is preserved verbatim (so/v4,/anthropic/v1, … survive; no protocol guessing, no/v1insertion). The proxy strips the/a/<appid>prefix, attributes the request to that application deterministically, and dispatches to the protocol handler by path suffix (/messages,/chat/completions,/models). Requests without the prefix fall back to the legacyUser-Agent/header sniff; an unknown app id or unsupported suffix returns 404. OpenClaw's OpenAI-protocol traffic (DeepSeek / Z.AI / …) is now logged underopenclawinstead ofunknown. Applies to all six connectable apps (OpenClaw global + per-agent providers, Claude Code, Cursor, Codex, OpenCode, Hermes), with the user's original endpoint saved for exact restore on disconnect. Internal:routing.Request.AgentNamerenamed toAppID(therequests.agentcolumn is unchanged).
[2.3.12] - 2026-05-26
Added
- OpenClaw takeover now covers every configured provider, not just Anthropic (#59): connecting OpenClaw used to redirect only the global
anthropicprovider (plusminimax-portal) to krouter, so its OpenAI-protocol providers (DeepSeek, Z.AI, OpenAI, …) kept talking to the upstream directly and could never be routed or saved on. Connect now rewrites every provider in the globalmodels.providersand in each sub-agent'sagents/<id>/agent/models.json, using the bare proxy base for anthropic-family providers and the/v1base for openai-family ones. The original endpoint is stashed in a krouter-managed_krouterOriginalBaseUrlsidecar so disconnect restores it exactly;apiKeyis never touched.
Removed
- Dropped the redundant
model_catalogtable (#60, migration015): it was written by the pricing sync but never read for routing — model counts and prices come fromtoken_price_api, and routable availability now comes from/v1/modelsdiscovery.
Fixed
- Providers recover after transient failures instead of being excluded forever (#48):
isHealthytreatedconsecutive_failures >= 3as permanently unhealthy, andcheapestProviderModel/mostExpensiveProviderModelskip unhealthy providers — so three transient 4xx/5xx (e.g. during a key-refresh window) demoted a provider with no way back, since nothing routed there to clear the count. A provider now gets a half-open probe oncehealthRecoveryTTL(5 min) has elapsed since its last failure; a success clears the count, a failure re-arms the window. - Savings numbers are no longer fabricated for unknown models (#53):
BaselineCostForused to substitute claude-sonnet-4-5's price when the requested model wasn't in the catalog, inventing a "savings" figure against a price the user never faced — and contradictingPriceFor, which returns 0 for the same model. It now returns 0 for unknown models, so the Router/Logs/budget savings only reflect a real baseline-vs-actual comparison. - Failed requests are no longer silently dropped from the dashboards (#52): when the fallback chain was exhausted (e.g. all upstreams unreachable), the proxy returned
502without recording anything — the request vanished from Router/Logs and per-provider stats. Both the Anthropic and OpenAI handlers now write a durable log row (status 502, last attempted provider/model, latency, error message) before responding.logRequestskips aprovider_statusfailure write when no provider was attempted. - Routing is deterministic and never selects keyless providers (#46, #47, #51):
Registry.All()/ForProtocol()now iterate in registration order instead of randomized Go map order, so two identical requests route reproducibly (previously the same request could land on different providers ~30s apart). AndpickHealthyProvider,pickProviderForModel, andfallbackOpenAInow skip providers without a configured key — consistent withcheapestProviderModel/mostExpensiveProviderModel— so routing no longer wastes a request on a guaranteed 401, and the fallback chain stops churning through keyless providers (fireworks → gemini → xai → …) one 4xx at a time.
Changed
- Routing model availability now comes from live
/v1/modelsdiscovery instead of the LiteLLM catalog (#60): the engine decides which models a provider can serve from its discovered model list — the accurate, per-key signal — and the LiteLLM sync now supplies per-token pricing only. The proxy lazily discovers a provider's models using the API key from the live request (async, deduplicated, 24h stale-guarded), so agents whose key krouter cannot read from config — Cursor (OS keychain) and Claude Code (shell env) — are covered as soon as traffic flows. Until a provider has been discovered its model list is empty and routing passes the request through unchanged. - Agent-config rescan polls every minute and skips unchanged files (#61): the periodic rescan re-parsed every config and rewrote
inherited_endpointson every tick. It now stats each scanner's input files and skips the parse / DB write / SSE broadcast when nothing changed since the last scan (reusinglast_scanned_at, no new column), so the cadence tightened from 5 minutes to 1 for faster pickup of edits while staying near-free when idle. OpenClaw's watch set includes each sub-agent'smodels.json/auth-profiles.json, so profile and OAuth-token edits are still detected even when the main config is untouched. - Post-connect restart notice is backend-driven (#62): the connect response now carries
needs_restart+restart_kind(shellfor env-based agents like Claude Code,processotherwise), and the dashboard renders the hint from that instead of hard-coding it per agent id — so every agent, including Hermes, gets the correct notice. krouter still never restarts the app itself; the hint clears on disconnect. - Settings & Notifications pages aligned with the dashboard style; top-bar settings icon is now a gear: the top-right settings icon switched from a sliders glyph (
Settings2) to the conventional gear (Settings). The Settings page's sections (Language, Data management) now use the same titled-Panelstyle as the dashboard panels, and both Settings and Notifications widen tomax-w-6xlso their left edge lines up with every other page. Notification cards adopt the shared card tokens.
[2.3.11] - 2026-05-25
Changed
- Dashboard logo unified to the kinthai.ai brand mark: the top-bar logo and browser favicon now use kinthai.ai's logo instead of the krouter-specific glyph, bringing the dashboard in line with the parent brand (and matching the krouter website).
[2.3.10] - 2026-05-25
Changed
- Page widths unified for a consistent left edge: the Free, Providers, and Budget pages widened to
max-w-6xl, matching Dashboard / Logs / Router / Agents, so content fills the canvas and the left margin no longer jumps when navigating between pages. About also moved tomax-w-6xlbut lays its (sparse) Version / Resources cards in a two-column grid with the update banner full-width below it, so it aligns with the other pages without leaving the right half blank.
[2.3.9] - 2026-05-25
Changed
- Agents page reframed around the AI agent rather than the host app: the page used to show one card per app (OpenClaw, Claude Code, Cursor, …). Operator framing surfaced the issue — it's the AI agent that calls providers and burns tokens; the host app is configuration metadata for that agent. The page now leads with a "Connected agents" section showing every agent currently routing through krouter, and pushes detected-but-disconnected hosts into a compact "Available" section below. For multi-profile hosts (OpenClaw), each sub-agent profile (
claude,deepseek,main, …) is rendered as a prominent stacked panel inside the card rather than buried behind a collapsed toggle. The page container widened frommax-w-3xltomax-w-6xl(Router-page parity) so the left margin no longer wastes screen real estate on dense pages. Every provider name reference (single-profile inherited list + per-sub-agent provider rows) is now a click-to-deep-link chip pointing at/providers#provider-<name>. - Providers page supports
#provider-<name>deep-links: eachProviderCardcarries anid="provider-<name>"anchor; navigating with that fragment auto-expands the matching card and scrolls it into view (scroll-mt-20keeps it clear of the sticky header). Enables one-click jumps from the Agents page (and anywhere else we want to refer to a specific provider) without forcing the user to scroll the active/inactive list. - Top-nav version chip removed: the
vX.Y.Ztext next to the brand was rarely scanned and crowded the LIVE indicator on narrow viewports. Version information remains on the About page and in/internal/status.
[2.3.8] - 2026-05-25
Added
- Free-providers catalog is now bilingual (English default, Chinese overlay): the curated
data/free_tokens.jsoncontent (provider summaries, conditions, setup hints, notes, and a few CJK display names) shipped Chinese-only, so the English-default dashboard still showed Chinese on the Free page. The base string fields are now English; each provider carries an optionali18nobject shaped{ "zh": { "free_summary": …, "conditions": …, … } }with the translations, and the Free page overlays the user's current UI language, falling back to the English base whenever a field/language is absent (alternate-protocol setup hints are localized the same way). Carried through storage by a new additivei18n_jsoncolumn (migration014); the sync schema stays version 1 so older daemons ignore the unknowni18nkey and keep rendering English. No routing impact — this is catalog/discovery content only. - Agents page surfaces OpenClaw sub-agent profiles: OpenClaw hosts multiple named profiles (
main,claude,deepseek, …) each with their own primary model and per-profile provider configuration in~/.openclaw/agents/<id>/agent/models.json. The dashboard's Agents card now lists every sub-agent under OpenClaw — display name, primary model id, OAuth presence chip, and a click-to-expand provider list (provider name, protocol, base URL, model id list, "this provider's primary model" highlight, key-configured chip). New read-only endpointGET /internal/agents/{name}/sub-agentsreturns the breakdown; secrets stay on the daemon (the response carrieshas_api_keybooleans, never the raw keys). The existing flatinherited_endpointsand routing engine are untouched — this is a UI-only surface.
Fixed
- Routing fallback now respects the request protocol: when an OpenAI-protocol request named an unknown model (e.g.
baidu/cobuddy:free), Balanced and Quality presets fell back to a hard-codedclaude-haiku-4-5-20251001— an Anthropic-only model name — and sent it to an OpenAI provider (mistral / groq / etc.). The upstream rejected with HTTP 401 / 400 because the model id wasn't theirs to serve. Fix: newfallbackModelFor(protocol)returnssaverOpenAIModel(deepseek-chat) for OpenAI andsaverAnthropicModel(claude-haiku-4-5-20251001) for Anthropic. The two call sites indecideBalancedanddecideQualityuse it. Adds 2 regression tests using a multi-provider registry that exercise the openai unknown-model path end-to-end.
Changed
- Routing no longer uses the curated
data/free_tokens.jsoncatalog as a decision filter: the spec/06 "free-credit-first" shortcut intersectedinherited_endpointswith the catalog and preferred the result over the paid cheapest path. The problem the user surfaced: the catalog can never enumerate every free provider in the world, so any user-configured free provider not on the list got silently routed at "paid", and inherited entries that happened to be expensive (e.g. Anthropic) won over actually-cheaper alternatives just because they were catalogued. The right "is this free?" signal is the per-token price intoken_price_api— a model priced at $0 falls out as cheapest automatically without a special-case path. Removed: therouting.FreeProviderSourceinterface,engine.WithFreeProviders,cheapestFreeProviderModel,cheapestPaidProviderModel, and thecmd/krouter/freeProviderSourceimplementation that joined catalog ∩ inherited. The catalog data layer (internal/storage/free_providers.go,internal/freeproviderssync,/internal/free-providersAPI, the dashboard's "Free tokens" page) is kept — it's discovery / signup UI, not a routing input.
[2.3.7] - 2026-05-25
Changed
- Web UI redesigned to a high-contrast "pro control panel" theme: the dashboard moved away from the generic admin-template look. Secondary text was the worst offender —
gray-400/gray-500were hard to read — so the entire neutral ramp is remapped to a cooler, higher-contrast scale and the brand color is now emerald (#0fa46a). The Dashboard leads with a KPI strip (today's requests / saved / spent / saved-%, each with an accent rail), a LIVE indicator in the top bar, a provider-distribution panel with inline bars, and a recent-requests table with a status column. Shared primitives (PageHeader,Panel,Badge,StatusDot) give every page — Router, Logs, Budget, Providers, Agents, Settings, Free, About, Announcements — a consistent bordered-card layout. Figures render in the system monospace stack (font-mono tabular-nums); no webfonts are bundled, so the UI stays offline-capable and renders CJK through the system sans.
[2.3.6] - 2026-05-24
Added
- About page now triggers a fresh update check on open: previously the page rendered cached state from the daemon's 24 h ticker, so a user who opened it 10 hours after a new release saw "no update available" until the next tick. Now the page fires a synchronous check the moment it mounts and shows one of four states: a spinning loader with "Checking for updates…", a green check with "You're on the latest version" (with a small "Check again" link), the existing yellow "Apply Update" banner when something newer is on the manifest, or a red error block with retry on network/signature failure. New endpoint
POST /internal/update-checkcallsupgrade.Service.CheckNow(ctx)synchronously and returns the same JSON shape as/internal/update-status.
[2.3.5] - 2026-05-24
Changed
- Agents page: unified card per agent — the separate "Agent Inheritance" overview section and per-agent detail cards have been merged. Each agent now appears as a single card that combines scanner metadata (config path, inherited providers, last scan time), proxy connection state (Connected / Not Connected badge), enable/disable toggle, and the full stats + logs panel. Eliminates the duplicate listing where the same agent appeared twice on the page.
- Top nav streamlined — nav items are now text-only (icons removed). Settings and Notifications move to the top-right icon cluster: a gear icon (⚙) links to Settings, a bell icon (🔔) links to Announcements. The bell shows a red dot when there are unread announcements. "Free tokens" nav label shortened to "Free".
[2.3.4] - 2026-05-24
Fixed
- FreeTokens page: hardcoded Chinese/English strings replaced with i18n keys: the
ProviderRowcomponent was rendering region labels ("国内" / "INT'L"), free-type badges ("试用赠送" / "永久免费" / "免费层"), configured/exhausted badges, validity/conditions meta, dual-protocol hint, apply/visit-site CTA, and last-verified footer — all hardcoded. All 14 label groups are now driven byen.json/zh.jsonkeys underfreeTokens.*. The English-default UI is now fully correct; Chinese appears only when the user switches to 中文. - Dashboard "Quota" section header was hardcoded English: now uses
t('dashboard.quota'). - About.tsx update description was hardcoded English: "Version X is ready to install." now uses
t('about.update_ready', { version }). - Protocol-constraint explanation in FreeTokens was Chinese-only:
freeTokens.howto_line2is now written in English inen.jsonand Chinese inzh.json, with proper i18n structure in both locales. 💡emoji in FreeTokens dual-protocol hint replaced with LucideInfoicon: emoji-as-icon is an anti-pattern in dashboard UIs (no scale control, OS-dependent rendering). The indigo hint panel now uses<Info className="w-3 h-3 text-indigo-500" />.
Changed
- Language toggle added to the main header: a small
EN / 中button appears in the top-right of the sticky navigation bar. Clicking it callsi18n.changeLanguage()and writes tolocalStorage(keykrouter:lang) for persistence across reloads. The toggle shows the other language as the action label ("中" when the UI is in English, "EN" when in Chinese). - Contrast improvements across all pages (WCAG AA): functional text that previously used
text-gray-400(contrast 2.96:1, below the 4.5:1 AA threshold) has been upgraded totext-gray-500(contrast 4.60:1, passes AA). Affected: page subtitles, stat labels, table column headers, timeline timestamps, section-divider text, install-wizard notes. Decorative elements (icons, transient loading pulses, truly de-emphasised footnotes) remain atgray-400. - Dashboard visual hierarchy: the "Today" stat card receives a 3 px brand-green left border to distinguish it as the live-state card vs the "This Week" summary card. All section headers normalised to
text-xs font-semibold uppercase tracking-wider text-gray-500. Weekly savings amount usestext-emerald-600for consistent color-coding. All numeric values gaintabular-numsto prevent layout jitter. Provider section dividers gainfont-semiboldweight.
[2.3.3] - 2026-05-24
Changed
- Dashboard layout: left sidebar → top nav bar: the sidebar made content cramped on narrower viewports (≤ 1280 px Router / Providers pages got squeezed). The nav is now a single sticky header at the top of the page; brand + version on the left, all 10 nav items in a horizontal row that wraps when needed. Content area is no longer competing with a 224 px aside, so dense pages (Router decision card, Providers cards, Logs rows) get the full viewport width. Removed: the collapse / expand toggle button and the
krouter:sidebar-collapsedlocalStorage preference (a vestige of the old aside);nav.collapse/nav.expandi18n keys are gone too. The Notifications red-badge moves to inline-with-the-nav-label on the top bar.
[2.3.2] - 2026-05-24
Changed
- Free LLM credits promoted from a Dashboard card to a top-level nav item: the catalogue now lives at
/krouter/free-tokenswith its own page header, subtitle, and proper empty-state — sidebar entry "Free tokens" / "免费 Token" sits between Dashboard and Router with the Gift icon. The oldFreeProvidersCardcomponent (and its standalone test) are removed; the Dashboard's per-section block is replaced by a one-line comment pointing at the new page. Rationale: the catalogue's discovery / dual-protocol explainer / configured-vs-available split is a substantial feature that deserves a peer-level surface, not a Dashboard sub-card. - Subscription providers (MiniMax, …) merged into the Providers page: each subscription-based provider now gets a purple
SUBSCRIPTIONbadge in the collapsed header. Expanding the card surfaces a new Subscription quotas section listing every active scenario — each row uses the more standard "X / Y (Z% used)" Western dashboard format with a USED-fill progress bar (emerald → yellow → red traffic-light tones), the local-time window and reset countdown, plus the per-call cost when known. The standaloneSubscriptionQuotaCardon Dashboard is removed (data shows on Providers); the previous "remaining + reverse-fill bar" representation is gone. A refresh button on each subscription block calls/internal/subscription/refreshwith the provider name. - MiniMax now persists all subscription scenarios, not just
MiniMax-M*:parseQuotaResponsepreviously dropped six of the seven scenarios MiniMax'stoken_plan/remainsendpoint returns (speech_synthesis,lyric_generation,image_generation,music_generation,mcp_image_understanding,mcp_web_search, plus the original text plan). All are now stored assubscription_quota_cacherows with their rawmodel_nameasmodel_pattern. A frontend scenario-name mapper recognises the common shapes and shows friendly i18n names ("Text generation" / "文本生成" etc.); unknown names fall through to the raw value, so future scenarios surface immediately without a UI change. Zero-quota / inactive rows are still skipped.
Fixed
- Apply Update now actually restarts the daemon: after successfully replacing the binary on disk, the daemon broadcasts an
update_restartingSSE event (so the dashboard shows "Restarting…"), waits 300 ms for the event to flush, then usessyscall.Exec(Unix) / spawn-and-exit (Windows) to replace the running process with the new binary. Previously the process kept running indefinitely, reporting the old version.
[2.3.1] - 2026-05-24
Added
- HTTP status code explanations in the dashboard: routing-decision rows, expanded cards, and the Providers "consecutive failure" banner now translate raw codes (
401,402,429,503, …) into plain English / Chinese so users don't have to memorise what each one means. Every status pill carries a hover tooltip with the code + explanation (e.g.402 — Payment required: provider quota exhausted or account out of credits); non-2xx codes also get an inline explanation below the latency line in the expanded view and below the failure-streak count on the Providers card. New helperfrontend/src/lib/statusCode.tsmaps the common codes (200/201/204/400/401/402/403/404/408/409/413/422/429/500/502/503/504) plus 2xx/3xx/4xx/5xx fallback buckets to i18n keys understatus.*.
Changed
- Router decision card redesigned with parallel Request / Response panels + savings banner: previously the two cards on the Router page used different field sets (left = protocol + model only; right = provider + model + cost) so they didn't read as a true comparison. Now: (1) a Request section shows two parallel cards with the same 6 fields each — endpoint, protocol, provider, model, input/output price per 1M tokens, estimated tokens — with the model name and prices visually highlighted because that's what differs between the two; (2) a Response section shows two parallel cards — actual tokens (in/out/cached) and actual cost — with the Actual cost prominently larger and highlighted; (3) a savings banner below the Response cards reads "Saved $X.XXXX (XX.X%) by routing" with a green trend-down icon, or "Cost $X more (Y%) than the requested model" with a red trend-up icon when routing was more expensive, or a neutral "no savings to report" line when the requested and routed sides match exactly.
- API:
/internal/logsJSON +request_completedSSE event gain 6 new fields:requested_provider,requested_input_per_mtok,requested_output_per_mtok,routed_input_per_mtok,routed_output_per_mtok,baseline_cost_usd. All omitempty / optional — legacy daemons or unknown-model records still parse, and the UI falls back to "—" cleanly. pricing.Service.ProviderFor(model): new public accessor returning the canonical provider name for a given model id.
Fixed
- macOS
krouter startgave cryptic exit-status-5 when plist was missing: now returns a clear message directing users to runkrouter installfirst. - Release checksums omitted bare binaries:
checksums.txtnow includeskrouter-linux-amd64,krouter-linux-arm64,krouter-apple-macos, andkrouter-windows.exealongside the archives — these are the files the auto-updater manifest points to. - v2.0.x upgrade:
pricing_cachetable not renamed on first run: users upgrading from v2.0.x kept the oldpricing_cache/pricing_sync_metatable names; the daemon now renames them totoken_price_api/token_price_api_metabefore running SQL migrations. - Provider models endpoint always returned
[](regression from v2.3.0):GET /internal/providers/{name}/modelswas reading frommodel_catalog, which the daemon never populates from the LiteLLM sync flow — so every Provider card in the dashboard showed "No models catalogued yet" universally, including for builtin providers likeanthropicandminimaxthat clearly have pricing on file. Fix: read fromtoken_price_api(where the LiteLLM sync actually writes), filter byprovider == name, return rows sorted bymodel_idfor stable order, and surfacecached_input_per_mtokin the response too. The providers-listmodel_countfield had the same root cause and is now derived from a newCountPricesByProvider()query that returns the grouped count in a single SQL trip.
[2.3.0] - 2026-05-23
Added
Agent scanners: Cursor, Hermes, OpenCode, Codex CLI (spec/04 Phase 2): krouter now auto-inherits endpoint configuration from four more AI coding tools.
- Cursor: reads
~/.cursor/settings.json; extractscursor.anthropic.baseUrlandcursor.openai.baseUrl. API key is left empty (Cursor stores credentials in the OS keychain). - Hermes: reads
~/.hermes/config.toml; iterates[providers.*]entries; mapsanthropic→anthropic-messages, everything else →openai-chat; reads inlineapi_keyif present. - OpenCode (SST opencode.ai): reads
~/.config/opencode/opencode.json(Linux/macOS) or%APPDATA%/opencode/opencode.json(Windows); single-provider flat JSON;providerdefaults to"openai"when absent;Connect/Disconnectwrite/deletebaseUrlin place. - Codex CLI (OpenAI): reads
~/.codex/config.toml; resolves the active provider viamodel_provider, reads the API key from the environment variable named byenv_key;Connectinjects a[model_providers.krouter]entry and setsmodel_provider = "krouter";Disconnectremoves the entry and restores the previous active provider.
- Cursor: reads
Remote subscription pricing sync (spec/05 §11.4): the daemon now polls
https://krouter.kinthai.ai/data/token_price_sub.jsonevery 24 h with ETag conditional requests, so price changes propagate to running daemons within a day — no binary upgrade needed. The kinthai.ai endpoint is the primary channel because operating it ourselves gives us access logs (fleet version distribution, daily unique IPs, 304 vs 200 ratio, geographic spread) thatraw.githubusercontent.comdoes not expose to repo owners; the daemon sends a versioned User-Agent (krouter-subpricing-sync/<version>) so the access log breaks down by deployed daemon version without any in-product telemetry. Falls back tohttps://raw.githubusercontent.com/kinthaiofficial/krouter/main/data/token_price_sub.jsonwhen the primary errors out. Single source-of-truth file: the samedata/token_price_sub.jsonisgo:embed-ed into the installer for the offline-seed path, so the file the daemon fetches at runtime is byte-identical to what shipped with its binary. Schema validation guards against accidental price typos (negative values, zerototal_count,monthly_price_cny> ¥100,000 are all rejected, leaving last-known-good rows intact). Newsubscription_pricing_updatedSSE event fires when rows are actually written so the dashboard refetches/internal/subscription/statusimmediately. Newinternal/subpricingpackage; 16 tests cover happy path, 304 cache hit, primary→fallback failover, both-endpoints-down, schema rejections, versioned UA, and SSE callback wiring.Free LLM credit discovery + automatic routing preference (spec/06): krouter now ships with a curated catalog of LLM providers that offer free tokens — DeepSeek (¥10 trial), Groq (永久免费 daily quota), NVIDIA NIM (1000 credits), Qwen / Zhipu / Moonshot / Cerebras / Together / OpenRouter / Mistral / Gemini and more (currently 19 entries, see
data/free_tokens.json). The dashboard surfaces them as a "Free LLM credits" card with one-click signup links; once a user pastes the API key into any agent (OpenClaw / Claude Code / etc.), krouter detects it via the existing inheritance flow and routing automatically prefers the free provider until the upstream returns 4xx (auth / quota / rate-limit), at which point the provider is marked exhausted for 1 h–24 h and routing falls back to the paid cheapest path. No dashboard configuration required from the user — pasting the key into the agent is the entire workflow.- New tables
free_provider_state(curated catalog) andprovider_exhausted_until(4xx exhaustion marks, TTL-based) in migration 011. - New
internal/freeproviderspackage mirrors the spec/05 §11.4 sync pattern: primaryhttps://krouter.kinthai.ai/data/free_tokens.json, fallbackhttps://raw.githubusercontent.com/kinthaiofficial/krouter/main/data/free_tokens.json, ETag-conditional, 24 h cadence, schema-validated before upsert. Daemon sendsUser-Agent: krouter-freeproviders-sync/<version>so kinthai.ai access logs can break down by fleet version. - Routing engine gains
WithFreeProviders(FreeProviderSource). When set,cheapestProviderModelconsults the free list first (provider infree_provider_state∩inherited_endpoints∩ not exhausted, protocol matches) and falls through to the paid cheapest path only when no free candidate qualifies. - Proxy's 4xx path now marks the provider exhausted: 401/403 → 1 h, 402 → 24 h, 429 → 5 min. Marks are harmless for paid providers (routing doesn't consult the table for non-free routes) and let free-credit users see "exhausted" badges on the dashboard with the upstream's status code as the reason.
- New
/internal/free-providersAPI joins the catalog withinherited_endpointsso dashboards can show "claimed / not yet claimed" state. Tests: 38 across 5 packages (storage / freeproviders / api / routing / cmd-krouter freeProviderSource / frontend FreeProvidersCard).
- New tables
Dual-protocol free providers (follow-up to spec/06): catalog entries now carry an optional
additional_protocolsarray for vendors that expose more than one protocol endpoint on the same account — OpenRouter, GLM/Zhipu, and Moonshot all offer an Anthropic-compatible/messagesalongside the OpenAI-compatible/chat/completions. Each alternate has its ownkrouter_provider_namebecause the user has to configure the alternate as a separate provider entry inside their agent (same API key, different baseURL). Routing's protocol filter is fully preserved (spec/00 §B2): a newFreeProviderKrouterNamesByProtocolreturns a per-protocol map, so an anthropic-protocol request only sees the anthropic-side krouter names and never cross-routes protocols even on dual-protocol vendors. The dashboard surfaces a top-of-card explainer about the same-protocol invariant plus a per-row indigo hint panel on dual-protocol rows showing the alternateprotocol → krouter_provider_namepair with the setup hint and a configured/未配置 badge. Migration 012 adds theadditional_protocols_jsoncolumn tofree_provider_state(pure additive ALTER — pre-existing rows keep[]and continue as single-protocol). Adds 8 dual-protocol tests across storage / sync schema validation / routing source / frontend FreeProvidersCard.Router page in the dashboard: new top-level nav item (between Dashboard and Agents) that surfaces routing decisions as they happen. The most-recent decision is shown as a large card with a requested-model → routed-result diff (two side panels showing protocol + model on the left and provider + routed model + cost on the right, arrow between them, highlighted green when the model actually changed). Older decisions collapse under a single "N earlier" toggle; each row can be individually expanded to the same large card view. Seeds from the last 50 records in
/internal/logsand live-extends from therequest_completedSSE channel — a green "live" pill with a pulsing dot indicates the SSE connection is open; new arrivals briefly pulse a green border on the latest card for ~1.5 s. The Logs page remains the searchable / paginated archive of the same data; this new page is the always-visible "what just happened" view.i18n key
router.*: new strings (title, subtitle, live/offline, waiting, agent_requested, krouter_routed, protocol/model/provider/cost, tokens_breakdown, latency_ms, status, request_id, saved_vs_requested, no_change, show_n_more, hide_history, latest_badge) in bothen.jsonandzh.json.
Changed
- Nav rename: "Announcements" → "Notifications" / "通知": the existing announcements feature is now surfaced as Notifications in both English and Chinese. Page title and the empty-state copy follow the same change; storage and API paths are unchanged so this is a strict UI rename with no behaviour or DB impact.
- Collapsible sidebar: a small toggle button at the bottom of the left sidebar shrinks it to an icon-only rail (56 px wide). Labels hide; the active nav item still highlights; the Notifications unread badge degrades to a tiny red dot at the icon's top-right corner. State is persisted in
localStorageunderkrouter:sidebar-collapsedso the preference survives reloads. Smooth 200 ms width transition. - Removed: bottom-bar
proxy :8402footer and its divider line from the sidebar. The version was the only useful bit there and it already sits beside "KRouter" in the header; the port number rarely changes after install and now lives only in the About page. - Logs page now mirrors the Router page schema exactly: replaces the fixed-column table with the same expandable row component the Router page uses. Each row collapses to a one-line summary (time · agent · requested → provider/routed · cost · latency · status pill) and expands inline to the full Router-style card with the requested→routed diff, protocol, cost, and the full tokens breakdown (
in · out · cached). New protocol filter dropdown next to the agent filter; the search box now also matches againstrequested_modeland requestid. CSV export gainsprotocol,requested_model,cached_tokenscolumns. The Router-page card components moved tofrontend/src/components/RoutingDecision.tsxso both pages share the same source of truth — Logs is now strictly Router's "search & archive" view. - API:
/internal/logsJSON gainscached_tokens(and so does the SSErequest_completedevent). The column already existed inrequest_records.cached_tokensfrom before — the API DTO was just not exposing it. Purely additive; existing clients ignore the new field. - Budget promoted from a Settings section to its own top-level page: new "Budget" / "预算" nav item (Wallet icon) between Providers and Settings. The page shows: today's spend bar (with 80% / 95% / 100% colour thresholds and a "Blocked" badge), spent / limit / remaining / requests stats, daily & weekly limit inputs (edit-in-place; commits on blur or Enter), savings-today copy when applicable, and a "Recent threshold events" timeline pulled from a new
budget_eventstable. SSEbudget_warninginvalidates the page's queries live. - Migration 013
budget_events: persists each 80% / 95% / blocked / unblocked transition the budget monitor goroutine detects. Previously these were SSE-broadcast-only and lost on disconnect; now they survive restarts and can be reviewed on the Budget page. Index onts_utc DESCfor the recent-events scan. The monitor's existing dedup means at most a few rows per day per threshold per UTC day. - API:
GET /internal/budget/events?limit=N(default 50, capped at 500) returns recent budget transitions newest-first. - Settings page cleanup: removed Routing Preset, Desktop Notifications, Pricing Data, and the inline Budget Limits section. Settings now contains only Language and Data Management (export logs by date range, reset data, uninstall). The removed sections weren't deleted from the backend — preset / notification_categories / pricing/status endpoints still exist and other places (CLI, install wizard) may still use them — only the dashboard surface area is trimmed.
- Providers page expanded with endpoint / models / pricing / lifetime usage: each provider is now a click-to-expand card. Header shows display name, protocol badge, builtin chip, and three summary chips (model count · lifetime requests · lifetime cost). Expanding reveals: base URL, path prefix and full endpoint (3 separate rows so they're individually copy-able); today's success rate / requests / cost / p50-p95 latency; lifetime in / out / cached tokens; the full per-model price table (
model_id, input/M, output/M, max tokens) pulled frommodel_catalogvia a new endpoint; a failure-streak banner when consecutive_failures > 0; and Test / Set Key / Remove buttons. The page no longer reorders providers by current request usage — that live "which provider is in use" view belongs on the Router page. - API:
GET /internal/providersJSON gainspath_prefix,requests_total,input_tokens_total,output_tokens_total,cached_tokens_total,cost_total_usd,model_count. Purely additive. - API:
GET /internal/providers/{name}/modelsreturns the per-provider model catalog with per-1M-token pricing (computed frommodel_catalog.input_cost_per_token × 1_000_000etc.) andmax_tokens. Same auth as the rest of/internal/*. - Storage:
ProviderTokenTotalsSince(provider, since)sums input / output / cached tokens + cost + count for a provider since a given time. Usetime.Time{}for lifetime totals.
Fixed
- Test fix in
TestAgentAction_LegacyVerbsStillReachableUnderNewDispatch: previously asserted that POST/internal/agents/openclaw/connectreturned"agent not found"to prove dispatch reached the handler. Failed on developer machines that have OpenClaw actually installed (the handler then succeeded with{"ok":true}). Switched to a guaranteed-not-installed agent name (nonexistent-vendor-xyz) so the assertion remains host-independent.
[2.2.0] - 2026-05-23
Added
- Daily budget hard stop: routing engine now checks
DailyPercent >= 1.0before selecting a provider. When the limit is reached,Decision.BudgetExceededis set and the proxy returns HTTP 429 — Anthropic-protocol callers receive{"type":"error","error":{"type":"rate_limit_error",...}}, OpenAI-protocol callers receive{"error":{"type":"insufficient_quota",...}}. Requests are blocked until the spend counter resets at midnight UTC. - Budget monitor goroutine:
monitorBudgetruns a 60 s ticker inserve.goand broadcasts SSE events (budget_warning) at 80%, 95%, and 100% of the daily limit. Each threshold fires at most once per day (dedup via last-fired timestamp). - Dashboard budget bar: new
BudgetBarcomponent shows a live spend-vs-limit progress bar that turns yellow at 80%, red at 95%, and adds a "Blocked" badge with an unblock hint when spending is stopped. - Install wizard budget step: new
BudgetSteppage added between "Shell" and "Done". Defaults to $50/day; callsPOST /api/install/set-budgeton the install server so the limit is written tosettings.jsonbefore the daemon starts for the first time. - Default $50/day limit:
applyDefaultsininternal/config/manager.gosetsbudget_warnings["daily"] = 50when the key is absent, so out-of-the-box installs are protected without requiring the wizard step. - UI localization (English / Chinese): all user-visible strings in the dashboard and install wizard are extracted to
frontend/src/locales/{en,zh}.jsonandfrontend-install/src/locales/{en,zh}.jsonusingreact-i18next. Language is detected fromnavigator.languageduring install and fromlocalStoragethereafter; the Settings page lets users switch at any time and syncs the choice tosettings.jsonimmediately.
Changed
GET /internal/budgetresponse now includesdaily_limit_usd,daily_percent_used, andbudget_blockedwhen a daily limit is configured.- Currency display unified to USD across both English and Chinese locales — no per-locale currency formatting.
[2.1.0] - 2026-05-22
Fixed
- Subscription routing could pick the wrong tier when minimax has multiple plans (spec/05 §8):
subscriptionSource.GetSubscriptionInfoiteratedGetAllSubscriptionQuotasand returned the firstIsAvailable()row, then hardcoded the rewrite-target model asMiniMax-M2.7regardless of which tier had matched. If a user'sMiniMax-M*(LLM) tier was exhausted butspeech-hd(TTS) still had quota, routing thought minimax was available, sent an LLM request asMiniMax-M2.7, and minimax replied 4xx because the M* tier was empty. Fixed by explicitly looking for the tier whosemodel_patternwildcard-matches the rewrite target (storage.SubscriptionQuota.MatchesModelusingpath.Match). Standard tier preferred over highspeed; highspeed used only as fallback. New tests cover the speech-hd masking scenario, standard-vs-highspeed precedence, the highspeed fallback path, and the non-minimax provider case.
Added
subscription_exhaustedSSE event (spec/05 §12.3): when the MiniMax quota poller observes a tier transitioning from "had quota" to "zero remaining" within the current window, the daemon broadcasts asubscription_exhaustedevent with{provider, tier, highspeed, window_end}. Dedupe key iswindow_endso we don't refire repeatedly within the same exhausted window. The dashboardSubscriptionQuotaCardlistens for the event, surfaces a transient banner ("quota exhausted, routing fell back to per-token vendors until window reset at …"), and force-refetches/internal/subscription/statusso the bars update immediately instead of waiting 60s for the next poll.
Changed
- MiniMax subscription card now shows the original CNY price: the dashboard formerly displayed e.g.
$6.76/mo plan, which felt odd to users who paid¥49/月on minimaxi.com. It now shows¥49/mo (≈ $6.76). The USD value is still computed (CNY × 0.138 fixed rate) so users can compare costs against per-token vendors. Newmonthly_price_cnyfield on/internal/subscription/statuscarries the original number. agentscan.PendingFileDir()now resolves to~/.kinthai/only (was:$XDG_CONFIG_HOME/krouter→~/Library/Application Support/krouter→~/.config/krouterfallback chain). This fixes a silent-data-loss bug where the installer running from a shell withXDG_CONFIG_HOMEset would writepending-agents.jsonsomewhere the launchd-started daemon never looks (LaunchAgent plists injectHOMEbut not arbitrary shell env vars). New regression test asserts the path is invariant underXDG_CONFIG_HOMEchanges.KROUTER_CONFIG_DIRcontinues to override (for tests and site-specific deployments).GET /internal/agentsis now a unified inheritance + legacy view (spec/04 §8.6): the existing JSON shape (name,connected,providers,stats) is preserved, but the response is now unioned with the Scanner registry plusagent_settings/inherited_endpointsso a dashboard refresh sees everything in one round-trip. New optional fields (allomitempty, so older clients ignore them):supported(Scanner compiled in for this agent),enabled(agent_settings.enabled),inherited_count,last_scanned_at,last_error. Scanner-registered agents that aren't yet installed on disk still appear (so the dashboard can show "OpenClaw (not installed)" rows); v2.0.47-style detection-only agents (Cursor / Hermes) still appear withsupported=falseuntil their Scanner ships.
Added
- Auto-rescan + SSE on MiniMax OAuth failure (spec/05 §15.2): when the
QuotaPollersees the token rejected — either via HTTP 401/403 or via the body'sbase_resp.status_code = 1004("login fail") that the token-plan endpoint returns as HTTP 200 — it now invokes a registeredUnauthorizedCallback. The daemon wires that callback toagentscan.RunAll(so any fresher OAuth token OpenClaw has written toauth-profiles.jsonsince the last scan gets picked up automatically) and broadcasts asubscription_unauthorizedSSE event so the dashboard can surface a "re-login OpenClaw" prompt if the rescan didn't help. NewErrMinimaxAuthsentinel error lets callerserrors.Is(err, ErrMinimaxAuth)instead of parsing message strings.
Added
- Periodic agent rescan (spec/04 §14 "Hot reload"): the daemon now runs
agentscan.RunAllevery 5 minutes in addition to the startup-time pass, so config changes the user makes to OpenClaw / Claude Code between daemon restarts are picked up without manual intervention. After each tick the daemon broadcasts anagents_changedSSE event withsource: periodic_rescanso the dashboard can refetch/internal/agents/configuredimmediately rather than waiting for its own react-query refetch interval. Implemented asagentscan.StartPeriodicRescan(ctx, store, logger, interval, onTick)— exported so tests (and future fsnotify variants) can drive it with a custom cadence.
Added
spec/00-design-principles.md: distilled cross-cutting design rules (product positioning, transparent-proxy invariants, data architecture, UX rules, engineering trade-offs, process meta-rules). New PRs and reviews are expected to reference principles by their letter+number (e.g. "violates C2 — single source of truth"). Compiled from the working-session history that producedspec/04andspec/05; each principle ships with the concrete incident or trade-off that motivated it, so a future contributor can decide whether the rule still applies when their case looks different.
Added
- Agent inheritance flow (spec/04): krouter now auto-extracts vendor endpoints, API keys, and OAuth tokens from the user's already-configured AI agents (OpenClaw, Claude Code) and persists them to a new
inherited_endpointstable. Wizard gains an "Agent Paths" step; Dashboard gains an inheritance section. Seespec/04-agent-inheritance.md. - Subscription quota dashboard (spec/05): new
/internal/subscription/statusand/internal/subscription/refreshendpoints; Dashboard gains a MiniMax subscription card showing effective cost, monthly price, and per-tier window-reset countdown. Seespec/05-subscription-quota.md. - Scanner architecture (
internal/agentscan): one Go file per AI agent implementing theScannerinterface (AgentID,DisplayName,DefaultConfigPath,Scan). Adding a new agent is purely additive — write the file, append the value to the registry — no schema change, no manifest layer. - Injectable MiniMax
QuotaPoller.WithTokenResolver: the poller now reads the OAuth token frominherited_endpoints.extras_jsonfirst, closing the cold-start gap where the daemon couldn't poll quota until the user had sent a first proxied MiniMax request. resolveProviderKeyhelper: routing engine and model discovery resolve vendor API keys through a single helper with precedenceinherited_endpoints > settings.ProviderKeys > env var. Vendors the user already configured in OpenClaw no longer require a second key entry in the dashboard Settings page.
Fixed
- MiniMax pricing — duplicate tables disagreed (caught in PR review): an earlier commit in this PR introduced
internal/providers/minimax/pricing.goas an independent lookup table (USD prices, formulamonthly_price_usd / total_count) that conflicted with the existinginternal/storage/subscription_quota.go::minimaxPlanPriceCNY(CNY prices, formula multiplied bywindows_per_month). Worst case: the dashboard reported an effective cost roughly 1043× the value the routing engine was using; the SKU{600, true}was treated as "free" by routing while the dashboard quoted $0.0483/call. Fix: deletedinternal/providers/minimax/pricing.gooutright, routed all callers throughstorage.SubscriptionQuota.EffectiveCostUSD()andMonthlyPriceUSD()(single source of truth), corrected thespec/05 §11formula to include thewindows_per_monthfactor, and added the pricing matrix + call graph + bug history to the spec as a cautionary note for future contributors.
[2.0.50] - 2026-05-19
Added
- 数据驱动 Provider 架构:新增
provider_configDB 表(migration 007),预置 15 个 provider 的元数据(name、display_name、base_url、path_prefix、protocol、is_builtin)。daemon 启动时从 DB 加载所有 OpenAI 兼容 provider,不再需要修改代码才能支持新 provider。 - 新增 7 个内置 provider:Google Gemini(
/v1beta/openai兼容端点)、xAI Grok、Mistral AI、Together AI、Fireworks AI、Perplexity、Ollama(本地)。加上现有 8 个,总计 15 个内置 provider,均通过 API key 配置即可激活。 - 自定义 provider CRUD:
POST /internal/providers(创建自定义 provider)、DELETE /internal/providers/:name(删除自定义 provider)。支持热添加/删除,无需重启 daemon。 - Providers 页面 UI 升级:显示 display_name 和 base_url(来自 DB),"Add Provider"对话框支持填写 name/base_url/api_key/path_prefix/protocol 添加任意 OpenAI 兼容端点;每个未配置 provider 卡片内联"Set Key"输入框,可直接在 Providers 页设置 API key;自定义 provider 显示删除按钮。
- provider_keys 校验放开:
PATCH /internal/settings的 provider_keys 从静态白名单改为格式校验(小写字母开头 + 字母数字 +-_),允许为任意 provider 设置 key。 - LiteLLM 映射扩展:
LiteLLMToKrouterProvider新增together_ai→together、fireworks_ai→fireworks,LiteLLM 同步的模型定价可自动关联到对应 provider。
Changed
serve.go移除了 deepseek、groq、moonshot、glm、qwen、openai 六个硬编码适配器注册,改为loadProvidersFromDB()统一加载。现有 per-provider 包(用于测试和文档)保持不变。GET /internal/providers响应新增display_name、base_url、is_builtin字段。
[2.0.49] - 2026-05-19
Fixed
- Bug F(P1)— Claude streaming token 永远 0 — 根因修复:Anthropic API 对发送了
Accept-Encoding: gzip的请求返回 gzip 压缩响应;krouter 直接把压缩字节写入 SSE capture buffer,parseAnthropicSSEUsage永远解析失败。修复:在 Anthropic 适配器的Forward方法里删除上游请求的Accept-Encoding头,强制返回未压缩文本。代价:每次响应多几 KB 流量,换取 token 统计可靠性。 - Bug Q — installer 端口 8404 静默切换:
stopRunningDaemon()只停 daemon service,不杀残留的老 installer 进程;老 installer 占用 8404 时新 installer 静默切到 8405,用户/脚本基于固定端口假设的操作全部 noop。修复:启动前新增stopRunningInstaller(8404)— 先 TCP 探测端口是否占用,若占用则用系统命令(macOS:lsof -ti | xargs kill -9,Linux:fuser -k,Windows:netstat + taskkill)强制杀掉。
[2.0.48] - 2026-05-19
Fixed
- Bug P(P0)— migration 005 UNIQUE 冲突致 daemon 启动失败:
005_rename_glm_to_zai.sql在 glm 和 zai 行并存时触发UNIQUE constraint failed,daemon 无法启动。修复:先删除与已有 zai 行冲突的 glm 行,再执行 UPDATE;对已完成 migration 的 DB 无影响(schema_migrations版本记录存在则跳过)。
Added
- Bug F 诊断工具:proxy 层在每次 Anthropic 流式响应结束后,将 SSE raw buffer(最多 4 KB)存入
Server.lastSSECapture;当parseAnthropicSSEUsage返回 0/0 token 时,debug 日志打印前 512 字节供排查。新增GET /internal/debug/last-sse-capture(需 Bearer auth)端点,返回上次捕获的原始 SSE bytes,方便研发判断流内容是否包含message_start/message_delta事件。
[2.0.47] - 2026-05-19
Added
- Dashboard 周报统计:新增
GET /internal/dashboard/stats端点,返回近 7 天请求数、花费、节省金额、provider 分布及已连接 agent 数量;Dashboard 页面新增"This Week"汇总卡片和 provider 分布饼图(recharts)。 - Provider 今日统计:
GET /internal/providers响应新增requests_today、cost_today_usd、latency_p50_ms、latency_p95_ms字段,Providers 页面展示今日请求量和延迟百分位。 - Provider Test 按钮:新增
POST /internal/providers/{name}/test端点,向 provider 发起轻量连通性探测(Anthropic 测 API 可达性,OpenAI-compatible 测 key 有效性);Providers 页面每个卡片增加 Test 按钮,实时显示延迟和 HTTP 状态码。 - Agent 配置 diff 预览:新增
POST /internal/agents/{name}/diff端点,返回 connect 前后的配置 JSON diff(不写文件);Agents 页面在 OpenClaw 点击 Connect 时先弹出 before/after 对比模态框,确认后再执行。 - Agent 备份管理:新增
GET /internal/agents/{name}/backups和POST /internal/agents/{name}/restore端点;Agents 页面每个已知 config-path 的 agent 卡片底部增加可折叠"Backups"面板,列出所有备份并支持一键还原。 - 日志日期范围过滤:
GET /internal/logs新增?from=YYYY-MM-DD&to=YYYY-MM-DD参数;Logs 页面新增两个日期选择器,设置后按范围过滤记录。 - 日志 CSV 导出接口:新增
GET /internal/logs/export?from=&to=端点,返回 CSV 文件下载;Settings 页面新增日期范围输入 + Export CSV 链接。 - 数据管理:新增
POST /internal/settings/reset-data(清空请求历史)、POST /internal/settings/uninstall(断开所有 agent 连接);Settings 页面新增"Data Management"分区含上述两个操作按钮。 - 通知类别扩展:
HandleEvent新增四种通知类型:free_credit(免费额度到账)、provider_news(provider 更新)、tip(节省建议)、critical_warning(严重告警);Settings 页面通知偏好列表同步补全这四项。 Pinger接口:providers包新增Pinger可选接口;Anthropic 和 OpenAI 适配器各实现Ping(ctx) (latencyMS, statusCode, err)方法。
[2.0.46] - 2026-05-19
Added
- 5xx Fallback 链:proxy 层新增
tryWithFallback,当 provider 返回 5xx 或网络错误时自动调用engine.FallbackDecide,最多重试 2 次;Anthropic 协议按 opus→sonnet→haiku 降档,OpenAI 协议切换到其他同协议 provider;4xx(401/429)直接透传,不触发重试。 - 复杂度评分(
ComplexityScore):替换原来的布尔判断(HasImages || HasTools && tokens>4000),综合图片、大 token 量、HasTools 组合、system prompt 关键词(debug / refactor / architect 等 10 个)计算 0.0~1.0 分值;Quality preset 以 ≥0.4 为复杂请求阈值,决定是否升级到 Opus。 EstimatedCostUSD填充:enrichDecision在 routing engine 内部自动计算路由决策的预估花费,填入Decision.EstimatedCostUSD;当路由模型比请求模型便宜时,在Reason末尾追加"比 X 便宜 Y%"。- per-agent 路由覆盖:
settings.json新增routing_overrides字段,支持按 agent 名(openclaw / claude-code / cursor / unknown)配置always_use(强制使用特定模型)或preset(覆盖活跃 preset);engine 通过新增的OverrideSource接口注入,daemon 启动时由config.Manager实现。
[2.0.45] - 2026-05-19
Added
- 请求元数据提取(HasImages / SystemPrompt):proxy 层在构建
routing.Request前调用extractAnthropicMeta/extractOpenAIMeta,解析请求体中的system字段和图片内容块,填充HasImages、SystemPrompt字段。路由引擎可据此在订阅优先路由时跳过含图片请求(MiniMax 不支持)。 - Anthropic 预算限制自动降级:routing engine 新增
QuotaSource接口和applyQuotaDowngrade逻辑;当每日/每周花费占比 ≥ 95% 时强制切换 Saver preset,≥ 80% 时将 Quality→Balanced、Balanced→Saver;OpusPercent(基于 500K token 软上限的窗口消耗比)≥ 80% 时屏蔽 Opus 系列,降级使用 Sonnet。 QuotaSource/SubscriptionSource接口:routing engine 通过WithQuota(QuotaSource)和WithSubscription(SubscriptionSource)注入依赖,保持路由层与存储层解耦,便于测试替换。
[2.0.44] - 2026-05-19
Added
- MiniMax 订阅 provider 优先路由:routing engine 新增
SubscriptionSource接口,当 MiniMax 订阅配额有剩余且请求不含图片时,三个 preset(Saver / Balanced / Quality 非复杂)均优先路由到 MiniMax,利用"月费÷月总次数"极低的有效成本(≈$0.000031/次)覆盖大多数请求。 - MiniMax 配额轮询(
QuotaPoller):每 30 分钟调用https://api.minimaxi.com/v1/token_plan/remains,当前窗口剩余 < 30 分钟时缩短为 5 分钟;结果存入新 DB 表subscription_quota_cache;daemon 启动后 20 秒触发首次拉取。 - OAuth token 缓存:proxy 在每次 MiniMax 请求中提取
Authorization: Bearer头并存入内存(minimax.CacheOAuthToken),供 QuotaPoller 调用配额 API 时使用;token 不写磁盘、不打印日志。 SubscriptionQuota存储层:新增internal/storage/subscription_quota.go,包含UpsertSubscriptionQuota、GetSubscriptionQuota、GetAllSubscriptionQuotas、IsSubscriptionAvailable方法;EffectiveCostUSD()按订阅套餐价格表计算每次调用等效美元成本。- DB migration
006_subscription_quota.sql:创建subscription_quota_cache表。
[2.0.43] - 2026-05-19
Fixed
- Bug F(Anthropic streaming token 为 0):
parseAnthropicSSEUsage和parseAnthropicJSONUsage改用 JSON 解析替代正则表达式,累加input_tokens + cache_creation_input_tokens + cache_read_input_tokens作为总 input token 数,修复了 prompt caching 激活时input_tokens字段为 0 导致统计清零的问题。cache_read_input_tokens作为cachedTokens传入定价函数,享受折扣费率。 - Bug O(glm→zai 重命名遗漏):新增 DB migration
005_rename_glm_to_zai.sql,将model_discovery表中旧的provider='glm'行批量改为'zai';新增config.Manager.MigrateKeys()在 daemon 启动时自动将settings.json中的provider_keys.glm迁移为provider_keys.zai,避免老版本升级后 API key 失效。 - Bug L(settings 接受未知 vendor):
PATCH /internal/settings的provider_keys字段现在校验 key 是否合法(deepseek / groq / moonshot / zai / qwen / openai / minimax),传入未知 key 时返回 400 Bad Request,附带可接受 key 列表。
Added
- Bug K(OpenAI 直连 provider):注册
openaiprovider(https://api.openai.com,OPENAI_API_KEY),支持gpt-4o、gpt-4o-mini、gpt-4-turbo、gpt-4、o1、o1-mini、o3-mini。兑现 README 承诺。
[2.0.42] - 2026-05-19
Added
- 全量模型 Catalog(
model_catalog表):LiteLLM JSON 每 24h 同步时,将所有模型条目(含 cost=0 的)写入新的model_catalog表,字段包括litellm_provider、model_id(去掉 provider 前缀)、raw_key、定价与max_tokens。此表作为全平台大模型目录的权威来源。 pricing.LiteLLMToKrouterProvider映射表:声明 LiteLLM provider 名称与 krouter adapter 名称不同的对应关系(目前仅dashscope → qwen;其余在两侧名字一致后直接对应)。providers.ModelSetter接口:允许在运行时替换 adapter 的SupportedModels()列表。anthropic和openai(含所有 OpenAI-compatible 子适配器)均实现此接口。pricing.Service.OnSync回调:每次 LiteLLM 同步完成后调用,传入按litellm_provider分组的模型列表。serve.go注册此回调,自动将 catalog 模型列表推送到对应 adapter,无需重启 daemon。- 新 DB migration
004_model_catalog.sql:创建model_catalog表及idx_model_catalog_provider索引。 storage.Store.UpsertModelCatalogBatch/GetModelsByLiteLLMProvider/GetAllModelCatalog:新增 catalog 相关 storage 方法。
Changed
- GLM adapter 改名
glm→zai:跟进智谱品牌重命名(Z.AI),与 LiteLLM 和 OpenClaw 命名保持一致。settings key 同步改为zai(旧配置需手动更新ZHIPU_API_KEY对应的 settings 键名)。静态模型列表补充新版glm-4.5系列。 - Moonshot adapter 改名
moonshot-cn→moonshot:与 LiteLLM 和 OpenClaw 命名保持一致。静态模型列表补充新版 Kimi 系列(kimi-k2.5、kimi-k2.6、kimi-latest等),保留旧版moonshot-v1-*作为 fallback。
[2.0.41] - 2026-05-19
Added
- Settings → Pricing 页面(spec/04-pricing.md §8):Settings 页面新增 Pricing Data 区块,展示:
- 最近同步时间、数据来源徽标(
live/cache/static)、已追踪模型数量 - 本月总花费与节省金额(micro-USD 精度)
- 过去 30 天 Top 10 模型使用表格(请求数、花费、输入/输出每百万 token 价格)
- 最近同步时间、数据来源徽标(
GET /internal/pricing/status管理 API 端点:返回同步元信息、模型统计、本月聚合花费及 30 天热门模型列表。pricing.Service.ModelCount() int:返回当前内存价格表中的模型数量,供 API 层展示。pricing.Service.PriceFor(model string) (inputPerMTok, outputPerMTok float64):按模型返回每百万 token 的输入/输出价格,便于前端展示不做单位换算。storage.Store.TopModelsByUsage(ctx, since, limit):按请求数降序汇总指定时间窗口内的模型使用统计。storage.Store.ListRequestsSince(ctx, since, limit):按时间段查询请求记录,用于月度节省计算。
[2.0.40] - 2026-05-19
Added
- 价格 tier 驱动路由(spec/04-pricing.md §5):routing engine 现在通过
WithPricing(PricingSource)接收 live 价格数据,在 Saver / Quality preset 下动态选择最便宜/最贵的 (provider, model) 对,不再依赖硬编码模型名。- Saver preset:
cheapestProviderModel遍历所有健康且有 key 的同协议 provider,按InputCostPerToken选最低价模型。若 DeepSeek 比当前注册的 OpenAI gpt-4o-mini 更便宜,saver 自动选 DeepSeek。 - Quality preset(复杂请求):
mostExpensiveProviderModel选最贵模型作为能力最强的升级目标,代替硬编码claude-opus-4-5。 - 向下兼容:pricing 未注入(
nil)或模型不在价格表时,自动回退到原有硬编码逻辑,现有行为不变。
- Saver preset:
pricing.Service.InputCostPerToken(model string) float64:新增公开方法,供 routing engine 按模型查询价格,无需构造完整 token 计数。routing.PricingSource接口:仅暴露InputCostPerToken,最小化 routing → pricing 的依赖,便于测试时替换。
[2.0.39] - 2026-05-19
Fixed
- pricing: 同步请求不走 OS 代理:
pricing.Service之前用硬编码&http.Client{}访问raw.githubusercontent.com,在国内受限网络下每 24h 同步必超时。新增WithHTTPClient(*http.Client) *Service方法;serve.go在创建bgTransport(与 notifications/upgrade 共享的代理感知 transport)后立即注入,然后才调用StartSync。 - pricing:
raw_json列有 schema 但从不写入:syncOnce写UpsertPrice时缺失RawJSON字段,导致pricing_cache.raw_json始终为空字符串,无法用于调试。修复:parseLiteLLM返回内部parsedEntry{PriceEntry, RawJSON}结构,将原始 JSON bytes 一并传到UpsertPrice;内存价格表(pricesmap)仍只保存PriceEntry,内存占用不变。
[2.0.38] - 2026-05-19
Fixed
- Bug H — 首次安装用户
anthropic.models=[]:RefreshModelsIfStale(daemon 启动 +10s 执行)之前只处理 settings 里有 key 的 provider,跳过了 Anthropic(key 在 openclaw.json 里,不在 krouter settings 里)。修复:RefreshModelsIfStale现在也会检查 OpenClaw 是否已连接,若 anthropic 缓存为空或超过 24h,则调用discoverOpenClawModels,首次安装用户的模型选择器将在 daemon 启动约 10s 后自动填充。 - Bug I —
minimax-portal.models永远[]:discoverOpenClawModels之前只处理 anthropic provider,从不更新 OpenClaw 的 minimax-portal 节点。修复:新增discoverOpenClawMiniMax,若 OpenClaw 配置中存在minimax-portal节点则自动写入模型列表——优先通过minimax-portal.apiKey做实时探测,若 key 不存在则写入 adapter 的静态模型列表(MiniMax-M2.7、MiniMax-M2.7-highspeed),确保 model selector 始终非空。同时新增ReadOpenClawProviderAPIKey(configPath, providerName)通用函数(ReadOpenClawAPIKey成为其包装)。 - Bug J — daemon 内部服务(announcements/upgrade)不走 OS 系统代理:
notifications.Service和upgrade.Service各自创建了硬编码&http.Client{},不使用proxycfg.Manager探测的代理,导致国际网络受限环境下持续 timeout。修复:两个 service 新增WithHTTPClient(*http.Client) *Service方法,serve.go在 daemon 启动时注入共享的代理感知 transport(Proxy: proxymgr.ProxyFunc()),upgrade 15s 超时、notifications 30s 超时,均通过proxymgr.ProxyFunc()闭包自动感知代理变更。
[2.0.37] - 2026-05-19
Fixed
- Bug G — Saver preset 在多 provider 环境下误路由到 MiniMax:当同时注册了 Anthropic 和 MiniMax(均使用 Anthropic 协议)时,
decideSaver之前调用pickHealthyProvider可能返回 MiniMax,再把claude-haiku-4-5-20251001这个 Claude model ID 发给它——MiniMax 不识别该 ID,请求静默失败。修复:改用pickProviderForModel(proto, saverAnthropicModel),优先选择在SupportedModels()中明确列出该 haiku 型号的 provider;若回退 provider 不支持该型号,保持原始请求 model 不变。 - Bug F — Anthropic streaming 请求 latency 统计错误(总是显示 6ms):
streamSSEWithCapture的done回调之前会传入自身内部计时的latencyMS,该计时在首字节已到达后才开始,导致记录的永远是流写入耗时(≈6ms)而非真实端到端延迟(如 7000ms)。修复:移除done回调中的latencyMS参数,两处调用处改为time.Since(start)计算从请求到达时刻起的完整延迟。 - Bug F — Anthropic streaming token 捕获失败(长响应丢失
message_delta):之前只保留头部 256KB,对于超过此大小的长响应,末尾的message_delta(含output_tokens)会被丢弃,导致 token 统计 output=0、cost=0。修复:改为 64KB 头部缓冲区 + 4KB 尾部滑动窗口,始终保留响应末尾内容,确保message_delta不丢失,同时大幅降低内存占用。
[2.0.36] - 2026-05-19
Added
- Model Discovery:krouter 现在会在用户连接 OpenClaw 后自动探测可用模型列表。连接时读取 OpenClaw 配置里的 Anthropic API key,调用
/v1/models接口,将返回的模型列表写入 OpenClaw 的models.providers.anthropic.models字段,使 OpenClaw 的模型选择器立即呈现真实可用的模型。所有已发现模型同时缓存到本地 SQLite DB(model_discovery表)。 - OpenAI-compatible provider 模型探测:daemon 启动 10s 后自动检查 settings 里有 key 的 provider(DeepSeek、Groq 等),若缓存超过 24h 则重新探测并更新 DB。
- 新端点
GET /internal/models:返回 DB 中所有已发现模型,按 provider 分组。 - 新端点
POST /internal/models/refresh:手动触发全量刷新(settings-keyed providers + 已连接的 OpenClaw Anthropic key),异步执行,立即返回{"ok":true}。 ModelDiscoverer可选接口:Anthropic 和 OpenAI adapter 均已实现,支持通过keyFn透传 API key,不暴露给 krouter 存储层。OpenAI adapter 的modelsEndpointURL()正确处理 GLM(/v4/models)和 Qwen(/compatible-mode/v1/models)的 pathReplace。
[2.0.35] - 2026-05-19
Added
- HTTP proxy 支持(OS 系统代理 + per-provider bypass):daemon 现在像 Chrome 一样自动读取 OS 系统代理设置(macOS
scutil --proxy、Windows 注册表HKCU\Internet Settings、Linux GNOMEgsettings),无需用户在 krouter 里单独配置代理。国内直连 provider(MiniMax、Moonshot、DeepSeek、GLM/Zhipu、Qwen)通过内置 no-proxy 后缀列表绕过代理,Anthropic/Groq 等国际 provider 走代理。代理检测每 60s 刷新一次,VPN 接入/断开后自动生效。 - 新端点字段:
GET /internal/status响应新增proxy字段({"url":"","source":"none","active":false}),未来 Web UI 可在 Providers 页面展示当前代理状态。
[2.0.34] - 2026-05-18
Changed
- Web UI 认证简化:废弃 session cookie + ticket-exchange 方案,改为基于
Originheader 的 CSRF 防护。认证中间件决策顺序:① 有效 Bearer token → 无条件放行(CLI / 程序化访问);② Origin 存在且不等于http://127.0.0.1:8403→ 403(跨域拦截,防 CSRF);③ 无 Origin 或正确 Origin → 放行(curl / 浏览器 dashboard)。浏览器访问 dashboard 不再需要任何登录流程,daemon 重启也不影响体验。 - 删除端点:
POST /internal/auth/ticket、GET /internal/auth/exchange已移除 - install server 简化:
handleDaemonReady不再 mint ticket,直接返回redirect_url: /krouter/;移除readInternalTokenFn/mintDaemonTicketFn依赖
Security
- 安全性等效:Origin header 由浏览器自动设置且不可被跨域 JS 伪造(W3C CORS 规范强制);curl / CLI 不发 Origin,不受影响;有效 Bearer token 可绕过 CSRF 检查(持有 token 即已认证)
[2.0.33] - 2026-05-18
Fixed
- Bug D —
defaultOpenClawModels格式错误:之前注入的是字符串数组(如["claude-opus-4-5", ...]),但 OpenClawModelDefinitionSchema要求每个元素是对象({id, name, ...}),全新机器首次运行krouter connect openclaw时 OpenClaw 会 schema validation crash。改为注入空数组[]:空数组满足z.array(ModelDefinitionSchema)schema、不会 crash;OpenClaw 通过 plugin 系统动态加载模型目录,不依赖该字段内容
[2.0.32] - 2026-05-18
Fixed
- MiniMax 请求计费:
MiniMax-M2.7和MiniMax-M2.7-highspeed加入静态价格表(LiteLLM 目前未收录 MiniMax 模型,导致cost_micro_usd = 0)。价格来源:MiniMax API 平台($0.30/1M input、$1.20/1M output) - Savings 误算:当
cost_micro_usd = 0(未知模型价格,非真正免费)时,savings 会被错误地计为baseline - 0 = baseline。现在仅在实际费用> 0时才计算 savings,影响/internal/budget和/internal/usage两个端点 - OpenClaw agent 识别:OpenClaw 使用 Anthropic TypeScript SDK,其 User-Agent 为
Anthropic/JS X.Y.Z,不含 "openclaw" 字符串,导致agent = "unknown"。新增通过anthropic-dangerous-direct-browser-access: true请求头识别 OpenClaw(该 header 由 OpenClaw SDK client 在所有 Anthropic provider 请求中注入,Claude Code CLI 不发送此 header) - per-agent 统计:随 agent 识别修复自动生效,OpenClaw 请求将正确计入
openclaw分组
[2.0.31] - 2026-05-18
Fixed
- MiniMax-portal OAuth 透传:v2.0.30 错误地将 MiniMax adapter 改成 Bearer key 注入模式。MiniMax-portal 的认证由 OpenClaw 的 OAuth 流程生成(从
auth-profiles.json读取 access token),krouter 不应注入任何 key,应完整透传 OpenClaw 生成的 Authorization header。MiniMax adapter 恢复为 Anthropic 协议透明代理,upstream 端点更新为正确的https://api.minimaxi.com/anthropic(portal API,区别于 chat APIapi.minimax.chat) ConnectOpenClaw新增minimax-portal支持:若用户 openclaw.json 中有minimax-portalprovider(含authHeader: trueOAuth 配置),connect 时自动将其baseUrl重定向到 krouter;disconnect 时恢复为https://api.minimaxi.com/anthropic/v1。OAuth 凭证(authHeader、models、access token 等)全程不接触
Architecture note
MiniMax-portal 的完整流程:
- OpenClaw → krouter:8402(OpenClaw OAuth 流程注入 Authorization header)
- krouter →
https://api.minimaxi.com/anthropic/v1/messages(透明代理,保留原始 header)
[2.0.30] - 2026-05-18
Fixed
- LaunchAgent 无
EnvironmentVariables:plist 新增EnvironmentVariables段,注入扩展PATH(含~/.claude/local、~/.local/bin、~/go/bin、/opt/homebrew/bin、/usr/local/bin)和HOME,彻底解决 daemon 作为 LaunchAgent 运行时工具不可见的问题 - Provider 注册依赖 shell env:所有 secondary provider(DeepSeek / Groq / Moonshot / GLM / Qwen / MiniMax)改为始终注册;API key 通过
keyFn在每次请求时动态读取,优先读~/.kinthai/settings.json的provider_keys,再 fallback 到环境变量。LaunchAgent 无 shell env 时仍可通过 settings.json 配置 key - Providers 页只显示 anthropic:
GET /internal/providers新增configured: bool字段;有 key 的 provider 显示为 "Active",无 key 的显示为 "Not configured"(不再从注册表缺失,始终可见) - Claude Code 在 Agents 页永远空:
DetectInstalledAgents在exec.LookPath("claude")失败后改为搜索已知路径(~/.claude/local/claude、~/.local/bin/claude、/usr/local/bin/claude、/opt/homebrew/bin/claude),LaunchAgent 最小 PATH 下也能找到 - MiniMax adapter 从 Anthropic 透明代理改为 OpenAI Bearer-auth 方式,key 注入逻辑与其他 secondary provider 一致
Added
Settings.ProviderKeys map[string]string:存储 secondary provider 的 API key,存入~/.kinthai/settings.json(0600 权限);PATCH /internal/settings支持provider_keys字段(空字符串 = 删除该 key)providers.Configurable可选接口:HasKey() bool,由openai.Adapter实现;透明代理(Anthropic)不实现,默认视为已配置openaiAdapter.NewWithKeyFn/NewWithPathReplaceAndKeyFn:接受func() string的 key getter,替代硬编码环境变量名- 所有 secondary adapter(deepseek / groq / moonshot / glm / qwen / minimax)加
NewWithKeyFn构造函数 - Providers 页 "Not configured" 卡片 hint 更新为同时提示 env var 和 settings.json 两种配置方式
[2.0.29] - 2026-05-18
Fixed
- OpenClaw connect 后仍 crash-loop(Bug C):
ConnectOpenClaw写入配置时未设置models.providers.anthropic.models字段,但 OpenClaw schema 强校验该字段必须为非空数组,导致 OpenClaw 启动即崩溃。现在连接时若models字段缺失,自动注入当前生产 Claude 模型列表;用户已有的自定义 models 列表不覆盖 DisconnectOpenClaw遗留垃圾节点:旧 krouter 版本断开后会在models.providers下残留空的或仅有models字段的anthropic对象。现在:若断开后 provider 中不存在真实apiKey,说明该 anthropic section 完全由 krouter 创建,一并清除(包括 krouter 注入的models数组和空节点)- 完善
TestDisconnectOpenClaw_RemovesOldPlaceholderApiKey:占位符 apiKey 被移除后,因无真实 key 留存,整个 anthropic section 一并删除
Added
- 新增测试
TestConnectOpenClaw_PreservesExistingModels:确认用户已有 models 列表不被覆盖 - 新增测试
TestDisconnectOpenClaw_RemovesKrouterAddedSectionWhenNoRealKey:无真实 apiKey 时整个节点被清除 - 新增测试
TestDisconnectOpenClaw_PreservesRealApiKeyAndCustomModels:用户真实 apiKey 和自定义 models 在断开后完整保留
[2.0.28] - 2026-05-18
Added
- Logs 页 SSE 实时追加:
request_completed事件直接推送到 Logs 页,新请求无需等待轮询即时出现- 单个稳定 SSE 连接;切换 agent 过滤器时通过 ref 避免 stale closure
- 内存上限 2000 条;超出后自动丢弃尾部
- Logs 页 Agent 下拉过滤器:选中 agent 后使用
?agent=name后端参数,只拉该 agent 的记录- 下拉选项从
/internal/agents动态读取 - 文本搜索框与 agent 过滤器可叠加使用
- 下拉选项从
proxy → SSE接线:proxy.Server.SetOnComplete回调 +serve.go接线,每次请求完成后立即广播request_completed事件(此前该事件类型存在但从未发送)- About 页显示 build_time:
/internal/status响应新增build_time字段;About 页在版本信息卡片中展示构建时间(unknown时不显示)
Fixed
request_completedSSE 事件此前已在 Dashboard 监听但后端从未广播,现补全发送逻辑
[2.0.27] - 2026-05-18
Added
- Agents 独立页(P0 gap 补全)
- 左侧导航新增「Agents」入口(
Bot图标),位于 Dashboard 下方 - 每个 agent 卡片显示:连接状态徽章、配置文件路径、provider 列表、今日请求数 / 今日费用 / 今日节省
- Connect / Disconnect 按钮:直接修改 agent 配置文件(openclaw.json / settings.json / config.toml / shell rc),无需手动编辑
- 连接后提示:Claude Code 需要打开新终端;OpenClaw / Cursor 需要重启
- 展开 / 折叠 per-agent 请求日志(Show logs / Hide logs)
- Re-detect 按钮强制重新扫描已安装 agent
- 左侧导航新增「Agents」入口(
POST /internal/agents/{name}/connect/POST /internal/agents/{name}/disconnect端点- 支持
openclaw/cursor/hermes/claude-code - 找不到 agent 返回 404;不支持的 agent 名返回 400
- 支持
GET /internal/logs?agent={name}过滤参数,返回指定 agent 的最近请求记录GET /internal/agents返回值新增stats字段(requests_today/cost_today_usd/savings_today_usd)storage.ListRequestsByAgent(ctx, agent, limit)— 带 agent WHERE 过滤的存储层方法AgentInfo字段加 JSON tag(name/config_path/cli_path),修复之前 PascalCase 序列化 bug
Changed
- Providers 页移除「AI Agents」子区块(内容已移至独立 Agents 页)
[2.0.26] - 2026-05-18
Fixed
- OpenClaw config 写入逻辑双重 bug 修复
- Bug A(crash loop):旧版
ConnectOpenClaw向models.providers.anthropic.apiKey写入字面量"${ANTHROPIC_API_KEY}"。OpenClaw 作为 LaunchAgent 运行,不继承 shell 环境,该占位符从不展开,直接作为无效 API key 发给 Anthropic,导致 OpenClaw crash loop。新版完全不写apiKey——用户自己的真实 key 必须保留在 OpenClaw 配置里。 - Bug B(字段覆盖):旧版用
setNestedJSON把整个anthropic节点替换成三个固定字段, 销毁用户原有的所有配置(如 minimax apiKey、models 列表)。新版改为 merge:只写baseUrl和api,所有其他字段原样保留。 - 旧版清理:
DisconnectOpenClaw现在会识别并删除旧版写入的${ANTHROPIC_API_KEY}占位符,修复升级路径;真实 apiKey 永远不会被 Disconnect 删除。 - 5 个测试覆盖:baseUrl+api 设置、apiKey 不注入、backup 创建、disconnect 保留真实 key、 disconnect 清除占位符
- Bug A(crash loop):旧版
[2.0.25] - 2026-05-18
Added
GET /internal/agents— 新 API 端点,实时探测已安装的 AI agent 并报告 krouter 连接状态:- OpenClaw: 检查
~/.openclaw/openclaw.json是否存在,models.providers.anthropic.baseUrl是否指向http://127.0.0.1:8402,并列出配置文件中的所有 LLM provider 名称 (如 anthropic、minimax) - Claude Code: 检查
~/.zshrc/~/.bashrc/~/.bash_profile是否包含 krouter shell integration marker block - Cursor / Hermes: 检测文件是否存在(连接状态检测后续实现)
- OpenClaw: 检查
- Providers 页 "AI Agents" 区块 — 展示每个 agent 的探测结果、连接状态 (Connected / Not connected 徽章)、配置文件路径、以及 agent 内部配置的 LLM provider 列表;每 15 s 自动刷新
IsOpenClawConnected(configPath)— 读取 OpenClaw 配置,判断是否已接入 krouterReadOpenClawProviderNames(configPath)— 读取 OpenClaw 配置内所有 provider 名称IsClaudeCodeConnected(shellRCPath)— 检查 shell RC 是否包含 krouter markerDetectAgentStatuses()— 在DetectInstalledAgents()基础上补充连接状态和 provider 列表- 8 个单元测试覆盖上述新函数
[2.0.24] - 2026-05-18
Changed
- Dashboard UI 白色主题 — 参照 kinthai.ai 设计风格重新配色:
背景色从
#f7f8fa改为#f0f2f5,字体改为系统 UI 栈(-apple-system, BlinkMacSystemFont, ...), 强制 light 模式(移除全部dark:Tailwind 类),logo 更新为 kinthai.ai 完整渐变 SVG。 - Tab 标题 — 浏览器标签从 "frontend" 改为 "KRouter";favicon 路径修正为
/krouter/favicon.svg - 版本号 double-v 修复 — 侧边栏版本显示从
v{version}改为{version},避免 git tagv2.0.x导致显示vv2.0.x
[2.0.23] - 2026-05-18
Fixed
- 安装完成后 installer 进程自动退出 —
krouter-installer此前以select {}永久阻塞,安装向导结束后进程一直存在,导致用户在 Activity Monitor 里看到两个 "krouter" 进程(一个有图标的 installer、一个无图标的 daemon)。 现在handleDaemonReady在返回ready: true后 500 ms 关闭一个 shutdown channel,main.go阻塞在该 channel 而非select {},收到信号后正常退出。 安装完成后用户只会看到一个 krouter daemon 进程。
[2.0.22] - 2026-05-18
Fixed
- "KRouter took too long to start" — 根本原因 — 管理 API(port 8403)
从未注册
/health路由;installer 的daemon-ready轮询一直得到 404 而非 200,永远返回{ready: false},导致 60 s 超时。现在/health已 注册且无需认证,1 个测试验证其行为。之前对waitPortFree、bootout、stopRunningDaemon的修复解决的是"两个进程"和"拖拽安装"问题, 但超时 bug 的真正原因一直是这个缺失的端点。
[2.0.21] - 2026-05-18
Fixed
- macOS: "krouter is still running" / 两个 krouter 进程 / 无法升级 —
krouter-installer现在启动时的第一个动作就是调用launchctl bootout, 将正在运行的旧 daemon 从 launchd 监管中完全移除。此前:旧 daemon 注册了KeepAlive=true,用户手动杀进程后 launchd 立即重启,导致永远无法替换; macOS Finder 看到同名进程报错 "krouter is still running",用户无从下手。 现在用户只需打开新版安装包,installer 会自动停掉旧进程再走安装流程, 无需任何手动操作。Linux/Windows 同理(systemctl stop / schtasks /End)。
[2.0.20] - 2026-05-18
Fixed
- macOS/Linux/Windows: "KRouter took too long to start" / two krouter processes —
the proxy port-conflict guard in
serve.gopreviously exited immediately (return nil) when port 8402 was already in use. During reinstall the old binary is still shutting down when launchd/systemd starts the new one, so the new binary would exit, launchd would wait its default 10 s ThrottleInterval before retrying, and the installer's 60 s poll window would time out. The guard now polls every 100 ms (up to 10 s) for the port to be freed; the new binary picks it up as soon as the old one releases it with no ThrottleInterval delay. Falls back to silent exit only if the port is still busy after 10 s (a permanent instance is genuinely running). - 4 unit tests covering
waitPortFree: immediate return when free, waits until released, times out correctly, distinguishes bound vs free address
[2.0.19] - 2026-05-18
Added
krouter start/krouter stopCLI commands: proper daemon lifecycle management- macOS:
launchctl bootstrap gui/<uid> <plist>/launchctl bootout gui/<uid> <plist> - Linux:
systemctl --user start krouter/systemctl --user stop krouter - Windows:
schtasks /Run /TN krouter-daemon/schtasks /End /TN krouter-daemon
- macOS:
Changed
- macOS:
LoadLaunchAgentnow useslaunchctl bootstrap/bootoutinstead of the deprecatedload/unload+pgreppolling approach.launchctl bootoutis synchronous and waits for the process to fully exit before returning, eliminating the port-conflict race without any process-name polling.
Removed
processExists(pgrep-based process checker) — no longer needed now thatlaunchctl bootoutprovides synchronous process exit semantics
[2.0.18] - 2026-05-18
Fixed
- macOS: two krouter processes / "KRouter took too long to start" —
launchctl unloadsends SIGTERM asynchronously and returns before the process exits; the immediately-followingloadwould start the new binary while the old one still held the ports, causing the new binary to silently exit (port-conflict guard inserve.go). launchd then waited its default 10 s before retrying, exceeding the installer's 60 s poll window.LoadLaunchAgentnow callsWaitForProcessExitafter unload, pollingpgrep -x krouterevery 100 ms (up to 5 s) before issuingload, so the new binary always starts against free ports.
Added
config.WaitForProcessExit(name, timeout, interval, checkFn)— injectable process-exit poller extracted fromLoadLaunchAgentfor unit testing- 6 unit tests for
WaitForProcessExit: immediate return when already gone, polls until checker returns false, times out correctly, passes correct name to checker, zero-timeout returns immediately, exits on first false response
[2.0.17] - 2026-05-18
Added
- Installer tests — DoneStep (13 cases): covers initial render, spinner on click,
navigation on first poll, navigation after later poll, timeout error (correct
/krouter/fallback URL), network-failure timeout, retry after timeout, error cleared on retry, finalize-410 swallowed, finalize-500 swallowed, missingredirect_urltriggers fallback error - Installer tests — ShellStep (13 cases): covers initial render, Skip without
API call, Applying… spinner, success banner + Open Dashboard button, API error
message + retry, finalize called before first poll, navigation to redirect_url,
timeout error with
/krouter/URL, connection-refused timeout, button reappears for retry, finalize-410 swallowed during Open Dashboard flow
Changed
DoneStepandShellStepacceptmaxAttemptsandpollIntervalMsprops (default 40 / 1500 ms) to enable deterministic testing without fake timers
[2.0.16] - 2026-05-18
Changed
- Dashboard URL: management UI now served at
/krouter/instead of/ui/— bookmarks and shell output now showhttp://127.0.0.1:8403/krouter/ - Dashboard branding: sidebar, active-nav highlight, preset buttons, quota bars,
and action buttons now use the KRouter green brand palette (
#25d366) to match the installer wizard; sidebar shows the KRouter logo and version tag - Routing Preset buttons: clicking a preset now gives immediate visual feedback (optimistic update) instead of waiting for the server round-trip to confirm
Fixed
- Providers page: raw
fetchnow throws on non-2xx responses and surfaces an error message instead of silently rendering an empty list - Providers page: MiniMax added to the known-providers list so its setup hint
(
Set MINIMAX_API_KEY to enable) appears when the key is not configured
[2.0.15] - 2026-05-18
Fixed
- macOS: "Open KRouter Dashboard" showed connection error on first install —
two root causes fixed:
launchctl load -wis a no-op when the service is already loaded (reinstall case), leaving the old process running;LoadLaunchAgentnow unloads first so the daemon is always restarted with the updated binary.- Even with the unload fix, timing cannot be fully guaranteed; the installer now
shows a "Starting KRouter daemon…" spinner and polls
/api/install/daemon-readyevery 1.5 s (up to 60 s) before navigating to the dashboard, so the browser only opens the URL once the daemon is actually accepting connections.
- macOS: skipping shell integration left
MarkInstalleduncalled —DoneStepnow callsfinalize(idempotent) before polling, ensuring the~/.kinthai/installedmarker is always written regardless of which path through the wizard the user takes.
[2.0.14] - 2026-05-17
Fixed
- macOS: install wizard opened a second browser tab at port 8405 —
krouter-installerpassed noSrcBinaryto the orchestrator, soCopyBinary()fell back toos.Executable()(the installer itself) and copied it to~/.local/bin/krouter; the LaunchAgent then startedkrouter-installerinstead ofkrouter, which spawned a fresh wizard on port 8405 and opened a new browser tab showing the installer's first page. Fixed by detecting the co-locatedkrouterdaemon binary (e.g. inside the.appbundle'sContents/MacOS/) and using it asSrcBinary. - macOS: "Open KRouter Dashboard" button navigated before daemon was ready —
handleFinalizenow polls:8403/healthfor up to 10 s before minting the session ticket, ensuring the redirect URL carries a valid ticket instead of falling back to an unauthenticated/ui/URL; the button shows "Opening dashboard…" while waiting.
[2.0.13] - 2026-05-15
Fixed
- Linux: shell integration written to wrong file —
DetectShellRC()mapped bash to~/.bash_profileon all platforms; bash on Linux now correctly targets~/.bashrc(macOS keeps~/.bash_profile, which is correct for macOS login shells) - Daemon token clobbering on port conflict — when
install --yestriggered multiple rapid daemon starts (e.g. idempotent re-runs), each short-lived instance would overwrite~/.kinthai/internal-tokenbefore failing to bind, leaving the real daemon holding a stale token;servenow exits silently before writing the token if the proxy port is already bound
[2.0.12] - 2026-05-15
Fixed
- SQLite driver replaced with pure-Go implementation — switched from
mattn/go-sqlite3(requires CGO) tomodernc.org/sqlite(pure Go), enablingCGO_ENABLED=0cross-compilation; removes the CGO toolchain requirement from all build environments including Windows cross-compilation
[2.0.11] - 2026-05-15
Fixed
- MiniMax base URL corrected — domestic (Chinese mainland) API keys only work
on
api.minimax.chat; the adapter was incorrectly usingapi.minimax.io
[2.0.10] - 2026-05-15
Fixed
- Windows: daemon not starting after installation — three root causes resolved:
service_other.gobuild tag (!linux && !darwin) inadvertently caught Windows, makingRegisterService()a silent no-op; replaced with a properservice_windows.gothat callsRegisterTask+StartTask- NSIS script registered the Task Scheduler task but never ran it, so the
daemon only started on the next login; added
schtasks /Run /TN "krouter-daemon"immediately aftertask-install orchestrator.RegisterService()hardcoded~/.local/bin/krouter(a Linux path); replaced withplatformDaemonPath()which returns%LOCALAPPDATA%\kinthai\krouter.exeon Windows
Added
config.StartTask()— runs the Task Scheduler task immediately viaschtasks /Run(Windows only; stub on other platforms)config.TaskName()— exports the task name constant ("krouter-daemon")
[2.0.9] - 2026-05-15
Fixed
- Transparent proxy correctness: removed accidental API key injection from the
Anthropic-protocol adapter. krouter now forwards the client's
x-api-keyheader unchanged to all Anthropic-compatible upstreams (including MiniMax). Previously, MiniMax requests incorrectly attempted to inject a server-sideMINIMAX_API_KEY.
Added
- Test coverage for ticket and session expiry (
TestExchangeTicket_ExpiredFails,TestSessionCookie_ExpiredFails) TestOrchestrator_ShellIntegration_Fish— verifies Fish shellconfig.fishpath is created with the krouter marker blockTestListen_PortConflict_TriesNext— verifies install server binds the next port when the requested port is already occupiedTestWriteLaunchAgentPlist_ReturnsError_OnNonMacOS— verifies the macOS LaunchAgent stub returns an error on Linux/Windows
[2.0.8] - 2026-05-14
Added
- MiniMax provider adapter (
internal/providers/minimax): Anthropic-messages protocol athttps://api.minimax.io/anthropic, enabled viaMINIMAX_API_KEY - Routing engine now prefers the provider that explicitly lists the requested model
(
pickProviderForModel), preventing MiniMax-model requests from being mis-routed to the Anthropic upstream
[2.0.7] - 2026-05-13
Fixed
- PATCH
/internal/settingsreturned 503 becauseapiSrv.SetSettings()was never called inserve.go
[2.0.6] - 2026-05-13
Fixed
- SPA routes (e.g.
/ui/logs) returned HTTP 301 due tohttp.FileServer's/index.html→./canonicalization; fixed by readingindex.htmldirectly - Linux systemd user service:
krouter install --yesnow runsloginctl enable-lingerand setsXDG_RUNTIME_DIR=/run/user/<uid>before callingsystemctl --user, so installation works on SSH-only servers without an active login session
[2.0.0] - 2026-05-13
BREAKING: Replaced Wails v2 desktop GUI with embedded React web UI served by the daemon. This changes the install flow, binary distribution, and GUI entry point.
Changed
- GUI architecture: daemon now embeds React web UI at
http://127.0.0.1:8403/ui/instead of shipping a separate Wails desktop binary - Install flow: replaced Wails app with
krouter-installer, a standalone binary that serves a browser-based wizard at:8404(no Electron, no native window) - Two-binary distribution:
krouter(daemon + CLI) +krouter-installer(one-shot wizard, exits after setup completes) - Authentication: management API now supports session cookies in addition to Bearer tokens; ticket-exchange flow allows tray/CLI to open the web UI without re-entering credentials
Added
krouter install— TTY installer with--yes/--dry-run/--skip-agentsflagskrouter uninstall— uninstaller with--keep-dataflag- Web UI pages: Dashboard, Logs, Providers, Settings, Announcements, About
- SSE event stream at
GET /internal/eventsfor real-time UI updates - Desktop notifications via
gen2brain/beeep(CGO-free; quota warnings, announcements, upgrade available) - Session cookie auth (
POST /internal/auth/ticket→GET /internal/auth/exchange) GET /internal/settingsandPATCH /internal/settingsendpointsGET /internal/budgetendpoint (today's savings breakdown)- macOS
.dmgrelease artifact (krouter.app bundle,LSUIElement=true) - Linux
.AppImagerelease artifact - Windows
krouter-setup.exeNSIS installer (launches wizard on completion) - Signed
manifest.json+manifest.json.sigon every release
Removed
cmd/krouter-gui/— Wails v2 desktop application and all Wails dependencies
[1.0.3] - 2026-05-13
Patch release; CI pipeline fixes only. No functional changes from v1.0.0.
[1.0.0] - 2026-05-13
Initial release (Wails-based architecture; superseded by v2.0.0).
Added
- HTTP proxy on 127.0.0.1:8402 (Anthropic + OpenAI protocols)
- Routing engine with Balanced + Saver presets
- Providers: Anthropic, OpenAI, DeepSeek, Groq, Moonshot, GLM, Qwen
- LaunchAgent / systemd --user / Windows Task Scheduler integration
- SQLite-based per-request logging
- Notification center with 6h CDN poll
- Self-update with ECDSA manifest verification
- LAN remote access with HTTPS and pairing tokens
- CLI: status, logs, budget, config, test