mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
fix(gateway): align provider plugin runtime identity
Provider compilation replaces display and capability aliases with stable runtime identifiers, but plugin targets and fallback headers could retain the precompiled names. Normalize both paths so routing and authentication refer to the provider identity that the gateway actually registers.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
Reference in New Issue
Block a user