mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-29 03:12:10 +08:00
Merge remote-tracking branch 'origin/dev/3.1' into codex/claude-design-profile
# Conflicts: # .test-dist/core/runtime/request-log-worker.js # .test-dist/core/test/integration/gateway/gateway-client-disconnect.test.js # .test-dist/core/test/integration/gateway/gateway-virtual-models.test.js # .test-dist/core/test/integration/mcp/grok-media-service.test.js # .test-dist/core/test/integration/mcp/local-ai-gateway-media-live.test.js # .test-dist/core/test/integration/observability/request-log-pricing-transaction.test.js # .test-dist/core/test/integration/observability/request-log-runtime.test.js # .test-dist/core/test/integration/observability/request-log-store.test.js # .test-dist/core/test/integration/plugins/plugin-service.test.js # .test-dist/core/test/integration/profiles/profile-service.test.js # .test-dist/core/test/integration/usage/usage-store.test.js # .test-dist/core/test/unit/agents/bot-gateway-env.test.js # .test-dist/core/test/unit/agents/claude-app-gateway-models.test.js # .test-dist/core/test/unit/agents/claude-app-launch.test.js # .test-dist/core/test/unit/agents/claude-environment.test.js # .test-dist/core/test/unit/agents/codex-app-model-catalog.test.js # .test-dist/core/test/unit/agents/codex-media-preview-bridge.test.js # .test-dist/core/test/unit/agents/codex-model-catalog.test.js # .test-dist/core/test/unit/agents/local-agent-provider-codex.test.js # .test-dist/core/test/unit/agents/local-agent-provider-grok.test.js # .test-dist/core/test/unit/agents/local-agent-provider-kimi.test.js # .test-dist/core/test/unit/agents/opencode-profile-config.test.js # .test-dist/core/test/unit/agents/provider-model-metadata.test.js # .test-dist/core/test/unit/config/config-env-interpolation.test.js # .test-dist/core/test/unit/config/theme-preference.test.js # .test-dist/core/test/unit/gateway/codex-patch-bridge.test.js # .test-dist/core/test/unit/gateway/gateway-billing-sync.test.js # .test-dist/core/test/unit/gateway/gateway-claude-code-oauth.test.js # .test-dist/core/test/unit/gateway/gateway-media-config.test.js # .test-dist/core/test/unit/gateway/gateway-runtime-change.test.js # .test-dist/core/test/unit/gateway/gateway-status.test.js # .test-dist/core/test/unit/gateway/router-builtins.test.js # .test-dist/core/test/unit/gateway/routing-architecture.test.js # .test-dist/core/test/unit/mcp/toolhub-browser-automation-config.test.js # .test-dist/core/test/unit/models/pricing-service.test.js # .test-dist/core/test/unit/observability/raw-trace-sync.test.js # .test-dist/core/test/unit/profiles/ccr-cli-runtime.test.js # .test-dist/core/test/unit/profiles/profile-app-process-detection.test.js # .test-dist/core/test/unit/profiles/profile-launch-core.test.js # .test-dist/core/test/unit/profiles/windows-ccr-launcher.test.js # .test-dist/core/test/unit/providers/credential-pool.test.js # .test-dist/core/test/unit/providers/provider-account-service.test.js # .test-dist/core/test/unit/providers/provider-model-catalog.test.js # .test-dist/core/test/unit/providers/provider-preset-utils.test.js # .test-dist/core/test/unit/providers/provider-probe.test.js # .test-dist/core/test/unit/proxy/proxy-upstream.test.js # .test-dist/core/test/unit/routing/route-script-runtime.test.js # .test-dist/core/test/unit/web/web-management-server.test.js # .test-dist/ui/test/component/components.test.js # .test-dist/ui/test/component/layout.test.js # .test-dist/ui/test/component/overview-components.test.js # .test-dist/ui/test/component/profiles.test.js # .test-dist/ui/test/component/tray-components.test.js # .test-dist/ui/test/integration/providers.test.js # .test-dist/ui/test/unit/mcp-server-config.test.js # .test-dist/ui/test/unit/model-selector-format.test.js # .test-dist/ui/test/unit/routing.test.js # .test-dist/ui/test/unit/usage-activity.test.js # .test-dist/ui/test/unit/usage-format.test.js # .test-dist/ui/test/unit/virtual-models.test.js # README.md # README_zh.md
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
|
||||
### Manage every agent and provider from one place.
|
||||
|
||||
Connect Claude Code, Codex, Grok CLI, Kimi CLI, Pi, ZCode, and compatible API clients to the providers you choose—then route, fail over, extend, and observe every request from one app.
|
||||
Connect Claude Code, Codex, Grok CLI, Kimi CLI, OpenCode, Pi, ZCode, and compatible API clients to the providers you choose—then route, fail over, extend, and observe every request from one app.
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/musistudio/claude-code-router/releases"><img alt="Download Desktop" src="https://img.shields.io/badge/Download-Desktop_App-2563EB?style=for-the-badge&logo=github&logoColor=white" /></a>
|
||||
@@ -59,7 +59,7 @@ Connect Claude Code, Codex, Grok CLI, Kimi CLI, Pi, ZCode, and compatible API cl
|
||||
|
||||
## Why use Claude Code Router?
|
||||
|
||||
Claude Code Router (CCR) is a local model gateway and control plane for coding agents. It gives Claude Code, Codex, Grok CLI, Kimi CLI, Pi, ZCode, and compatible API clients **one stable local endpoint**, while you manage the providers, models, accounts, routing rules, and tools behind it from one place.
|
||||
Claude Code Router (CCR) is a local model gateway and control plane for coding agents. It gives Claude Code, Codex, Grok CLI, Kimi CLI, OpenCode, Pi, ZCode, and compatible API clients **one stable local endpoint**, while you manage the providers, models, accounts, routing rules, and tools behind it from one place.
|
||||
|
||||
Use CCR to:
|
||||
|
||||
@@ -78,7 +78,7 @@ CCR supports OpenAI Chat / Responses, Anthropic Messages, Gemini Generate Conten
|
||||
1. **[Download Claude Code Router](https://github.com/musistudio/claude-code-router/releases)** for macOS, Windows, or Linux, then launch the app.
|
||||
2. Open **Providers → Add Provider**. Choose a built-in preset or a custom endpoint, enter the API key, select the protocol and models, then save.
|
||||
3. Open **Server** and click **Start**. The local model gateway listens on `http://127.0.0.1:3456` by default.
|
||||
4. Open **Agent Profiles**, choose Claude Code, Codex, Grok CLI, Kimi CLI, Pi, or ZCode, select a model, and apply the profile.
|
||||
4. Open **Agent Profiles**, choose Claude Code, Codex, Grok CLI, Kimi CLI, OpenCode, Pi, or ZCode, select a model, and apply the profile.
|
||||
5. Start using your agent. Open **Logs** to confirm the resolved provider, model, status, tokens, latency, and errors.
|
||||
|
||||
Your agent is now connected to CCR. To add conditions, retries, request rewrites, or fallback models, open **Routing**.
|
||||
@@ -117,7 +117,7 @@ Docker exposes the management UI and gateway routes through `http://127.0.0.1:34
|
||||
## How it works
|
||||
|
||||
```text
|
||||
Claude Code · Codex · Grok CLI · Kimi CLI · Pi · ZCode · Compatible API clients
|
||||
Claude Code · Codex · Grok CLI · Kimi CLI · OpenCode · Pi · ZCode · Compatible API clients
|
||||
│
|
||||
▼
|
||||
Claude Code Router :3456
|
||||
@@ -131,7 +131,7 @@ Claude Code · Codex · Grok CLI · Kimi CLI · Pi · ZCode · Compatible API cl
|
||||
|
||||
| Area | Highlights |
|
||||
| --- | --- |
|
||||
| **Agents** | Profiles for Claude Code, Codex, Grok CLI, Kimi CLI, Pi, and ZCode; model overrides; scopes; environment settings; CLI and app launch entries; multi-instance workflows |
|
||||
| **Agents** | Profiles for Claude Code, Codex, Grok CLI, Kimi CLI, OpenCode, Pi, and ZCode; model overrides; scopes; environment settings; CLI and app launch entries; multi-instance workflows |
|
||||
| **Providers** | Presets and custom endpoints; protocol probing; model discovery; connectivity checks; local login import where supported; single keys and credential pools |
|
||||
| **Models & routing** | Searchable catalog; model descriptions for task selection; conditions on headers and bodies; prefixes; rewrites; retries; ordered fallbacks |
|
||||
| **Tools & extensions** | Fusion models; ToolHub; built-in browser automation; Chrome login-state import; wrapper and core gateway plugins; local routes and virtual models |
|
||||
@@ -285,6 +285,13 @@ Codex support is powered by [musistudio/codexl](https://github.com/musistudio/co
|
||||
<strong>Unity2.Ai</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://infistar.ai">
|
||||
<img src="/docs/public/provider-icons/infistar-ai.jpg" width="42" height="42" alt="无限星河 icon" />
|
||||
<br />
|
||||
<strong>无限星河</strong>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
+12
-5
@@ -36,7 +36,7 @@
|
||||
|
||||
### 在一个地方,管理你所有的 Agent 与 Provider
|
||||
|
||||
让 Claude Code、Codex、Grok CLI、Kimi CLI、Pi、ZCode 和兼容 API 客户端连接你选择的供应商,并在一个应用里完成每次请求的路由、降级、增强与观测。
|
||||
让 Claude Code、Codex、Grok CLI、Kimi CLI、OpenCode、Pi、ZCode 和兼容 API 客户端连接你选择的供应商,并在一个应用里完成每次请求的路由、降级、增强与观测。
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/musistudio/claude-code-router/releases"><img alt="下载桌面端" src="https://img.shields.io/badge/%E7%AB%8B%E5%8D%B3%E4%B8%8B%E8%BD%BD-%E6%A1%8C%E9%9D%A2%E5%AE%A2%E6%88%B7%E7%AB%AF-2563EB?style=for-the-badge&logo=github&logoColor=white" /></a>
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
## 为什么使用 Claude Code Router?
|
||||
|
||||
Claude Code Router(CCR)是面向编程 Agent 的本地模型网关与控制平面。它为 Claude Code、Codex、Grok CLI、Kimi CLI、Pi、ZCode 和兼容 API 客户端提供**一个稳定的本地入口**,让你在一个地方管理入口背后的供应商、模型、账号、路由规则与工具。
|
||||
Claude Code Router(CCR)是面向编程 Agent 的本地模型网关与控制平面。它为 Claude Code、Codex、Grok CLI、Kimi CLI、OpenCode、Pi、ZCode 和兼容 API 客户端提供**一个稳定的本地入口**,让你在一个地方管理入口背后的供应商、模型、账号、路由规则与工具。
|
||||
|
||||
你可以使用 CCR:
|
||||
|
||||
@@ -78,7 +78,7 @@ CCR 支持 OpenAI Chat / Responses、Anthropic Messages、Gemini Generate Conten
|
||||
1. **[下载 Claude Code Router](https://github.com/musistudio/claude-code-router/releases)**,选择 macOS、Windows 或 Linux 版本并启动应用。
|
||||
2. 打开 **供应商 → 添加供应商**。选择内置预设或自定义端点,填写 API Key,选择协议与模型,然后保存。
|
||||
3. 打开 **服务** 并点击 **启动**。本地模型网关默认监听 `http://127.0.0.1:3456`。
|
||||
4. 打开 **Agent 配置档案**,选择 Claude Code、Codex、Grok CLI、Kimi CLI、Pi 或 ZCode,指定模型并应用配置档案。
|
||||
4. 打开 **Agent 配置档案**,选择 Claude Code、Codex、Grok CLI、Kimi CLI、OpenCode、Pi 或 ZCode,指定模型并应用配置档案。
|
||||
5. 开始使用 Agent。在 **日志** 中确认最终供应商、模型、状态、Token、耗时与错误。
|
||||
|
||||
现在 Agent 已经连接到 CCR。如需增加条件规则、自动重试、请求改写或 Fallback 模型,请打开 **路由**。
|
||||
@@ -117,7 +117,7 @@ Docker 默认通过 `http://127.0.0.1:3458` 提供管理界面与网关路由。
|
||||
## 工作方式
|
||||
|
||||
```text
|
||||
Claude Code · Codex · Grok CLI · Kimi CLI · Pi · ZCode · 兼容 API 客户端
|
||||
Claude Code · Codex · Grok CLI · Kimi CLI · OpenCode · Pi · ZCode · 兼容 API 客户端
|
||||
│
|
||||
▼
|
||||
Claude Code Router :3456
|
||||
@@ -131,7 +131,7 @@ Claude Code · Codex · Grok CLI · Kimi CLI · Pi · ZCode · 兼容 API 客户
|
||||
|
||||
| 能力领域 | 功能亮点 |
|
||||
| --- | --- |
|
||||
| **Agent** | Claude Code、Codex、Grok CLI、Kimi CLI、Pi 和 ZCode 配置档案;模型覆盖;作用范围;环境变量;CLI / App 启动入口;多开工作流 |
|
||||
| **Agent** | Claude Code、Codex、Grok CLI、Kimi CLI、OpenCode、Pi 和 ZCode 配置档案;模型覆盖;作用范围;环境变量;CLI / App 启动入口;多开工作流 |
|
||||
| **供应商** | 内置预设和自定义端点;协议探测;模型发现;连通性检测;按支持情况导入本机登录态;单 Key 与凭据池 |
|
||||
| **模型与路由** | 可搜索模型目录;用于任务选择的模型描述;Header / Body 条件;模型前缀;请求改写;重试;有序 Fallback |
|
||||
| **工具与扩展** | Fusion 模型;ToolHub;内置浏览器自动化;Chrome 登录态导入;wrapper / core gateway plugin;本地路由与虚拟模型 |
|
||||
@@ -285,6 +285,13 @@ Claude Code · Codex · Grok CLI · Kimi CLI · Pi · ZCode · 兼容 API 客户
|
||||
<strong>Unity2.Ai</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://infistar.ai">
|
||||
<img src="/docs/public/provider-icons/infistar-ai.jpg" width="42" height="42" alt="无限星河图标" />
|
||||
<br />
|
||||
<strong>无限星河</strong>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -102,6 +102,10 @@ Choose a provider below to get started. CCR shows what will be added before savi
|
||||
<span class="provider-import-icon-shell"><img src="../../../provider-icons/fenno.jpg" alt="" loading="lazy" /></span>
|
||||
<span class="provider-import-copy"><span class="provider-import-name">Fenno.ai</span><span class="provider-import-meta">Chat / Responses / Anthropic</span></span>
|
||||
</a>
|
||||
<a class="provider-import-button provider-infistar-ai" href="ccr://provider?name=%E6%97%A0%E9%99%90%E6%98%9F%E6%B2%B3&base_url=https%3A%2F%2Finfistar.ai%2Fv1&protocol=openai_chat_completions&models=gpt-4o&source=https%3A%2F%2Finfistar.ai" aria-label="Import 无限星河 provider">
|
||||
<span class="provider-import-icon-shell"><img src="../../../provider-icons/infistar-ai.jpg" alt="" loading="lazy" /></span>
|
||||
<span class="provider-import-copy"><span class="provider-import-name">无限星河</span><span class="provider-import-meta">OpenAI compatible gateway</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Embeddable Button Component
|
||||
|
||||
@@ -102,6 +102,10 @@ lead: 快速添加常见模型供应商,确认无误后即可保存,减少
|
||||
<span class="provider-import-icon-shell"><img src="../../provider-icons/fenno.jpg" alt="" loading="lazy" /></span>
|
||||
<span class="provider-import-copy"><span class="provider-import-name">Fenno.ai</span><span class="provider-import-meta">Chat / Responses / Anthropic</span></span>
|
||||
</a>
|
||||
<a class="provider-import-button provider-infistar-ai" href="ccr://provider?name=%E6%97%A0%E9%99%90%E6%98%9F%E6%B2%B3&base_url=https%3A%2F%2Finfistar.ai%2Fv1&protocol=openai_chat_completions&models=gpt-4o&source=https%3A%2F%2Finfistar.ai" aria-label="导入无限星河供应商">
|
||||
<span class="provider-import-icon-shell"><img src="../../provider-icons/infistar-ai.jpg" alt="" loading="lazy" /></span>
|
||||
<span class="provider-import-copy"><span class="provider-import-name">无限星河</span><span class="provider-import-meta">OpenAI 兼容网关</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 嵌入式按钮组件
|
||||
|
||||
@@ -1408,6 +1408,12 @@ h1 {
|
||||
--provider-brand-3: #b4fff4;
|
||||
}
|
||||
|
||||
.doc-markdown a.provider-import-button.provider-infistar-ai {
|
||||
--provider-brand: #111f8d;
|
||||
--provider-brand-2: #1fb7ee;
|
||||
--provider-brand-3: #9b18ff;
|
||||
}
|
||||
|
||||
.doc-markdown > pre,
|
||||
.doc-markdown > .code-panel {
|
||||
margin: 22px 0 32px;
|
||||
|
||||
@@ -2882,13 +2882,8 @@ function buildAgentAnalysisTotals(requests: AnalyzedAgentRequest[]): AgentAnalys
|
||||
const cacheWriteTokens = sum(requests, (request) => request.cacheWriteTokens);
|
||||
const cacheTokens = cacheReadTokens;
|
||||
const costUsd = sum(requests, (request) => request.costUsd ?? 0);
|
||||
const totalTokens = sum(requests, (request) => request.totalTokens || request.inputTokens + request.outputTokens + request.cacheReadTokens + request.cacheWriteTokens);
|
||||
const promptTokens = sum(requests, (request) => {
|
||||
const promptTokensFromTotal = request.totalTokens - request.outputTokens;
|
||||
return promptTokensFromTotal > 0
|
||||
? Math.max(request.inputTokens, promptTokensFromTotal)
|
||||
: request.inputTokens + request.cacheReadTokens + request.cacheWriteTokens;
|
||||
});
|
||||
const totalTokens = sum(requests, agentAnalysisTotalTokenCount);
|
||||
const promptTokens = sum(requests, agentAnalysisPromptTokenCount);
|
||||
const successfulRequests = requests.filter((request) => request.ok).length;
|
||||
const sessionCount = new Set(requests.map((request) => `${request.agent}:${request.sessionId}`)).size;
|
||||
const durations = requests.map((request) => request.durationMs).sort((a, b) => a - b);
|
||||
@@ -2917,6 +2912,19 @@ function buildAgentAnalysisTotals(requests: AnalyzedAgentRequest[]): AgentAnalys
|
||||
};
|
||||
}
|
||||
|
||||
function agentAnalysisPromptTokenCount(request: AnalyzedAgentRequest): number {
|
||||
const cacheTokens = request.cacheReadTokens + request.cacheWriteTokens;
|
||||
const promptTokensFromTotal = request.totalTokens - request.outputTokens;
|
||||
return Math.max(request.inputTokens + cacheTokens, promptTokensFromTotal);
|
||||
}
|
||||
|
||||
function agentAnalysisTotalTokenCount(request: AnalyzedAgentRequest): number {
|
||||
return Math.max(
|
||||
request.totalTokens,
|
||||
request.inputTokens + request.outputTokens + request.cacheReadTokens + request.cacheWriteTokens
|
||||
);
|
||||
}
|
||||
|
||||
function buildStatusCodeCounts(requests: AnalyzedAgentRequest[]): Array<{ count: number; statusCode: number }> {
|
||||
const counts = new Map<number, number>();
|
||||
for (const request of requests) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { code0ProviderPreset } from "@ccr/core/providers/presets/code0/index";
|
||||
import { deepSeekProviderPreset } from "@ccr/core/providers/presets/deepseek/index";
|
||||
import { fennoProviderPreset } from "@ccr/core/providers/presets/fenno/index";
|
||||
import { geminiProviderPreset } from "@ccr/core/providers/presets/gemini/index";
|
||||
import { infistarAiProviderPreset } from "@ccr/core/providers/presets/infistar-ai/index";
|
||||
import { kimiCodingProviderPreset } from "@ccr/core/providers/presets/kimi-coding/index";
|
||||
import { minimaxChinaProviderPreset, minimaxGlobalProviderPreset } from "@ccr/core/providers/presets/minimax/index";
|
||||
import { mistralProviderPreset } from "@ccr/core/providers/presets/mistral/index";
|
||||
@@ -53,6 +54,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
siliconFlowProviderPreset,
|
||||
qiniuAiProviderPreset,
|
||||
fennoProviderPreset,
|
||||
infistarAiProviderPreset,
|
||||
runApiProviderPreset,
|
||||
teamoRouterProviderPreset,
|
||||
unity2ProviderPreset,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const infistarAiProviderPreset: ProviderPreset = {
|
||||
account: defaultProviderAccountConfig,
|
||||
aliases: ["infistar", "infistar ai", "无限星河", "无限星河ai", "无限星河 ai"],
|
||||
defaultModels: ["gpt-4o"],
|
||||
endpoints: [
|
||||
{
|
||||
baseUrl: "https://infistar.ai/v1",
|
||||
protocols: ["openai_chat_completions"]
|
||||
}
|
||||
],
|
||||
id: "infistar-ai",
|
||||
name: "无限星河",
|
||||
websiteUrl: "https://infistar.ai"
|
||||
};
|
||||
@@ -630,14 +630,14 @@ const usageTotalsSelect = `
|
||||
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
||||
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN total_tokens > 0 THEN total_tokens
|
||||
WHEN total_tokens > input_tokens + output_tokens + cache_read_tokens + cache_write_tokens THEN total_tokens
|
||||
ELSE input_tokens + output_tokens + cache_read_tokens + cache_write_tokens
|
||||
END), 0) AS computed_total_tokens,
|
||||
COALESCE(SUM(COALESCE(cost_usd, 0)), 0) AS cost_usd,
|
||||
COALESCE(SUM(duration_ms), 0) AS duration_ms,
|
||||
COALESCE(SUM(CASE WHEN status_code >= 200 AND status_code < 400 THEN 1 ELSE 0 END), 0) AS success_count,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN total_tokens - output_tokens > 0 THEN MAX(input_tokens, total_tokens - output_tokens)
|
||||
WHEN total_tokens - output_tokens > input_tokens + cache_read_tokens + cache_write_tokens THEN total_tokens - output_tokens
|
||||
ELSE input_tokens + cache_read_tokens + cache_write_tokens
|
||||
END), 0) AS prompt_tokens
|
||||
`;
|
||||
@@ -1025,7 +1025,7 @@ function buildTotals(events: StoredUsageEvent[]): UsageTotals {
|
||||
const outputTokens = sum(events, (event) => event.outputTokens);
|
||||
const cacheTokens = sum(events, (event) => event.cacheReadTokens);
|
||||
const costUsd = sum(events, (event) => event.costUsd);
|
||||
const totalTokens = sum(events, (event) => event.totalTokens || event.inputTokens + event.outputTokens + event.cacheReadTokens + event.cacheWriteTokens);
|
||||
const totalTokens = sum(events, totalTokenCount);
|
||||
const promptTokens = sum(events, promptTokenCount);
|
||||
const successfulRequests = events.filter((event) => event.statusCode >= 200 && event.statusCode < 400).length;
|
||||
const errorCount = requestCount - successfulRequests;
|
||||
@@ -1047,10 +1047,14 @@ function buildTotals(events: StoredUsageEvent[]): UsageTotals {
|
||||
function promptTokenCount(event: StoredUsageEvent): number {
|
||||
const cacheTokens = event.cacheReadTokens + event.cacheWriteTokens;
|
||||
const promptTokensFromTotal = event.totalTokens - event.outputTokens;
|
||||
if (promptTokensFromTotal > 0) {
|
||||
return Math.max(event.inputTokens, promptTokensFromTotal);
|
||||
}
|
||||
return event.inputTokens + cacheTokens;
|
||||
return Math.max(event.inputTokens + cacheTokens, promptTokensFromTotal);
|
||||
}
|
||||
|
||||
function totalTokenCount(event: StoredUsageEvent): number {
|
||||
return Math.max(
|
||||
event.totalTokens,
|
||||
event.inputTokens + event.outputTokens + event.cacheReadTokens + event.cacheWriteTokens
|
||||
);
|
||||
}
|
||||
|
||||
function extractUsageFromBillingHeaders(headers: Headers): UsageNumbers | undefined {
|
||||
|
||||
@@ -1010,6 +1010,58 @@ test("RequestLogStore analyzes agent sessions and exposes trace payloads", async
|
||||
}
|
||||
});
|
||||
|
||||
test("RequestLogStore agent analysis cache ratio denominator includes cache tokens when total tokens omit cache", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-cache-ratio-test-"));
|
||||
try {
|
||||
const store = new RequestLogStore(path.join(dir, "request-logs.sqlite"));
|
||||
const startedAt = new Date(Date.now() - 1000).toISOString();
|
||||
const completedAt = new Date().toISOString();
|
||||
|
||||
await store.record({
|
||||
completedAt,
|
||||
durationMs: 100,
|
||||
method: "POST",
|
||||
path: "/v1/messages",
|
||||
providerName: "zhipu",
|
||||
providerProtocol: "anthropic_messages",
|
||||
requestBody: Buffer.from(JSON.stringify({
|
||||
messages: [{ content: "hello", role: "user" }],
|
||||
model: "glm-cache",
|
||||
session_id: "cache-session"
|
||||
}), "utf8"),
|
||||
requestHeaders: {
|
||||
"content-type": "application/json",
|
||||
"user-agent": "openai-codex test",
|
||||
"x-ccr-route-reason": "default",
|
||||
"x-codex-session-id": "cache-session"
|
||||
},
|
||||
requestId: "request-log-cache-ratio",
|
||||
responseBodyText: JSON.stringify({
|
||||
model: "glm-cache",
|
||||
usage: {
|
||||
cache_read_tokens: 90,
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15
|
||||
}
|
||||
}),
|
||||
responseHeaders: { "content-type": "application/json" },
|
||||
startedAt,
|
||||
statusCode: 200,
|
||||
url: "http://127.0.0.1:3456/v1/messages"
|
||||
});
|
||||
|
||||
const analysis = await store.analyze({ range: "30d" });
|
||||
assert.equal(analysis.totals.totalTokens, 105);
|
||||
assert.equal(analysis.totals.cacheRatio, 0.9);
|
||||
assert.equal(analysis.sessions[0]?.cacheRatio, 0.9);
|
||||
assert.equal(analysis.agents[0]?.cacheRatio, 0.9);
|
||||
assert.equal(analysis.routes[0]?.cacheRatio, 0.9);
|
||||
} finally {
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("RequestLogStore analyzes large bodies without dropping agent metadata", {
|
||||
skip: isBoundedHeapWorker,
|
||||
timeout: 30000
|
||||
|
||||
@@ -149,6 +149,39 @@ test("UsageStore aggregates stats in SQLite without loading all events", async (
|
||||
}
|
||||
});
|
||||
|
||||
test("UsageStore cache ratio denominator includes cache tokens when total tokens omit cache", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-cache-ratio-test-"));
|
||||
try {
|
||||
const store = new UsageStore(path.join(dir, "usage.sqlite"));
|
||||
|
||||
await store.record({
|
||||
createdAt: new Date().toISOString(),
|
||||
durationMs: 50,
|
||||
method: "POST",
|
||||
model: "glm-cache",
|
||||
path: "/v1/messages",
|
||||
provider: "zhipu",
|
||||
requestId: "cache-ratio-total-omits-cache",
|
||||
statusCode: 200,
|
||||
usage: {
|
||||
cacheReadTokens: 90,
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
totalTokens: 15
|
||||
}
|
||||
});
|
||||
|
||||
const stats = await store.getStats("30d");
|
||||
assert.equal(stats.totals.totalTokens, 105);
|
||||
assert.equal(stats.totals.cacheRatio, 0.9);
|
||||
assert.equal(stats.models[0]?.cacheRatio, 0.9);
|
||||
assert.equal(stats.recentRequests[0]?.totalTokens, 105);
|
||||
assert.equal(stats.recentRequests[0]?.cacheRatio, 0.9);
|
||||
} finally {
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("UsageStore excludes proxy rows by default and includes them on request", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-proxy-test-"));
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
import {
|
||||
fennoProviderPreset
|
||||
} from "@ccr/core/providers/presets/fenno/index.ts";
|
||||
import {
|
||||
infistarAiProviderPreset
|
||||
} from "@ccr/core/providers/presets/infistar-ai/index.ts";
|
||||
import {
|
||||
moonshotChinaProviderPreset,
|
||||
moonshotGlobalProviderPreset
|
||||
@@ -108,6 +111,16 @@ test("sponsor provider presets expose requested endpoints and protocols", () =>
|
||||
assert.deepEqual(unity2ProviderPreset.endpoints[0]?.protocols, [
|
||||
"openai_chat_completions"
|
||||
]);
|
||||
|
||||
assert.equal(providerPresets.find((preset) => preset.id === "infistar-ai"), infistarAiProviderPreset);
|
||||
assert.equal(infistarAiProviderPreset.name, "无限星河");
|
||||
assert.equal(infistarAiProviderPreset.websiteUrl, "https://infistar.ai");
|
||||
assert.deepEqual(infistarAiProviderPreset.defaultModels, ["gpt-4o"]);
|
||||
assert.equal(providerPresetMatchesBaseUrl(infistarAiProviderPreset, "https://infistar.ai/v1/models"), true);
|
||||
assert.equal(providerPresetMatchesBaseUrl(infistarAiProviderPreset, "https://api.infistar.ai/v1"), false);
|
||||
assert.deepEqual(infistarAiProviderPreset.endpoints[0]?.protocols, [
|
||||
"openai_chat_completions"
|
||||
]);
|
||||
});
|
||||
|
||||
test("NVIDIA preset exposes the hosted NIM OpenAI-compatible endpoint", () => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -49,6 +49,8 @@ import code0ProviderIconUrl from "@/assets/provider-icons/code0.png";
|
||||
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
|
||||
import fennoProviderIconUrl from "@/assets/provider-icons/fenno.jpg";
|
||||
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
|
||||
import infistarAiProviderIconUrl from "@/assets/provider-icons/infistar-ai.jpg";
|
||||
import minimaxProviderIconUrl from "@/assets/provider-icons/minimax.ico";
|
||||
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
|
||||
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
|
||||
import nvidiaProviderIconUrl from "@/assets/provider-icons/nvidia.svg";
|
||||
@@ -359,7 +361,10 @@ export const providerPresetIconUrls: Record<string, string> = {
|
||||
deepseek: deepseekProviderIconUrl,
|
||||
fenno: fennoProviderIconUrl,
|
||||
gemini: geminiProviderIconUrl,
|
||||
"infistar-ai": infistarAiProviderIconUrl,
|
||||
"kimi-coding": moonshotProviderIconUrl,
|
||||
"minimax-cn": minimaxProviderIconUrl,
|
||||
"minimax-global": minimaxProviderIconUrl,
|
||||
mistral: mistralProviderIconUrl,
|
||||
moonshot: moonshotProviderIconUrl,
|
||||
"moonshot-global": moonshotProviderIconUrl,
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { newApiKeyUsageAccountConfig } from "@ccr/core/providers/new-api.ts";
|
||||
import { geminiProviderPreset } from "@ccr/core/providers/presets/gemini/index.ts";
|
||||
import { minimaxChinaProviderPreset } from "@ccr/core/providers/presets/minimax/index.ts";
|
||||
import { moonshotGlobalProviderPreset } from "@ccr/core/providers/presets/moonshot/index.ts";
|
||||
import { qiniuAiProviderPreset } from "@ccr/core/providers/presets/qiniu-ai/index.ts";
|
||||
import { AddProviderDialog, AddProviderForm, ProvidersView, uniqueProviderProbeProtocolRows } from "@ccr/ui/pages/home/components/providers.tsx";
|
||||
@@ -981,7 +982,7 @@ test("provider deep link config saves anthropic probe prefix as capability URL",
|
||||
});
|
||||
|
||||
test("provider display icon prefers custom icons and falls back to preset icons", () => {
|
||||
setProviderPresets([geminiProviderPreset]);
|
||||
setProviderPresets([geminiProviderPreset, minimaxChinaProviderPreset]);
|
||||
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
@@ -1002,6 +1003,15 @@ test("provider display icon prefers custom icons and falls back to preset icons"
|
||||
}),
|
||||
providerPresetIconUrls.gemini
|
||||
);
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
api_base_url: "https://api.minimaxi.com/v1",
|
||||
models: [],
|
||||
name: "MiniMax (China)",
|
||||
type: "openai_chat_completions"
|
||||
}),
|
||||
providerPresetIconUrls["minimax-cn"]
|
||||
);
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
api_base_url: "https://cli-chat-proxy.grok.com/v1",
|
||||
|
||||
Reference in New Issue
Block a user