@@ -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.
163176export 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