Fix provider/model drift in Docker auth and free-mode selection#171
Fix provider/model drift in Docker auth and free-mode selection#171friuns2 wants to merge 25 commits into
Conversation
Review Summary by QodoFix provider/model drift in Docker auth and free-mode selection
WalkthroughsDescription• **Provider-scoped model selection**: Introduced StoredModelSelection type to store models with provider IDs, preventing cross-provider model leakage and ensuring model compatibility during provider switches. • **Live error overlay deduplication**: Modified error handling to suppress duplicate errors only after persistence, keeping new live errors visible while preventing redundant display. • **Thread materialization error handling**: Added detection of transient first-turn materialization errors and mapping them to in-progress states instead of visible errors. • **Free-mode auth suppression**: Implemented logic to suppress community fallback providers when usable Codex auth exists, while preserving user-configured providers. • **External auth detection and import**: Added automatic detection and import of copied auth.json into accounts list when Codex auth becomes available, triggering provider refresh. • **Provider model list authority**: Refactored model fetching to respect exclusive flag from provider models endpoint, ensuring correct model list merging and deduplication. • **Failed turn error rendering**: Added normalization and UI rendering of persisted turn errors as system messages with feedback links. • **Comprehensive test coverage**: Added unit and integration tests for provider-scoped model selection, free-mode behavior, gateway normalization, and server bridge handling. • **Documentation and wiki updates**: Captured Docker auth flows, provider selection drift behavior, and regression testing procedures in wiki notes and manual test guidance. Diagramflowchart LR
A["No-auth Docker startup"] -->|"OpenCode Zen fallback"| B["Provider-scoped model selection"]
C["Copied auth.json"] -->|"External auth detection"| D["Account import & provider refresh"]
B -->|"Provider switch"| E["Reject incompatible models"]
D -->|"Codex auth available"| F["Suppress community providers"]
G["First-turn materialization"] -->|"Transient errors"| H["Map to in-progress state"]
B -->|"Model list authority"| I["Respect exclusive flag"]
J["Failed turns"] -->|"Error normalization"| K["Render as system messages"]
File Changes1. src/composables/useDesktopState.test.ts
|
Code Review by Qodo
1. Provider id alias breaks restore
|
📝 WalkthroughWalkthroughThis PR implements Docker provider/auth handling including provider-scoped model selection, turn error rendering, free-mode state refactoring, and external Codex auth import. Changes span provider-backed model loading with fallbacks, synthetic error messages in chat, server-side free-mode state cleanup, localStorage migration to structured selections, and app-level external auth account synchronization. ChangesDocker Provider/Auth & Provider-Scoped Model Selection
Sequence Diagram(s)sequenceDiagram
participant UI as UI Layer
participant App as App.vue
participant Gateway as codexGateway
participant Provider as Provider Models Endpoint
participant Codex as Codex Server
participant Accounts as Accounts Import
UI->>App: user detected external auth
App->>App: track externalCodexAuthAvailable
App->>Accounts: maybeImportExternalCodexAuthAccount()
Accounts->>Accounts: refreshAccountsFromAuth()
Accounts->>App: conditionally refreshAll()
Gateway->>Provider: fetchProviderModelIds()
alt exclusive or requireProviderModels
Provider-->>Gateway: exclusive: true, ids: [...]
Gateway-->>UI: return provider IDs only
else
Provider-->>Gateway: 404/error
Gateway->>Codex: model/list RPC
Codex-->>Gateway: [modelIds]
Gateway-->>UI: return merged IDs
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/server/codexAppServerBridge.ts (1)
6361-6408:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle pending materialization in
thread-turn-pagetoo.This route still converts the same
thread/readcondition into a 500 that Lines 6517-6526 now normalize to an in-progress empty thread. A newly created thread can therefore recover in/codex-api/thread-live-statebut still fail when the UI asks this endpoint for a turn page.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/codexAppServerBridge.ts` around lines 6361 - 6408, The thread-turn-page handler currently treats a pending/materializing thread the same as an invalid response and returns a 500; update the logic (after calling mergeStreamTurnErrorsIntoThreadResult and computing record and thread using asRecord) to detect the same "in-progress empty thread" condition handled elsewhere (i.e. record && thread exist but thread.turns is empty because materialization is pending) and return a 200 payload matching the normalized in-progress response (result with the same record but thread.turns: [], startTurnIndex: 0, hasMoreOlder: false) instead of proceeding to slice turns and/or raising a 500; adjust before the slicing code that computes turns/beforeIndex/startIndex so this early-return mirrors the behavior implemented for live-state recovery and avoids failing sanitizeThreadTurnsInlinePayloads or mergeSessionSkillInputsIntoThreadResult.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/api/codexGateway.ts`:
- Around line 1919-1923: The current logic skips calling fetchProviderModelIds
when options.includeProviderModels is false, which causes requireProviderModels
to be ignored; change the condition so that fetchProviderModelIds is invoked if
options.requireProviderModels is true OR options.includeProviderModels !== false
(e.g., compute a flag like shouldFetchProviderModels =
options.requireProviderModels || options.includeProviderModels !== false), use
that flag instead of shouldIncludeProviderModels when calling
fetchProviderModelIds, and keep the subsequent check of
providerModels?.exclusive or options.requireProviderModels to return
providerModels?.ids ?? [] as before; update references to
shouldIncludeProviderModels, fetchProviderModelIds,
options.includeProviderModels, and options.requireProviderModels accordingly.
In `@src/App.vue`:
- Around line 2116-2132: The function maybeImportExternalCodexAuthAccount sets
externalAuthImportAttempted too early, preventing retries after a failed
refresh; change the logic so externalAuthImportAttempted is only set when an
import actually succeeds (i.e., after refreshAccountsFromAuth resolves and
accounts are updated) or ensure the flag is cleared/reset inside the catch path
(where loadAccountsState is called) so subsequent calls can retry; update the
control flow around externalAuthImportAttempted in
maybeImportExternalCodexAuthAccount and keep
refreshAccountsFromAuth/loadAccountsState handling intact.
In `@src/composables/useDesktopState.ts`:
- Around line 1548-1562: The suppression logic currently hides live error
overlays when the persisted turnError text matches liveErrorText; change this to
suppress only when the persisted turnError belongs to the same turn by using the
turnId in TurnErrorState. In the block that scans persistedMessagesByThreadId
(used by persistedMessages and latestPersistedTurnErrorText), also read the live
turnId from turnErrorByThreadId.value[threadId]?.turnId and when iterating
messages only consider messages with messageType === 'turnError' and
message.turnId === liveTurnId before setting latestPersistedTurnErrorText;
finally, change the final errorText condition to compare persisted turnId match
(or the fact that latestPersistedTurnErrorText was set from the same turn)
rather than comparing message text so only errors from the same turn suppress
the live overlay.
- Around line 1611-1620: readCompatibleStoredModelId currently accepts a stored
{providerId, modelId} when the provider matches even if the modelId is not in
availableModelIds, allowing reuse of stale, unavailable models; update
readCompatibleStoredModelId (and the checks around normalizeStoredModelId,
normalizeStoredModelProviderId, normalizeProviderContextId) so that when a
storedProviderId exists you return normalizedModelId only if storedProviderId
=== currentProviderId AND availableModelIds.value includes(normalizedModelId);
otherwise return '' (keep the existing fallback behaviour when no
storedProviderId).
In `@src/server/codexAppServerBridge.ts`:
- Around line 3352-3363: ensureDefaultFreeModeStateForMissingAuthSync currently
relies on hasUsableCodexAuthSync which only treats access_token as usable;
mirror the async auth logic so refresh-token-only auth is recognized. Update
hasUsableCodexAuthSync (or add a sync helper used by
ensureDefaultFreeModeStateForMissingAuthSync) to implement the same checks as
the async path that sets hasCodexAuth (i.e., treat presence of a valid refresh
token as usable auth), then use that value in
shouldSuppressCommunityFreeModeForCodexAuth and
shouldCreateDefaultFreeModeStateForMissingAuth so the sync path reports
hasCodexAuth=true and avoids recreating community free-mode when only a refresh
token exists.
In `@src/server/freeMode.ts`:
- Line 151: The change to FREE_MODE_STATE_FILE needs a backward-compatibility
fallback: add a LEGACY_FREE_MODE_STATE_FILE constant (value
"webui-free-mode.json") and update the state-load logic (the server bridge /
state loader that reads FREE_MODE_STATE_FILE) to first attempt reading
FREE_MODE_STATE_FILE, and if it does not exist, attempt to read
LEGACY_FREE_MODE_STATE_FILE; if the legacy file is successfully loaded, persist
that data back to FREE_MODE_STATE_FILE so upgraded users keep their
provider/model state. Ensure references to FREE_MODE_STATE_FILE remain for
future reads/writes and only use the legacy constant as a fallback/migration
step.
---
Outside diff comments:
In `@src/server/codexAppServerBridge.ts`:
- Around line 6361-6408: The thread-turn-page handler currently treats a
pending/materializing thread the same as an invalid response and returns a 500;
update the logic (after calling mergeStreamTurnErrorsIntoThreadResult and
computing record and thread using asRecord) to detect the same "in-progress
empty thread" condition handled elsewhere (i.e. record && thread exist but
thread.turns is empty because materialization is pending) and return a 200
payload matching the normalized in-progress response (result with the same
record but thread.turns: [], startTurnIndex: 0, hasMoreOlder: false) instead of
proceeding to slice turns and/or raising a 500; adjust before the slicing code
that computes turns/beforeIndex/startIndex so this early-return mirrors the
behavior implemented for live-state recovery and avoids failing
sanitizeThreadTurnsInlinePayloads or mergeSessionSkillInputsIntoThreadResult.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5195ce66-6c27-4da6-ae65-07c433e13567
📒 Files selected for processing (24)
AGENTS.mdllm-wiki/raw/fixes/copied-auth-provider-promotion.mdllm-wiki/raw/fixes/opencode-zen-docker-auth-provider-models.mdllm-wiki/raw/fixes/provider-selection-drift-docker-cycle.mdllm-wiki/wiki/concepts/opencode-zen-big-pickle.mdllm-wiki/wiki/entities/codex-web-local.mdllm-wiki/wiki/index.mdllm-wiki/wiki/log.mdllm-wiki/wiki/overview.mdsrc/App.vuesrc/api/codexGateway.test.tssrc/api/codexGateway.tssrc/api/normalizers/v2.test.tssrc/api/normalizers/v2.tssrc/components/content/ThreadConversation.vuesrc/composables/useDesktopState.test.tssrc/composables/useDesktopState.tssrc/server/codexAppServerBridge.archive.test.tssrc/server/codexAppServerBridge.tssrc/server/freeMode.test.tssrc/server/freeMode.tssrc/style.csstests.mdwhatToTest.md
| const shouldIncludeProviderModels = options.includeProviderModels !== false | ||
| const providerModels = shouldIncludeProviderModels ? await fetchProviderModelIds() : null | ||
|
|
||
| if (providerModels?.exclusive || options.requireProviderModels) { | ||
| return providerModels?.ids ?? [] |
There was a problem hiding this comment.
Honor requireProviderModels even when includeProviderModels is false.
At Line 1919, includeProviderModels: false prevents fetching provider models entirely. If requireProviderModels: true is also set, Line 1922 returns [] without querying /codex-api/provider-models.
💡 Proposed fix
-export async function getAvailableModelIds(options: { includeProviderModels?: boolean; requireProviderModels?: boolean } = {}): Promise<string[]> {
- const shouldIncludeProviderModels = options.includeProviderModels !== false
+export async function getAvailableModelIds(options: { includeProviderModels?: boolean; requireProviderModels?: boolean } = {}): Promise<string[]> {
+ const shouldIncludeProviderModels =
+ options.requireProviderModels === true || options.includeProviderModels !== false
const providerModels = shouldIncludeProviderModels ? await fetchProviderModelIds() : null📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const shouldIncludeProviderModels = options.includeProviderModels !== false | |
| const providerModels = shouldIncludeProviderModels ? await fetchProviderModelIds() : null | |
| if (providerModels?.exclusive || options.requireProviderModels) { | |
| return providerModels?.ids ?? [] | |
| const shouldIncludeProviderModels = | |
| options.requireProviderModels === true || options.includeProviderModels !== false | |
| const providerModels = shouldIncludeProviderModels ? await fetchProviderModelIds() : null | |
| if (providerModels?.exclusive || options.requireProviderModels) { | |
| return providerModels?.ids ?? [] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/api/codexGateway.ts` around lines 1919 - 1923, The current logic skips
calling fetchProviderModelIds when options.includeProviderModels is false, which
causes requireProviderModels to be ignored; change the condition so that
fetchProviderModelIds is invoked if options.requireProviderModels is true OR
options.includeProviderModels !== false (e.g., compute a flag like
shouldFetchProviderModels = options.requireProviderModels ||
options.includeProviderModels !== false), use that flag instead of
shouldIncludeProviderModels when calling fetchProviderModelIds, and keep the
subsequent check of providerModels?.exclusive or options.requireProviderModels
to return providerModels?.ids ?? [] as before; update references to
shouldIncludeProviderModels, fetchProviderModelIds,
options.includeProviderModels, and options.requireProviderModels accordingly.
| async function maybeImportExternalCodexAuthAccount(): Promise<boolean> { | ||
| if (!externalCodexAuthAvailable) return false | ||
| if (externalAuthImportAttempted) return false | ||
| if (selectedProvider.value !== 'codex') return false | ||
| if (accounts.value.length > 0) return false | ||
| if (accountRateLimitSnapshots.value.length === 0) return false | ||
| externalAuthImportAttempted = true | ||
| const previousAccountsJson = JSON.stringify(accounts.value.map((account) => account.accountId).sort()) | ||
| try { | ||
| const result = await refreshAccountsFromAuth() | ||
| accounts.value = result.accounts | ||
| } catch { | ||
| await loadAccountsState({ silent: true }) | ||
| } | ||
| const nextAccountsJson = JSON.stringify(accounts.value.map((account) => account.accountId).sort()) | ||
| return previousAccountsJson !== nextAccountsJson | ||
| } |
There was a problem hiding this comment.
Retry lockout after failed external auth import
Line 2122 sets externalAuthImportAttempted before a successful import. If the first import attempt fails, later snapshot updates cannot retry, so account sync can stay stuck empty.
Suggested fix
+let externalAuthImportInFlight = false
+
async function maybeImportExternalCodexAuthAccount(): Promise<boolean> {
if (!externalCodexAuthAvailable) return false
if (externalAuthImportAttempted) return false
+ if (externalAuthImportInFlight) return false
if (selectedProvider.value !== 'codex') return false
if (accounts.value.length > 0) return false
if (accountRateLimitSnapshots.value.length === 0) return false
- externalAuthImportAttempted = true
+ externalAuthImportInFlight = true
const previousAccountsJson = JSON.stringify(accounts.value.map((account) => account.accountId).sort())
try {
const result = await refreshAccountsFromAuth()
accounts.value = result.accounts
} catch {
await loadAccountsState({ silent: true })
+ } finally {
+ externalAuthImportInFlight = false
}
const nextAccountsJson = JSON.stringify(accounts.value.map((account) => account.accountId).sort())
- return previousAccountsJson !== nextAccountsJson
+ const imported = previousAccountsJson !== nextAccountsJson
+ if (imported) externalAuthImportAttempted = true
+ return imported
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/App.vue` around lines 2116 - 2132, The function
maybeImportExternalCodexAuthAccount sets externalAuthImportAttempted too early,
preventing retries after a failed refresh; change the logic so
externalAuthImportAttempted is only set when an import actually succeeds (i.e.,
after refreshAccountsFromAuth resolves and accounts are updated) or ensure the
flag is cleared/reset inside the catch path (where loadAccountsState is called)
so subsequent calls can retry; update the control flow around
externalAuthImportAttempted in maybeImportExternalCodexAuthAccount and keep
refreshAccountsFromAuth/loadAccountsState handling intact.
| const liveErrorText = (turnErrorByThreadId.value[threadId]?.message ?? '').trim() | ||
| let latestPersistedTurnErrorText = '' | ||
| if (!isInProgress && liveErrorText) { | ||
| const persistedMessages = persistedMessagesByThreadId.value[threadId] ?? [] | ||
| for (let index = persistedMessages.length - 1; index >= 0; index -= 1) { | ||
| const message = persistedMessages[index] | ||
| if (message.messageType !== 'turnError') continue | ||
| latestPersistedTurnErrorText = normalizeMessageText(message.text) | ||
| break | ||
| } | ||
| } | ||
| const errorText = | ||
| !isInProgress && liveErrorText && latestPersistedTurnErrorText === liveErrorText | ||
| ? '' | ||
| : liveErrorText |
There was a problem hiding this comment.
Suppress duplicate error overlays by turn, not by message text.
This now hides the live overlay whenever the latest persisted turnError text matches the current live error. Repeating a common failure like a 401/auth error on a later turn will therefore hide the fresh failure until message sync catches up. Track the failing turnId in TurnErrorState and only suppress when the persisted error belongs to that same turn.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/composables/useDesktopState.ts` around lines 1548 - 1562, The suppression
logic currently hides live error overlays when the persisted turnError text
matches liveErrorText; change this to suppress only when the persisted turnError
belongs to the same turn by using the turnId in TurnErrorState. In the block
that scans persistedMessagesByThreadId (used by persistedMessages and
latestPersistedTurnErrorText), also read the live turnId from
turnErrorByThreadId.value[threadId]?.turnId and when iterating messages only
consider messages with messageType === 'turnError' and message.turnId ===
liveTurnId before setting latestPersistedTurnErrorText; finally, change the
final errorText condition to compare persisted turnId match (or the fact that
latestPersistedTurnErrorText was set from the same turn) rather than comparing
message text so only errors from the same turn suppress the live overlay.
| function readCompatibleStoredModelId(value: StoredModelSelection | undefined): string { | ||
| const normalizedModelId = normalizeStoredModelId(value) | ||
| if (!normalizedModelId) return '' | ||
| const storedProviderId = normalizeStoredModelProviderId(value) | ||
| const currentProviderId = normalizeProviderContextId(activeProviderId.value) | ||
| if (storedProviderId) { | ||
| return storedProviderId === currentProviderId ? normalizedModelId : '' | ||
| } | ||
| return availableModelIds.value.includes(normalizedModelId) ? normalizedModelId : '' | ||
| } |
There was a problem hiding this comment.
Reject provider-scoped models that are no longer in the active provider list.
readCompatibleStoredModelId() accepts any structured { providerId, modelId } whose provider matches, even when that modelId is absent from availableModelIds. That lets readModelIdForThread() reuse a stale model until refreshModelPreferences() happens to repair it, so a send can still go out with a model the active provider no longer offers.
Suggested fix
function readCompatibleStoredModelId(value: StoredModelSelection | undefined): string {
const normalizedModelId = normalizeStoredModelId(value)
if (!normalizedModelId) return ''
const storedProviderId = normalizeStoredModelProviderId(value)
const currentProviderId = normalizeProviderContextId(activeProviderId.value)
+ const isAvailable = availableModelIds.value.includes(normalizedModelId)
if (storedProviderId) {
- return storedProviderId === currentProviderId ? normalizedModelId : ''
+ return storedProviderId === currentProviderId && isAvailable ? normalizedModelId : ''
}
- return availableModelIds.value.includes(normalizedModelId) ? normalizedModelId : ''
+ return isAvailable ? normalizedModelId : ''
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function readCompatibleStoredModelId(value: StoredModelSelection | undefined): string { | |
| const normalizedModelId = normalizeStoredModelId(value) | |
| if (!normalizedModelId) return '' | |
| const storedProviderId = normalizeStoredModelProviderId(value) | |
| const currentProviderId = normalizeProviderContextId(activeProviderId.value) | |
| if (storedProviderId) { | |
| return storedProviderId === currentProviderId ? normalizedModelId : '' | |
| } | |
| return availableModelIds.value.includes(normalizedModelId) ? normalizedModelId : '' | |
| } | |
| function readCompatibleStoredModelId(value: StoredModelSelection | undefined): string { | |
| const normalizedModelId = normalizeStoredModelId(value) | |
| if (!normalizedModelId) return '' | |
| const storedProviderId = normalizeStoredModelProviderId(value) | |
| const currentProviderId = normalizeProviderContextId(activeProviderId.value) | |
| const isAvailable = availableModelIds.value.includes(normalizedModelId) | |
| if (storedProviderId) { | |
| return storedProviderId === currentProviderId && isAvailable ? normalizedModelId : '' | |
| } | |
| return isAvailable ? normalizedModelId : '' | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/composables/useDesktopState.ts` around lines 1611 - 1620,
readCompatibleStoredModelId currently accepts a stored {providerId, modelId}
when the provider matches even if the modelId is not in availableModelIds,
allowing reuse of stale, unavailable models; update readCompatibleStoredModelId
(and the checks around normalizeStoredModelId, normalizeStoredModelProviderId,
normalizeProviderContextId) so that when a storedProviderId exists you return
normalizedModelId only if storedProviderId === currentProviderId AND
availableModelIds.value includes(normalizedModelId); otherwise return '' (keep
the existing fallback behaviour when no storedProviderId).
| export function ensureDefaultFreeModeStateForMissingAuthSync(statePath: string): FreeModeState | null { | ||
| const current = readFreeModeStateSync(statePath) | ||
| if (!shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuthSync())) { | ||
| const hasUsableCodexAuth = hasUsableCodexAuthSync() | ||
| if (shouldSuppressCommunityFreeModeForCodexAuth(current, hasUsableCodexAuth)) { | ||
| return null | ||
| } | ||
| if (!shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuth)) { | ||
| return current | ||
| } | ||
|
|
||
| const fallback = createDefaultOpenCodeZenFreeModeState() | ||
|
|
||
| mkdirSync(dirname(statePath), { recursive: true }) | ||
| writeFileSync(statePath, JSON.stringify(fallback), { encoding: 'utf8', mode: 0o600 }) | ||
| return fallback | ||
| return createDefaultOpenCodeZenFreeModeState() | ||
| } |
There was a problem hiding this comment.
Mirror the async auth check here.
ensureDefaultFreeModeStateForMissingAuthSync() now drives both free-mode suppression and the new hasCodexAuth status field, but the sync path only treats access_token as usable auth. If auth.json only has a refresh token, this will still recreate/keep community free-mode state and report hasCodexAuth: false, which reintroduces the provider/model drift this PR is trying to eliminate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/codexAppServerBridge.ts` around lines 3352 - 3363,
ensureDefaultFreeModeStateForMissingAuthSync currently relies on
hasUsableCodexAuthSync which only treats access_token as usable; mirror the
async auth logic so refresh-token-only auth is recognized. Update
hasUsableCodexAuthSync (or add a sync helper used by
ensureDefaultFreeModeStateForMissingAuthSync) to implement the same checks as
the async path that sets hasCodexAuth (i.e., treat presence of a valid refresh
token as usable auth), then use that value in
shouldSuppressCommunityFreeModeForCodexAuth and
shouldCreateDefaultFreeModeStateForMissingAuth so the sync path reports
hasCodexAuth=true and avoids recreating community free-mode when only a refresh
token exists.
| export const FREE_MODE_DEFAULT_MODEL = 'openrouter/free' | ||
|
|
||
| export const FREE_MODE_STATE_FILE = 'webui-free-mode.json' | ||
| export const FREE_MODE_STATE_FILE = 'webui-custom-providers.json' |
There was a problem hiding this comment.
Add a legacy-state fallback before switching the persisted filename.
Changing FREE_MODE_STATE_FILE to a new filename without a read-fallback/migration can silently reset existing persisted provider/model state for upgraded users. Please keep backward compatibility by reading webui-free-mode.json when the new file is absent (and optionally writing back to the new file).
Suggested compatibility approach
-export const FREE_MODE_STATE_FILE = 'webui-custom-providers.json'
+export const FREE_MODE_STATE_FILE = 'webui-custom-providers.json'
+export const LEGACY_FREE_MODE_STATE_FILE = 'webui-free-mode.json'// in the state-load path (server bridge/state loader):
// 1) try FREE_MODE_STATE_FILE
// 2) if missing, try LEGACY_FREE_MODE_STATE_FILE
// 3) if legacy is loaded successfully, persist to FREE_MODE_STATE_FILE🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/freeMode.ts` at line 151, The change to FREE_MODE_STATE_FILE needs
a backward-compatibility fallback: add a LEGACY_FREE_MODE_STATE_FILE constant
(value "webui-free-mode.json") and update the state-load logic (the server
bridge / state loader that reads FREE_MODE_STATE_FILE) to first attempt reading
FREE_MODE_STATE_FILE, and if it does not exist, attempt to read
LEGACY_FREE_MODE_STATE_FILE; if the legacy file is successfully loaded, persist
that data back to FREE_MODE_STATE_FILE so upgraded users keep their
provider/model state. Ensure references to FREE_MODE_STATE_FILE remain for
future reads/writes and only use the legacy constant as a fallback/migration
step.
Summary
Testing
Summary by CodeRabbit
New Features
Bug Fixes
Documentation