diff --git a/src/domain/models/models-dev.ts b/src/domain/models/models-dev.ts index 66f136f..ab0b78b 100644 --- a/src/domain/models/models-dev.ts +++ b/src/domain/models/models-dev.ts @@ -226,13 +226,14 @@ const resolveAllowedMappings = (input: { }); }; -const patchCanonicalProviders = (input: { - registry: ModelsDevRegistry; +const mergeKleisProviderModels = (input: { upstreamRegistry: ModelsDevRegistry; baseOrigin: string; mappings: readonly ProxyMapping[]; modelScopes: readonly string[] | null; -}): void => { +}): JsonObject => { + const models: JsonObject = {}; + for (const mapping of input.mappings) { const sourceProvider = getObjectProperty( input.upstreamRegistry, @@ -249,49 +250,6 @@ const patchCanonicalProviders = (input: { continue; } - const apiUrl = `${input.baseOrigin}${mapping.routeBasePath}`; - const providerModels = cloneProviderModels({ - sourceModels: getObjectProperty(sourceProvider, "models") ?? {}, - apiUrl, - npm: mapping.npm, - shouldIncludeModel: (modelId) => - isModelSupportedByProxyProvider(mapping.internalProvider, modelId) && - isModelInScope({ - model: modelId, - route, - modelScopes: input.modelScopes, - }), - }); - if (!Object.keys(providerModels).length) { - continue; - } - - const provider = cloneJsonValue(sourceProvider); - provider.id = mapping.canonicalProvider; - provider.env = [PROXY_API_KEY_ENV]; - provider.api = apiUrl; - provider.npm = mapping.npm; - provider.models = providerModels; - input.registry[mapping.canonicalProvider] = provider; - } -}; - -const mergeKleisProviderModels = (input: { - registry: ModelsDevRegistry; - baseOrigin: string; - mappings: readonly ProxyMapping[]; -}): JsonObject => { - const models: JsonObject = {}; - - for (const mapping of input.mappings) { - const sourceProvider = getObjectProperty( - input.registry, - mapping.canonicalProvider - ); - if (!sourceProvider) { - continue; - } - Object.assign( models, cloneProviderModels({ @@ -301,7 +259,12 @@ const mergeKleisProviderModels = (input: { modelPrefix: mapping.canonicalProvider, sourceLabel: mapping.canonicalProvider, shouldIncludeModel: (modelId) => - isModelSupportedByProxyProvider(mapping.internalProvider, modelId), + isModelSupportedByProxyProvider(mapping.internalProvider, modelId) && + isModelInScope({ + model: modelId, + route, + modelScopes: input.modelScopes, + }), }) ); } @@ -310,9 +273,10 @@ const mergeKleisProviderModels = (input: { }; const toKleisProviderEntry = (input: { - registry: ModelsDevRegistry; + upstreamRegistry: ModelsDevRegistry; baseOrigin: string; mappings: readonly ProxyMapping[]; + modelScopes: readonly string[] | null; }): JsonObject => { return { id: KLEIS_PROVIDER_ID, @@ -324,13 +288,16 @@ const toKleisProviderEntry = (input: { const appendKleisProviderEntry = (input: { registry: ModelsDevRegistry; + upstreamRegistry: ModelsDevRegistry; baseOrigin: string; mappings: readonly ProxyMapping[]; + modelScopes: readonly string[] | null; }): void => { const generatedProvider = toKleisProviderEntry({ - registry: input.registry, + upstreamRegistry: input.upstreamRegistry, baseOrigin: input.baseOrigin, mappings: input.mappings, + modelScopes: input.modelScopes, }); const existingProvider = getObjectProperty(input.registry, KLEIS_PROVIDER_ID); @@ -375,25 +342,15 @@ export const buildProxyModelsRegistry = ( providerScopes, accountProviderScopes, }); - const registry: ModelsDevRegistry = input.apiKeyScopes - ? {} - : cloneJsonValue(input.upstreamRegistry); + const registry = cloneJsonValue(input.upstreamRegistry); const baseOrigin = normalizeOrigin(input.baseOrigin); - if (input.apiKeyScopes) { - patchCanonicalProviders({ - registry, - upstreamRegistry: input.upstreamRegistry, - baseOrigin, - mappings, - modelScopes, - }); - } - appendKleisProviderEntry({ registry, + upstreamRegistry: input.upstreamRegistry, baseOrigin, mappings, + modelScopes, }); return registry; diff --git a/tests/domain/models-dev-contract.test.ts b/tests/domain/models-dev-contract.test.ts index a8f2c80..3e28d19 100644 --- a/tests/domain/models-dev-contract.test.ts +++ b/tests/domain/models-dev-contract.test.ts @@ -247,7 +247,7 @@ describe("models registry contract", () => { expect(Object.keys(kleis.models ?? {})).toHaveLength(0); }); - test("applies api key provider and model scopes", () => { + test("applies api key scopes only to the kleis aggregate provider", () => { const registry = buildProxyModelsRegistry({ upstreamRegistry: upstreamRegistry as unknown as Record, baseOrigin: "https://kleis.example/api/kmd_abc123", @@ -255,10 +255,12 @@ describe("models registry contract", () => { apiKeyScopes: { providerScopes: ["codex", "copilot"], modelScopes: ["openai/gpt-5.3-codex", "gpt-5-mini"], + accountProviderScopes: null, }, }); expect(Object.keys(registry).sort()).toEqual([ + "anthropic", "github-copilot", "kleis", "openai", @@ -268,16 +270,20 @@ describe("models registry contract", () => { env?: string[]; models?: Record; }; - expect(openai.env).toEqual(["KLEIS_API_KEY"]); - expect(Object.keys(openai.models ?? {})).toEqual(["gpt-5.3-codex"]); + expect(openai.env).toEqual(["OPENAI_API_KEY"]); + expect(Object.keys(openai.models ?? {})).toEqual([ + "gpt-5.3-codex", + "gpt-5", + "text-embedding-3-large", + ]); expect(openai.models?.["gpt-5.3-codex"]?.provider?.api).toBe( - "https://kleis.example/api/kmd_abc123/openai/v1" + "https://api.openai.com/v1" ); const copilot = registry["github-copilot"] as { models?: Record; }; - expect(Object.keys(copilot.models ?? {})).toEqual(["gpt-5-mini"]); + expect(Object.keys(copilot.models ?? {})).toEqual(["gpt-5", "gpt-5-mini"]); const kleis = registry.kleis as { models?: Record; @@ -288,7 +294,7 @@ describe("models registry contract", () => { ]); }); - test("scoped mode omits non-proxy upstream providers", () => { + test("scoped mode preserves upstream providers unchanged", () => { const registry = buildProxyModelsRegistry({ upstreamRegistry: upstreamRegistry as unknown as Record, baseOrigin: "https://kleis.example/api/kmd_xyz789", @@ -296,11 +302,25 @@ describe("models registry contract", () => { apiKeyScopes: { providerScopes: ["codex"], modelScopes: null, + accountProviderScopes: null, }, }); - expect(registry.anthropic).toBeUndefined(); - expect(registry["github-copilot"]).toBeUndefined(); + const anthropic = registry.anthropic as { + env?: string[]; + models?: Record; + }; + expect(anthropic.env).toEqual(["ANTHROPIC_API_KEY"]); + expect(anthropic.models?.["claude-sonnet-4"]?.provider?.api).toBe( + "https://api.anthropic.com/v1" + ); + + const copilot = registry["github-copilot"] as { + env?: string[]; + models?: Record; + }; + expect(copilot.env).toEqual(["GITHUB_TOKEN"]); + expect(Object.keys(copilot.models ?? {})).toEqual(["gpt-5", "gpt-5-mini"]); const kleis = registry.kleis as { models?: Record; @@ -308,7 +328,7 @@ describe("models registry contract", () => { expect(Object.keys(kleis.models ?? {})).toEqual(["openai/gpt-5.3-codex"]); }); - test("account-scoped mode only exposes providers backed by scoped accounts", () => { + test("account-scoped mode only narrows the kleis aggregate provider", () => { const registry = buildProxyModelsRegistry({ upstreamRegistry: upstreamRegistry as unknown as Record, baseOrigin: "https://kleis.example/api/kmd_acc123", @@ -320,20 +340,32 @@ describe("models registry contract", () => { }, }); - expect(Object.keys(registry).sort()).toEqual(["anthropic", "kleis"]); + expect(Object.keys(registry).sort()).toEqual([ + "anthropic", + "github-copilot", + "kleis", + "openai", + ]); const anthropic = registry.anthropic as { env?: string[]; models?: Record; }; - expect(anthropic.env).toEqual(["KLEIS_API_KEY"]); + expect(anthropic.env).toEqual(["ANTHROPIC_API_KEY"]); expect(Object.keys(anthropic.models ?? {})).toEqual(["claude-sonnet-4"]); expect(anthropic.models?.["claude-sonnet-4"]?.provider?.api).toBe( - "https://kleis.example/api/kmd_acc123/anthropic/v1" + "https://api.anthropic.com/v1" ); + + const kleis = registry.kleis as { + models?: Record; + }; + expect(Object.keys(kleis.models ?? {})).toEqual([ + "anthropic/claude-sonnet-4", + ]); }); - test("account scopes further narrow provider scopes", () => { + test("account scopes further narrow the kleis aggregate provider", () => { const registry = buildProxyModelsRegistry({ upstreamRegistry: upstreamRegistry as unknown as Record, baseOrigin: "https://kleis.example/api/kmd_acc456", @@ -345,7 +377,7 @@ describe("models registry contract", () => { }, }); - expect(registry.openai).toBeUndefined(); + expect(registry.openai).toBeDefined(); expect(registry.anthropic).toBeDefined(); const kleis = registry.kleis as {