diff --git a/packages/core/src/gateway/core-runtime/config-compiler.ts b/packages/core/src/gateway/core-runtime/config-compiler.ts index 0a273610..274eda8f 100644 --- a/packages/core/src/gateway/core-runtime/config-compiler.ts +++ b/packages/core/src/gateway/core-runtime/config-compiler.ts @@ -11,7 +11,7 @@ import { isRecord, stringListValue, stringValue } from "@ccr/core/gateway/intern import { fusionBuiltinToolArtifacts, fusionToolFallbackMcpServer, normalizeFusionWebSearchProfileToolName, toolHubMcpServer, withCodexCompatibleVirtualModelProfiles, withFusionVirtualModelAliases, withFusionWebSearchToolInstructions } from "@ccr/core/mcp/fusion-config"; import { mediaToolsMcpServer } from "@ccr/core/mcp/grok-media-config"; import { resolveGatewayPublicModelId } from "@ccr/core/gateway/features/model-discovery"; -import { activeProviderCredentials, inferProtocol, normalizedProviderCapabilities, normalizeProviderProtocol, providerCapabilityForClientProtocol, providerCapabilityInternalName, providerCredentialInternalName, providerProtocolForClientProtocol, sortProviderCredentialsForConfig, toCoreGatewayProviders } from "@ccr/core/providers/runtime-topology"; +import { activeProviderCredentials, inferProtocol, normalizedProviderCapabilities, normalizeProviderProtocol, providerCapabilityForClientProtocol, providerCapabilityInternalName, providerCapabilityNameMatches, providerCredentialInternalName, providerProtocolForClientProtocol, sortProviderCredentialsForConfig, toCoreGatewayProviders } from "@ccr/core/providers/runtime-topology"; import { buildRawTraceConfig } from "@ccr/core/observability/raw-trace-sync"; import { endpoint, resolveUndiciProxyAgentModule, writeGatewayProxyPreloadFile } from "@ccr/core/gateway/core-runtime/supervisor"; import { billingUsageSyncHeader, billingUsageSyncPath, claudeCodeOauthBetaHeader, claudeCodeOauthRequiredBeta, coreGatewayAuthHeader, coreGatewayAuthTokenEnv } from "@ccr/core/gateway/internal/shared"; @@ -40,10 +40,15 @@ export async function compileCoreGatewayConfig( ) : []; const pluginBillingConfig = isRecord(pluginCoreGatewayConfig.billing) ? pluginCoreGatewayConfig.billing : {}; - const configuredProviderPlugins = normalizeClaudeCodeOauthProviderPlugins([ - ...(config.providerPlugins ?? []).filter(providerPluginEnabled), - ...pluginService.getCoreProviderPlugins().filter(providerPluginEnabled) - ]); + const configuredProviderPlugins = normalizeClaudeCodeOauthProviderPlugins( + normalizeCoreProviderPluginNames( + [ + ...(config.providerPlugins ?? []).filter(providerPluginEnabled), + ...pluginService.getCoreProviderPlugins().filter(providerPluginEnabled) + ], + config.Providers + ) + ); const providerPlugins = await withKimiOauthRuntimeDefaults( await withGrokOauthRuntimeDefaults(withCodexOauthRuntimeDefaults(configuredProviderPlugins)) ); @@ -562,6 +567,57 @@ export function normalizeClaudeCodeOauthProviderPlugins(providerPlugins: unknown } +function normalizeCoreProviderPluginNames( + providerPlugins: unknown[], + providers: GatewayProviderConfig[] +): unknown[] { + return providerPlugins.map((plugin) => { + if (!isRecord(plugin)) { + return plugin; + } + const configuredName = stringValue(plugin.providerName); + if (!configuredName) { + return plugin; + } + const providerName = compiledProviderNameForPlugin(configuredName, providers); + return providerName === configuredName ? plugin : { ...plugin, providerName }; + }); +} + + +function compiledProviderNameForPlugin( + configuredName: string, + providers: GatewayProviderConfig[] +): string { + for (const provider of providers) { + const capabilities = normalizedProviderCapabilities(provider); + if (capabilities.length === 0) { + const protocol: GatewayProviderProtocol = + normalizeProviderProtocol(provider.type) ?? + normalizeProviderProtocol(provider.provider) ?? + inferProtocol(provider); + const normalizedConfiguredName = configuredName.trim().toLowerCase(); + if ( + providerRuntimeId(provider).toLowerCase() === normalizedConfiguredName || + provider.name.trim().toLowerCase() === normalizedConfiguredName || + providerCapabilityNameMatches(provider, protocol, configuredName) + ) { + return providerRuntimeId(provider); + } + continue; + } + + for (const capability of capabilities) { + if (providerCapabilityNameMatches(provider, capability.type, configuredName)) { + return providerCapabilityInternalName(provider, capability.type); + } + } + } + + return configuredName; +} + + function configuredAnthropicBetaDefault(value: unknown): string | undefined { if (typeof value === "string") { return value; diff --git a/packages/core/src/gateway/upstream/executor.ts b/packages/core/src/gateway/upstream/executor.ts index e1fb2aba..6fd0e919 100644 --- a/packages/core/src/gateway/upstream/executor.ts +++ b/packages/core/src/gateway/upstream/executor.ts @@ -649,7 +649,7 @@ function resolvePlannedProviderCredentialRoutingTarget( function targetProviderHeaderValue(provider: GatewayProviderConfig, protocol: GatewayProviderProtocol): string { const capability = normalizedProviderCapabilities(provider).find((item) => item.type === protocol); - return capability ? providerCapabilityInternalName(provider, capability.type) : provider.name || providerRuntimeId(provider); + return capability ? providerCapabilityInternalName(provider, capability.type) : providerRuntimeId(provider); } diff --git a/packages/core/test/unit/agents/provider-plugin-runtime-identity.test.mjs b/packages/core/test/unit/agents/provider-plugin-runtime-identity.test.mjs new file mode 100644 index 00000000..00c683f7 --- /dev/null +++ b/packages/core/test/unit/agents/provider-plugin-runtime-identity.test.mjs @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts"; +import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler.ts"; +import { prepareGatewayUpstreamAttemptForTest } from "@ccr/core/gateway/upstream/executor.ts"; + +test("provider plugins use compiled runtime and capability identities", async () => { + const unchangedPlugin = { key: "unscoped-plugin" }; + const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-provider-plugin-runtime-identity.json" }); + config.providerPlugins = [ + { + key: "single-protocol-plugin", + providerName: "single-provider::anthropic_messages" + }, + { + key: "single-protocol-display-name-plugin", + providerName: "Single Provider" + }, + { + key: "multi-protocol-plugin", + providerName: "Multi Provider::openai_responses" + }, + { + key: "external-plugin", + providerName: "External Provider" + }, + unchangedPlugin + ]; + config.Providers = [ + { + api_base_url: "https://single.example.test/v1", + id: "single-provider", + models: ["single-model"], + name: "Single Provider", + type: "anthropic_messages" + }, + { + api_base_url: "https://multi.example.test/v1", + capabilities: [ + { baseUrl: "https://multi.example.test/anthropic", type: "anthropic_messages" }, + { baseUrl: "https://multi.example.test/responses", type: "openai_responses" } + ], + id: "multi-provider", + models: ["multi-model"], + name: "Multi Provider", + type: "anthropic_messages" + } + ]; + + const compiled = await compileCoreGatewayConfig( + config, + "raw-trace-token", + "billing-usage-token", + "core-auth-token" + ); + const [runtimePlugin, displayNamePlugin, capabilityPlugin, unmatchedPlugin, unscopedPlugin] = compiled.providerPlugins; + + assert.equal(runtimePlugin.providerName, "single-provider"); + assert.equal(displayNamePlugin.providerName, "single-provider"); + assert.equal(capabilityPlugin.providerName, "multi-provider::openai_responses"); + assert.equal(unmatchedPlugin.providerName, "External Provider"); + assert.equal(unscopedPlugin, unchangedPlugin); +}); + +test("credential-free fallback headers use the provider runtime identity", () => { + const provider = { + api_base_url: "https://api.example.test", + id: "provider-runtime-test", + models: ["example-model"], + name: "Display Provider", + type: "anthropic_messages" + }; + const config = { + Providers: [provider], + Router: { fallback: { mode: "off", models: [], retryCount: 0 } }, + gateway: {} + }; + + const attempt = prepareGatewayUpstreamAttemptForTest({ + body: { + messages: [{ content: "hi", role: "user" }], + model: "Display Provider/example-model" + }, + config, + headers: {}, + method: "POST", + path: "/v1/messages" + }); + + assert.equal(attempt.headers["x-target-provider"], "provider-runtime-test"); +});