Conversation
…ic format
OpenCode stores credentials as {"providerName": {"api": "key"}} in
~/.local/share/opencode/auth.json. The generic fileContainsCredential
function only recognises keys like api_key, auth_token, etc. — the
single-word "api" key that OpenCode uses is not matched, so WeSight
always showed "未检测到本机登录或密钥" even for users whose CLI auth
works fine.
Fix: add openCodeAuthJsonLoggedIn() which explicitly understands the
OpenCode auth.json shape, and call it from summarizeCliAuthStatus for
the opencode app-type after the generic credential scan.
Closes freestylefly#78
|
Someone is attempting to deploy a commit to the canghe's projects Team on Vercel. A member of the Team first needs to authorize it. |
The first version of this fix looked for {provider: {api|token|key|apiKey}},
but OpenCode's Auth.Info schema is a discriminated union:
api -> { type: 'api', key }
oauth -> { type: 'oauth', refresh, access, expires }
wellknown -> { type: 'wellknown', key, token }
An api login stores the secret under 'key', not 'api', and an oauth login has
no key-like field at all, so OAuth users stayed undetected.
- Add openCodeAuthEntryLoggedIn covering all three variants, with a field-sniffing
fallback for older/unknown formats
- Scan every auth.json candidate instead of only the secondaryConfigPaths entry
- Read auth.json via readJsonObject so a .jsonc data dir also parses
- Broaden opencode env keys (DEEPSEEK_API_KEY, GEMINI_API_KEY, etc.); the issue
reporter had DEEPSEEK_API_KEY and GEMINI_API_KEY exported but neither was checked
- Add 12 regression tests; 9 of them fail against the previous implementation
Follow-up: the assumed auth.json schema was wrongSelf-review against OpenCode upstream ( OpenCode's Oauth = { type: "oauth", refresh: string, access: string, expires: number, ... }
Api = { type: "api", key: string, metadata?: ... }
WellKnown = { type: "wellknown", key: string, token: string }The first implementation checked
Fix in
|
Issue freestylefly#78 has two halves. The credential *detection* half was fixed earlier in this branch. This commit fixes the remaining half: the model WeSight actually hands to `opencode run`. When `opencode.json(c)` carries no `model` field (the exact state in the report: the file holds only `"$schema"`), `listOpenCodeModelProviders` synthesized `DEFAULT_OPENCODE_MODEL` = `anthropic/claude-sonnet-4-5` and stored it as the current provider. The reporter had only DeepSeek and Google credentials in `auth.json`, so WeSight launched OpenCode with `--model anthropic/claude-sonnet-4-5`, a provider they never authenticated. That is consistent with a task that emits `OpenCode step started.` and then makes no further progress. Changes: - openCodeConfig.ts: add `listOpenCodeAuthProviderIds()`, which reads OpenCode's `Auth.Info` union (api | oauth | wellknown) and returns the provider IDs that hold a usable credential. `listOpenCodeModelProviders` accepts an optional `authProviderIds` and drops the *synthesized* default when its provider is not logged in. An explicitly configured `model` is always kept: that is the user's own declaration. - externalAgentProviderStore.ts: pass the auth.json provider IDs when syncing OpenCode live providers. - externalAgentEnvironment.ts: move the auth-schema check to openCodeConfig.ts and re-export it, so the credential reader and the model lister cannot drift apart. Behaviour is unchanged; verified against the assertions already shipped in openCodeAuthDetection.test.ts. Omitting `authProviderIds` preserves the previous behaviour, so no other caller changes semantics. Tests: openCodeAuthModel.test.ts, 9 cases. Reverse-verified — 6 of the 9 fail against the pre-change implementation.
|
Follow-up on this branch (commit The credential detection fix makes WeSight see the DeepSeek/Google logins. It does not change which model WeSight actually runs — and that is what matches the reported symptom.
So WeSight was launching OpenCode with The fix gates only the synthesized default on whether its provider appears in I also moved the Verification: 9 new cases in One honest limitation: with no logged-in provider, OpenCode now simply receives no |
…model list Dropping the synthesized anthropic/claude-sonnet-4-5 default for users who never authenticated Anthropic left listOpenCodeModelProviders returning an empty list for the reporter's config (only "$schema"). An empty list makes syncOpenCodeLiveProviders fall through to importLiveProviderIfEmpty -> readLiveSettingsConfig, which rebuilds `model` as DEFAULT_OPENCODE_LOCAL_MODEL - the same unusable Anthropic default, handed straight to `opencode run --model`. The stall in issue freestylefly#78 therefore survived the previous fix. Emit one credential-backed record per logged-in provider when the list would otherwise be empty, carrying no model so the runtime omits --model and lets OpenCode resolve the model itself. Guard the two places that re-derive a model from an empty one (summarizeOpenCodeSettingsConfig and applyProviderToLive) with an explicit OPENCODE_MODEL_UNSET_KEY marker, so the default cannot be resurrected - and in particular is never persisted into the user's own opencode.json(c), where it would count as an explicit declaration. Refs freestylefly#78
|
Follow-up on this PR: the earlier commits fixed credential detection but the reported stall was still reproducible, so I pushed What was still broken. For the exact inputs in the issue — I wrote the reproduction as a failing assertion first ( What Verification. 12 new tests; reverse-verified — 4 fail against the previous implementation and pass now. Five separate tests pin down that nothing changes outside this scenario (omitted Two things worth your attention rather than a rubber stamp:
Also unchanged from before: when no provider is authenticated, the deferral to OpenCode is silent — the engine picker shows no "no authenticated provider" hint. That's a UI change beyond this issue's scope; happy to open a separate issue if you want it. |
Status update (2026-09-20): rebased onto the new
|
Merge-order note (2026-09-21): #83 and this PR now conflict in one blockHeads up on an interaction that appeared today rather than a defect in this PR.
This PR rewrites the same block in A three-way merge of the two branches now reports 1 conflict, localized entirely to that one Both changes are wanted and they are not in opposition:
The resolution is to keep both — the unset-marker branch should write through the comment-preserving writer with an empty changed-key set for if (settingsConfig[OPENCODE_MODEL_UNSET_KEY] === true) {
writeJsonConfigFile(getOpenCodeConfigPath(), baseConfig, Object.keys(storedConfig));
return;
}
const selectedModel = /* unchanged */;
writeJsonConfigFile(
getOpenCodeConfigPath(),
{ ...baseConfig, model: selectedModel },
['model', ...Object.keys(storedConfig)],
);I have deliberately not pushed that to either branch: it would make this PR depend on #83's unmerged head, and whichever of the two you merge second is the natural place to resolve it. Flagging it now so the conflict is not a surprise at merge time. Say the word and I will prepare the resolution on whichever branch you plan to merge second. Nothing in this PR needs to change for its own sake — CI on |
Rebased onto
|
2026-10-03: the conflict resolution, written out and verifiedNo upstream movement since yesterday ( Verification method. Pulled all three versions of
The resolved block (replaces the whole conflicted if (provider.appType === OPENCODE_APP_TYPE) {
const existingConfig = readJsonObject(getOpenCodeConfigPath()) ?? {};
const storedConfig = parseOpenCodeConfig(settingsConfig.config);
const baseConfig = {
...existingConfig,
...(Object.keys(storedConfig).length > 0 ? storedConfig : {}),
};
// A provider stored with the unset marker is credential-backed but carries no
// model. Persisting DEFAULT_OPENCODE_LOCAL_MODEL here would write the
// unauthenticated Anthropic default into the user's own opencode.json(c) and
// permanently resurrect the stall reported in issue #78 (PR #85).
// The write still goes through writeJsonConfigFile so hand-written comments in
// opencode.jsonc survive (PR #83); 'model' is excluded from changedKeys so the
// user's own model declaration is left untouched.
if (settingsConfig[OPENCODE_MODEL_UNSET_KEY] === true) {
writeJsonConfigFile(
getOpenCodeConfigPath(),
baseConfig,
[...Object.keys(storedConfig)],
);
return;
}
const selectedModel = getString(settingsConfig.model)
|| summarizeOpenCodeSettingsConfig(settingsConfig).model
|| DEFAULT_OPENCODE_LOCAL_MODEL;
const nextConfig = { ...baseConfig, model: selectedModel };
writeJsonConfigFile(
getOpenCodeConfigPath(),
nextConfig,
['model', ...Object.keys(storedConfig)],
);
return;
}What I checked on it, beyond "it looks right":
Behaviour preserved from each side: the unset branch still returns without ever writing a Still not pushing this — it would make this branch depend on #83's unmerged head, and the merge order is yours to pick. Merge either PR first; the second one needs exactly this one block. One caveat I want to keep visible: the end-to-end check is still unperformed, and I cannot do it from here. Take a real |
Problem
WeSight shows "未检测到本机登录或密钥" (No local login or key detected) for OpenCode even when the user has successfully authenticated, and tasks stall at "OpenCode step started.".
Root Cause
OpenCode stores credentials in
~/.local/share/opencode/auth.jsonas a map of provider ID to a discriminated union (Auth.Infoinpackages/opencode/src/auth/index.ts):So a real file looks like:
{ "deepseek": { "type": "api", "key": "sk-..." }, "anthropic": { "type": "oauth", "refresh": "...", "access": "...", "expires": 0 } }WeSight's generic
fileContainsCredentialmatches credential-like key names (api_key,auth_token,access_token, …). None ofkey,access, orrefreshqualify on their own, so the scan finds nothing andsummarizeCliAuthStatusfalls through tologged_out.Separately, the
opencodeentry inlocalEnvKeysByAppTypeonly listedOPENAI_API_KEY/ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN, so env-provided keys for other providers were also missed — the reporter hadDEEPSEEK_API_KEYandGEMINI_API_KEYexported.Fix
openCodeAuthEntryLoggedIn(entry)— schema-aware check that switches ontypeand validates the fields each variant actually uses:api→key;oauth→refreshoraccess;wellknown→keyortoken. Falls back to field sniffing when the discriminator is absent, for older or future formats. Reuses the existingisNonPlaceholderSecrethelper so placeholder values are rejected consistently.summarizeCliAuthStatus— OpenCode branch — after the generic scan, checks everyauth.jsonamong the auth path candidates (not just thesecondaryConfigPathsentry, since the data dir can be relocated), reading throughreadJsonObject.Broader env keys — the
opencodeenv-key list now covers the common Models.dev providers, since OpenCode resolves credentials for any of them.Part 2 — the stall itself: an unauthenticated default model
The detection fix above makes WeSight see the credentials. It does not
change which model WeSight runs, and that turned out to be the second half
of #78.
listOpenCodeModelProviderssynthesizedDEFAULT_OPENCODE_MODEL(
anthropic/claude-sonnet-4-5) as the current provider wheneveropencode.json(c)carried nomodelfield — which is exactly the reportedstate, where the config holds only
"$schema". That record was written toexternal_agent_providersasis_current, andExternalCliRuntimeAdapterpassesselectedProvider.summary.modelstraight to
opencode run --model.The reporter's
auth.jsonheld DeepSeek and Google only. So WeSightinvoked OpenCode with
--model anthropic/claude-sonnet-4-5, a providerthey never authenticated — consistent with a task that prints
OpenCode step started.and then makes no further progress, while thesame CLI works fine from a terminal.
Fix:
listOpenCodeAuthProviderIds(authJson)— returns the provider IDs inauth.jsonthat hold a usable credential, reusing the sameAuth.Infoschema logic as part 1.
listOpenCodeModelProviders(config, { authProviderIds })— drops thesynthesized default when its provider is not logged in. An explicitly
configured
modelis always kept: that is the user's own declaration,and a missing credential there is a different problem. Omitting the
option preserves the previous behaviour exactly, so no other caller
changes semantics.
syncOpenCodeLiveProviders— passes theauth.jsonprovider IDs in.To avoid two copies of the schema rules, the auth-entry check now lives in
openCodeConfig.tsand is re-exported fromexternalAgentEnvironment.ts,so the credential reader and the model lister cannot drift apart. Behaviour
is unchanged — verified against the assertions already shipped in
openCodeAuthDetection.test.ts.Files Changed
src/main/libs/externalAgentEnvironment.tssrc/main/libs/externalAgentProviderStore.tssrc/main/libs/openCodeConfig.tssrc/main/libs/openCodeAuthDetection.test.ts(new)src/main/libs/openCodeAuthModel.test.ts(new)Self-review
appType === 'opencode'; no other app type changes behaviour.auth.jsonfrom OpenCode credentials not detected although CLI auth works; task stalls atOpenCode step started.#78. 9 of them fail against the previous implementation, confirming they are genuine regression tests.npx tsc --noEmitexit 0;npx eslintclean;npm test→ 76 files / 563 tests passed.auth.jsonreturnsfalse— no regression for fresh installs.Self-review — part 2
listOpenCodeAuthProviderIdsis pure; the new option onlistOpenCodeModelProvidersis optional, so every existing call site keepsits current behaviour. The repo's 7 pre-existing
openCodeConfigtests passunmodified.
openCodeAuthModel.test.ts. Reverse-verified: 6 of the 9fail against the pre-change implementation, so they are genuine regression
tests rather than tautologies.
even with no visible credential; configured
provider.*.modelsentries arestill listed when the synthesized default is dropped; blank and placeholder
secrets are rejected; malformed input returns an empty list.
npx tsc --noEmit --strictexit 0.eslintclean under the repo's owneslint.config.mjs(the import-sort error the autofixer flagged on the newimport is fixed in this commit, not suppressed).
Remaining scope caveat
With no logged-in provider, OpenCode now receives no
--modeloverride andfalls back to its own resolution, which is the correct deferral. What this PR
deliberately does not do is surface a user-facing "no authenticated
OpenCode provider" message in the engine picker — that is a UI change beyond
the scope of #78 and worth a separate issue if you want it. Please confirm the
end-to-end run on a machine with a partial
auth.jsonbefore closing #78.Part 3 — the previous fix still left #78 reproducible (2026-09-18)
Dropping the unauthenticated default was necessary but not sufficient. For the
reporter's exact inputs —
opencode.json(c)containing only"$schema", and anauth.jsonholding DeepSeek + Google — Part 2 madelistOpenCodeModelProvidersreturn an empty list. That is not a safe end state:
syncOpenCodeLiveProviderswrites no rows, soimportLiveProviderIfEmptyfires,which calls
readLiveSettingsConfig, which rebuildsmodelasDEFAULT_OPENCODE_LOCAL_MODEL— the veryanthropic/claude-sonnet-4-5that wasjust dropped. It is then handed to
opencode run --model. The user is back to atask that stalls at
OpenCode step started.with a provider they never loggedinto.
Change
record per logged-in provider (
opencode-auth-<providerKey>), reusing anyname/apiKey/baseURLthe config declares for that provider.model: '', so the runtime'sif (model) args.push('--model', model)omits the flag and OpenCode resolvesthe model itself.
explicit
OPENCODE_MODEL_UNSET_KEYmarker:summarizeOpenCodeSettingsConfigreturns''instead of the default;applyProviderToLiveno longer writes amodelkey at all. This onematters most: persisting the default into the user's own
opencode.json(c)would turn it into an explicit declaration and make the stall permanent
and invisible.
Self-review — part 3
openCodeAuthFallback.test.tsfail against the Part 2 implementation and pass after this change. The
starting reproduction (
records.length === 0) was confirmed failing beforeany code was written.
openCodeAuthModel.test.tsencoded theincomplete behaviour (
expect(records.filter(isCurrent)).toEqual([])). It isupdated, not deleted, and now asserts the corrected intent. Flagging it
explicitly since changing an existing test deserves a look.
omitting
authProviderIdsis byte-for-byte the historical path; an explicitmodelalways wins;provider.*.modelsentries take precedence over thefallback; an authenticated default provider still yields the normal record;
authProviderIds: []correctly yields an empty list rather than a bogus model.isCurrent, so the store's"no current → promote records[0]" branch stays consistent.
tsc --noEmit --strictexit 0.eslint0 problems under the repo's owneslint.config.mjsat the pinned versions (eslint 9.39.4, ts 5.7.3),import order included. 21 tests green on vitest 4.1.0.
Verification environment (disclosure)
This round was verified in the automation sandbox, not on the usual Mac
checkout:
git clonereturnedinvalid credentialsand the tarball endpointreturned HTTP 502, so files were read through the contents API and exercised in
a harness pinned to the repo's own
eslint.config.mjsand dependency versions.Type-check, lint and the unit tests are real runs; a full
npm testacross thewhole suite and an end-to-end OpenCode launch were not performed here.
Please still confirm one real run with a partial
auth.jsonbefore closing #78.Closes #78