diff --git a/packages/core/src/observability/raw-trace-sync.ts b/packages/core/src/observability/raw-trace-sync.ts index 16a8ad34..02b5f0ed 100644 --- a/packages/core/src/observability/raw-trace-sync.ts +++ b/packages/core/src/observability/raw-trace-sync.ts @@ -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 { + 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 { 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); } diff --git a/packages/core/src/providers/presets/index.ts b/packages/core/src/providers/presets/index.ts index 2663f753..f1cf4ba6 100644 --- a/packages/core/src/providers/presets/index.ts +++ b/packages/core/src/providers/presets/index.ts @@ -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, diff --git a/packages/core/src/providers/presets/xiaomi/index.ts b/packages/core/src/providers/presets/xiaomi/index.ts new file mode 100644 index 00000000..4e897207 --- /dev/null +++ b/packages/core/src/providers/presets/xiaomi/index.ts @@ -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" +}); diff --git a/packages/core/test/unit/observability/raw-trace-sync.test.mjs b/packages/core/test/unit/observability/raw-trace-sync.test.mjs index 21786925..0ff38e1b 100644 --- a/packages/core/test/unit/observability/raw-trace-sync.test.mjs +++ b/packages/core/test/unit/observability/raw-trace-sync.test.mjs @@ -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; diff --git a/packages/core/test/unit/providers/provider-preset-utils.test.mjs b/packages/core/test/unit/providers/provider-preset-utils.test.mjs index 0cc9535a..daea37e5 100644 --- a/packages/core/test/unit/providers/provider-preset-utils.test.mjs +++ b/packages/core/test/unit/providers/provider-preset-utils.test.mjs @@ -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", diff --git a/packages/ui/src/assets/provider-icons/xiaomi-mimo.png b/packages/ui/src/assets/provider-icons/xiaomi-mimo.png new file mode 100644 index 00000000..a90545bf Binary files /dev/null and b/packages/ui/src/assets/provider-icons/xiaomi-mimo.png differ diff --git a/packages/ui/src/pages/home/shared/i18n.tsx b/packages/ui/src/pages/home/shared/i18n.tsx index 1df41ebc..51bb6c4f 100644 --- a/packages/ui/src/pages/home/shared/i18n.tsx +++ b/packages/ui/src/pages/home/shared/i18n.tsx @@ -2128,6 +2128,10 @@ export const appCopy: Record = { "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 (国内) - 通用端点", diff --git a/packages/ui/src/pages/home/shared/options.ts b/packages/ui/src/pages/home/shared/options.ts index 7fbebf6c..5a986ee3 100644 --- a/packages/ui/src/pages/home/shared/options.ts +++ b/packages/ui/src/pages/home/shared/options.ts @@ -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 = { 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, diff --git a/packages/ui/test/integration/providers.test.ts b/packages/ui/test/integration/providers.test.ts index 4d2dacd6..00c78e65 100644 --- a/packages/ui/test/integration/providers.test.ts +++ b/packages/ui/test/integration/providers.test.ts @@ -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",