Skip to content

Commit 00a44cb

Browse files
committed
refactor(config): let an OAuth projection choose models per profile
1 parent ab29f49 commit 00a44cb

5 files changed

Lines changed: 65 additions & 16 deletions

File tree

‎src/config/codex-providers.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const CODEX_PROVIDER_PREFIX = "codex/";
1313
const projection = createOAuthProviderProjection<CodexProfile>({
1414
prefix: CODEX_PROVIDER_PREFIX,
1515
baseURL: CODEX_BASE_URL,
16-
defaultModels: CODEX_DEFAULT_MODELS,
16+
models: () => CODEX_DEFAULT_MODELS,
1717
catalogExtras: (profile) => ({
1818
codexProfile: profile.name,
1919
...(profile.tokens.accountId !== undefined

‎src/config/meta-providers.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const META_PROVIDER_PREFIX = "meta/";
77
const projection = createOAuthProviderProjection<MetaProfile>({
88
prefix: META_PROVIDER_PREFIX,
99
baseURL: META_BASE_URL,
10-
defaultModels: META_DEFAULT_MODELS,
10+
models: () => META_DEFAULT_MODELS,
1111
catalogExtras: (profile) => ({ metaProfile: profile.name }),
1212
});
1313

‎src/config/oauth-providers.test.ts‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { describe, expect, test } from "bun:test";
33
import type { CodexProfile } from "../auth/codex/store.js";
44
import type { XaiProfile } from "../auth/xai/store.js";
55
import { codexProfilesToCatalogEntries } from "./codex-providers.js";
6+
import { createOAuthProviderProjection } from "./oauth-providers.js";
67
import { xaiProfilesToCatalogEntries } from "./xai-providers.js";
78

89
// Characterization tests pinning the projection behavior both provider
910
// wrappers must preserve. Name round-trips and settings projection are
1011
// characterized in codex-providers.test.ts and xai-providers.test.ts — only
11-
// cross-provider marker isolation lives here.
12+
// cross-provider marker isolation and the per-profile models contract live
13+
// here.
1214

1315
const codexProfile: CodexProfile = {
1416
name: "work",
@@ -50,3 +52,45 @@ describe("catalog projection", () => {
5052
expect(entries[0]?.codexAccountId).toBeUndefined();
5153
});
5254
});
55+
56+
describe("per-profile models", () => {
57+
const profiles = [
58+
{ name: "a", tokens: { access: "ta" } },
59+
{ name: "b", tokens: { access: "tb" } },
60+
];
61+
const perProfile: Record<string, readonly [string, ...string[]]> = {
62+
a: ["m-a1", "m-a2"],
63+
b: ["m-b1"],
64+
};
65+
const projection = createOAuthProviderProjection({
66+
prefix: "p/",
67+
baseURL: "https://example.invalid",
68+
models: (profile) => {
69+
const list = perProfile[profile.name];
70+
if (list === undefined) throw new Error(`no models for ${profile.name}`);
71+
return list;
72+
},
73+
catalogExtras: () => ({}),
74+
});
75+
76+
test("the settings projection lists each profile's models with the first as default", () => {
77+
const settings = projection.providersAsSettings(profiles);
78+
expect(settings["p/a"]?.models).toEqual(["m-a1", "m-a2"]);
79+
expect(settings["p/a"]?.defaultModel).toBe("m-a1");
80+
expect(settings["p/b"]?.models).toEqual(["m-b1"]);
81+
expect(settings["p/b"]?.defaultModel).toBe("m-b1");
82+
});
83+
84+
test("the catalog projection lists each profile's models with the first as default", () => {
85+
const entries = projection.profilesToCatalogEntries(profiles);
86+
expect(entries.map((e) => [e.name, e.models, e.defaultModel])).toEqual([
87+
["p/a", ["m-a1", "m-a2"], "m-a1"],
88+
["p/b", ["m-b1"], "m-b1"],
89+
]);
90+
});
91+
92+
test("projected lists are copies the caller can mutate without touching the source", () => {
93+
projection.providersAsSettings(profiles)["p/a"]?.models.push("mutated");
94+
expect(perProfile["a"]).toEqual(["m-a1", "m-a2"]);
95+
});
96+
});

‎src/config/oauth-providers.ts‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ export interface OAuthProviderProjectionOptions<P extends OAuthProfileLike> {
2626
// Provider namespace including the trailing slash (e.g. "codex/").
2727
prefix: string;
2828
baseURL: string;
29-
// Non-empty: the first entry doubles as the default model.
30-
defaultModels: readonly [string, ...string[]];
29+
// Models a profile can select. Non-empty: the first entry doubles as the
30+
// default model.
31+
models: (profile: P) => readonly [string, ...string[]];
3132
// Provider-specific catalog fields (profile marker, account id, …) merged
3233
// into each catalog entry so the runtime can route it to the right adapter.
3334
catalogExtras: (profile: P) => Partial<ProviderCatalogEntry>;
@@ -60,12 +61,13 @@ export function createOAuthProviderProjection<P extends OAuthProfileLike>(
6061
): Record<string, ProviderSettings> {
6162
const entries: Record<string, ProviderSettings> = {};
6263
for (const profile of profiles) {
64+
const models = options.models(profile);
6365
entries[providerName(profile.name)] = {
6466
name: providerName(profile.name),
6567
baseURL: options.baseURL,
6668
apiKey: profile.tokens.access,
67-
models: [...options.defaultModels],
68-
defaultModel: options.defaultModels[0],
69+
models: [...models],
70+
defaultModel: models[0],
6971
};
7072
}
7173
return entries;
@@ -74,14 +76,17 @@ export function createOAuthProviderProjection<P extends OAuthProfileLike>(
7476
// these carry the provider's marker fields (via catalogExtras) so the
7577
// runtime can route them to the matching adapter and headers.
7678
profilesToCatalogEntries(profiles: readonly P[]): ProviderCatalogEntry[] {
77-
return profiles.map((profile) => ({
78-
name: providerName(profile.name),
79-
baseURL: options.baseURL,
80-
apiKey: profile.tokens.access,
81-
models: [...options.defaultModels],
82-
defaultModel: options.defaultModels[0],
83-
...options.catalogExtras(profile),
84-
}));
79+
return profiles.map((profile) => {
80+
const models = options.models(profile);
81+
return {
82+
name: providerName(profile.name),
83+
baseURL: options.baseURL,
84+
apiKey: profile.tokens.access,
85+
models: [...models],
86+
defaultModel: models[0],
87+
...options.catalogExtras(profile),
88+
};
89+
});
8590
},
8691
};
8792
}

‎src/config/xai-providers.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const XAI_PROVIDER_PREFIX = "xai/";
77
const projection = createOAuthProviderProjection<XaiProfile>({
88
prefix: XAI_PROVIDER_PREFIX,
99
baseURL: XAI_BASE_URL,
10-
defaultModels: XAI_DEFAULT_MODELS,
10+
models: () => XAI_DEFAULT_MODELS,
1111
catalogExtras: (profile) => ({ xaiProfile: profile.name }),
1212
});
1313

0 commit comments

Comments
 (0)