mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-10-19 17:03:59 +00:00
Merge remote-tracking branch 'origin/main' into feat/v3.1.1
This commit is contained in:
commit
e6207860c1
6 changed files with 111 additions and 5 deletions
|
|
@ -2,6 +2,7 @@ import {
|
|||
buildBrowserRenderer,
|
||||
buildCoreServer,
|
||||
buildRenderer,
|
||||
buildRequestLogBodyWorker,
|
||||
buildStyles,
|
||||
buildTrayRenderer,
|
||||
buildWebClientBridge,
|
||||
|
|
@ -25,6 +26,7 @@ await Promise.all([
|
|||
buildCoreServer({ mode }),
|
||||
buildBrowserRenderer({ mode }),
|
||||
buildRenderer({ mode }),
|
||||
buildRequestLogBodyWorker({ mode }),
|
||||
buildTrayRenderer({ mode }),
|
||||
buildWebClientBridge({ mode }),
|
||||
buildStyles({ minify: mode === "production" })
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export function resolveUsageModelAttribution(
|
|||
return attributionFromSelector(logicalModel, logicalModel, isKnownProviderSelector(config, logicalModel));
|
||||
}
|
||||
|
||||
function isKnownProviderSelector(
|
||||
export function isKnownProviderSelector(
|
||||
config: Pick<AppConfig, "Providers" | "virtualModelProfiles">,
|
||||
value: string
|
||||
): boolean {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { REQUEST_LOGS_DB_FILE, USAGE_DB_FILE } from "@ccr/core/config/constants"
|
|||
import { estimateUsageCostUsd, providerModelPricingForUsage } from "@ccr/core/models/pricing-service";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
|
||||
import { normalizeUsageInputTokens } from "@ccr/core/usage/normalization";
|
||||
import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
|
||||
import { isKnownProviderSelector, resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
|
||||
import type {
|
||||
AppConfig,
|
||||
GatewayProviderProtocol,
|
||||
|
|
@ -584,6 +584,18 @@ function resolveUsageResponseModelAttribution(
|
|||
const attribution = resolveUsageModelAttribution(config, decodedClaudeRouteModel);
|
||||
return !config || attribution.provider ? attribution : {};
|
||||
}
|
||||
if (config && model && isKnownProviderSelector(config, model)) {
|
||||
// The gateway rewrites the response model back to the selector the client
|
||||
// requested, so a "provider/model" string here is a client-visible route
|
||||
// selector, not the physical model. Attribute through the selector so
|
||||
// per-model stats aggregate on the bare model name the provider is
|
||||
// configured with; physical echoes (unknown provider or no slash) still
|
||||
// fall through and are kept verbatim.
|
||||
const attribution = resolveUsageModelAttribution(config, model);
|
||||
if (attribution.provider) {
|
||||
return attribution;
|
||||
}
|
||||
}
|
||||
return resolveUsageModelAttribution(config, model, { physicalModel: true });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -394,6 +394,98 @@ test("UsageStore attributes Claude App encoded response model IDs to the routed
|
|||
}
|
||||
});
|
||||
|
||||
test("UsageStore attributes client-visible provider-prefixed response models to the bare model", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-client-visible-selector-test-"));
|
||||
try {
|
||||
const store = new UsageStore(path.join(dir, "usage.sqlite"));
|
||||
const config = {
|
||||
Providers: [
|
||||
{
|
||||
baseUrl: "https://dashscope.example.com/api/v2/apps/anthropic",
|
||||
models: ["ZHIPU/GLM-5.3"],
|
||||
name: "dashscope-private",
|
||||
type: "anthropic_messages"
|
||||
}
|
||||
],
|
||||
virtualModelProfiles: []
|
||||
};
|
||||
|
||||
await store.recordCapture({
|
||||
bodyText: [
|
||||
"event: message_start",
|
||||
'data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"dashscope-private/ZHIPU/GLM-5.3","usage":{"input_tokens":12,"output_tokens":8,"total_tokens":20}}}',
|
||||
"",
|
||||
"data: [DONE]",
|
||||
""
|
||||
].join("\n"),
|
||||
client: "Claude Code",
|
||||
config,
|
||||
durationMs: 100,
|
||||
fallbackModel: "dashscope-private/ZHIPU/GLM-5.3",
|
||||
method: "POST",
|
||||
path: "/v1/messages",
|
||||
providerProtocol: "anthropic_messages",
|
||||
requestId: "client-visible-selector-model",
|
||||
responseHeaders: new Headers({ "content-type": "text/event-stream; charset=utf-8" }),
|
||||
statusCode: 200
|
||||
});
|
||||
|
||||
const stats = await store.getStats("today", { includeProxy: true });
|
||||
assert.equal(stats.models[0]?.model, "ZHIPU/GLM-5.3");
|
||||
assert.equal(stats.models[0]?.provider, "dashscope-private");
|
||||
assert.notEqual(stats.models[0]?.model, "dashscope-private/ZHIPU/GLM-5.3");
|
||||
} finally {
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("UsageStore keeps physical response models with unknown provider prefixes verbatim", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-physical-slash-model-test-"));
|
||||
try {
|
||||
const store = new UsageStore(path.join(dir, "usage.sqlite"));
|
||||
const config = {
|
||||
Providers: [
|
||||
{
|
||||
baseUrl: "https://dashscope.example.com/api/v2/apps/anthropic",
|
||||
models: ["ZHIPU/GLM-5.3"],
|
||||
name: "dashscope-private",
|
||||
type: "anthropic_messages"
|
||||
}
|
||||
],
|
||||
virtualModelProfiles: []
|
||||
};
|
||||
|
||||
// The upstream echoes the physical model id, whose own name contains a
|
||||
// slash; "ZHIPU" is not a configured provider, so the echo must be kept
|
||||
// verbatim instead of being re-attributed as a route selector.
|
||||
await store.recordCapture({
|
||||
bodyText: [
|
||||
"event: message_start",
|
||||
'data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"ZHIPU/GLM-5.3","usage":{"input_tokens":12,"output_tokens":8,"total_tokens":20}}}',
|
||||
"",
|
||||
"data: [DONE]",
|
||||
""
|
||||
].join("\n"),
|
||||
client: "Claude Code",
|
||||
config,
|
||||
durationMs: 100,
|
||||
fallbackModel: "dashscope-private/ZHIPU/GLM-5.3",
|
||||
method: "POST",
|
||||
path: "/v1/messages",
|
||||
providerProtocol: "anthropic_messages",
|
||||
requestId: "physical-slash-model",
|
||||
responseHeaders: new Headers({ "content-type": "text/event-stream; charset=utf-8" }),
|
||||
statusCode: 200
|
||||
});
|
||||
|
||||
const stats = await store.getStats("today", { includeProxy: true });
|
||||
assert.equal(stats.models[0]?.model, "ZHIPU/GLM-5.3");
|
||||
assert.equal(stats.models[0]?.provider, "dashscope-private");
|
||||
} finally {
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("lightweight usage synchronization records and deduplicates Fusion internal upstream calls", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-fusion-internal-test-"));
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./packages/ui/src/*"],
|
||||
"@ccr/cli/*": ["./packages/cli/src/*"],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": ".tmp/tsconfig-node",
|
||||
"tsBuildInfoFile": ".tmp/tsconfig-node/tsconfig.node.tsbuildinfo",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["build/**/*.mjs"]
|
||||
"include": ["playwright.config.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue