getSelectedProvider() fell back to 'openrouter', so a brand-new user with no
configuration was pointed at OpenRouter: they needed an OpenRouter key to use
OUR product, and the usage was metered by them. A second store
(providers/storage, behind the settings panel) fell back to 'openai', and
llm-client held a third opinion — so the app gave two different competitors as
the answer to one question. DEFAULT_PROVIDER now states it once, as 'hanzo',
and all three read it. NEXT_PUBLIC_DEFAULT_PROVIDER still overrides, and
OpenRouter stays a first-class BYO-key choice.
Flipping the default alone would have been incoherent, because the catalogue
did not follow the selection: fetchAvailableModels() always fetched
openrouter.ai. Our gateway serves 107 bare ids; OpenRouter's are vendor/model.
It now takes the provider and fetches that provider's own /models, adapting
both wire shapes into ProviderModel — the shape the picker, the model cache and
the pricing cache already read. OpenRouterModel and its four satellite
interfaces are gone; so is the 68-line copy of the adapter that lived inside
the picker, and registerOpenRouterPricingFromApi, whose per-million heuristic
duplicated the conversion.
The unit trap: OpenRouter prices are STRINGS in USD per token, ours are NUMBERS
per MILLION tokens. That is 1e6, so the conversion happens in one place and
everything downstream is per-million. Verified live: enso arrives {4, 20}
unscaled, OpenRouter's '0.000002' becomes $2/MTok.
Fields a provider does not publish stay undefined rather than being invented.
api.hanzo.ai states a context window for 23 of 107 models and tool support for
7, so contextLength is now optional and the two render sites omit it instead of
printing NaN. The old getDefaultModels() fallback — four OpenRouter ids at 2024
prices — is deleted: a failed fetch now throws, and both callers already catch.
Three things the flip made load-bearing, all wrong before it:
- getDefaultModel('hanzo') fell through to 'deepseek/deepseek-chat', an id our
gateway does not serve. It reads lib/providers' DEFAULT_MODEL now, so the
BYOK picker and the builder open on the same model.
- The picker auto-selected models[0], which made display ORDER the default —
on our own catalogue that opened on a resold claude-*/gpt-* rung instead of
Enso. It takes the provider's declared default when the provider serves it.
- CostCalculator.isKnownProvider was a hand-kept copy of the registry and had
drifted: hanzo, minimax, llamacpp and openai-codex were missing, so the live
prices the picker had already cached were ignored and every call was quoted
at DEFAULT_PRICING. It asks the registry.
Also: validate-key had no hanzo case, and the generic branch probes
baseUrl/models — which is public on our gateway and answers 200 for any
string, so every Hanzo key validated and a typo only surfaced at generation.
It now probes the endpoint that authenticates. judge.ts fell back to
openrouter.ai when a provider had no baseUrl, which would have sent the user's
key somewhere they never chose; it states the error instead. generation-api.ts
is deleted — an OpenRouter-only client with zero callers.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>