Skip to content

Commit b6fed7e

Browse files
committed
Keep hand-named codex and xai provider rows across OAuth projections
1 parent 9c1d126 commit b6fed7e

1 file changed

Lines changed: 52 additions & 13 deletions

File tree

‎src/config/index.ts‎

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,10 @@ export function dropOrphanedOAuthEntries(
142142
if (settings === null) return null;
143143
const providers = Object.fromEntries(
144144
Object.entries(settings.providers).filter(
145-
([name]) =>
145+
([name, provider]) =>
146146
(!isCodexProviderName(name) && !isXaiProviderName(name)) ||
147-
projected[name] !== undefined,
147+
projected[name] !== undefined ||
148+
isHandNamedProviderEntry(provider),
148149
),
149150
);
150151
const { defaultProvider, ...rest } = settings;
@@ -158,19 +159,32 @@ export function dropOrphanedOAuthEntries(
158159
};
159160
}
160161

162+
// A codex/<slug> or xai/<slug> settings row carrying its own credential is the
163+
// operator's explicit config, not an OAuth placeholder: OAuth profile
164+
// projections must never overwrite it, orphan-sweep it, or drop it from the
165+
// catalog. Credential-less namespaced rows stay placeholders (CL-6728).
166+
function isHandNamedProviderEntry(
167+
entry: Pick<ProviderSettings, "apiKey" | "keyless"> | undefined,
168+
): boolean {
169+
if (entry === undefined) return false;
170+
if (entry.keyless === true) return true;
171+
return typeof entry.apiKey === "string" && entry.apiKey.length > 0;
172+
}
173+
161174
// Overlay live OAuth profile projections onto settings for runtime provider
162175
// resolution. Exported for tests; loadConfig is the only production caller.
163176
export function overlayOAuthProjections(
164177
settings: Settings | null,
165178
projected: Record<string, ProviderSettings>,
166179
): Settings | null {
167180
if (Object.keys(projected).length === 0) return settings;
181+
const providers = { ...(settings?.providers ?? {}) };
182+
for (const [name, entry] of Object.entries(projected)) {
183+
if (!isHandNamedProviderEntry(providers[name])) providers[name] = entry;
184+
}
168185
return {
169186
...(settings ?? { providers: {} }),
170-
providers: {
171-
...(settings?.providers ?? {}),
172-
...projected,
173-
},
187+
providers,
174188
};
175189
}
176190

@@ -1181,15 +1195,29 @@ export function mergeOAuthCatalog(
11811195
...(codexEntries.length > 0 ? ["codex"] : []),
11821196
...(xaiEntries.length > 0 ? ["xai"] : []),
11831197
]);
1198+
const settingsRows = buildProviderCatalog(settings, resolved);
1199+
// A hand-named codex/<slug> or xai/<slug> API-key row is the operator's
1200+
// explicit config, not an OAuth placeholder: keep it and skip the colliding
1201+
// live profile projection instead of overwriting it (CL-6728).
1202+
const handNamed = new Set(
1203+
settingsRows
1204+
.filter(
1205+
(e) =>
1206+
(isCodexProviderName(e.name) || isXaiProviderName(e.name)) &&
1207+
isHandNamedProviderEntry(e),
1208+
)
1209+
.map((e) => e.name),
1210+
);
11841211
return [
1185-
...buildProviderCatalog(settings, resolved).filter(
1212+
...settingsRows.filter(
11861213
(e) =>
1187-
!isCodexProviderName(e.name) &&
1188-
!isXaiProviderName(e.name) &&
1189-
!dropBare.has(e.name),
1214+
handNamed.has(e.name) ||
1215+
(!isCodexProviderName(e.name) &&
1216+
!isXaiProviderName(e.name) &&
1217+
!dropBare.has(e.name)),
11901218
),
1191-
...codexEntries,
1192-
...xaiEntries,
1219+
...codexEntries.filter((e) => !handNamed.has(e.name)),
1220+
...xaiEntries.filter((e) => !handNamed.has(e.name)),
11931221
].map((entry) =>
11941222
isOpenCodeGoProvider(entry)
11951223
? { ...entry, models: [...selectableGoModelIds()] }
@@ -1259,11 +1287,22 @@ export function runtimeSettingsWithCatalog(
12591287
if (settings === undefined) {
12601288
return { providers: fromCatalog };
12611289
}
1290+
// OAuth-marked catalog rows carry live profile tokens; they overlay
1291+
// credential-less placeholders but never a hand-named API-key row (CL-6728).
1292+
const overlaid = { ...fromCatalog };
1293+
for (const name of Object.keys(overlaid)) {
1294+
if (
1295+
(isCodexProviderName(name) || isXaiProviderName(name)) &&
1296+
isHandNamedProviderEntry(settings.providers[name])
1297+
) {
1298+
delete overlaid[name];
1299+
}
1300+
}
12621301
return {
12631302
...settings,
12641303
providers: {
12651304
...settings.providers,
1266-
...fromCatalog,
1305+
...overlaid,
12671306
},
12681307
};
12691308
}

0 commit comments

Comments
 (0)