mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
Merge branch 'main' of github.com:musistudio/claude-code-router into dev/3.1
This commit is contained in:
@@ -1022,12 +1022,7 @@ async function writeDurableJsonFile(
|
||||
const filePath = join(directory, fileName);
|
||||
const temporaryPath = join(directory, `.${fileName}-${randomUUID()}.tmp`);
|
||||
await writeFile(temporaryPath, JSON.stringify(value));
|
||||
const temporaryFile = await open(temporaryPath, "r");
|
||||
try {
|
||||
await temporaryFile.sync();
|
||||
} finally {
|
||||
await temporaryFile.close();
|
||||
}
|
||||
await syncFile(temporaryPath);
|
||||
await rename(temporaryPath, filePath);
|
||||
await syncDirectory(directory);
|
||||
}
|
||||
@@ -1049,14 +1044,7 @@ async function syncRawTracePartFiles(
|
||||
throw new Error(`Raw trace part is shorter than its manifest: ${filePath}`);
|
||||
}
|
||||
if (syncPartFile) await syncPartFile(filePath);
|
||||
else {
|
||||
const handle = await open(filePath, "r");
|
||||
try {
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
else await syncFile(filePath);
|
||||
}
|
||||
await syncDirectory(directory);
|
||||
}
|
||||
@@ -1096,14 +1084,47 @@ async function readRawTraceDeliveryState(
|
||||
};
|
||||
}
|
||||
|
||||
// Windows maps fsync to FlushFileBuffers, which requires the handle to carry
|
||||
// write access: flushing a handle opened read-only fails with EPERM. Every
|
||||
// file flushed here was just written by this process, so reopening it as "r+"
|
||||
// keeps the barrier real on Windows instead of trading it for a tolerated
|
||||
// error. POSIX accepts a read-write handle for fsync just as readily.
|
||||
async function syncFile(filePath: string): Promise<void> {
|
||||
const handle = await open(filePath, "r+");
|
||||
try {
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Flushing a directory entry is a best-effort durability barrier: POSIX never
|
||||
// required it to succeed, and platforms are free to reject the request. Linux
|
||||
// and macOS answer with EINVAL/ENOTSUP/EISDIR depending on the filesystem;
|
||||
// Windows rejects fsync on a directory handle with EPERM. All of those mean
|
||||
// "this platform will not flush a directory", not "this spool is broken", so
|
||||
// the rename that precedes the call still stands and we continue. Any other
|
||||
// code (EACCES, EIO, ENOSPC, EBADF, ...) is a real storage failure and must
|
||||
// keep propagating, so the caller can refuse the bundle instead of reporting a
|
||||
// durability guarantee it never obtained.
|
||||
const toleratedDirectorySyncErrorCodes = new Set(["EINVAL", "EISDIR", "ENOTSUP", "EPERM"]);
|
||||
|
||||
function isToleratedDirectorySyncError(error: unknown): boolean {
|
||||
const code = nodeErrorCode(error);
|
||||
return code !== undefined && toleratedDirectorySyncErrorCodes.has(code);
|
||||
}
|
||||
|
||||
export function isToleratedDirectorySyncErrorForTest(error: unknown): boolean {
|
||||
return isToleratedDirectorySyncError(error);
|
||||
}
|
||||
|
||||
async function syncDirectory(directory: string): Promise<void> {
|
||||
let handle;
|
||||
try {
|
||||
handle = await open(directory, "r");
|
||||
await handle.sync();
|
||||
} catch (error) {
|
||||
const code = nodeErrorCode(error);
|
||||
if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR") throw error;
|
||||
if (!isToleratedDirectorySyncError(error)) throw error;
|
||||
} finally {
|
||||
await handle?.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ import { runApiProviderPreset } from "@ccr/core/providers/presets/runapi/index";
|
||||
import { siliconFlowProviderPreset } from "@ccr/core/providers/presets/siliconflow/index";
|
||||
import { teamoRouterProviderPreset } from "@ccr/core/providers/presets/teamorouter/index";
|
||||
import { unity2ProviderPreset } from "@ccr/core/providers/presets/unity2/index";
|
||||
import {
|
||||
xiaomiMimoProviderPreset,
|
||||
xiaomiMimoTokenPlanChinaProviderPreset,
|
||||
xiaomiMimoTokenPlanEuropeProviderPreset,
|
||||
xiaomiMimoTokenPlanSingaporeProviderPreset
|
||||
} from "@ccr/core/providers/presets/xiaomi/index";
|
||||
import { zaiGlobalCodingProviderPreset } from "@ccr/core/providers/presets/zai-global-coding/index";
|
||||
import { zaiGlobalGeneralProviderPreset } from "@ccr/core/providers/presets/zai-global-general/index";
|
||||
import { zhipuCnCodingProviderPreset } from "@ccr/core/providers/presets/zhipu-cn-coding/index";
|
||||
@@ -40,6 +46,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
openRouterProviderPreset,
|
||||
nvidiaProviderPreset,
|
||||
deepSeekProviderPreset,
|
||||
xiaomiMimoProviderPreset,
|
||||
xiaomiMimoTokenPlanChinaProviderPreset,
|
||||
xiaomiMimoTokenPlanSingaporeProviderPreset,
|
||||
xiaomiMimoTokenPlanEuropeProviderPreset,
|
||||
kimiCodingProviderPreset,
|
||||
zhipuCnCodingProviderPreset,
|
||||
zhipuCnGeneralProviderPreset,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const xiaomiMimoDefaultModels = ["mimo-v2.5-pro", "mimo-v2.5"];
|
||||
const xiaomiMimoWebsiteUrl = "https://platform.xiaomimimo.com/";
|
||||
|
||||
function createXiaomiMimoProviderPreset(input: {
|
||||
aliases: string[];
|
||||
anthropicBaseUrl: string;
|
||||
id: string;
|
||||
name: string;
|
||||
openAiBaseUrl: string;
|
||||
}): ProviderPreset {
|
||||
return {
|
||||
account: defaultProviderAccountConfig,
|
||||
aliases: input.aliases,
|
||||
defaultModels: xiaomiMimoDefaultModels,
|
||||
endpoints: [
|
||||
{
|
||||
baseUrl: input.openAiBaseUrl,
|
||||
label: "OpenAI",
|
||||
protocols: ["openai_responses", "openai_chat_completions"]
|
||||
},
|
||||
{
|
||||
baseUrl: input.anthropicBaseUrl,
|
||||
label: "Anthropic",
|
||||
protocols: ["anthropic_messages"]
|
||||
}
|
||||
],
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
websiteUrl: xiaomiMimoWebsiteUrl
|
||||
};
|
||||
}
|
||||
|
||||
export const xiaomiMimoProviderPreset = createXiaomiMimoProviderPreset({
|
||||
aliases: ["mimo", "xiaomi", "xiaomi mimo", "小米", "小米 mimo"],
|
||||
anthropicBaseUrl: "https://api.xiaomimimo.com/anthropic",
|
||||
id: "xiaomi",
|
||||
name: "Xiaomi MiMo",
|
||||
openAiBaseUrl: "https://api.xiaomimimo.com/v1"
|
||||
});
|
||||
|
||||
export const xiaomiMimoTokenPlanChinaProviderPreset = createXiaomiMimoProviderPreset({
|
||||
aliases: ["mimo token plan china", "xiaomi token plan china", "xiaomi token plan cn"],
|
||||
anthropicBaseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
id: "xiaomi-token-plan-cn",
|
||||
name: "Xiaomi Token Plan (China)",
|
||||
openAiBaseUrl: "https://token-plan-cn.xiaomimimo.com/v1"
|
||||
});
|
||||
|
||||
export const xiaomiMimoTokenPlanSingaporeProviderPreset = createXiaomiMimoProviderPreset({
|
||||
aliases: ["mimo token plan singapore", "xiaomi token plan singapore", "xiaomi token plan sgp"],
|
||||
anthropicBaseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic",
|
||||
id: "xiaomi-token-plan-sgp",
|
||||
name: "Xiaomi Token Plan (Singapore)",
|
||||
openAiBaseUrl: "https://token-plan-sgp.xiaomimimo.com/v1"
|
||||
});
|
||||
|
||||
export const xiaomiMimoTokenPlanEuropeProviderPreset = createXiaomiMimoProviderPreset({
|
||||
aliases: ["mimo token plan europe", "xiaomi token plan ams", "xiaomi token plan europe"],
|
||||
anthropicBaseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic",
|
||||
id: "xiaomi-token-plan-ams",
|
||||
name: "Xiaomi Token Plan (Europe)",
|
||||
openAiBaseUrl: "https://token-plan-ams.xiaomimimo.com/v1"
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
applyRawTraceRequestLogPolicy,
|
||||
buildRawTraceConfig,
|
||||
createBodySampler,
|
||||
isToleratedDirectorySyncErrorForTest,
|
||||
readRawTraceRequestLogBundle,
|
||||
RawTraceSynchronizer
|
||||
} from "@ccr/core/observability/raw-trace-sync.ts";
|
||||
@@ -874,6 +875,48 @@ test("fallback raw bundles keep unique bundle ids while sharing the logical requ
|
||||
}
|
||||
});
|
||||
|
||||
// A real directory handle cannot drive these cases: on Linux and macOS the
|
||||
// fsync either succeeds or fails with a code that depends on the filesystem
|
||||
// under the temp directory, and on Windows it always fails. The predicate is
|
||||
// the only place where the platform-independent contract can be pinned down.
|
||||
test("raw trace tolerates every directory fsync rejection a platform may return", () => {
|
||||
// Node rejects fsync on a directory handle with EPERM on Windows
|
||||
// (errno -4048, verified on Node 24.14.1 win32) and with EINVAL, ENOTSUP or
|
||||
// EISDIR across POSIX filesystems. Flushing a directory is a best-effort
|
||||
// barrier, so none of these may fail the spool.
|
||||
for (const code of ["EINVAL", "EISDIR", "ENOTSUP", "EPERM"]) {
|
||||
assert.equal(
|
||||
isToleratedDirectorySyncErrorForTest(directorySyncError(code)),
|
||||
true,
|
||||
`expected a directory fsync ${code} to be tolerated`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("raw trace still propagates real storage failures from a directory fsync", () => {
|
||||
// The tolerated set must stay a set. If it ever degrades into a blanket
|
||||
// catch, an unwritable or failing spool would be acknowledged as durable.
|
||||
for (const code of ["EACCES", "EIO", "ENOSPC", "EROFS", "EBADF", "EMFILE"]) {
|
||||
assert.equal(
|
||||
isToleratedDirectorySyncErrorForTest(directorySyncError(code)),
|
||||
false,
|
||||
`expected a directory fsync ${code} to keep propagating`
|
||||
);
|
||||
}
|
||||
assert.equal(isToleratedDirectorySyncErrorForTest(new Error("fsync failed")), false);
|
||||
assert.equal(isToleratedDirectorySyncErrorForTest(directorySyncError("")), false);
|
||||
assert.equal(isToleratedDirectorySyncErrorForTest(undefined), false);
|
||||
});
|
||||
|
||||
function directorySyncError(code) {
|
||||
// Shaped like the rejection Node produces for fsync on a directory handle,
|
||||
// e.g. "EPERM: operation not permitted, fsync" on Windows.
|
||||
const error = new Error(`${code}: directory fsync rejected, fsync`);
|
||||
error.code = code;
|
||||
error.syscall = "fsync";
|
||||
return error;
|
||||
}
|
||||
|
||||
function createConfig() {
|
||||
const config = createDefaultAppConfig();
|
||||
config.observability.requestLogs = true;
|
||||
|
||||
@@ -37,6 +37,12 @@ import {
|
||||
import {
|
||||
unity2ProviderPreset
|
||||
} from "@ccr/core/providers/presets/unity2/index.ts";
|
||||
import {
|
||||
xiaomiMimoProviderPreset,
|
||||
xiaomiMimoTokenPlanChinaProviderPreset,
|
||||
xiaomiMimoTokenPlanEuropeProviderPreset,
|
||||
xiaomiMimoTokenPlanSingaporeProviderPreset
|
||||
} from "@ccr/core/providers/presets/xiaomi/index.ts";
|
||||
|
||||
const openAiPreset = {
|
||||
aliases: ["OpenAI", "ChatGPT"],
|
||||
@@ -140,6 +146,50 @@ test("NVIDIA preset exposes the hosted NIM OpenAI-compatible endpoint", () => {
|
||||
assert.equal(providerPresetMatchesBaseUrl(nvidiaProviderPreset, "https://build.nvidia.com/models"), false);
|
||||
});
|
||||
|
||||
test("Xiaomi MiMo presets expose official Responses, Chat, and Anthropic endpoints", () => {
|
||||
const cases = [
|
||||
[
|
||||
xiaomiMimoProviderPreset,
|
||||
"https://api.xiaomimimo.com/v1",
|
||||
"https://api.xiaomimimo.com/anthropic"
|
||||
],
|
||||
[
|
||||
xiaomiMimoTokenPlanChinaProviderPreset,
|
||||
"https://token-plan-cn.xiaomimimo.com/v1",
|
||||
"https://token-plan-cn.xiaomimimo.com/anthropic"
|
||||
],
|
||||
[
|
||||
xiaomiMimoTokenPlanSingaporeProviderPreset,
|
||||
"https://token-plan-sgp.xiaomimimo.com/v1",
|
||||
"https://token-plan-sgp.xiaomimimo.com/anthropic"
|
||||
],
|
||||
[
|
||||
xiaomiMimoTokenPlanEuropeProviderPreset,
|
||||
"https://token-plan-ams.xiaomimimo.com/v1",
|
||||
"https://token-plan-ams.xiaomimimo.com/anthropic"
|
||||
]
|
||||
];
|
||||
|
||||
for (const [preset, openAiBaseUrl, anthropicBaseUrl] of cases) {
|
||||
assert.equal(providerPresets.find((candidate) => candidate.id === preset.id), preset);
|
||||
assert.deepEqual(preset.defaultModels, ["mimo-v2.5-pro", "mimo-v2.5"]);
|
||||
assert.deepEqual(preset.endpoints, [
|
||||
{
|
||||
baseUrl: openAiBaseUrl,
|
||||
label: "OpenAI",
|
||||
protocols: ["openai_responses", "openai_chat_completions"]
|
||||
},
|
||||
{
|
||||
baseUrl: anthropicBaseUrl,
|
||||
label: "Anthropic",
|
||||
protocols: ["anthropic_messages"]
|
||||
}
|
||||
]);
|
||||
assert.equal(providerPresetMatchesBaseUrl(preset, `${openAiBaseUrl}/responses`), true);
|
||||
assert.equal(providerPresetMatchesBaseUrl(preset, `${anthropicBaseUrl}/v1/messages`), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("NVIDIA preset ignores stale Responses detection and converts Codex requests to Chat Completions", () => {
|
||||
const provider = {
|
||||
api_base_url: "https://integrate.api.nvidia.com/v1",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -2128,6 +2128,10 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
||||
"Kimi API (China)": "Kimi API(国内)",
|
||||
"Kimi API (Global)": "Kimi API(海外)",
|
||||
"Kimi Code - Coding Plan": "Kimi Code - Coding Plan",
|
||||
"Xiaomi MiMo": "小米 MiMo",
|
||||
"Xiaomi Token Plan (China)": "MiMo 套餐(国内)",
|
||||
"Xiaomi Token Plan (Europe)": "MiMo 套餐(欧洲)",
|
||||
"Xiaomi Token Plan (Singapore)": "MiMo 套餐(新加坡)",
|
||||
"Zhipu AI (China)": "智谱 AI (国内)",
|
||||
"Zhipu AI (China) - Coding Plan": "智谱 AI (国内) - Coding Plan",
|
||||
"Zhipu AI (China) - General Endpoint": "智谱 AI (国内) - 通用端点",
|
||||
|
||||
@@ -62,6 +62,7 @@ import runapiProviderIconUrl from "@/assets/provider-icons/runapi.jpg";
|
||||
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
|
||||
import teamorouterProviderIconUrl from "@/assets/provider-icons/teamorouter.png";
|
||||
import unity2ProviderIconUrl from "@/assets/provider-icons/unity2.jpg";
|
||||
import xiaomiMimoProviderIconUrl from "@/assets/provider-icons/xiaomi-mimo.png";
|
||||
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
|
||||
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
|
||||
import zhipuCnCodingProviderIconUrl from "@/assets/provider-icons/zhipu-cn-coding.png";
|
||||
@@ -389,6 +390,10 @@ export const providerPresetIconUrls: Record<string, string> = {
|
||||
siliconflow: siliconflowProviderIconUrl,
|
||||
teamorouter: teamorouterProviderIconUrl,
|
||||
unity2: unity2ProviderIconUrl,
|
||||
xiaomi: xiaomiMimoProviderIconUrl,
|
||||
"xiaomi-token-plan-ams": xiaomiMimoProviderIconUrl,
|
||||
"xiaomi-token-plan-cn": xiaomiMimoProviderIconUrl,
|
||||
"xiaomi-token-plan-sgp": xiaomiMimoProviderIconUrl,
|
||||
"zai-global-coding": zaiGlobalCodingProviderIconUrl,
|
||||
"zai-global-general": zaiGlobalGeneralProviderIconUrl,
|
||||
"zhipu-cn-coding": zhipuCnCodingProviderIconUrl,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { geminiProviderPreset } from "@ccr/core/providers/presets/gemini/index.t
|
||||
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 { xiaomiMimoProviderPreset } from "@ccr/core/providers/presets/xiaomi/index.ts";
|
||||
import { AddProviderDialog, AddProviderForm, ProviderConnectivityCheckDialog, ProvidersView, uniqueProviderProbeProtocolRows } from "@ccr/ui/pages/home/components/providers.tsx";
|
||||
import {
|
||||
applyProviderProbeResult,
|
||||
@@ -1266,7 +1267,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, minimaxChinaProviderPreset]);
|
||||
setProviderPresets([geminiProviderPreset, minimaxChinaProviderPreset, xiaomiMimoProviderPreset]);
|
||||
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
@@ -1296,6 +1297,18 @@ test("provider display icon prefers custom icons and falls back to preset icons"
|
||||
}),
|
||||
providerPresetIconUrls["minimax-cn"]
|
||||
);
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
api_base_url: "https://api.xiaomimimo.com/v1",
|
||||
models: [],
|
||||
name: "Xiaomi MiMo",
|
||||
type: "openai_responses"
|
||||
}),
|
||||
providerPresetIconUrls.xiaomi
|
||||
);
|
||||
assert.equal(providerPresetIconUrls["xiaomi-token-plan-cn"], providerPresetIconUrls.xiaomi);
|
||||
assert.equal(providerPresetIconUrls["xiaomi-token-plan-sgp"], providerPresetIconUrls.xiaomi);
|
||||
assert.equal(providerPresetIconUrls["xiaomi-token-plan-ams"], providerPresetIconUrls.xiaomi);
|
||||
assert.equal(
|
||||
providerDisplayIcon({
|
||||
api_base_url: "https://cli-chat-proxy.grok.com/v1",
|
||||
|
||||
Reference in New Issue
Block a user