Skip to content

Pre-fill a per-Provider default model for PPIO and Novita, and point the key affordance at key management #76

Description

@yujiezhang-ops

Problem

A first-time user who signs up with PPIO or Novita currently has to supply a model ID by hand before anything works. Product feedback is that this is the wrong ask: a beginner has no basis for choosing between deepseek/deepseek-v3.2 and deepseek/deepseek-v4-flash, and the field is mandatory in every path that reaches a Profile.

The goal is that for the two built-in Providers, the onboarding ask reduces to sign in / register → paste an API key. The model comes pre-filled with a sane default the user can still change.

A second, smaller ask rides along: the "get a key" affordance should land the user on the key-management page rather than the marketing home page.

Current behaviour, verified

The model is required at every Profile boundary. internal/profile/write.go:52-55 rejects an empty model in Save, and write.go:169-173 rejects it in WriteActive when Configure is true. internal/app/desktopapp.go:187-190 rejects it for desktop Agents.

The UI never pre-fills it. frontend/src/pages/ProfilesPage.tsx:78 seeds a new Profile with model: "", and ProfilesPage.tsx:260 is a plain required free-text input whose only hint is a placeholder. frontend/src/components/ModelPicker.tsx renders the discovered list plus a free-text box, with no default selection. The wizard reducer picks state.model || state.probeModel.trim() || action.result.models[0] || "" (frontend/src/state/wizardReducer.ts:309), so a default only appears as a side effect of discovery succeeding.

