@@ -54,6 +54,7 @@ import {
5454 metaProfilesToCatalogEntries ,
5555 metaProvidersAsSettings ,
5656} from "./meta-providers.js" ;
57+ import { separateCollidingMetaApiKeys } from "./meta-api-key.js" ;
5758import { fetchBifrostModels } from "./bifrost.js" ;
5859import { customReasoningSettings } from "./providers.js" ;
5960
@@ -108,6 +109,7 @@ import {
108109 resolveLocalSettingsPath ,
109110 normalizeOpenAICompatibleBaseURL ,
110111 resolveProvider ,
112+ saveGlobalSettings ,
111113 hasMcpTransport ,
112114 isExaMCPPreset ,
113115 type MCPServerSettingsEntry ,
@@ -459,6 +461,7 @@ export function buildMetaSource(fields: {
459461export function buildMetaApiKeySource ( fields : {
460462 id : string ;
461463 apiKey ?: string ;
464+ baseURL ?: string ;
462465 model : string ;
463466 sessionId : string ;
464467 reasoningEffort ?: ReasoningEffort ;
@@ -467,10 +470,14 @@ export function buildMetaApiKeySource(fields: {
467470 if ( fields . reasoningEffort !== undefined )
468471 providerOptions [ META_REASONING_EFFORT_OPTION ] = fields . reasoningEffort ;
469472 registerSourceSecret ( fields . id , fields . apiKey ) ;
473+ const configured = fields . baseURL ?. trim ( ) ?? "" ;
470474 return {
471475 id : fields . id ,
472476 provider : META_PROVIDER ,
473- baseURL : META_BASE_URL ,
477+ baseURL :
478+ configured . length > 0
479+ ? normalizeOpenAICompatibleBaseURL ( configured )
480+ : META_BASE_URL ,
474481 credentialId : fields . id ,
475482 model : fields . model ,
476483 defaults : { maxTokens : SOURCE_MAX_TOKENS , providerOptions } ,
@@ -1104,7 +1111,7 @@ export async function loadConfig(
11041111 ...xaiProvidersAsSettings ( xaiProfiles ) ,
11051112 ...metaProvidersAsSettings ( metaProfiles ) ,
11061113 } ;
1107- const settings =
1114+ let settings =
11081115 configPath !== undefined
11091116 ? await loadSettingsRecoveringClobberedOAuthSelection (
11101117 configPath ,
@@ -1122,6 +1129,25 @@ export async function loadConfig(
11221129 projectedOAuthProviders ,
11231130 { persist : true } ,
11241131 ) ;
1132+ const separatedMeta = separateCollidingMetaApiKeys (
1133+ settings ,
1134+ metaProfiles . map ( ( profile ) => profile . name ) ,
1135+ ) ;
1136+ if (
1137+ separatedMeta . changed &&
1138+ separatedMeta . settings !== null &&
1139+ configPath === undefined &&
1140+ useOAuthProfiles
1141+ ) {
1142+ try {
1143+ await saveGlobalSettings ( effectiveSettingsPath , separatedMeta . settings ) ;
1144+ } catch {
1145+ process . stderr . write (
1146+ `settings: failed to persist Meta API-key separation for ${ effectiveSettingsPath } ; continuing with in-memory settings.\n` ,
1147+ ) ;
1148+ }
1149+ }
1150+ settings = separatedMeta . settings ;
11251151
11261152 // True when the value came from settings, not this invocation's
11271153 // --dangerously-skip-permissions flag (Config.skipPermissionsFromSettings).
@@ -1388,30 +1414,39 @@ export function mergeOAuthCatalog(
13881414 xaiProfiles : readonly XaiProfile [ ] ,
13891415 metaProfiles : readonly MetaProfile [ ] ,
13901416) : ProviderCatalogEntry [ ] {
1417+ const separated = separateCollidingMetaApiKeys (
1418+ settings ,
1419+ metaProfiles . map ( ( profile ) => profile . name ) ,
1420+ ) . settings ;
13911421 const codexEntries = codexProfilesToCatalogEntries ( codexProfiles ) ;
13921422 const xaiEntries = xaiProfilesToCatalogEntries ( xaiProfiles ) ;
13931423 const metaEntries = metaProfilesToCatalogEntries ( metaProfiles ) ;
1424+ const bareMeta = separated ?. providers [ "meta" ] ;
1425+ const bareMetaHasApiKey =
1426+ typeof bareMeta ?. apiKey === "string" && bareMeta . apiKey . length > 0 ;
13941427 // Legacy bare `codex`/`xai` row (the original single-instance connect key)
13951428 // next to live credential-backed `<kind>/<profile>` entries reads as a
13961429 // second provider. Drop it once that family has a live profile; when
13971430 // nothing is connected the bare row is the only ChatGPT/Grok access and
13981431 // stays. A bare row at a different endpoint is a distinct provider (see
1399- // sameEndpoint), so it stays too.
1432+ // sameEndpoint), so it stays too. A bare `meta` row with its own API key
1433+ // is not a legacy placeholder and stays beside OAuth profiles.
14001434 const dropBare = new Set ( [
14011435 ...( codexEntries . length > 0 &&
1402- sameEndpoint ( settings ?. providers [ "codex" ] ?. baseURL , CODEX_BASE_URL )
1436+ sameEndpoint ( separated ?. providers [ "codex" ] ?. baseURL , CODEX_BASE_URL )
14031437 ? [ "codex" ]
14041438 : [ ] ) ,
14051439 ...( xaiEntries . length > 0 &&
1406- sameEndpoint ( settings ?. providers [ "xai" ] ?. baseURL , XAI_BASE_URL )
1440+ sameEndpoint ( separated ?. providers [ "xai" ] ?. baseURL , XAI_BASE_URL )
14071441 ? [ "xai" ]
14081442 : [ ] ) ,
14091443 ...( metaEntries . length > 0 &&
1410- sameEndpoint ( settings ?. providers [ "meta" ] ?. baseURL , META_BASE_URL )
1444+ ! bareMetaHasApiKey &&
1445+ sameEndpoint ( bareMeta ?. baseURL , META_BASE_URL )
14111446 ? [ "meta" ]
14121447 : [ ] ) ,
14131448 ] ) ;
1414- const settingsRows = buildProviderCatalog ( settings , resolved ) ;
1449+ const settingsRows = buildProviderCatalog ( separated , resolved ) ;
14151450 // A hand-named codex/<slug> or xai/<slug> API-key row is explicit config,
14161451 // not an OAuth placeholder (see isHandNamedProviderEntry): keep it and skip
14171452 // the colliding live profile projection. buildProviderCatalog
@@ -1420,7 +1455,7 @@ export function mergeOAuthCatalog(
14201455 // profile marker — treating it as hand-named would eject the real marked
14211456 // entry for a stale token snapshot.
14221457 const handNamed = new Set (
1423- Object . entries ( settings ?. providers ?? { } )
1458+ Object . entries ( separated ?. providers ?? { } )
14241459 . filter (
14251460 ( [ name , provider ] ) =>
14261461 ( isCodexProviderName ( name ) ||
0 commit comments