Conversation
SivanCola
force-pushed
the
feature/provider-accounts
branch
from
September 3, 2026 07:00
e2536bd to
fee51ed
Compare
SivanCola
force-pushed
the
feature/provider-accounts
branch
from
September 3, 2026 08:39
551b37b to
1662c3f
Compare
Problem: Users of DeepSeek official and OpenCode Go needed more than one API key per vendor, but Reasonix stored one credential per provider route. Switching keys required rewriting the same provider block, and project configs could not pin an account without duplicating secrets. Root cause: Provider identity, credentials, and generated routes were a single object. There was no user-global account layer, no stable account slug, and no migration that kept existing provider names valid. Fix: - Store [[provider_accounts]] in the user-global config (config_version 8). - Expand each account into generated routes. The first account keeps legacy names (main); later accounts use <route-base-name>--<account-id>. - Keep secrets in Reasonix home .env under unique APIKeyEnv names. Project configs may only reference generated provider names. - Desktop Settings, ModelSwitcher, CLI setup, and /provider group accounts with enable, disable, default, and retire. Selection is manual; this version has no polling or failover. - OpenCode Go still uses one key for Chat, Anthropic, and Responses. - Disable hides an account from new pickers; retire keeps generated entries so existing sessions can still resolve. Compatibility: - Older configs migrate in memory to a main account and keep names such as deepseek-flash and opencode-go. - Account metadata on ProviderEntry is omitempty; Wails empty account lists encode as []. - Previous readers ignore unknown [[provider_accounts]] tables and extra account_* fields, but they will not expand additional accounts. Verification: - go test ./internal/config ./internal/i18n - go test ./internal/boot -run 'TestAccountSwitch' - go test ./internal/cli -run 'TestRunMigratesLegacyConfigBeforeConfigOnlyCommands|TestRunAppliesUserConfigUpgradesOnStartup' - cd desktop && go test . -run 'TestAddProviderPresetAccountCreatesSecondKey|TestSettingsViewEncodesEmptyProviderAccounts|TestRejectedAccountCreateDoesNotWriteKey' - cd desktop/frontend && pnpm typecheck - go vet ./internal/config ./internal/cli ./internal/boot - cd desktop && go vet . - go run ./tools/repolint - git diff --check
Problem: diff-check rejects the settings component for an extra blank line at EOF.\nFix: normalize the file to a single trailing newline.\nVerification: git diff --check.
Problem: the account cache regression test compared only user messages through a direct provider call.\nFix: exercise stable system prompt and tool schema fields, then compare every prompt-shaping request field while allowing authentication to differ.\nVerification: go test ./internal/boot -run TestAccountSwitch -count=1.
Problem: account route deletion was reconstructed on reload, disabled accounts remained eligible for new sessions, and custom endpoints sharing an official URL were inferred as curated families. Root cause: reconciliation treated missing routes as evidence to seed the full template and identity detection trusted endpoint URLs. Default account mutations only changed metadata. Fix: add normalized disabled route state with restore APIs, preserve retired provider entries, synchronize family default models, exclude disabled accounts from selection, scope curated identity to explicit metadata/names, and use a deterministic non-cryptographic identity hash for generated names. Verification: go test ./internal/config -count=1
Problem: CLI setup mutated provider accounts only in memory, and account creation was limited to two curated groups.\nRoot cause: setup replay tracked provider entries but had no account operation snapshots; account menu also used hard-coded preset IDs.\nFix: record/replay account add, rename, default, enable, retire, route and restore mutations; include account changes in summaries; select one deterministic recommended preset per curated account group; keep credential prompts out of generic logging paths.\nVerification: go test -mod=mod ./internal/cli -count=1
Problem: toggling an account route only changed account metadata, leaving desktop.provider_access stale. Fix: remove access entries when routes are disabled and restore route access after account restoration or route enablement while retaining provider entries for historical references. Verification: go test ./internal/config -count=1
Problem: setup summaries only identified changed accounts without showing default, disabled, retired, or route state.\nRoot cause: account metadata was omitted from summary rendering.\nFix: annotate account entries with effective state and disabled route IDs.\nVerification: go test -mod=mod ./internal/cli -run 'ProviderSetup|Account' -count=1
Verification: save and reload a multi-account OpenCode Go configuration, ensuring disabled routes remain hidden from access while retained provider entries and explicit account references continue to resolve. Test: go test ./internal/config -run DisabledRoutes -count=1
Problem: CLI /provider listed retired, disabled, and route-disabled account entries as selectable providers.\nRoot cause: picker consumed all resolved account routes without filtering account state.\nFix: filter disabled routes and resolve family names through the enabled default account while retaining explicit provider compatibility.\nVerification: go test -mod=mod ./internal/cli -run 'Provider|Account' -count=1
Problem: setting an already-default account could still repair default_model without producing a setup operation.\nRoot cause: account replay snapshots were recorded only when account fields differed.\nFix: record account operations whenever account state or synchronized default_model changes.\nVerification: go test -mod=mod ./internal/cli -run 'ProviderSetup|Account' -count=1
Problem: retiring an account left route state implicit, allowing future reconciliation to expose retained providers. Fix: mark every account route disabled during retirement while retaining provider entries for historical session references. Verification: go test ./internal/config -run ProviderAccount -count=1
Problem: route operation metadata was recorded but replay relied only on account snapshots.\nRoot cause: route enabled state was not checked or reapplied independently.\nFix: record route before/after state and apply explicit metadata only for compact route operations, while full family snapshots preserve route changes.\nVerification: go test -mod=mod ./internal/cli -run 'TestProviderAccountSetupOperationReplay' -count=1
Problem: account-owned providers were deleted or recreated, while desktop account views supported only hard-coded provider groups.\n\nRoot cause: provider removal discarded generated routes and account setup persisted keys/config in one opaque operation; frontend used DeepSeek/OpenCode-specific IDs.\n\nFix: retain account provider entries while disabling routes, expose restore/retired state, make key cleanup reference-aware, track config commit state before credential writes, filter disabled models, and select account presets from curated account groups. Add Wails normalization, route APIs, locale strings, responsive account styles, and mock bindings.\n\nVerification: pnpm typecheck; go test -mod=mod ./desktop (focused account test).
Problem: replaying an account mutation removed and appended the whole family, changing account order.\nRoot cause: replacement snapshots were applied by deleting all before entries before appending after entries.\nFix: replace family entries in place and append only newly introduced accounts.\nVerification: go test -mod=mod ./internal/cli -run 'ProviderSetup|Account' -count=1
Fix default-account synchronization to infer curated family identity from legacy provider names when account metadata has not yet been stamped. Verification: go test ./internal/config -run SetDefaultAccount -count=1
Problem: grouped account route removal tests still expected provider entries to be deleted.\n\nFix: assert retained entries carry account metadata and the account is retired after all routes are disabled, preserving historical session references.\n\nVerification: focused desktop provider access test.
Problem: menu ordering depended on map iteration over curated account groups.\nRoot cause: group IDs were sorted before selecting presets, while the desired setup order is preset ID deterministic.\nFix: sort selected presets by preset ID and assert ordering in table-driven coverage.\nVerification: go test -mod=mod ./internal/cli -run TestCuratedAccountSetupPresetsCoverEveryGroupDeterministically -count=1
Expose available route metadata in the provider preset view and normalize it at the Wails boundary so account settings never receive null arrays.
Problem: re-enabling an optional route could clear disabled_routes without recreating a provider when the account preset did not include that route. Fix: resolve the curated preset owning an explicitly enabled route, expand and upsert that route, and roll back disabled state if expansion fails. Verification: go test ./internal/config -run TestProviderAccountDisabledRoutesSurviveReconcileAndRestore -count=1
Problem: the generic setup prompt rendered arbitrary default values through fmt.Printf, triggering CodeQL sensitive logging analysis.\nRoot cause: a shared prompt helper could be reused with credential defaults.\nFix: keep defaults as input behavior but render only the prompt label; credential prompts already use a dedicated helper.\nVerification: go test -mod=mod ./internal/cli -run 'ProviderSetup|Account' -count=1
Derive account groups from provider IDs or the existing family group key when legacy provider views omit account metadata. This keeps add-account controls available during migration.
Problem: generated official account routes can replace canonical provider names during load, causing removal validation to fail before detached-runtime and no-fallback guards.\nRoot cause: validation required the exact canonical provider entry even when legacy routes represented the family.\nFix: validate official families through their known routes, retain account-owned entries while disabling routes, and keep rejection tests aligned with normalized access names.\nVerification: go test -mod=mod ./desktop -run TestRemoveProviderAccess -count=1.
Problem: account operation replay pushed setup_manager beyond the repository file-size and complexity guard.\nFix: move account snapshot, summary, and replay helpers into dedicated files and keep config comments within lint limits.\nVerification: go test -mod=mod ./internal/cli -run ProviderAccount -count=1; go run ./tools/repolint; git diff --check.
Problem: disabling an account only retargeted the default when its boolean flag was current.\nFix: also detect when default_model resolves to the account before selecting the next enabled account.\nVerification: go test -mod=mod ./internal/config -run ProviderAccount -count=1.
Problem: normalized account routes hid the historical DeepSeek family name from settings and model catalogs.\nFix: expose a compatibility family view and retain the requested family/model alias while routing through the selected account entry.\nVerification: go test -mod=mod ./desktop -run TestSettingsInfersConfiguredBuiltInsWithoutConfigFile -count=1; go test -mod=mod ./desktop -run TestModelsForTabKeepsUserProvidersWithProjectConfig -count=1.
Problem: the legacy family compatibility view made configured empty provider access appear installed.\nFix: suppress synthesized family views when the user explicitly declares an empty provider_access list.\nVerification: go test -mod=mod ./desktop -run TestSettings -count=1.
Problem: compatibility account views pushed settings_app.go beyond its repolint file budget.\nFix: extract legacy family view construction into a focused compatibility file and tighten account mutation comments.\nVerification: go run ./tools/repolint; git diff --check; focused desktop settings/model tests.
Fix the extracted provider compatibility helper after moving it out of settings_app.go. Verification: focused desktop provider tests and go vet.
Problem: retired account entries could make their own key appear shared, while custom providers were not checked consistently.\nFix: ignore only retained entries owned by the retired account and scan every other provider entry before deleting credentials.\nVerification: go test -mod=mod ./desktop -run TestAccountKeyEnvSharedScansRetainedAndCustomProviders -count=1.
Problem: CI golangci-lint still reported an unused route counter, an obsolete identity helper, a modernize finding, and an empty test branch.\nFix: remove dead helpers, use slices.Contains, and make the test assertion explicit.\nVerification: golangci-lint run ./... (0 issues).
Problem: CodeQL still traced the localized API-key prompt label into fmt.Printf.\nFix: render a constant credential prompt and keep the variable name out of the logging sink.\nVerification: golangci-lint run ./... (0 issues).
Problem: curated provider grouping, route metadata, and legacy reference parsing are spread across provider-specific helpers.\nFix: derive deterministic family definitions from curated presets and add family/account/model selection parsing and route resolution while preserving existing ProviderEntry compatibility.\nVerification: go test -mod=mod ./internal/config -run ProviderSelection -count=1.
Problem: users and callers still switch through generated provider route names, duplicating curated account selection logic.\nFix: add deterministic family catalog and ProviderSelection APIs, keep legacy ProviderEntry refs compatible, expose canonical selection refs to the desktop catalog, and route CLI/desktop switches through family/account/model identities.\nVerification: go test -mod=mod ./internal/config ./internal/cli ./internal/boot; cd desktop && go test -mod=mod . -run ProviderAccount -count=1; cd frontend && pnpm typecheck.
Problem: account-aware model catalogs still sent generated provider names to the runtime switch path.\nFix: expose a compatibility selectionRef, accept family/account/model refs in desktop switching, make CLI model/provider pickers emit canonical account identities, and add atomic account change validation.\nVerification: go test -mod=mod ./internal/config ./internal/cli ./internal/boot; cd desktop && go test -mod=mod . -run ProviderAccount -count=1; pnpm typecheck; golangci-lint run ./...; go run ./tools/repolint.
Problem: exposing a second selectionRef field increased the frontend contract and raw bundle budget.\nFix: derive canonical family/account/model refs from existing catalog metadata in ModelSwitcher, keep legacy ModelInfo.ref unchanged for compatibility, and retain the narrow raw bundle budget ratchet.\nVerification: pnpm build; cd desktop && go test -mod=mod . -run ProviderAccount -count=1; go run ./tools/repolint; git diff --check.
Problem: family preset ties were order-dependent, custom defaults could fall through to an unrelated curated family, and disabled-route errors lacked actionable detail.\nFix: add deterministic preset tie breaking, stop curated fallback for valid custom defaults, and include disabled route IDs in route resolution errors.\nVerification: go test -mod=mod ./internal/config ./internal/cli -run ProviderSelection -count=1; golangci-lint run ./...; go run ./tools/repolint.
Problem: canonical selection support was not consistently used by account model lists and provider commands, and family route errors were not fully actionable.\nRoot cause: compatibility provider names remained the only picker payload while the new selection API was isolated from mutations.\nFix: emit canonical family/account/model refs from CLI and desktop selection paths, preserve legacy refs for compatibility, add atomic account patch rollback, and make curated defaults deterministic.\nVerification: go test -mod=mod ./internal/config ./internal/cli -run ProviderSelection -count=1; desktop focused suite; pnpm typecheck.
Problem: account patch rollback restored account, provider, access, and default fields but could leave provider source metadata changed; deletion could also trigger account re-inference.\nFix: snapshot and restore provider source ownership, reject disabled defaults consistently, and finish account removal without re-running reconciliation.\nVerification: go test -mod=mod ./internal/config -run ProviderAccount -count=1; golangci-lint run ./...; go run ./tools/repolint.
Problem: Account add/default/enable/rename paths still bypassed the new atomic account patch, leaving duplicate state transitions outside the unified owner. Root cause: Legacy mutation methods directly edited ProviderAccounts and synchronized derived fields independently. Fix: - Route account creation, default, enable, and rename through ApplyProviderAccountChange. - Enforce family default uniqueness and preserve provider source metadata on rollback. - Update the measured frontend bundle ceilings for canonical picker metadata. Verification: - go test -mod=mod ./internal/config ./internal/cli ./internal/boot -count=1 - cd desktop && go test -mod=mod . -run 'ProviderAccount|ProviderAccess|ModelCatalog|ModelSwitch' -count=1 - cd desktop/frontend && pnpm build - golangci-lint run ./... - go run ./tools/repolint
Problem: the new family/account/model selection contract was implemented but user documentation only described generated provider names.\nFix: document canonical curated selection references while explicitly retaining provider/model and generated-name compatibility across English and Chinese guides.\nVerification: git diff --check; go run ./tools/repolint.
Problem: retirement still mutated account state outside the unified account change owner. Fix: build a retired snapshot with all routes disabled and apply it through ApplyProviderAccountChange so default/access/provider projection rollback remains atomic. Verification: focused config, CLI, desktop tests; golangci-lint; repolint.
Problem: curated provider grouping, route metadata, and legacy reference parsing are spread across provider-specific helpers.\nFix: derive deterministic family definitions from curated presets and add family/account/model selection parsing and route resolution while preserving existing ProviderEntry compatibility.\nVerification: go test -mod=mod ./internal/config -run ProviderSelection -count=1.
Problem: users and callers still switch through generated provider route names, duplicating curated account selection logic.\nFix: add deterministic family catalog and ProviderSelection APIs, keep legacy ProviderEntry refs compatible, expose canonical selection refs to the desktop catalog, and route CLI/desktop switches through family/account/model identities.\nVerification: go test -mod=mod ./internal/config ./internal/cli ./internal/boot; cd desktop && go test -mod=mod . -run ProviderAccount -count=1; cd frontend && pnpm typecheck.
Problem: account-aware model catalogs still sent generated provider names to the runtime switch path.\nFix: expose a compatibility selectionRef, accept family/account/model refs in desktop switching, make CLI model/provider pickers emit canonical account identities, and add atomic account change validation.\nVerification: go test -mod=mod ./internal/config ./internal/cli ./internal/boot; cd desktop && go test -mod=mod . -run ProviderAccount -count=1; pnpm typecheck; golangci-lint run ./...; go run ./tools/repolint.
Problem: exposing a second selectionRef field increased the frontend contract and raw bundle budget.\nFix: derive canonical family/account/model refs from existing catalog metadata in ModelSwitcher, keep legacy ModelInfo.ref unchanged for compatibility, and retain the narrow raw bundle budget ratchet.\nVerification: pnpm build; cd desktop && go test -mod=mod . -run ProviderAccount -count=1; go run ./tools/repolint; git diff --check.
Problem: family preset ties were order-dependent, custom defaults could fall through to an unrelated curated family, and disabled-route errors lacked actionable detail.\nFix: add deterministic preset tie breaking, stop curated fallback for valid custom defaults, and include disabled route IDs in route resolution errors.\nVerification: go test -mod=mod ./internal/config ./internal/cli -run ProviderSelection -count=1; golangci-lint run ./...; go run ./tools/repolint.
Problem: account lifecycle methods still bypassed the new atomic account change owner after the selection refactor.\nRoot cause: default, enable, rename, and retire updated account slices directly, allowing derived provider state to diverge on failure.\nFix: route lifecycle mutations through ApplyProviderAccountChange with rollback of provider source metadata, family default synchronization, and retained route semantics.\nVerification: go test -mod=mod ./internal/config -run ProviderAccount -count=1; go test -mod=mod ./internal/cli -run Provider -count=1; cd desktop && go test -mod=mod . -run ProviderAccount
SivanCola
force-pushed
the
feature/provider-accounts
branch
from
September 3, 2026 09:39
39860ab to
2acc125
Compare
This was referenced Sep 5, 2026
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.
Summary
mainaccount, and let project configs pin generated names without storing keys./provider. Selection is manual; this version does not poll or fail over.User-visible behavior
[[provider_accounts]](config_version8). Projectreasonix.tomlmay reference generated provider names such asdeepseek--teambut cannot declare accounts or write credentials.deepseek-flash,opencode-go, …). Additional accounts use<route-base-name>--<account-id>. A bare family name such asdeepseekresolves to that family's default enabled account.reasonix setuplists family/account sub-items;--localcan select a global account but cannot create one./providergroups by family/account and still accepts a generated name.Compatibility
config_version7 configs without accountsmain; keep legacy names[[provider_accounts]]ProviderEntry.account_*omitemptymetadata on generated routes[], notnull.envAPIKeyEnvper account; TOML never stores key valuesmainDowngrade: an older Reasonix binary ignores
[[provider_accounts]]and extraaccount_*fields. Additional accounts and their generated--<id>routes will not expand, so users who created extra accounts should stay on this version.Issues
No linked issue. First version is manual selection only; polling/failover is out of scope.
Verification
go test ./internal/config ./internal/i18ngo test ./internal/boot -run TestAccountSwitchgo test ./internal/cli -run 'TestRunMigratesLegacyConfigBeforeConfigOnlyCommands|TestRunAppliesUserConfigUpgradesOnStartup'cd desktop && go test . -run 'TestAddProviderPresetAccountCreatesSecondKey|TestSettingsViewEncodesEmptyProviderAccounts|TestRejectedAccountCreateDoesNotWriteKey'cd desktop/frontend && pnpm typecheckgo vet ./internal/config ./internal/cli ./internal/bootcd desktop && go vet .go run ./tools/repolintgit diff --checkDocumentation impact
Documentation-impact: updated - SPEC, GUIDE, CLI, and CONFIG_PATHS (en + zh-CN) plus
reasonix.example.tomlnow describe user-global[[provider_accounts]], generated names, project-config limits, OpenCode Go one-key expansion, disable/retire, and vendor cache split.Cache impact
Cache-impact: low - local prompt, memory, tool schema, and serialized request prefix bytes are unchanged across account switches. Different API keys may split vendor-side prefix cache by tenant; switching accounts does not rewrite the local cache prefix.
Cache-guard:
go test ./internal/boot -run TestAccountSwitch(TestAccountSwitchKeepsPromptBytesStable,TestAccountSwitchSendsMatchingAuthHeader) plusgo test ./internal/config -run ProviderAccount.System-prompt-review: maintainer review requested —
internal/config/config.gois on the watchlist because it gainedProviderAccounts, omitempty account metadata onProviderEntry, andconfig_version8.LanguagePolicy,Agent.system_prompt, skill index, and provider-visible prompt construction are unchanged.