A correct per-Provider default already exists in the repo, and already reaches config writing. providers.lock.json carries fallback_probe_model per Provider, and despite the name it is not probe-only: internal/app/provider.go:260-275 (resolveProviderModel) and internal/app/install.go:187-200 both fall back to it, so the value already ends up written into Agent configs today. Both are now pinned to deepseek/deepseek-v4-pro (advanced from deepseek-v3 in #77).

But the frontend cannot read it. catalog.PublicProviders (internal/catalog/public.go:59-68) and ProviderByID (public.go:70-77) deliberately zero fallbackModel before returning, and internal/catalog/catalog_test.go:62-71 fails if the serialized public providers contain "fallback" or "deepseek". So the pre-fill cannot be done by simply reading the existing provider list — it needs a deliberate new public field and that test updated on purpose.

Registration opens the marketing home page. internal/binding/services.go:253-272 (OpenRegistration) resolves entry.Home and opens it — https://ppio.com/ or https://novita.ai/. Two frontend affordances use the same value: frontend/src/pages/ProviderKeyPage.tsx:118-123 ("Register and get a key", via the Wails opener) and frontend/src/pages/ProvidersPage.tsx:197-201 ("Website", a plain <a href> that bypasses the opener and its URL validation). The CLI does the same at cmd/oneagent/main.go:275-286, defaulting unknown Providers to PPIO's home.

providers.lock.json also has a referral_url field, empty for both Providers. It is not consumed anywhere — there is no such field on providerFileEntry (internal/catalog/providers.go:20-26), so json.Unmarshal silently drops it. Same for relationship, disclosure, and order, which exist for the public site only.

Findings that shape the design

1. The two model IDs differ by one character — no longer true, but the mechanism must still be per-Provider. At v3 the two IDs were spelled differently (PPIO deepseek/deepseek-v3, Novita deepseek/deepseek_v3), so no shared constant could serve both. At v4 both publish the identical deepseek/deepseek-v4-pro, verified present in each /v1/models listing, and #77 pinned both to it.

This removes the immediate hazard but is not a licence to hardcode. The two Providers are independent catalogues that happened to converge on one release; they can diverge again, and a custom Provider has no reason to match either. Keep the default a per-Provider manifest field.

2. Both /v1/models endpoints answer without a key. Verified: https://api.ppio.com/openai/v1/models returns 108 models and https://api.novita.ai/openai/v1/models returns 145, both anonymously. Both lock-file fallbacks are still present upstream. This means the model list can be populated before the key is entered, which is a cheaper fix than a static per-Provider catalog and stays correct as the upstream catalogue moves.

3. The two current defaults are stale — resolved in #77. The pin was four releases behind (deepseek-v3 while both Providers listed v4-pro, v4-flash, and v3.2), which would have put a visibly old model on every new user's first screen. #77 advanced it to deepseek/deepseek-v4-pro.

Worth recording why pro and not flash: this value reaches real Agent configs, so coding capability was weighted over per-token cost. The gap is large — 3x on PPIO, 11x on Novita. If first-run cost turns out to matter more, switching to v4-flash is three lines in the lock file. Also note v4-flash-0731 has the newest timestamp but is a dated snapshot and would go stale on the next one; the rolling aliases are the right pin target.

4. Codex needs the Responses protocol. internal/catalog/public.go:12 maps codexresponses, and providers.lock.json marks responses as release-candidate-required for both Providers. A default model that works for OpenCode/Kilo/Aider (Chat Completions) and Claude Code (Anthropic Messages) is not automatically a working Codex default. The pre-fill must not imply a verified Codex path.

5. Sponsor neutrality is a written commitment. Both entries carry "Sponsorship does not affect Agent rank, compatibility conclusions, default selection, or connection tests." Pre-filling a default model for the two sponsor Providers is defensible as an ergonomic default derived from published data, but the wording of that disclosure says "default selection", so the framing needs care and the same mechanism should be open to custom Providers rather than hardcoded to the two sponsors.

6. https://ppio.com/settings/key-management and https://novita.ai/settings/key-management both return 200. PPIO's locale-prefixed variants (/zh/…, /en/…) 404, so PPIO must stay unprefixed. Novita answers on /settings/…, /zh/settings/…, and /en/settings/…; use the unprefixed form so the site picks the user's locale. Note that a key-management page requires an existing account, so the button label ("Register and get a key") stops matching where it lands — it becomes closer to "Get an API key".

Proposed scope

A. Expose a per-Provider default model.

Add a new, explicitly public field to providers.lock.json (suggested default_model, distinct from fallback_probe_model so the probe hint and the user-facing default can diverge), add it to providerFileEntry, and carry it through catalog.Providerprovider.Entry → the Wails DTO → frontend/src/types/api.ts. Regenerate frontend/bindings. Update TestPublicProjectionDoesNotExposeFallbackModel deliberately — the assertion currently keys on the substring "deepseek", which a public default model would trip; it should assert on the internal field's absence instead of on a model vendor name.

B. Pre-fill it in the UI, without making it sticky.

ProfilesPage openCreate and the wizard's model step should seed from the selected Provider's default rather than "". The value must remain editable and must be re-seeded when the Provider changes (wizardReducer.ts:210 already clears the model on Provider change — that is the hook). ProfilesPage.tsx:260 should keep working as a free-text field; this is a default, not a constraint.

C. Load the model list before the key is entered.

Since both endpoints answer anonymously, ModelSelectionPage can populate the list without a key, which turns the pre-filled default into a visible, checkable choice rather than an opaque string. Keep the existing keyed path as the primary — an anonymous list is a convenience and must not become a hard dependency, since a Provider is free to start requiring auth there.

D. Point the key affordance at key management.

Add a separate field (suggested key_management_url) rather than overwriting home: home is still the right target for the "Website" link on ProvidersPage and is validated as HTTPS for every built-in Provider (internal/catalog/providers.go:92-94). Set it to https://ppio.com/settings/key-management and https://novita.ai/settings/key-management. OpenRegistration should prefer key_management_url and fall back to home, keeping the existing scheme/host/userinfo validation. Relabel the button in ProviderKeyPage so it describes where it goes. Decide whether ProvidersPage.tsx:197-201 should stop being a raw <a href> and go through the same validated opener — right now it is the one place a Provider URL reaches the browser without any check.

The empty referral_url should either be wired up in the same precedence chain or deleted from the lock file. Leaving a field that nothing reads invites someone to fill it in and assume it took effect.

Out of scope

Advancing the pinned DeepSeek version, and any per-Agent model recommendation (Codex/Responses in particular). Both are worth separate issues.

Acceptance

  • A new Profile created against PPIO or Novita arrives with a model already filled in, read from that Provider's manifest entry, and still editable.
  • Changing the Provider re-seeds the default rather than keeping the previous Provider's.
  • The onboarding path for a user with no key is: open key management → paste key → the model is already set.
  • Custom Providers with no default behave exactly as today (empty field, required).
  • go test ./... and pnpm run test pass, including the deliberately updated catalog projection test.
  • A test reads each built-in default from the manifest rather than comparing against a literal, so the two Providers converging on one model id today does not become a hardcoded assumption tomorrow.

Update: #77 landed the model-version half of this issue — both built-in Providers are now pinned to deepseek/deepseek-v4-pro, and the interface placeholders were updated to match. Findings 1 and 3 above are struck through accordingly. The substance of this issue is unchanged: the default is still invisible to the frontend, ProfilesPage.tsx:78 still seeds model: "", and the key affordance still opens the marketing home page.

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions