Skip to content

Commit 876d118

Browse files
committed
fix(provider): follow a relocated meta api key in project settings
1 parent 2ba34ce commit 876d118

3 files changed

Lines changed: 86 additions & 6 deletions

File tree

‎src/config/index.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ import {
5959
metaProfilesToCatalogEntries,
6060
metaProvidersAsSettings,
6161
} from "./meta-providers.js";
62-
import { separateCollidingMetaApiKeys } from "./meta-api-key.js";
62+
import {
63+
providerAfterMetaApiKeySeparation,
64+
separateCollidingMetaApiKeys,
65+
} from "./meta-api-key.js";
6366
import { customReasoningSettings } from "./providers.js";
6467
import { CODEX_BASE_URL } from "../auth/codex/constants.js";
6568
import { XAI_BASE_URL } from "../auth/xai/constants.js";
@@ -114,6 +117,7 @@ import {
114117
normalizeOpenAICompatibleBaseURL,
115118
resolveProvider,
116119
saveGlobalSettings,
120+
saveLocalSettings,
117121
hasMcpTransport,
118122
isExaMCPPreset,
119123
type MCPServerSettingsEntry,
@@ -1307,7 +1311,26 @@ export async function loadConfig(
13071311
if (localResultSettled.status === "rejected") throw localResultSettled.reason;
13081312
if (profileResult.status === "rejected") throw profileResult.reason;
13091313
const localResult = localResultSettled.value;
1310-
const local = localResult.settings;
1314+
const storedLocal = localResult.settings;
1315+
const separatedLocalProvider = providerAfterMetaApiKeySeparation(
1316+
storedLocal?.provider,
1317+
separatedMeta.renames,
1318+
);
1319+
const local =
1320+
storedLocal !== null &&
1321+
separatedLocalProvider !== undefined &&
1322+
separatedLocalProvider !== storedLocal.provider
1323+
? { ...storedLocal, provider: separatedLocalProvider }
1324+
: storedLocal;
1325+
if (local !== storedLocal && local !== null && localSettingsFile !== null) {
1326+
try {
1327+
await saveLocalSettings(localSettingsFile, local);
1328+
} catch {
1329+
process.stderr.write(
1330+
`settings: failed to persist Meta API-key separation for ${localSettingsFile}; continuing with in-memory settings.\n`,
1331+
);
1332+
}
1333+
}
13111334
const settingsDiagnostics = localResult.diagnostics;
13121335
const profile = profileResult.value;
13131336

‎src/config/meta-api-key.test.ts‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { describe, expect, test } from "bun:test";
22

33
import { META_BASE_URL } from "../auth/meta/constants.js";
4-
import { assignMetaOAuthProviderRow } from "./meta-api-key.js";
4+
import {
5+
assignMetaOAuthProviderRow,
6+
providerAfterMetaApiKeySeparation,
7+
separateCollidingMetaApiKeys,
8+
} from "./meta-api-key.js";
59
import type { ProviderSettings, Settings } from "./settings.js";
610

711
function settingsWith(providers: Record<string, ProviderSettings>): Settings {
@@ -32,6 +36,37 @@ describe("assignMetaOAuthProviderRow", () => {
3236
expect(next.defaultProvider).toBe("meta/work");
3337
});
3438

39+
test("reports the rename a project-local provider must follow", () => {
40+
const separated = separateCollidingMetaApiKeys(
41+
{
42+
...settingsWith({
43+
"meta/work": {
44+
baseURL: META_BASE_URL,
45+
apiKey: "LLM|work-key",
46+
models: ["muse-spark-1.3"],
47+
},
48+
}),
49+
defaultProvider: "meta/work",
50+
},
51+
["work"],
52+
);
53+
expect(separated.changed).toBe(true);
54+
expect(separated.renames).toEqual([
55+
{ from: "meta/work", to: "meta-api/work" },
56+
]);
57+
expect(separated.settings?.defaultProvider).toBe("meta-api/work");
58+
expect(separated.settings?.providers["meta/work"]?.apiKey).toBeUndefined();
59+
expect(separated.settings?.providers["meta-api/work"]?.apiKey).toBe(
60+
"LLM|work-key",
61+
);
62+
expect(
63+
providerAfterMetaApiKeySeparation("meta/work", separated.renames),
64+
).toBe("meta-api/work");
65+
expect(providerAfterMetaApiKeySeparation("openai", separated.renames)).toBe(
66+
"openai",
67+
);
68+
});
69+
3570
test("a free OAuth slug still writes a credential-less row", () => {
3671
const next = assignMetaOAuthProviderRow(
3772
settingsWith({}),

‎src/config/meta-api-key.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ function remapProviderRefs(
5454
);
5555
}
5656

57+
export interface MetaApiKeyRename {
58+
readonly from: string;
59+
readonly to: string;
60+
}
61+
5762
/**
5863
* Move a hand-saved Meta API key off `meta/<slug>` when that slug is an OAuth
5964
* profile. The key keeps its secret under `meta-api/<slug>`; `meta/<slug>`
@@ -62,9 +67,13 @@ function remapProviderRefs(
6267
export function separateCollidingMetaApiKeys(
6368
settings: Settings | null,
6469
oauthSlugs: readonly string[],
65-
): { settings: Settings | null; changed: boolean } {
70+
): {
71+
settings: Settings | null;
72+
changed: boolean;
73+
renames: readonly MetaApiKeyRename[];
74+
} {
6675
if (settings === null || oauthSlugs.length === 0) {
67-
return { settings, changed: false };
76+
return { settings, changed: false, renames: [] };
6877
}
6978
const slugs = new Set(oauthSlugs);
7079
let providers: Record<string, ProviderSettings> = {
@@ -74,6 +83,7 @@ export function separateCollidingMetaApiKeys(
7483
let recentModels = settings.recentModels;
7584
let favoriteModels = settings.favoriteModels;
7685
let changed = false;
86+
const renames: MetaApiKeyRename[] = [];
7787
for (const [name, provider] of Object.entries(settings.providers)) {
7888
if (!isMetaProviderName(name) || !hasApiKey(provider)) continue;
7989
const slug = metaProfileFromProviderName(name);
@@ -89,9 +99,10 @@ export function separateCollidingMetaApiKeys(
8999
if (defaultProvider === name) defaultProvider = relocated;
90100
recentModels = remapProviderRefs(recentModels, name, relocated);
91101
favoriteModels = remapProviderRefs(favoriteModels, name, relocated);
102+
renames.push({ from: name, to: relocated });
92103
changed = true;
93104
}
94-
if (!changed) return { settings, changed: false };
105+
if (!changed) return { settings, changed: false, renames: [] };
95106
return {
96107
settings: {
97108
...settings,
@@ -101,9 +112,20 @@ export function separateCollidingMetaApiKeys(
101112
...(favoriteModels !== undefined ? { favoriteModels } : {}),
102113
},
103114
changed: true,
115+
renames,
104116
};
105117
}
106118

119+
/** Project-local `provider` follows a key that moved to `meta-api/<slug>`. */
120+
export function providerAfterMetaApiKeySeparation(
121+
provider: string | undefined,
122+
renames: readonly MetaApiKeyRename[],
123+
): string | undefined {
124+
if (provider === undefined) return undefined;
125+
const rename = renames.find((entry) => entry.from === provider);
126+
return rename === undefined ? provider : rename.to;
127+
}
128+
107129
/**
108130
* OAuth save for `oauthName`. An existing API key at that name is moved aside;
109131
* the placeholder written for OAuth never carries the key.

0 commit comments

Comments
 (0)