feat: pre-fill a Provider's default model and open its key page - #79
Merged
yujiezhang-ops merged 1 commit intoAug 6, 2026
Conversation
A first-time user on PPIO or Novita had to supply a model ID before anything worked, with no basis for choosing one. The value they needed already existed as fallback_probe_model, but the frontend could not read it: PublicProviders zeroes that field, and a test asserted the public projection contained no "deepseek" at all. Adds two manifest fields rather than exposing the probe model, because they answer different questions. default_model is a recommendation a user sees and can edit; the probe model is the cheapest way to prove a key works. Conflating them would have meant either probing with the expensive model or recommending the cheap one. They now hold different values, which also makes a probe-model leak detectable -- with identical values, no substring test could see one. default_model is required per built-in Provider and deliberately has no global fallback: a custom endpoint gets no guessed model, since a guess produces a config that fails on first use. key_management_url is optional and falls back to home, so user-added Providers keep working. Seeding lives in the wizard reducer, next to the Provider selection it follows, rather than in each page. Every path that changes the Provider re-seeds, since model IDs are not portable between Providers. Two precedence traps came out of this: MODELS_RESULT used `state.model ||`, which would now let the seeded default outrank a hand-typed probe model, and STATUS_LOADED had to stay first-load-only so a refresh cannot overwrite a half-typed field. Store.Public rebuilt its overlay from scratch, so saving a key against a built-in Provider would have silently dropped its default model. Relabels the button: it opens a key page that needs an existing account, so "Register and get a key" promised a sign-up flow it no longer starts. Documents in public-site-operations.md that referral_url, relationship, disclosure, order and protocols are site-only and discarded by json.Unmarshal, with a test pinning it -- the guarantee rests on providerFileEntry omitting them, which is invisible at the call site. Leaves ProvidersPage's plain anchor alone: home is validated against javascript:, data:, file: and embedded credentials when saved, so it can only ever hold an http(s) URL.
yujiezhang-ops
merged commit Aug 6, 2026
cf12d28
into
chore/default-model-deepseek-v4-pro
4 checks passed
yujiezhang-ops
deleted the
feat/provider-default-model-and-key-management
branch
August 7, 2026 09:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #76. Stacked on #77 — review that one first; this branch contains its commit, so the base should be switched to
mainonce #77 merges.A first-time user on PPIO or Novita had to supply a model ID before anything worked, with no basis for choosing between
deepseek-v3.2anddeepseek-v4-flash. Onboarding is now sign in → paste key, with the model pre-filled and still editable.Two fields, not one
The value already existed as
fallback_probe_modeland already reached config writing viaresolveProviderModel. It was unreadable from the frontend by design:PublicProviderszeroes it and a test asserted the public projection contained no"deepseek"at all.Rather than un-hide it, this adds
default_modeland keeps the probe model internal, because they answer different questions.default_modelis a recommendation the user sees and edits; the probe model is the cheapest way to prove a key works. Conflating them means either probing with the expensive model on every connection test or recommending the cheap one for daily coding.So they now hold different values —
deepseek-v4-profor the default,deepseek-v4-flashfor probing. That difference is also what makes a leak detectable: with identical values, no substring assertion could tell whether the probe model had escaped into the public projection.TestPublicProjectionDoesNotExposeFallbackModelnow compares against the probe model's actual value instead of the string"deepseek", which would otherwise fail on correct output while still passing if the probe model were renamed.default_modelis required per built-in Provider and deliberately has no global fallback, unlikeFallbackProbeModel. A custom endpoint gets no guessed model — a guess produces a config that fails on the user's first request.key_management_urlis optional and falls back tohome, so user-added Providers keep working unchanged.Where the seeding lives
In the wizard reducer, next to the Provider selection it follows, rather than duplicated per page. Every path that changes the Provider re-seeds (
SET_PROVIDER,SELECT_AGENT,START_SETUP,START_DESKTOP_SETUP,START_NEW_PROFILE, and firstSTATUS_LOADED), because a model ID from one Provider is rarely valid at another.Two precedence traps surfaced while doing this, both now covered by tests:
MODELS_RESULTresolved the model asstate.model || state.probeModel.trim() || models[0]. That was correct whenstate.modelwas usually empty; now that it is seeded, the default would silently outrank a probe model the user typed by hand. The check is now against the seeded value, so an explicit pick wins and the default is only a starting point.STATUS_LOADEDhad to stay first-load-only. A status refresh re-enters that branch, and seeding there unconditionally would overwrite a model the user was midway through typing.In
ProfilesPage, switching Provider only replaces the model when the field still holds the previous Provider's default or nothing — a model the user typed is theirs to keep.A bug found on the way
Store.Publicrebuilt the overlay record from scratch for any Provider with a saved user entry. Since a built-in Provider gets an entry the moment you save a key against it, PPIO would have lost its default model exactly when it started being useful. Fixed by re-reading both catalog fields in the overlay.Key affordance
OpenRegistrationpreferskey_management_url, falls back tohome, and keeps the existing scheme/host/userinfo validation. The frontend still passes no URL — the backend re-resolves it, so a tampered frontend cannot choose what opens. The CLI'spromptForKeyuses the same precedence.The button is relabelled from "Register and get a key" to "Get an API key": a key-management page needs an existing account, so the old wording promised a sign-up flow it no longer starts. Its render guard was
providerMeta?.home, which would have hidden the button for a Provider that publishes only a key page.ProvidersPage's plain<a href>is left as-is. I flagged it in #76 as the one place a Provider URL reaches the browser unvalidated. It is not exploitable:validateEntryrunsValidateBaseURLonhomeat save time, and I verified it rejectsjavascript:,data:,file:and embedded credentials. It is a "Website" link, sohomeis the right target. Routing it through the opener would be churn for no security gain.Lock files
Audited all three.
agents.lock.jsonandruntimes.lock.jsonhave no unparsed fields.providers.lock.jsonhas five that Go never reads —relationship,disclosure,order,protocols,referral_url— whichdocs/public-site-operations.mdsays are for the public site that vendors this file.That is now documented explicitly, including the consequence: filling in
referral_urlchanges nothing, because there is no such field onproviderFileEntryandjson.Unmarshaldiscards it. Routing users through a referral link would need the field parsed and given an explicit precedence, not just a value set.TestSiteOnlyProviderFieldsAreNotParsedpins this — the guarantee currently rests on a struct omitting fields, which is invisible to anyone adding one.Verification
go test ./...,go test -race ./...,go vet ./...all pass.pnpm run testis 215 passing, up from 205.pnpm run typecheckandpnpm run buildpass.python3 scripts/check-docs.pyreports 50 files, links resolve, language split holds. The desktop target compiles withCGO_ENABLED=1 go build -tags wails.Bindings regenerated with the pinned Wails CLI (
v3.0.0-beta.4, same flags asbuild/Taskfile.yml); the diff is confined to the two model files.frontend/src/types/api.tsneeded no change — it re-exportsCatalogModels.ProviderandProviderModels.Entrydirectly.The frozen status fixture gained exactly the two new provider fields and nothing else drifted.
Three new tests were mutation-checked rather than assumed: reverting the
PublicProviderszeroing, makingDefaultModelfall back for unknown Providers, and wiringreferral_urlintoproviderFileEntryeach fail the intended test and only that test.Not in scope
Loading the model list before a key is entered (part C of #76). Both
/v1/modelsendpoints do answer anonymously, but the pre-fill makes the default visible without it, and depending on an unauthenticated endpoint a Provider is free to lock down deserves its own change. Also unchanged:ModelPicker's<small>OpenAI-compatible model</small>, which is wrong for the Anthropic protocol, and any per-Agent model recommendation for Codex/Responses.🤖 Generated with Claude Code