mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-19 21:43:24 +00:00
Propagate provider context for Fusion model discovery
This commit is contained in:
parent
9943ab273b
commit
df42fe8683
3 changed files with 148 additions and 10 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import type { AppConfig } from "@ccr/core/contracts/app";
|
||||
import { availableGatewayModelIds, normalizeProfileScopeValue } from "@ccr/core/contracts/app";
|
||||
import { modelRegistryForConfig } from "@ccr/core/routing/model-registry";
|
||||
import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
|
||||
|
||||
export const CLAUDE_APP_ONE_MILLION_CONTEXT_SUFFIX = "[1m]";
|
||||
const CLAUDE_APP_ENCODED_ROUTE_PREFIX = "anthropic/claude-ccr-h";
|
||||
|
|
@ -165,14 +166,21 @@ function claudeAppGatewaySupportsOneMillionContext(
|
|||
}
|
||||
|
||||
const providerOverride = claudeAppGatewayProviderSupportsOneMillionContext(baseModel, config);
|
||||
return providerOverride ?? Boolean(options.supportsOneMillionContext?.(baseModel));
|
||||
if (providerOverride !== undefined) {
|
||||
return providerOverride;
|
||||
}
|
||||
const physicalSelector = claudeAppGatewayPhysicalModelSelector(baseModel, config);
|
||||
return Boolean(
|
||||
options.supportsOneMillionContext?.(physicalSelector ?? baseModel) ||
|
||||
(physicalSelector && physicalSelector !== baseModel && options.supportsOneMillionContext?.(baseModel))
|
||||
);
|
||||
}
|
||||
|
||||
function claudeAppGatewayProviderSupportsOneMillionContext(
|
||||
model: string,
|
||||
config: Pick<AppConfig, "Providers" | "virtualModelProfiles">
|
||||
): boolean | undefined {
|
||||
const resolved = modelRegistryForConfig(config).resolveProviderModel(model);
|
||||
const resolved = claudeAppGatewayResolvedProviderModel(model, config);
|
||||
if (!resolved) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -188,6 +196,34 @@ function claudeAppGatewayProviderSupportsOneMillionContext(
|
|||
return Math.floor((contextWindow * effectivePercent) / 100) >= 1_000_000;
|
||||
}
|
||||
|
||||
function claudeAppGatewayPhysicalModelSelector(
|
||||
model: string,
|
||||
config: Pick<AppConfig, "Providers" | "virtualModelProfiles">
|
||||
): string | undefined {
|
||||
const resolved = claudeAppGatewayResolvedProviderModel(model, config);
|
||||
return resolved ? `${resolved.provider.name}/${resolved.model}` : undefined;
|
||||
}
|
||||
|
||||
function claudeAppGatewayResolvedProviderModel(
|
||||
model: string,
|
||||
config: Pick<AppConfig, "Providers" | "virtualModelProfiles">
|
||||
) {
|
||||
const registry = modelRegistryForConfig(config);
|
||||
const direct = registry.resolveProviderModel(model);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const attribution = resolveUsageModelAttribution(config, model);
|
||||
if (!attribution.provider || !attribution.model) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = registry.resolve(`${attribution.provider}/${attribution.model}`);
|
||||
return resolved?.kind === "provider"
|
||||
? { model: resolved.model, provider: resolved.provider }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function positiveInteger(value: number | undefined): number | undefined {
|
||||
return value !== undefined && Number.isFinite(value) && value > 0
|
||||
? Math.trunc(value)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type { ClaudeCodeDiscoverableModel } from "@ccr/core/gateway/internal/sha
|
|||
import { parseJsonObjectSafe, serializeJsonBodyWithModel } from "@ccr/core/gateway/http/body";
|
||||
import { uniqueStrings } from "@ccr/core/gateway/internal/collections";
|
||||
import { contextArchiveConfigForApiKey, contextArchiveMcpEnabled } from "@ccr/core/gateway/context-archive";
|
||||
import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
|
||||
|
||||
|
||||
export function shouldServeGatewayModelsResponse(method: string, path: string): boolean {
|
||||
|
|
@ -128,8 +129,9 @@ function createClaudeAppGatewayModelsResponse(
|
|||
const routes = buildClaudeAppGatewayModelRoutes(config, claudeAppGatewayModelRouteOptions);
|
||||
const data = routes.map((route) => {
|
||||
const catalogId = stripClaudeCodeOneMillionContextSuffix(route.targetModel);
|
||||
const catalogEntry = findModelCatalogEntry(catalogId);
|
||||
const modelMetadata = providerModelMetadataForSelector(config, catalogId);
|
||||
const modelDiscovery = providerModelDiscoveryForSelector(config, catalogId);
|
||||
const catalogEntry = modelDiscovery.catalogEntry;
|
||||
const modelMetadata = modelDiscovery.metadata;
|
||||
const maxInputTokens = claudeGatewayModelContextWindow(catalogEntry, route.oneMillionContext, modelMetadata);
|
||||
const maxOutputTokens = modelCatalogMaxOutputTokens(catalogEntry);
|
||||
const exposeOneMillionContextVariant = options.claudeCode && route.oneMillionContext;
|
||||
|
|
@ -165,8 +167,9 @@ function createClaudeCodeModelsResponse(config: AppConfig, contextArchiveCompact
|
|||
const data = models.map((model) => {
|
||||
const claudeId = claudeCodeDiscoveryModelId(model.id);
|
||||
const catalogId = stripClaudeCodeOneMillionContextSuffix(model.id);
|
||||
const catalogEntry = findModelCatalogEntry(catalogId);
|
||||
const modelMetadata = providerModelMetadataForSelector(config, catalogId);
|
||||
const modelDiscovery = providerModelDiscoveryForSelector(config, catalogId);
|
||||
const catalogEntry = modelDiscovery.catalogEntry;
|
||||
const modelMetadata = modelDiscovery.metadata;
|
||||
const maxInputTokens = claudeGatewayModelContextWindow(catalogEntry, model.oneMillionContext, modelMetadata);
|
||||
const maxOutputTokens = modelCatalogMaxOutputTokens(catalogEntry);
|
||||
return {
|
||||
|
|
@ -226,11 +229,27 @@ function effectiveProviderContextWindow(metadata: ProviderModelMetadata | undefi
|
|||
}
|
||||
|
||||
|
||||
function providerModelMetadataForSelector(config: AppConfig, selector: string): ProviderModelMetadata | undefined {
|
||||
const resolved = modelRegistryForConfig(config).resolveProviderModel(selector);
|
||||
function providerModelDiscoveryForSelector(
|
||||
config: AppConfig,
|
||||
selector: string
|
||||
): { catalogEntry?: ModelCatalogEntry; metadata?: ProviderModelMetadata } {
|
||||
const resolved = providerModelResolutionForSelector(config, selector);
|
||||
if (!resolved) {
|
||||
return undefined;
|
||||
return {
|
||||
catalogEntry: findModelCatalogEntry(selector)
|
||||
};
|
||||
}
|
||||
const physicalSelector = `${resolved.provider.name}/${resolved.model}`;
|
||||
return {
|
||||
catalogEntry: findModelCatalogEntry(physicalSelector) ?? findModelCatalogEntry(selector),
|
||||
metadata: providerModelMetadataForResolvedModel(resolved)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function providerModelMetadataForResolvedModel(
|
||||
resolved: NonNullable<ReturnType<typeof providerModelResolutionForSelector>>
|
||||
): ProviderModelMetadata | undefined {
|
||||
const metadata = resolved.provider.modelMetadata ?? {};
|
||||
const direct = metadata[resolved.model];
|
||||
if (direct) {
|
||||
|
|
@ -241,6 +260,34 @@ function providerModelMetadataForSelector(config: AppConfig, selector: string):
|
|||
}
|
||||
|
||||
|
||||
function providerModelResolutionForSelector(config: AppConfig, selector: string) {
|
||||
const registry = modelRegistryForConfig(config);
|
||||
const direct = registry.resolveProviderModel(selector);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const attribution = resolveUsageModelAttribution(config, selector);
|
||||
if (!attribution.provider || !attribution.model) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = registry.resolve(`${attribution.provider}/${attribution.model}`);
|
||||
return resolved?.kind === "provider"
|
||||
? { model: resolved.model, provider: resolved.provider }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
|
||||
function gatewayModelSupportsOneMillionContext(config: AppConfig, selector: string): boolean {
|
||||
const discovery = providerModelDiscoveryForSelector(config, selector);
|
||||
const metadataContextWindow = effectiveProviderContextWindow(discovery.metadata);
|
||||
return Boolean(
|
||||
(metadataContextWindow && metadataContextWindow >= 1_000_000) ||
|
||||
discovery.catalogEntry?.limits?.supports1MContext
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function percentage(value: number | undefined): number | undefined {
|
||||
return value !== undefined && Number.isFinite(value) && value > 0 && value <= 100
|
||||
? value
|
||||
|
|
@ -336,7 +383,7 @@ function buildClaudeCodeDiscoverableModels(config: AppConfig): ClaudeCodeDiscove
|
|||
for (const id of buildClaudeCodeDiscoverableModelIds(config)) {
|
||||
pushModel(id, hasClaudeCodeOneMillionContextSuffix(id));
|
||||
const baseId = stripClaudeCodeOneMillionContextSuffix(id);
|
||||
if (!hasClaudeCodeOneMillionContextSuffix(id) && findModelCatalogEntry(baseId)?.limits?.supports1MContext) {
|
||||
if (!hasClaudeCodeOneMillionContextSuffix(id) && gatewayModelSupportsOneMillionContext(config, baseId)) {
|
||||
pushModel(claudeCodeOneMillionContextModelId(baseId), true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,6 +276,61 @@ test("Claude Code exposes a configured 1M context window as a visible model vari
|
|||
assert.equal(rewrite?.routedModel, "Zhipu AI (China) - Coding Plan/aaa");
|
||||
});
|
||||
|
||||
test("issue 1632 Claude discovery inherits Fusion fixed model provider context", () => {
|
||||
const config = createConfig({
|
||||
providers: [
|
||||
{
|
||||
modelMetadata: {
|
||||
"glm-5.2": {
|
||||
contextWindow: 1_000_000,
|
||||
maxContextWindow: 1_000_000
|
||||
}
|
||||
},
|
||||
models: ["glm-5.2"],
|
||||
name: "Zhipu AI (China) - Coding Plan",
|
||||
type: "openai_chat_completions"
|
||||
}
|
||||
],
|
||||
virtualModelProfiles: [
|
||||
{
|
||||
baseModel: { fixedModel: "Zhipu AI (China) - Coding Plan/glm-5.2", mode: "fixed" },
|
||||
displayName: "GLM 5.2 Fusion",
|
||||
enabled: true,
|
||||
execution: { clientToolsPolicy: "allow", mode: "tool_loop", streamMode: "optimistic" },
|
||||
id: "glm-5-2-fusion",
|
||||
key: "glm-5.2-fusion",
|
||||
match: { exactAliases: ["glm-5.2-fusion"], prefixes: [], suffixes: [] },
|
||||
materialization: { enabled: true, includeInGatewayModels: true },
|
||||
tools: []
|
||||
}
|
||||
]
|
||||
});
|
||||
const route = buildClaudeAppGatewayModelRoutes(config).find((item) => item.targetModel === "Fusion/glm-5.2-fusion");
|
||||
const inferenceModel = buildClaudeAppGatewayInferenceModels(config).find((item) => item.name === route?.id);
|
||||
const appModel = createClaudeModelsResponse(config).data.find((item) => item.id === route?.id);
|
||||
const codeModel = createClaudeCodeModelsResponse(config).data.find((item) =>
|
||||
item.display_name === "Fusion/glm-5.2-fusion (1M context)"
|
||||
);
|
||||
|
||||
assert.ok(route);
|
||||
assert.equal(route.oneMillionContext, true);
|
||||
assert.equal(inferenceModel?.supports1m, true);
|
||||
assert.equal(appModel?.max_input_tokens, 1_000_000);
|
||||
assert.equal(appModel?.capabilities.context_window.max_input_tokens, 1_000_000);
|
||||
assert.equal(appModel?.capabilities.context_window.supports_1m_context, true);
|
||||
assert.ok(codeModel);
|
||||
assert.match(codeModel.id, /\[1m\]$/);
|
||||
assert.equal(codeModel.max_input_tokens, 1_000_000);
|
||||
|
||||
const rewrite = prepareClaudeAppDiscoveredModelRequest(
|
||||
config,
|
||||
"POST",
|
||||
"/v1/messages",
|
||||
Buffer.from(JSON.stringify({ messages: [], model: codeModel.id }))
|
||||
);
|
||||
assert.equal(rewrite?.routedModel, "Fusion/glm-5.2-fusion");
|
||||
});
|
||||
|
||||
test("Claude App discovery publishes the effective provider context for uncatalogued models", () => {
|
||||
const config = createConfig({
|
||||
providers: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue