@@ -54,6 +54,7 @@ import {
5454 metaProfilesToCatalogEntries ,
5555 metaProvidersAsSettings ,
5656} from "./meta-providers.js" ;
57+ import { separateCollidingMetaApiKeys } from "./meta-api-key.js" ;
5758import { customReasoningSettings } from "./providers.js" ;
5859import { CODEX_BASE_URL } from "../auth/codex/constants.js" ;
5960import { XAI_BASE_URL } from "../auth/xai/constants.js" ;
@@ -105,6 +106,7 @@ import {
105106 resolveLocalSettingsPath ,
106107 normalizeOpenAICompatibleBaseURL ,
107108 resolveProvider ,
109+ saveGlobalSettings ,
108110 hasMcpTransport ,
109111 isExaMCPPreset ,
110112 type MCPServerSettingsEntry ,
@@ -456,6 +458,7 @@ export function buildMetaSource(fields: {
456458export function buildMetaApiKeySource ( fields : {
457459 id : string ;
458460 apiKey ?: string ;
461+ baseURL ?: string ;
459462 model : string ;
460463 sessionId : string ;
461464 reasoningEffort ?: ReasoningEffort ;
@@ -464,10 +467,14 @@ export function buildMetaApiKeySource(fields: {
464467 if ( fields . reasoningEffort !== undefined )
465468 providerOptions [ META_REASONING_EFFORT_OPTION ] = fields . reasoningEffort ;
466469 registerSourceSecret ( fields . id , fields . apiKey ) ;
470+ const configured = fields . baseURL ?. trim ( ) ?? "" ;
467471 return {
468472 id : fields . id ,
469473 provider : META_PROVIDER ,
470- baseURL : META_BASE_URL ,
474+ baseURL :
475+ configured . length > 0
476+ ? normalizeOpenAICompatibleBaseURL ( configured )
477+ : META_BASE_URL ,
471478 credentialId : fields . id ,
472479 model : fields . model ,
473480 defaults : { maxTokens : SOURCE_MAX_TOKENS , providerOptions } ,
@@ -1084,7 +1091,7 @@ export async function loadConfig(
10841091 ...xaiProvidersAsSettings ( xaiProfiles ) ,
10851092 ...metaProvidersAsSettings ( metaProfiles ) ,
10861093 } ;
1087- const settings =
1094+ let settings =
10881095 configPath !== undefined
10891096 ? await loadSettingsRecoveringClobberedOAuthSelection (
10901097 configPath ,
@@ -1102,6 +1109,25 @@ export async function loadConfig(
11021109 projectedOAuthProviders ,
11031110 { persist : true } ,
11041111 ) ;
1112+ const separatedMeta = separateCollidingMetaApiKeys (
1113+ settings ,
1114+ metaProfiles . map ( ( profile ) => profile . name ) ,
1115+ ) ;
1116+ if (
1117+ separatedMeta . changed &&
1118+ separatedMeta . settings !== null &&
1119+ configPath === undefined &&
1120+ useOAuthProfiles
1121+ ) {
1122+ try {
1123+ await saveGlobalSettings ( effectiveSettingsPath , separatedMeta . settings ) ;
1124+ } catch {
1125+ process . stderr . write (
1126+ `settings: failed to persist Meta API-key separation for ${ effectiveSettingsPath } ; continuing with in-memory settings.\n` ,
1127+ ) ;
1128+ }
1129+ }
1130+ settings = separatedMeta . settings ;
11051131
11061132 // True when the value came from settings, not this invocation's
11071133 // --dangerously-skip-permissions flag (Config.skipPermissionsFromSettings).
@@ -1367,30 +1393,39 @@ export function mergeOAuthCatalog(
13671393 xaiProfiles : readonly XaiProfile [ ] ,
13681394 metaProfiles : readonly MetaProfile [ ] ,
13691395) : ProviderCatalogEntry [ ] {
1396+ const separated = separateCollidingMetaApiKeys (
1397+ settings ,
1398+ metaProfiles . map ( ( profile ) => profile . name ) ,
1399+ ) . settings ;
13701400 const codexEntries = codexProfilesToCatalogEntries ( codexProfiles ) ;
13711401 const xaiEntries = xaiProfilesToCatalogEntries ( xaiProfiles ) ;
13721402 const metaEntries = metaProfilesToCatalogEntries ( metaProfiles ) ;
1403+ const bareMeta = separated ?. providers [ "meta" ] ;
1404+ const bareMetaHasApiKey =
1405+ typeof bareMeta ?. apiKey === "string" && bareMeta . apiKey . length > 0 ;
13731406 // Legacy bare `codex`/`xai` row (the original single-instance connect key)
13741407 // next to live credential-backed `<kind>/<profile>` entries reads as a
13751408 // second provider. Drop it once that family has a live profile; when
13761409 // nothing is connected the bare row is the only ChatGPT/Grok access and
13771410 // stays. A bare row at a different endpoint is a distinct provider (see
1378- // sameEndpoint), so it stays too.
1411+ // sameEndpoint), so it stays too. A bare `meta` row with its own API key
1412+ // is not a legacy placeholder and stays beside OAuth profiles.
13791413 const dropBare = new Set ( [
13801414 ...( codexEntries . length > 0 &&
1381- sameEndpoint ( settings ?. providers [ "codex" ] ?. baseURL , CODEX_BASE_URL )
1415+ sameEndpoint ( separated ?. providers [ "codex" ] ?. baseURL , CODEX_BASE_URL )
13821416 ? [ "codex" ]
13831417 : [ ] ) ,
13841418 ...( xaiEntries . length > 0 &&
1385- sameEndpoint ( settings ?. providers [ "xai" ] ?. baseURL , XAI_BASE_URL )
1419+ sameEndpoint ( separated ?. providers [ "xai" ] ?. baseURL , XAI_BASE_URL )
13861420 ? [ "xai" ]
13871421 : [ ] ) ,
13881422 ...( metaEntries . length > 0 &&
1389- sameEndpoint ( settings ?. providers [ "meta" ] ?. baseURL , META_BASE_URL )
1423+ ! bareMetaHasApiKey &&
1424+ sameEndpoint ( bareMeta ?. baseURL , META_BASE_URL )
13901425 ? [ "meta" ]
13911426 : [ ] ) ,
13921427 ] ) ;
1393- const settingsRows = buildProviderCatalog ( settings , resolved ) ;
1428+ const settingsRows = buildProviderCatalog ( separated , resolved ) ;
13941429 // A hand-named codex/<slug> or xai/<slug> API-key row is explicit config,
13951430 // not an OAuth placeholder (see isHandNamedProviderEntry): keep it and skip
13961431 // the colliding live profile projection. buildProviderCatalog
@@ -1399,7 +1434,7 @@ export function mergeOAuthCatalog(
13991434 // profile marker — treating it as hand-named would eject the real marked
14001435 // entry for a stale token snapshot.
14011436 const handNamed = new Set (
1402- Object . entries ( settings ?. providers ?? { } )
1437+ Object . entries ( separated ?. providers ?? { } )
14031438 . filter (
14041439 ( [ name , provider ] ) =>
14051440 ( isCodexProviderName ( name ) ||
0 commit comments