1 line
1.0 MiB
Plaintext
1 line
1.0 MiB
Plaintext
{"repo": "axapp/gettokens", "files": {"frontend/src/features/accounts/model/vendorPresets.ts": {"6a6556121411b7445b1e0e28d0b7af6920236b59": {"t": 1785595971.29112, "n": 22376, "h": "9ac511bce3e2c452", "c": "import type { ApiFormat } from \"../../../types\";\n\nexport type VendorCredentialFieldID = string;\n\nexport interface VendorCredentialField {\n id: VendorCredentialFieldID;\n label: string;\n placeholder?: string;\n help?: string;\n secret?: boolean;\n variableName?: string;\n scope: \"curl\" | \"model_fetch\";\n}\n\nexport interface VendorPreset {\n id: string;\n name: string;\n apiFormat: ApiFormat;\n supportedFormats: ApiFormat[];\n baseUrl: string;\n formatBaseUrls?: Partial<Record<ApiFormat, string>>;\n apiKeyPlaceholder: string;\n modelSuggestions: string[];\n category:\n | \"official\"\n | \"cn_official\"\n | \"aggregator\"\n | \"third_party\"\n | \"cloud_provider\";\n icon?: string;\n iconColor?: string;\n websiteUrl?: string;\n quotaCurlTemplate?: string;\n billingCurlTemplate?: string;\n quotaSetupGuide?: string[];\n billingSetupGuide?: string[];\n notes?: string;\n consoleUrl?: string;\n variantLabel?: string;\n variantDescription?: string;\n modelFetchBaseUrl?: string;\n modelFetchApiKeyPlaceholder?: string;\n requiresModelFetchApiKey?: boolean;\n credentialFields?: VendorCredentialField[];\n}\n\nexport function getFormatBaseUrl(\n preset: VendorPreset,\n format: ApiFormat,\n): string {\n return preset.formatBaseUrls?.[format] ?? preset.baseUrl;\n}\n\n\nconst XIAOMI_MIMO_COOKIE_GUIDE = [\n \"登录 platform.xiaomimimo.com,并保持当前浏览器会话有效。\",\n \"打开目标页面:余额进入 console/balance,Token Plan 用量进入 console/plan-manage。\",\n \"打开浏览器开发者工具 Network,刷新页面或点击页面上的查询/刷新按钮。\",\n \"找到 /api/v1/balance 或 /api/v1/tokenPlan/usage 请求,复制 Request Headers 里的 Cookie 值。\",\n \"在账号配置里的“平台 Cookie”输入框粘贴 Cookie;cURL 模板保持 {{platformCookie}} 占位符。\",\n \"Cookie 属于登录凭证且可能过期,请仅保存在本机可信环境,失效后重新复制。\",\n];\n\nconst XIAOMI_MIMO_PLATFORM_COOKIE_FIELD: VendorCredentialField = {\n id: \"platformCookie\",\n label: \"平台 Cookie\",\n placeholder: \"用于 {{platformCookie}},不要包含 Cookie: 前缀\",\n help: \"仅用于平台余额 / 用量 cURL 的 {{platformCookie}} 变量,不参与 agent 对话。\",\n secret: true,\n variableName: \"platformCookie\",\n scope: \"curl\",\n};\n\nconst XIAOMI_MIMO_MODEL_FETCH_FIELDS: VendorCredentialField[] = [\n {\n id: \"modelFetchApiKey\",\n label: \"模型拉取 API Key(sk)\",\n placeholder: \"sk-...\",\n help: \"仅用于 /models 拉取模型列表,不参与 agent 对话,也不用于 cURL。\",\n secret: true,\n scope: \"model_fetch\",\n },\n {\n id: \"modelFetchBaseUrl\",\n label: \"Model Fetch Base URL\",\n placeholder: \"https://api.xiaomimimo.com/v1\",\n help: \"模型列表拉取使用的 API Base URL;agent 对话继续使用上方 Base URL。\",\n scope: \"model_fetch\",\n },\n];\n\nexport const vendorPresets: VendorPreset[] = [\n // ==================== Official ====================\n {\n id: \"anthropic\",\n name: \"Anthropic (Official)\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://api.anthropic.com\",\n apiKeyPlaceholder: \"sk-ant-...\",\n modelSuggestions: [\n \"claude-sonnet-4-6\",\n \"claude-opus-4-7\",\n \"claude-haiku-4-5\",\n ],\n category: \"official\",\n icon: \"anthropic\",\n iconColor: \"var(--color-provider-anthropic)\",\n },\n // ==================== Chinese Vendors (Anthropic-compatible) ====================\n {\n id: \"deepseek\",\n name: \"DeepSeek\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.deepseek.com/v1\",\n quotaCurlTemplate:\n 'curl -sS \"https://api.deepseek.com/user/balance\" -H \"Authorization: Bearer {{apiKey}}\"',\n billingCurlTemplate:\n 'curl -sS \"https://api.deepseek.com/user/balance\" -H \"Authorization: Bearer {{apiKey}}\"',\n formatBaseUrls: {\n anthropic: \"https://api.deepseek.com/anthropic\",\n openai_chat: \"https://api.deepseek.com/v1\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"deepseek-v4-pro[1m]\", \"deepseek-v4-flash\", \"deepseek-v4-pro\"],\n category: \"cn_official\",\n icon: \"deepseek\",\n iconColor: \"var(--color-provider-deepseek)\",\n websiteUrl: \"https://platform.deepseek.com\",\n },\n {\n id: \"zhipu\",\n name: \"Zhipu GLM\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://open.bigmodel.cn/api/paas/v4\",\n formatBaseUrls: {\n anthropic: \"https://open.bigmodel.cn/api/anthropic\",\n openai_chat: \"https://open.bigmodel.cn/api/paas/v4\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"glm-5.2[1m]\", \"glm-5.2\", \"glm-4.5-air\", \"glm-5\", \"glm-4.7\"],\n category: \"cn_official\",\n icon: \"zhipu\",\n iconColor: \"var(--color-provider-zhipu)\",\n websiteUrl: \"https://open.bigmodel.cn\",\n },\n {\n id: \"kimi\",\n name: \"Kimi (Moonshot)\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.moonshot.cn/v1\",\n formatBaseUrls: {\n anthropic: \"https://api.moonshot.cn/anthropic\",\n openai_chat: \"https://api.moonshot.cn/v1\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"kimi-k2.7-code\", \"kimi-k2.6\", \"kimi-k2.5\"],\n category: \"cn_official\",\n icon: \"kimi\",\n iconColor: \"var(--color-provider-kimi)\",\n websiteUrl: \"https://platform.moonshot.cn\",\n },\n {\n id: \"stepfun\",\n name: \"StepFun\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.stepfun.com/v1\",\n formatBaseUrls: {\n openai_chat: \"https://api.stepfun.com/v1\",\n anthropic: \"https://api.stepfun.com/step_plan\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"step-3.7-flash\", \"step-3.6\", \"step-3.5-flash-2603\"],\n category: \"cn_official\",\n icon: \"stepfun\",\n iconColor: \"var(--color-provider-stepfun)\",\n websiteUrl: \"https://platform.stepfun.com\",\n },\n {\n id: \"bailian\",\n name: \"Bailian (DashScope)\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n formatBaseUrls: {\n openai_chat: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n anthropic: \"https://dashscope.aliyuncs.com/apps/anthropic\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"qwen3.5-plus\", \"qwen3.5-flash\"],\n category: \"cn_official\",\n icon: \"bailian\",\n iconColor: \"var(--color-provider-bailian)\",\n websiteUrl: \"https://bailian.console.aliyun.com\",\n },\n {\n id: \"minimax\",\n name: \"MiniMax\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.minimaxi.com/v1\",\n formatBaseUrls: {\n openai_chat: \"https://api.minimaxi.com/v1\",\n anthropic: \"https://api.minimaxi.com/anthropic\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"MiniMax-M3\", \"MiniMax-M2.7\"],\n category: \"cn_official\",\n icon: \"minimax\",\n iconColor: \"var(--color-provider-minimax)\",\n websiteUrl: \"https://platform.minimaxi.com\",\n },\n {\n id: \"doubao\",\n name: \"Doubao (Ark)\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n formatBaseUrls: {\n openai_chat: \"https://ark.cn-beijing.volces.com/api/v3\",\n anthropic: \"https://ark.cn-beijing.volces.com/api/coding\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"ark-code-latest\", \"doubao-seed-2-0-code-preview-latest\"],\n category: \"cn_official\",\n icon: \"doubao\",\n iconColor: \"var(--color-provider-doubao)\",\n websiteUrl: \"https://www.volcengine.com/product/doubao\",\n },\n {\n id: \"longcat\",\n name: \"Longcat\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.longcat.chat/anthropic\",\n formatBaseUrls: {\n openai_chat: \"https://api.longcat.chat/anthropic\",\n anthropic: \"https://api.longcat.chat/anthropic\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"LongCat-Flash-Chat\"],\n category: \"cn_official\",\n icon: \"longcat\",\n iconColor: \"var(--color-provider-longcat)\",\n websiteUrl: \"https://longcat.chat/platform\",\n },\n {\n id: \"xiaomimimo\",\n name: \"Xiaomi MiMo API\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.xiaomimimo.com/v1\",\n billingCurlTemplate:\n 'curl -sS \"https://platform.xiaomimimo.com/api/v1/balance\" -H \"accept: */*\" -H \"accept-language: zh\" -H \"content-type: application/json\" -b \"{{platformCookie}}\" -H \"referer: https://platform.xiaomimimo.com/console/balance\" -H \"x-timezone: Asia/Shanghai\"',\n billingSetupGuide: XIAOMI_MIMO_COOKIE_GUIDE,\n credentialFields: [XIAOMI_MIMO_PLATFORM_COOKIE_FIELD],\n formatBaseUrls: {\n openai_chat: \"https://api.xiaomimimo.com/v1\",\n anthropic: \"https://api.xiaomimimo.com/anthropic\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\n \"mimo-v2.5-pro\",\n \"mimo-v2-pro\",\n \"mimo-v2.5\",\n \"mimo-v2-omni\",\n \"mimo-v2-flash\",\n \"mimo-v2.5-asr\",\n \"mimo-v2.5-tts\",\n \"mimo-v2.5-tts-voiceclone\",\n \"mimo-v2.5-tts-voicedesign\",\n \"mimo-v2-tts\",\n ],\n category: \"cn_official\",\n icon: \"xiaomimimo\",\n iconColor: \"var(--color-provider-xiaomimimo)\",\n websiteUrl: \"https://platform.xiaomimimo.com\",\n consoleUrl: \"https://platform.xiaomimimo.com/#/console/api-keys\",\n variantLabel: \"API / sk\",\n variantDescription: \"按量 API\",\n notes:\n \"按量 API 模式使用 sk-xxxxx;agent 对话和模型列表拉取均使用此 API Key。\",\n },\n {\n id: \"xiaomimimo-token-plan\",\n name: \"Xiaomi MiMo Token Plan\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://token-plan-cn.xiaomimimo.com/v1\",\n quotaCurlTemplate:\n 'curl -sS \"https://platform.xiaomimimo.com/api/v1/tokenPlan/usage\" -H \"accept: */*\" -H \"accept-language: zh\" -H \"content-type: application/json\" -b \"{{platformCookie}}\" -H \"referer: https://platform.xiaomimimo.com/console/plan-manage\" -H \"x-timezone: Asia/Shanghai\"',\n quotaSetupGuide: XIAOMI_MIMO_COOKIE_GUIDE,\n billingCurlTemplate:\n 'curl -sS \"https://platform.xiaomimimo.com/api/v1/balance\" -H \"accept: */*\" -H \"accept-language: zh\" -H \"content-type: application/json\" -b \"{{platformCookie}}\" -H \"referer: https://platform.xiaomimimo.com/console/balance\" -H \"x-timezone: Asia/Shanghai\"',\n billingSetupGuide: XIAOMI_MIMO_COOKIE_GUIDE,\n credentialFields: [XIAOMI_MIMO_PLATFORM_COOKIE_FIELD, ...XIAOMI_MIMO_MODEL_FETCH_FIELDS],\n formatBaseUrls: {\n openai_chat: \"https://token-plan-cn.xiaomimimo.com/v1\",\n anthropic: \"https://token-plan-cn.xiaomimimo.com/anthropic\",\n },\n apiKeyPlaceholder: \"tp-...\",\n modelSuggestions: [\n \"mimo-v2.5-pro\",\n \"mimo-v2-pro\",\n \"mimo-v2.5\",\n \"mimo-v2-omni\",\n \"mimo-v2-flash\",\n \"mimo-v2.5-asr\",\n \"mimo-v2.5-tts\",\n \"mimo-v2.5-tts-voiceclone\",\n \"mimo-v2.5-tts-voicedesign\",\n \"mimo-v2-tts\",\n ],\n category: \"cn_official\",\n icon: \"xiaomimimo\",\n iconColor: \"var(--color-provider-xiaomimimo)\",\n websiteUrl: \"https://platform.xiaomimimo.com\",\n consoleUrl: \"https://platform.xiaomimimo.com/#/console/plan-manage\",\n variantLabel: \"Token Plan / tp\",\n variantDescription: \"订阅套餐\",\n modelFetchBaseUrl: \"https://api.xiaomimimo.com/v1\",\n modelFetchApiKeyPlaceholder: \"sk-...\",\n requiresModelFetchApiKey: true,\n notes:\n \"订阅模式使用 tp-xxxxx 执行 agent 对话;额外的 sk-xxxxx 仅用于 /v1/models 拉取模型列表,不参与 agent 对话。Token Plan Base URL 以订阅页显示为准,可改为 cn / sgp / ams。\",\n },\n {\n id: \"bailing\",\n name: \"BaiLing\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.tbox.cn/api/anthropic\",\n formatBaseUrls: {\n openai_chat: \"https://api.tbox.cn/api/anthropic\",\n anthropic: \"https://api.tbox.cn/api/anthropic\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"Ling-2.5-1T\"],\n category: \"cn_official\",\n },\n // ==================== Aggregators / Third-party (Anthropic-compatible) ====================\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://openrouter.ai/api\",\n formatBaseUrls: {\n openai_chat: \"https://openrouter.ai/api/v1\",\n anthropic: \"https://openrouter.ai/api\",\n },\n apiKeyPlaceholder: \"sk-or-...\",\n modelSuggestions: [\n \"~openai/gpt-latest\",\n \"anthropic/claude-sonnet-4.6\",\n \"anthropic/claude-opus-4.7\",\n ],\n category: \"aggregator\",\n icon: \"openrouter\",\n iconColor: \"var(--color-provider-openrouter)\",\n websiteUrl: \"https://openrouter.ai\",\n },\n {\n id: \"siliconflow\",\n name: \"SiliconFlow\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.siliconflow.cn\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"Pro/MiniMaxAI/MiniMax-M2.7\"],\n category: \"aggregator\",\n icon: \"siliconflow\",\n iconColor: \"var(--color-provider-siliconflow)\",\n websiteUrl: \"https://siliconflow.cn\",\n },\n {\n id: \"aihubmix\",\n name: \"AiHubMix\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://aihubmix.com\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"aggregator\",\n icon: \"aihubmix\",\n iconColor: \"var(--color-provider-aihubmix)\",\n websiteUrl: \"https://aihubmix.com\",\n },\n {\n id: \"shengsuanyun\",\n name: \"Shengsuanyun\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://router.shengsuanyun.com/api\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"aggregator\",\n icon: \"shengsuanyun\",\n websiteUrl: \"https://www.shengsuanyun.com\",\n },\n {\n id: \"modelscope\",\n name: \"ModelScope\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api-inference.modelscope.cn\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"ZhipuAI/GLM-5\"],\n category: \"aggregator\",\n icon: \"modelscope\",\n iconColor: \"var(--color-provider-bailian)\",\n websiteUrl: \"https://modelscope.cn\",\n },\n {\n id: \"compshare\",\n name: \"Compshare\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.modelverse.cn\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"aggregator\",\n icon: \"ucloud\",\n iconColor: \"var(--color-provider-compshare)\",\n websiteUrl: \"https://www.compshare.cn\",\n },\n {\n id: \"therouter\",\n name: \"TheRouter\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.therouter.ai\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"anthropic/claude-sonnet-4.6\"],\n category: \"aggregator\",\n websiteUrl: \"https://therouter.ai\",\n },\n {\n id: \"novita\",\n name: \"Novita AI\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"anthropic\"],\n baseUrl: \"https://api.novita.ai/v1\",\n formatBaseUrls: {\n openai_chat: \"https://api.novita.ai/v1\",\n anthropic: \"https://api.novita.ai/anthropic\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"zai-org/glm-5\"],\n category: \"aggregator\",\n icon: \"novita\",\n websiteUrl: \"https://novita.ai\",\n },\n {\n id: \"sub2api\",\n name: \"sub2api\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"openai_responses\", \"anthropic\"],\n baseUrl: \"http://localhost:8080/v1\",\n formatBaseUrls: {\n openai_chat: \"http://localhost:8080/v1\",\n openai_responses: \"http://localhost:8080/v1\",\n anthropic: \"http://localhost:8080/antigravity\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"gpt-4.1\", \"gpt-5-codex\", \"claude-sonnet-4-6\"],\n category: \"aggregator\",\n websiteUrl: \"https://github.com/Wei-Shaw/sub2api\",\n notes: \"Self-hosted relay preset; edit each endpoint to match your deployment.\",\n },\n {\n id: \"new-api\",\n name: \"New API\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"openai_responses\", \"anthropic\"],\n baseUrl: \"http://localhost:3000/v1\",\n formatBaseUrls: {\n openai_chat: \"http://localhost:3000/v1\",\n openai_responses: \"http://localhost:3000/v1\",\n anthropic: \"http://localhost:3000\",\n },\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"gpt-4.1\", \"gpt-5-codex\", \"claude-sonnet-4-6\"],\n category: \"aggregator\",\n websiteUrl: \"https://github.com/QuantumNous/new-api\",\n notes: \"Self-hosted relay preset; edit each endpoint to match your deployment.\",\n },\n // ==================== OpenAI-Compatible Only ====================\n {\n id: \"openai\",\n name: \"OpenAI\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\", \"openai_responses\"],\n baseUrl: \"https://api.openai.com/v1\",\n quotaCurlTemplate:\n 'curl -sS \"https://api.openai.com/v1/organization/usage?limit=1\" -H \"Authorization: Bearer {{apiKey}}\"',\n billingCurlTemplate:\n 'curl -sS \"https://api.openai.com/v1/organization/costs?limit=30\" -H \"Authorization: Bearer {{apiKey}}\"',\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"gpt-5.4\", \"gpt-5.2\"],\n category: \"official\",\n icon: \"openai\",\n iconColor: \"var(--color-provider-openai)\",\n websiteUrl: \"https://platform.openai.com\",\n },\n {\n id: \"groq\",\n name: \"Groq\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\"],\n baseUrl: \"https://api.groq.com/openai/v1\",\n apiKeyPlaceholder: \"gsk_...\",\n modelSuggestions: [\"llama3-70b-8192\", \"llama3-8b-8192\"],\n category: \"aggregator\",\n icon: \"groq\",\n websiteUrl: \"https://groq.com\",\n },\n {\n id: \"together\",\n name: \"Together\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\"],\n baseUrl: \"https://api.together.xyz/v1\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo\"],\n category: \"aggregator\",\n websiteUrl: \"https://together.ai\",\n },\n {\n id: \"nvidia\",\n name: \"Nvidia NIM\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\"],\n baseUrl: \"https://integrate.api.nvidia.com\",\n apiKeyPlaceholder: \"nvapi-...\",\n modelSuggestions: [\"moonshotai/kimi-k2.5\"],\n category: \"aggregator\",\n icon: \"nvidia\",\n iconColor: \"var(--color-provider-nvidia)\",\n websiteUrl: \"https://build.nvidia.com\",\n },\n // ==================== Non-Anthropic / Need Format Conversion ====================\n {\n id: \"gemini\",\n name: \"Gemini Native\",\n apiFormat: \"gemini_native\",\n supportedFormats: [\"gemini_native\"],\n baseUrl: \"https://generativelanguage.googleapis.com\",\n apiKeyPlaceholder: \"AIza...\",\n modelSuggestions: [\"gemini-3.1-pro\", \"gemini-3-flash\"],\n category: \"official\",\n icon: \"gemini\",\n iconColor: \"var(--color-provider-gemini)\",\n websiteUrl: \"https://ai.google.dev\",\n },\n {\n id: \"copilot\",\n name: \"GitHub Copilot\",\n apiFormat: \"openai_chat\",\n supportedFormats: [\"openai_chat\"],\n baseUrl: \"https://api.githubcopilot.com\",\n apiKeyPlaceholder: \"(OAuth)\",\n modelSuggestions: [\"claude-sonnet-4.6\"],\n category: \"third_party\",\n icon: \"github\",\n iconColor: \"var(--color-provider-github)\",\n websiteUrl: \"https://github.com/features/copilot\",\n },\n // ==================== Cloud Providers ====================\n {\n id: \"aws-bedrock\",\n name: \"AWS Bedrock\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://bedrock-runtime.us-west-2.amazonaws.com\",\n apiKeyPlaceholder: \"AKIA...\",\n modelSuggestions: [\n \"global.anthropic.claude-sonnet-4-6\",\n \"global.anthropic.claude-opus-4-7\",\n ],\n category: \"cloud_provider\",\n icon: \"aws\",\n iconColor: \"var(--color-provider-aws)\",\n websiteUrl: \"https://aws.amazon.com/bedrock/\",\n },\n // ==================== Partners ====================\n {\n id: \"packycode\",\n name: \"PackyCode\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://www.packyapi.com\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"third_party\",\n icon: \"packycode\",\n websiteUrl: \"https://www.packyapi.com\",\n },\n {\n id: \"cubence\",\n name: \"Cubence\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://api.cubence.com\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"third_party\",\n icon: \"cubence\",\n websiteUrl: \"https://cubence.com\",\n },\n {\n id: \"aigocode\",\n name: \"AIGoCode\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://api.aigocode.com\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"third_party\",\n icon: \"aigocode\",\n iconColor: \"var(--color-provider-aigocode)\",\n websiteUrl: \"https://aigocode.com\",\n },\n {\n id: \"dmxapi\",\n name: \"DMXAPI\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://www.dmxapi.cn\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"aggregator\",\n websiteUrl: \"https://www.dmxapi.cn\",\n },\n {\n id: \"aicodemirror\",\n name: \"AICodeMirror\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://api.aicodemirror.com/api/claudecode\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [],\n category: \"third_party\",\n icon: \"aicodemirror\",\n websiteUrl: \"https://www.aicodemirror.com\",\n },\n {\n id: \"pipellm\",\n name: \"PIPELLM\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl: \"https://cc-api.pipellm.ai\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"claude-opus-4-7\", \"claude-sonnet-4-6\"],\n category: \"aggregator\",\n icon: \"pipellm\",\n websiteUrl: \"https://code.pipellm.ai\",\n },\n {\n id: \"katcoder\",\n name: \"KAT-Coder\",\n apiFormat: \"anthropic\",\n supportedFormats: [\"anthropic\"],\n baseUrl:\n \"https://vanchin.streamlake.ai/api/gateway/v1/endpoints/EP_ID/claude-code-proxy\",\n apiKeyPlaceholder: \"sk-...\",\n modelSuggestions: [\"KAT-Coder-Pro V1\", \"KAT-Coder-Air V1\"],\n category: \"cn_official\",\n icon: \"catcoder\",\n websiteUrl: \"https://console.streamlake.ai\",\n },\n];\n\nconst presetByID = new Map(vendorPresets.map((p) => [p.id, p]));\n\nexport function getVendorPreset(id: string): VendorPreset | undefined {\n return presetByID.get(id);\n}\n\nexport function getVendorPresets(): readonly VendorPreset[] {\n return vendorPresets;\n}\n"}}, "internal/wailsapp/accounts_test.go": {"6a6556121411b7445b1e0e28d0b7af6920236b59": {"t": 1785746068.2278643, "n": 32473, "h": "62ae3f1f9a52ca72", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\taccountsdomain \"github.com/linhay/gettokens/internal/accounts\"\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tRevision: 4,\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tif write.Title != \"Renamed Key\" {\n\t\t\t\t\t\tt.Fatalf(\"title = %q, want atomic label update\", write.Title)\n\t\t\t\t\t}\n\t\t\t\t\tif write.ExpectedRevision == nil || *write.ExpectedRevision != 4 {\n\t\t\t\t\t\tt.Fatalf(\"expected revision = %v, want 4\", write.ExpectedRevision)\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\tupdated := account\n\t\t\t\t\tupdated.Title = write.Title\n\t\t\t\t\tupdated.Revision = account.Revision + 1\n\t\t\t\t\tupdated.CodexAPIKey = write.CodexAPIKey\n\t\t\t\t\treturn testAccountResponse(t, updated), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tlabel := \"Renamed Key\"\n\tupdated, err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tExpectedRevision: intPointer(4),\n\t\tLabel: &label,\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n\tif updated == nil || updated.ID != \"acct_codex_key\" || updated.DisplayName != \"Renamed Key\" || updated.Revision != 5 {\n\t\tt.Fatalf(\"updated account = %#v\", updated)\n\t}\n}\n\nfunc TestGetAccountDetailLoadsSecretsOnlyForRequestedAccount(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_detail\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Detail\",\n\t\tProvider: \"codex\",\n\t\tRevision: 8,\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-detail-secret\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tProxyURL: \"http://user:pass@proxy.example.com\",\n\t\t\tPlatformCookie: \"session=secret\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_detail\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tdetail, err := app.GetAccountDetail(\"acct_detail\")\n\tif err != nil {\n\t\tt.Fatalf(\"GetAccountDetail: %v\", err)\n\t}\n\tif detail.APIKey != \"sk-detail-secret\" || detail.ProxyURL == \"\" || detail.PlatformCookie == \"\" {\n\t\tt.Fatalf(\"detail missing credential fields: %#v\", detail)\n\t}\n\tif !detail.DetailLoaded || detail.Revision != 8 {\n\t\tt.Fatalf(\"detail metadata = %#v\", detail)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledClearsLegacyManualDisabledRuntimeState(t *testing.T) {\n\tt.Skip(\"obsolete: Wails no longer reads or writes channel runtimeStates\")\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\tif err := saveChannelRoutingStore(channelRoutingStore{\n\t\tRuntimeStates: map[string]ChannelAccountRuntimeState{\n\t\t\t\"acct_codex_key\": {\n\t\t\t\tAccountID: \"acct_codex_key\",\n\t\t\t\tSources: map[string]ChannelRuntimeStateSource{\n\t\t\t\t\t\"manual-disabled\": {\n\t\t\t\t\t\tSource: \"manual-disabled\",\n\t\t\t\t\t\tReason: \"account disabled\",\n\t\t\t\t\t},\n\t\t\t\t\t\"quota-empty\": {\n\t\t\t\t\t\tSource: \"quota-empty\",\n\t\t\t\t\t\tReason: \"requests exhausted\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"saveChannelRoutingStore: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":false`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_codex_key\", false); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tstate := store.RuntimeStates[\"acct_codex_key\"]\n\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\tt.Fatalf(\"manual-disabled source should be cleared: %#v\", state.Sources)\n\t}\n\tif source := state.Sources[\"quota-empty\"]; source.Source != \"quota-empty\" {\n\t\tt.Fatalf(\"quota-empty source = %#v, want preserved\", source)\n\t}\n}\n\nfunc TestSetAccountDisabledDoesNotPersistManualDisabledRuntimeState(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tif state, ok := store.RuntimeStates[\"acct_codex_key\"]; ok {\n\t\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\t\tt.Fatalf(\"manual-disabled source should not be persisted: %#v\", state.Sources)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif _, err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif _, err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestListAccountsInfersCodexAuthFileProviderFromUnifiedAuthJSON(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err != nil {\n\t\tt.Fatalf(\"ListAccounts: %v\", err)\n\t}\n\tif len(accounts) != 1 {\n\t\tt.Fatalf(\"account count = %d, want 1\", len(accounts))\n\t}\n\tif got := accounts[0].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"Provider = %q, want codex\", got)\n\t}\n}\n\nfunc TestListCodexAccountInventoryProjectsAllCodexRoutableAccountKinds(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"priority\":3,\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}},{\"account_key\":\"acct_claude_auth\",\"kind\":\"auth-file\",\"title\":\"claude.json\",\"provider\":\"claude\",\"auth_file\":{\"source_file_name\":\"claude.json\",\"auth_json\":\"{\\\"auth_mode\\\":\\\"claude\\\"}\",\"auth_type\":\"claude\"}},{\"account_key\":\"acct_codex_key\",\"kind\":\"codex-api-key\",\"title\":\"Codex Relay\",\"provider\":\"codex\",\"priority\":2,\"codex_api_key\":{\"api_key\":\"sk-test\",\"base_url\":\"https://api.openai.com/v1\",\"models_json\":\"[{\\\"name\\\":\\\"gpt-5.4\\\",\\\"alias\\\":\\\"gpt-5\\\"}]\"}},{\"account_key\":\"acct_openrouter\",\"kind\":\"openai-compatible\",\"title\":\"OpenRouter\",\"provider\":\"openrouter\",\"priority\":1,\"openai_compatible\":{\"provider_name\":\"openrouter\",\"base_url\":\"https://openrouter.ai/api/v1\",\"api_key_entries_json\":\"[{\\\"api-key\\\":\\\"sk-or\\\"}]\",\"models_json\":\"[{\\\"name\\\":\\\"deepseek-chat\\\",\\\"alias\\\":\\\"deepseek\\\"}]\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListCodexAccountInventory()\n\tif err != nil {\n\t\tt.Fatalf(\"ListCodexAccountInventory: %v\", err)\n\t}\n\tif got, want := len(accounts), 3; got != want {\n\t\tt.Fatalf(\"account count = %d, want %d: %#v\", got, want, accounts)\n\t}\n\n\tbyID := map[string]accountsdomain.AccountRecord{}\n\tfor _, account := range accounts {\n\t\tbyID[account.ID] = account\n\t}\n\tif got := byID[\"acct_codex_auth\"].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"codex auth provider = %q, want codex\", got)\n\t}\n\tif _, ok := byID[\"acct_claude_auth\"]; ok {\n\t\tt.Fatalf(\"claude auth-file should not be in Codex inventory: %#v\", byID[\"acct_claude_auth\"])\n\t}\n\tif got := byID[\"acct_codex_key\"].AccountKind; got != accountsdomain.AccountKindCodexAPIKey {\n\t\tt.Fatalf(\"codex api key kind = %q\", got)\n\t}\n\tif got := byID[\"acct_openrouter\"].AccountKind; got != accountsdomain.AccountKindOpenAICompatible {\n\t\tt.Fatalf(\"openai-compatible kind = %q\", got)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\t_, err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t\treturn err\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\t_, err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t\treturn err\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\t_, err := app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1, nil)\n\t\t\t\treturn err\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeleteAccountsBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"DELETE\" && strings.HasPrefix(path, \"/v0/management/accounts/\") {\n\t\t\t\t\tt.Fatalf(\"batch delete must not call single account delete: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"deleted_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestDeleteAccountsBatchFallsBackToSingleDeleteWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_a\" {\n\t\t\t\t\treturn nil, http.StatusNoContent, nil\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_b\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-delete | DELETE /v0/management/accounts/acct_a | DELETE /v0/management/accounts/acct_b\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"deleted ids = %#v\", result.DeletedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared after fallback: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestSetAccountsDisabledBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && strings.Contains(path, \"/status\") {\n\t\t\t\t\tt.Fatalf(\"batch status must not call single account status: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-status\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) || !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"updated_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.SetAccountsDisabledBatch(SetAccountsDisabledBatchInput{\n\t\tAccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"},\n\t\tDisabled: true,\n\t})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"SetAccountsDisabledBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.UpdatedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n}\n\nfunc TestSetAccountsDisabledBatchFallsBackToSingleStatusWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-status\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_a/status\" {\n\t\t\t\t\treturn []byte(`{\"account_key\":\"acct_a\",\"disabled\":false}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_b/status\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.SetAccountsDisabledBatch(SetAccountsDisabledBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"SetAccountsDisabledBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-status | PATCH /v0/management/accounts/acct_a/status | PATCH /v0/management/accounts/acct_b/status\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.UpdatedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"updated ids = %#v\", result.UpdatedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", mutationError(app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"}))},\n\t\t{\"config\", mutationError(app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"}))},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", mutationError(app.UpdateCodexAPIKeyPriority(legacyID, 1, nil))},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc mutationError(_ *accountsdomain.AccountRecord, err error) error {\n\treturn err\n}\n\nfunc intPointer(value int) *int {\n\treturn &value\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "79d2d7f61f695cc764a870f8345c159dcadc9640": {"t": 1785759457.797244, "n": 32473, "h": "62ae3f1f9a52ca72", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\taccountsdomain \"github.com/linhay/gettokens/internal/accounts\"\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tRevision: 4,\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tif write.Title != \"Renamed Key\" {\n\t\t\t\t\t\tt.Fatalf(\"title = %q, want atomic label update\", write.Title)\n\t\t\t\t\t}\n\t\t\t\t\tif write.ExpectedRevision == nil || *write.ExpectedRevision != 4 {\n\t\t\t\t\t\tt.Fatalf(\"expected revision = %v, want 4\", write.ExpectedRevision)\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\tupdated := account\n\t\t\t\t\tupdated.Title = write.Title\n\t\t\t\t\tupdated.Revision = account.Revision + 1\n\t\t\t\t\tupdated.CodexAPIKey = write.CodexAPIKey\n\t\t\t\t\treturn testAccountResponse(t, updated), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tlabel := \"Renamed Key\"\n\tupdated, err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tExpectedRevision: intPointer(4),\n\t\tLabel: &label,\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n\tif updated == nil || updated.ID != \"acct_codex_key\" || updated.DisplayName != \"Renamed Key\" || updated.Revision != 5 {\n\t\tt.Fatalf(\"updated account = %#v\", updated)\n\t}\n}\n\nfunc TestGetAccountDetailLoadsSecretsOnlyForRequestedAccount(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_detail\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Detail\",\n\t\tProvider: \"codex\",\n\t\tRevision: 8,\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-detail-secret\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tProxyURL: \"http://user:pass@proxy.example.com\",\n\t\t\tPlatformCookie: \"session=secret\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_detail\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tdetail, err := app.GetAccountDetail(\"acct_detail\")\n\tif err != nil {\n\t\tt.Fatalf(\"GetAccountDetail: %v\", err)\n\t}\n\tif detail.APIKey != \"sk-detail-secret\" || detail.ProxyURL == \"\" || detail.PlatformCookie == \"\" {\n\t\tt.Fatalf(\"detail missing credential fields: %#v\", detail)\n\t}\n\tif !detail.DetailLoaded || detail.Revision != 8 {\n\t\tt.Fatalf(\"detail metadata = %#v\", detail)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledClearsLegacyManualDisabledRuntimeState(t *testing.T) {\n\tt.Skip(\"obsolete: Wails no longer reads or writes channel runtimeStates\")\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\tif err := saveChannelRoutingStore(channelRoutingStore{\n\t\tRuntimeStates: map[string]ChannelAccountRuntimeState{\n\t\t\t\"acct_codex_key\": {\n\t\t\t\tAccountID: \"acct_codex_key\",\n\t\t\t\tSources: map[string]ChannelRuntimeStateSource{\n\t\t\t\t\t\"manual-disabled\": {\n\t\t\t\t\t\tSource: \"manual-disabled\",\n\t\t\t\t\t\tReason: \"account disabled\",\n\t\t\t\t\t},\n\t\t\t\t\t\"quota-empty\": {\n\t\t\t\t\t\tSource: \"quota-empty\",\n\t\t\t\t\t\tReason: \"requests exhausted\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"saveChannelRoutingStore: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":false`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_codex_key\", false); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tstate := store.RuntimeStates[\"acct_codex_key\"]\n\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\tt.Fatalf(\"manual-disabled source should be cleared: %#v\", state.Sources)\n\t}\n\tif source := state.Sources[\"quota-empty\"]; source.Source != \"quota-empty\" {\n\t\tt.Fatalf(\"quota-empty source = %#v, want preserved\", source)\n\t}\n}\n\nfunc TestSetAccountDisabledDoesNotPersistManualDisabledRuntimeState(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tif state, ok := store.RuntimeStates[\"acct_codex_key\"]; ok {\n\t\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\t\tt.Fatalf(\"manual-disabled source should not be persisted: %#v\", state.Sources)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif _, err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif _, err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif _, err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestListAccountsInfersCodexAuthFileProviderFromUnifiedAuthJSON(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err != nil {\n\t\tt.Fatalf(\"ListAccounts: %v\", err)\n\t}\n\tif len(accounts) != 1 {\n\t\tt.Fatalf(\"account count = %d, want 1\", len(accounts))\n\t}\n\tif got := accounts[0].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"Provider = %q, want codex\", got)\n\t}\n}\n\nfunc TestListCodexAccountInventoryProjectsAllCodexRoutableAccountKinds(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"priority\":3,\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}},{\"account_key\":\"acct_claude_auth\",\"kind\":\"auth-file\",\"title\":\"claude.json\",\"provider\":\"claude\",\"auth_file\":{\"source_file_name\":\"claude.json\",\"auth_json\":\"{\\\"auth_mode\\\":\\\"claude\\\"}\",\"auth_type\":\"claude\"}},{\"account_key\":\"acct_codex_key\",\"kind\":\"codex-api-key\",\"title\":\"Codex Relay\",\"provider\":\"codex\",\"priority\":2,\"codex_api_key\":{\"api_key\":\"sk-test\",\"base_url\":\"https://api.openai.com/v1\",\"models_json\":\"[{\\\"name\\\":\\\"gpt-5.4\\\",\\\"alias\\\":\\\"gpt-5\\\"}]\"}},{\"account_key\":\"acct_openrouter\",\"kind\":\"openai-compatible\",\"title\":\"OpenRouter\",\"provider\":\"openrouter\",\"priority\":1,\"openai_compatible\":{\"provider_name\":\"openrouter\",\"base_url\":\"https://openrouter.ai/api/v1\",\"api_key_entries_json\":\"[{\\\"api-key\\\":\\\"sk-or\\\"}]\",\"models_json\":\"[{\\\"name\\\":\\\"deepseek-chat\\\",\\\"alias\\\":\\\"deepseek\\\"}]\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListCodexAccountInventory()\n\tif err != nil {\n\t\tt.Fatalf(\"ListCodexAccountInventory: %v\", err)\n\t}\n\tif got, want := len(accounts), 3; got != want {\n\t\tt.Fatalf(\"account count = %d, want %d: %#v\", got, want, accounts)\n\t}\n\n\tbyID := map[string]accountsdomain.AccountRecord{}\n\tfor _, account := range accounts {\n\t\tbyID[account.ID] = account\n\t}\n\tif got := byID[\"acct_codex_auth\"].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"codex auth provider = %q, want codex\", got)\n\t}\n\tif _, ok := byID[\"acct_claude_auth\"]; ok {\n\t\tt.Fatalf(\"claude auth-file should not be in Codex inventory: %#v\", byID[\"acct_claude_auth\"])\n\t}\n\tif got := byID[\"acct_codex_key\"].AccountKind; got != accountsdomain.AccountKindCodexAPIKey {\n\t\tt.Fatalf(\"codex api key kind = %q\", got)\n\t}\n\tif got := byID[\"acct_openrouter\"].AccountKind; got != accountsdomain.AccountKindOpenAICompatible {\n\t\tt.Fatalf(\"openai-compatible kind = %q\", got)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\t_, err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t\treturn err\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\t_, err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t\treturn err\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\t_, err := app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1, nil)\n\t\t\t\treturn err\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeleteAccountsBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"DELETE\" && strings.HasPrefix(path, \"/v0/management/accounts/\") {\n\t\t\t\t\tt.Fatalf(\"batch delete must not call single account delete: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"deleted_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestDeleteAccountsBatchFallsBackToSingleDeleteWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_a\" {\n\t\t\t\t\treturn nil, http.StatusNoContent, nil\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_b\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-delete | DELETE /v0/management/accounts/acct_a | DELETE /v0/management/accounts/acct_b\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"deleted ids = %#v\", result.DeletedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared after fallback: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestSetAccountsDisabledBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && strings.Contains(path, \"/status\") {\n\t\t\t\t\tt.Fatalf(\"batch status must not call single account status: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-status\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) || !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"updated_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.SetAccountsDisabledBatch(SetAccountsDisabledBatchInput{\n\t\tAccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"},\n\t\tDisabled: true,\n\t})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"SetAccountsDisabledBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.UpdatedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n}\n\nfunc TestSetAccountsDisabledBatchFallsBackToSingleStatusWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-status\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_a/status\" {\n\t\t\t\t\treturn []byte(`{\"account_key\":\"acct_a\",\"disabled\":false}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_b/status\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.SetAccountsDisabledBatch(SetAccountsDisabledBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"SetAccountsDisabledBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-status | PATCH /v0/management/accounts/acct_a/status | PATCH /v0/management/accounts/acct_b/status\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.UpdatedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"updated ids = %#v\", result.UpdatedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", mutationError(app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"}))},\n\t\t{\"config\", mutationError(app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"}))},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", mutationError(app.UpdateCodexAPIKeyPriority(legacyID, 1, nil))},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc mutationError(_ *accountsdomain.AccountRecord, err error) error {\n\treturn err\n}\n\nfunc intPointer(value int) *int {\n\treturn &value\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "7529356ab82e5179cd3017da0373522ff9300130": {"t": 1785759459.251795, "n": 30104, "h": "1c34c5f9a8ccba23", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\taccountsdomain \"github.com/linhay/gettokens/internal/accounts\"\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledClearsLegacyManualDisabledRuntimeState(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\tif err := saveChannelRoutingStore(channelRoutingStore{\n\t\tRuntimeStates: map[string]ChannelAccountRuntimeState{\n\t\t\t\"acct_codex_key\": {\n\t\t\t\tAccountID: \"acct_codex_key\",\n\t\t\t\tSources: map[string]ChannelRuntimeStateSource{\n\t\t\t\t\t\"manual-disabled\": {\n\t\t\t\t\t\tSource: \"manual-disabled\",\n\t\t\t\t\t\tReason: \"account disabled\",\n\t\t\t\t\t},\n\t\t\t\t\t\"quota-empty\": {\n\t\t\t\t\t\tSource: \"quota-empty\",\n\t\t\t\t\t\tReason: \"requests exhausted\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"saveChannelRoutingStore: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":false`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", false); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tstate := store.RuntimeStates[\"acct_codex_key\"]\n\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\tt.Fatalf(\"manual-disabled source should be cleared: %#v\", state.Sources)\n\t}\n\tif source := state.Sources[\"quota-empty\"]; source.Source != \"quota-empty\" {\n\t\tt.Fatalf(\"quota-empty source = %#v, want preserved\", source)\n\t}\n}\n\nfunc TestSetAccountDisabledDoesNotPersistManualDisabledRuntimeState(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tif state, ok := store.RuntimeStates[\"acct_codex_key\"]; ok {\n\t\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\t\tt.Fatalf(\"manual-disabled source should not be persisted: %#v\", state.Sources)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestListAccountsInfersCodexAuthFileProviderFromUnifiedAuthJSON(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err != nil {\n\t\tt.Fatalf(\"ListAccounts: %v\", err)\n\t}\n\tif len(accounts) != 1 {\n\t\tt.Fatalf(\"account count = %d, want 1\", len(accounts))\n\t}\n\tif got := accounts[0].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"Provider = %q, want codex\", got)\n\t}\n}\n\nfunc TestListCodexAccountInventoryProjectsAllCodexRoutableAccountKinds(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"priority\":3,\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}},{\"account_key\":\"acct_claude_auth\",\"kind\":\"auth-file\",\"title\":\"claude.json\",\"provider\":\"claude\",\"auth_file\":{\"source_file_name\":\"claude.json\",\"auth_json\":\"{\\\"auth_mode\\\":\\\"claude\\\"}\",\"auth_type\":\"claude\"}},{\"account_key\":\"acct_codex_key\",\"kind\":\"codex-api-key\",\"title\":\"Codex Relay\",\"provider\":\"codex\",\"priority\":2,\"codex_api_key\":{\"api_key\":\"sk-test\",\"base_url\":\"https://api.openai.com/v1\",\"models_json\":\"[{\\\"name\\\":\\\"gpt-5.4\\\",\\\"alias\\\":\\\"gpt-5\\\"}]\"}},{\"account_key\":\"acct_openrouter\",\"kind\":\"openai-compatible\",\"title\":\"OpenRouter\",\"provider\":\"openrouter\",\"priority\":1,\"openai_compatible\":{\"provider_name\":\"openrouter\",\"base_url\":\"https://openrouter.ai/api/v1\",\"api_key_entries_json\":\"[{\\\"api-key\\\":\\\"sk-or\\\"}]\",\"models_json\":\"[{\\\"name\\\":\\\"deepseek-chat\\\",\\\"alias\\\":\\\"deepseek\\\"}]\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListCodexAccountInventory()\n\tif err != nil {\n\t\tt.Fatalf(\"ListCodexAccountInventory: %v\", err)\n\t}\n\tif got, want := len(accounts), 3; got != want {\n\t\tt.Fatalf(\"account count = %d, want %d: %#v\", got, want, accounts)\n\t}\n\n\tbyID := map[string]accountsdomain.AccountRecord{}\n\tfor _, account := range accounts {\n\t\tbyID[account.ID] = account\n\t}\n\tif got := byID[\"acct_codex_auth\"].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"codex auth provider = %q, want codex\", got)\n\t}\n\tif _, ok := byID[\"acct_claude_auth\"]; ok {\n\t\tt.Fatalf(\"claude auth-file should not be in Codex inventory: %#v\", byID[\"acct_claude_auth\"])\n\t}\n\tif got := byID[\"acct_codex_key\"].AccountKind; got != accountsdomain.AccountKindCodexAPIKey {\n\t\tt.Fatalf(\"codex api key kind = %q\", got)\n\t}\n\tif got := byID[\"acct_openrouter\"].AccountKind; got != accountsdomain.AccountKindOpenAICompatible {\n\t\tt.Fatalf(\"openai-compatible kind = %q\", got)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeleteAccountsBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"DELETE\" && strings.HasPrefix(path, \"/v0/management/accounts/\") {\n\t\t\t\t\tt.Fatalf(\"batch delete must not call single account delete: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"deleted_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestDeleteAccountsBatchFallsBackToSingleDeleteWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_a\" {\n\t\t\t\t\treturn nil, http.StatusNoContent, nil\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_b\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-delete | DELETE /v0/management/accounts/acct_a | DELETE /v0/management/accounts/acct_b\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"deleted ids = %#v\", result.DeletedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared after fallback: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestSetAccountsDisabledBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && strings.Contains(path, \"/status\") {\n\t\t\t\t\tt.Fatalf(\"batch status must not call single account status: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-status\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) || !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"updated_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.SetAccountsDisabledBatch(SetAccountsDisabledBatchInput{\n\t\tAccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"},\n\t\tDisabled: true,\n\t})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"SetAccountsDisabledBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.UpdatedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n}\n\nfunc TestSetAccountsDisabledBatchFallsBackToSingleStatusWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-status\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_a/status\" {\n\t\t\t\t\treturn []byte(`{\"account_key\":\"acct_a\",\"disabled\":false}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_b/status\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.SetAccountsDisabledBatch(SetAccountsDisabledBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"SetAccountsDisabledBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-status | PATCH /v0/management/accounts/acct_a/status | PATCH /v0/management/accounts/acct_b/status\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.UpdatedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"updated ids = %#v\", result.UpdatedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "627d693bd015908c08aba874fb4f7ea259e41d50": {"t": 1785759460.7961414, "n": 26794, "h": "b1e03dd73f97867d", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\taccountsdomain \"github.com/linhay/gettokens/internal/accounts\"\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledClearsLegacyManualDisabledRuntimeState(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\tif err := saveChannelRoutingStore(channelRoutingStore{\n\t\tRuntimeStates: map[string]ChannelAccountRuntimeState{\n\t\t\t\"acct_codex_key\": {\n\t\t\t\tAccountID: \"acct_codex_key\",\n\t\t\t\tSources: map[string]ChannelRuntimeStateSource{\n\t\t\t\t\t\"manual-disabled\": {\n\t\t\t\t\t\tSource: \"manual-disabled\",\n\t\t\t\t\t\tReason: \"account disabled\",\n\t\t\t\t\t},\n\t\t\t\t\t\"quota-empty\": {\n\t\t\t\t\t\tSource: \"quota-empty\",\n\t\t\t\t\t\tReason: \"requests exhausted\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"saveChannelRoutingStore: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":false`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", false); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tstate := store.RuntimeStates[\"acct_codex_key\"]\n\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\tt.Fatalf(\"manual-disabled source should be cleared: %#v\", state.Sources)\n\t}\n\tif source := state.Sources[\"quota-empty\"]; source.Source != \"quota-empty\" {\n\t\tt.Fatalf(\"quota-empty source = %#v, want preserved\", source)\n\t}\n}\n\nfunc TestSetAccountDisabledDoesNotPersistManualDisabledRuntimeState(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"GETTOKENS_APP_PROFILE\", \"dev\")\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\tstore, err := loadChannelRoutingStore()\n\tif err != nil {\n\t\tt.Fatalf(\"loadChannelRoutingStore: %v\", err)\n\t}\n\tif state, ok := store.RuntimeStates[\"acct_codex_key\"]; ok {\n\t\tif _, ok := state.Sources[\"manual-disabled\"]; ok {\n\t\t\tt.Fatalf(\"manual-disabled source should not be persisted: %#v\", state.Sources)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestListAccountsInfersCodexAuthFileProviderFromUnifiedAuthJSON(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err != nil {\n\t\tt.Fatalf(\"ListAccounts: %v\", err)\n\t}\n\tif len(accounts) != 1 {\n\t\tt.Fatalf(\"account count = %d, want 1\", len(accounts))\n\t}\n\tif got := accounts[0].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"Provider = %q, want codex\", got)\n\t}\n}\n\nfunc TestListCodexAccountInventoryProjectsAllCodexRoutableAccountKinds(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"priority\":3,\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}},{\"account_key\":\"acct_claude_auth\",\"kind\":\"auth-file\",\"title\":\"claude.json\",\"provider\":\"claude\",\"auth_file\":{\"source_file_name\":\"claude.json\",\"auth_json\":\"{\\\"auth_mode\\\":\\\"claude\\\"}\",\"auth_type\":\"claude\"}},{\"account_key\":\"acct_codex_key\",\"kind\":\"codex-api-key\",\"title\":\"Codex Relay\",\"provider\":\"codex\",\"priority\":2,\"codex_api_key\":{\"api_key\":\"sk-test\",\"base_url\":\"https://api.openai.com/v1\",\"models_json\":\"[{\\\"name\\\":\\\"gpt-5.4\\\",\\\"alias\\\":\\\"gpt-5\\\"}]\"}},{\"account_key\":\"acct_openrouter\",\"kind\":\"openai-compatible\",\"title\":\"OpenRouter\",\"provider\":\"openrouter\",\"priority\":1,\"openai_compatible\":{\"provider_name\":\"openrouter\",\"base_url\":\"https://openrouter.ai/api/v1\",\"api_key_entries_json\":\"[{\\\"api-key\\\":\\\"sk-or\\\"}]\",\"models_json\":\"[{\\\"name\\\":\\\"deepseek-chat\\\",\\\"alias\\\":\\\"deepseek\\\"}]\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListCodexAccountInventory()\n\tif err != nil {\n\t\tt.Fatalf(\"ListCodexAccountInventory: %v\", err)\n\t}\n\tif got, want := len(accounts), 3; got != want {\n\t\tt.Fatalf(\"account count = %d, want %d: %#v\", got, want, accounts)\n\t}\n\n\tbyID := map[string]accountsdomain.AccountRecord{}\n\tfor _, account := range accounts {\n\t\tbyID[account.ID] = account\n\t}\n\tif got := byID[\"acct_codex_auth\"].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"codex auth provider = %q, want codex\", got)\n\t}\n\tif _, ok := byID[\"acct_claude_auth\"]; ok {\n\t\tt.Fatalf(\"claude auth-file should not be in Codex inventory: %#v\", byID[\"acct_claude_auth\"])\n\t}\n\tif got := byID[\"acct_codex_key\"].AccountKind; got != accountsdomain.AccountKindCodexAPIKey {\n\t\tt.Fatalf(\"codex api key kind = %q\", got)\n\t}\n\tif got := byID[\"acct_openrouter\"].AccountKind; got != accountsdomain.AccountKindOpenAICompatible {\n\t\tt.Fatalf(\"openai-compatible kind = %q\", got)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeleteAccountsBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"DELETE\" && strings.HasPrefix(path, \"/v0/management/accounts/\") {\n\t\t\t\t\tt.Fatalf(\"batch delete must not call single account delete: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"deleted_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestDeleteAccountsBatchFallsBackToSingleDeleteWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_a\" {\n\t\t\t\t\treturn nil, http.StatusNoContent, nil\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_b\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-delete | DELETE /v0/management/accounts/acct_a | DELETE /v0/management/accounts/acct_b\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"deleted ids = %#v\", result.DeletedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared after fallback: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "96324381ca49add3e43951626958eee5982ac227": {"t": 1785759463.4126067, "n": 23062, "h": "3800955beffc8174", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\taccountsdomain \"github.com/linhay/gettokens/internal/accounts\"\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestListAccountsInfersCodexAuthFileProviderFromUnifiedAuthJSON(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err != nil {\n\t\tt.Fatalf(\"ListAccounts: %v\", err)\n\t}\n\tif len(accounts) != 1 {\n\t\tt.Fatalf(\"account count = %d, want 1\", len(accounts))\n\t}\n\tif got := accounts[0].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"Provider = %q, want codex\", got)\n\t}\n}\n\nfunc TestListCodexAccountInventoryProjectsAllCodexRoutableAccountKinds(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn []byte(`{\"accounts\":[{\"account_key\":\"acct_codex_auth\",\"kind\":\"auth-file\",\"title\":\"codex-auth.json\",\"provider\":\"unknown\",\"priority\":3,\"auth_file\":{\"source_file_name\":\"codex-auth.json\",\"auth_json\":\"{\\\"type\\\":\\\"codex\\\",\\\"email\\\":\\\"ops@example.com\\\",\\\"plan_type\\\":\\\"pro\\\"}\",\"auth_type\":\"unknown\"}},{\"account_key\":\"acct_claude_auth\",\"kind\":\"auth-file\",\"title\":\"claude.json\",\"provider\":\"claude\",\"auth_file\":{\"source_file_name\":\"claude.json\",\"auth_json\":\"{\\\"auth_mode\\\":\\\"claude\\\"}\",\"auth_type\":\"claude\"}},{\"account_key\":\"acct_codex_key\",\"kind\":\"codex-api-key\",\"title\":\"Codex Relay\",\"provider\":\"codex\",\"priority\":2,\"codex_api_key\":{\"api_key\":\"sk-test\",\"base_url\":\"https://api.openai.com/v1\",\"models_json\":\"[{\\\"name\\\":\\\"gpt-5.4\\\",\\\"alias\\\":\\\"gpt-5\\\"}]\"}},{\"account_key\":\"acct_openrouter\",\"kind\":\"openai-compatible\",\"title\":\"OpenRouter\",\"provider\":\"openrouter\",\"priority\":1,\"openai_compatible\":{\"provider_name\":\"openrouter\",\"base_url\":\"https://openrouter.ai/api/v1\",\"api_key_entries_json\":\"[{\\\"api-key\\\":\\\"sk-or\\\"}]\",\"models_json\":\"[{\\\"name\\\":\\\"deepseek-chat\\\",\\\"alias\\\":\\\"deepseek\\\"}]\"}}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListCodexAccountInventory()\n\tif err != nil {\n\t\tt.Fatalf(\"ListCodexAccountInventory: %v\", err)\n\t}\n\tif got, want := len(accounts), 3; got != want {\n\t\tt.Fatalf(\"account count = %d, want %d: %#v\", got, want, accounts)\n\t}\n\n\tbyID := map[string]accountsdomain.AccountRecord{}\n\tfor _, account := range accounts {\n\t\tbyID[account.ID] = account\n\t}\n\tif got := byID[\"acct_codex_auth\"].Provider; got != \"codex\" {\n\t\tt.Fatalf(\"codex auth provider = %q, want codex\", got)\n\t}\n\tif _, ok := byID[\"acct_claude_auth\"]; ok {\n\t\tt.Fatalf(\"claude auth-file should not be in Codex inventory: %#v\", byID[\"acct_claude_auth\"])\n\t}\n\tif got := byID[\"acct_codex_key\"].AccountKind; got != accountsdomain.AccountKindCodexAPIKey {\n\t\tt.Fatalf(\"codex api key kind = %q\", got)\n\t}\n\tif got := byID[\"acct_openrouter\"].AccountKind; got != accountsdomain.AccountKindOpenAICompatible {\n\t\tt.Fatalf(\"openai-compatible kind = %q\", got)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeleteAccountsBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"DELETE\" && strings.HasPrefix(path, \"/v0/management/accounts/\") {\n\t\t\t\t\tt.Fatalf(\"batch delete must not call single account delete: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"deleted_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestDeleteAccountsBatchFallsBackToSingleDeleteWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_a\" {\n\t\t\t\t\treturn nil, http.StatusNoContent, nil\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_b\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-delete | DELETE /v0/management/accounts/acct_a | DELETE /v0/management/accounts/acct_b\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"deleted ids = %#v\", result.DeletedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared after fallback: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "d59ddc0117eeb96f5e9b9b7c37b38c7972481e46": {"t": 1785759465.0488663, "n": 19505, "h": "d07cca9458314be0", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeleteAccountsBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"DELETE\" && strings.HasPrefix(path, \"/v0/management/accounts/\") {\n\t\t\t\t\tt.Fatalf(\"batch delete must not call single account delete: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"deleted_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestDeleteAccountsBatchFallsBackToSingleDeleteWhenBatchRouteMissing(t *testing.T) {\n\tvar requests []string\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\trequests = append(requests, method+\" \"+path)\n\t\t\t\tif method == \"POST\" && path == \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): 404 NOT FOUND\")\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_a\" {\n\t\t\t\t\treturn nil, http.StatusNoContent, nil\n\t\t\t\t}\n\t\t\t\tif method == \"DELETE\" && path == \"/v0/management/accounts/acct_b\" {\n\t\t\t\t\treturn nil, http.StatusNotFound, errors.New(\"sidecar 请求失败 (404): account not found\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch fallback = %#v, err = %v\", result, err)\n\t}\n\tif got := strings.Join(requests, \" | \"); got != \"POST /v0/management/accounts/batch-delete | DELETE /v0/management/accounts/acct_a | DELETE /v0/management/accounts/acct_b\" {\n\t\tt.Fatalf(\"unexpected request sequence: %s\", got)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 {\n\t\tt.Fatalf(\"result counts = %#v\", result)\n\t}\n\tif strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"deleted ids = %#v\", result.DeletedAccountIDs)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared after fallback: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "4037261ca6c65f8b904ed5386a16b37655261f16": {"t": 1785759466.3549066, "n": 17569, "h": "cf478c8bae31efef", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeleteAccountsBatchUsesBatchManagementAPI(t *testing.T) {\n\tapp := &App{\n\t\tauthFileMetadataCache: map[string]authFileMetadataCacheEntry{\n\t\t\t\"codex.json\": {},\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"DELETE\" && strings.HasPrefix(path, \"/v0/management/accounts/\") {\n\t\t\t\t\tt.Fatalf(\"batch delete must not call single account delete: %s\", path)\n\t\t\t\t}\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts/batch-delete\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"account_keys\":[\"acct_a\",\"acct_b\"]`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn []byte(`{\"deleted_account_keys\":[\"acct_a\"],\"errors\":[{\"account_key\":\"acct_b\",\"error\":\"account not found\"}],\"succeeded\":1,\"failed\":1}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tresult, err := app.DeleteAccountsBatch(DeleteAccountsBatchInput{AccountIDs: []string{\"acct_a\", \"acct_a\", \"acct_b\"}})\n\tif err != nil || result == nil {\n\t\tt.Fatalf(\"DeleteAccountsBatch = %#v, err = %v\", result, err)\n\t}\n\tif result.Succeeded != 1 || result.Failed != 1 || strings.Join(result.DeletedAccountIDs, \",\") != \"acct_a\" {\n\t\tt.Fatalf(\"result = %#v\", result)\n\t}\n\tif len(result.Errors) != 1 || result.Errors[0].AccountID != \"acct_b\" {\n\t\tt.Fatalf(\"errors = %#v\", result.Errors)\n\t}\n\tif len(app.authFileMetadataCache) != 0 {\n\t\tt.Fatalf(\"auth metadata cache was not cleared: %#v\", app.authFileMetadataCache)\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "407f4f1408a6ea04366a75e400f248eccee55914": {"t": 1785759467.9665096, "n": 15893, "h": "f38e976fd704d72b", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsFormatBaseURLs(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-old\",\n\t\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar write cliproxyapi.AccountWriteRequest\n\t\t\t\t\tif err := json.Unmarshal(payload, &write); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif write.CodexAPIKey == nil {\n\t\t\t\t\t\tt.Fatalf(\"missing codex api key payload: %s\", string(payload))\n\t\t\t\t\t}\n\t\t\t\t\tvar formatBaseURLs map[string]string\n\t\t\t\t\tif err := json.Unmarshal([]byte(write.CodexAPIKey.FormatBaseURLsJSON), &formatBaseURLs); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal format base urls: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_chat\"] != \"https://relay.example.com/openai/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_chat base url = %q\", formatBaseURLs[\"openai_chat\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"openai_responses\"] != \"https://relay.example.com/codex/v1\" {\n\t\t\t\t\t\tt.Fatalf(\"openai_responses base url = %q\", formatBaseURLs[\"openai_responses\"])\n\t\t\t\t\t}\n\t\t\t\t\tif formatBaseURLs[\"anthropic\"] != \"https://relay.example.com/anthropic\" {\n\t\t\t\t\t\tt.Fatalf(\"anthropic base url = %q\", formatBaseURLs[\"anthropic\"])\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"acct_codex_key\",\n\t\tAPIKey: \"sk-new\",\n\t\tBaseURL: \"https://relay.example.com/v1\",\n\t\tFormatBaseURLs: map[string]string{\n\t\t\t\"openai_chat\": \"https://relay.example.com/openai/v1\",\n\t\t\t\"openai_responses\": \"https://relay.example.com/codex/v1\",\n\t\t\t\"anthropic\": \"https://relay.example.com/anthropic\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig returned error: %v\", err)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "335fe301dcc28bcc72c1a15edd5f70b3a5b047cc": {"t": 1785759469.5684552, "n": 13396, "h": "a6d8dea89e651b25", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestSetCodexAPIKeyStatusIgnoresPruneErrorAndSchedulesRefresh(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tcachePath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath: %v\", err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {\n\t\tt.Fatalf(\"mkdir cache dir: %v\", err)\n\t}\n\tif err := os.WriteFile(cachePath, []byte(\"{ invalid json\"), 0600); err != nil {\n\t\tt.Fatalf(\"write corrupt cache: %v\", err)\n\t}\n\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\trefreshCalled := make(chan struct{}, 1)\n\tpatchCalled := false\n\tapp := &App{\n\t\tctx: context.Background(),\n\t\tcodexModelCatalogRefreshDebounce: time.Millisecond,\n\t\tcodexModelCatalogRefreshFunc: func() error {\n\t\t\trefreshCalled <- struct{}{}\n\t\t\treturn nil\n\t\t},\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpatchCalled = true\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetCodexAPIKeyStatus(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetCodexAPIKeyStatus returned prune error after successful patch: %v\", err)\n\t}\n\tif !patchCalled {\n\t\tt.Fatalf(\"expected PatchAccountStatus to be called\")\n\t}\n\tselect {\n\tcase <-refreshCalled:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"expected catalog refresh to be scheduled despite prune failure\")\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "a3e484140e75b5411a0a6054876caba8049ab0c9": {"t": 1785759471.0776083, "n": 11451, "h": "e1f7aaa3cd2aecef", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestListAccountsDoesNotFallbackToLegacyWhenAccountStoreErrors(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn nil, 500, errors.New(\"account store unavailable\")\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\taccounts, err := app.ListAccounts()\n\tif err == nil {\n\t\tt.Fatalf(\"ListAccounts succeeded with legacy fallback accounts: %#v\", accounts)\n\t}\n\tif !strings.Contains(err.Error(), \"account store unavailable\") {\n\t\tt.Fatalf(\"ListAccounts error = %v, want account store error\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyDoesNotFallbackToLegacyWhenAccountStoreCreateFails(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\treturn nil, 500, errors.New(\"account store write failed\")\n\t\t\t})\n\t\t},\n\t}\n\n\terr := app.CreateCodexAPIKey(CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-no-legacy-fallback\",\n\t\tLabel: \"Should fail\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"CreateCodexAPIKey succeeded, want account-store error\")\n\t}\n\tif !strings.Contains(err.Error(), \"account store write failed\") {\n\t\tt.Fatalf(\"CreateCodexAPIKey error = %v, want account-store error\", err)\n\t}\n}\n\nfunc TestUnifiedCodexAPIKeyMutationsDoNotFallbackToLegacyOnAccountStoreErrors(t *testing.T) {\n\tnewFailingApp := func() *App {\n\t\treturn &App{\n\t\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\t\tif path == \"/v0/management/codex-api-key\" {\n\t\t\t\t\t\tt.Fatalf(\"unexpected legacy codex-api-key sync: %s %s\", method, path)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 500, errors.New(\"account store mutation failed\")\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\trun func(*App) error\n\t}{\n\t\t{\n\t\t\tname: \"label\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: \"acct_codex_key\", Label: \"Changed\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"config\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: \"acct_codex_key\", APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"delete\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.DeleteCodexAPIKey(\"acct_codex_key\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"priority\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.UpdateCodexAPIKeyPriority(\"acct_codex_key\", 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"status\",\n\t\t\trun: func(app *App) error {\n\t\t\t\treturn app.SetCodexAPIKeyStatus(\"acct_codex_key\", true)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.run(newFailingApp())\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"%s succeeded, want account-store error\", tc.name)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"account store mutation failed\") {\n\t\t\t\tt.Fatalf(\"%s error = %v, want account-store error\", tc.name, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLegacyCodexAPIKeyIDsRejectedOutsideMigration(t *testing.T) {\n\tapp := &App{}\n\tlegacyID := \"codex-api-key:stable-001\"\n\n\tchecks := []struct {\n\t\tname string\n\t\terr error\n\t}{\n\t\t{\"label\", app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{ID: legacyID, Label: \"Changed\"})},\n\t\t{\"config\", app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{ID: legacyID, APIKey: \"sk-new\", BaseURL: \"https://api.example.com/v1\"})},\n\t\t{\"delete\", app.DeleteCodexAPIKey(legacyID)},\n\t\t{\"priority\", app.UpdateCodexAPIKeyPriority(legacyID, 1)},\n\t\t{\"status\", app.SetCodexAPIKeyStatus(legacyID, true)},\n\t}\n\n\tfor _, check := range checks {\n\t\tif check.err == nil {\n\t\t\tt.Fatalf(\"%s accepted legacy id outside migration\", check.name)\n\t\t}\n\t\tif !strings.Contains(check.err.Error(), \"不支持的账号类型\") {\n\t\t\tt.Fatalf(\"%s error = %v, want unsupported account type\", check.name, check.err)\n\t\t}\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n"}, "dd315de538344613bf9ff6b9188f00b9bdb04070": {"t": 1785759472.7002487, "n": 16574, "h": "b09e6940556d17ff", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsUnifiedOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"acct_deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsUnifiedCodexAPIKey(t *testing.T) {\n\taccount := cliproxyapi.UnifiedAccount{\n\t\tAccountKey: \"acct_codex_key\",\n\t\tKind: cliproxyapi.AccountKindCodexAPIKey,\n\t\tTitle: \"Codex Key\",\n\t\tProvider: \"codex\",\n\t\tCodexAPIKey: &cliproxyapi.CodexAPIKeyAccountCredential{\n\t\t\tAPIKey: \"sk-test-1111\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t},\n\t}\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_codex_key/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_codex_key\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.SetAccountDisabled(id, true); err == nil {\n\t\t\tt.Fatalf(\"SetAccountDisabled(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestUpdateAccountPriorityRejectsLegacyPrefixedRuntimeIDs(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tt.Fatalf(\"unexpected request for legacy runtime id: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tfor _, id := range []string{\"auth-file:codex.json\", \"codex-api-key:stable-001\", \"openai-compatible:deepseek\"} {\n\t\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{ID: id, Priority: 5}); err == nil {\n\t\t\tt.Fatalf(\"UpdateAccountPriority(%q) succeeded, want unsupported account type\", id)\n\t\t}\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"acct_deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsQuotaCurl(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif strings.Contains(string(payload), \"quota-curl\") || strings.Contains(string(payload), \"quota-enabled\") {\n\t\t\t\t\tt.Fatalf(\"quota curl must stay local and not sync to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tconst quotaCurl = `curl -sS \"https://quota.example.com/api/codex/usage\" -H \"Authorization: Bearer {{apiKey}}\"`\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tQuotaCurl: quotaCurl,\n\t\tQuotaEnabled: true,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].QuotaCurl; got != quotaCurl {\n\t\tt.Fatalf(\"QuotaCurl = %q, want %q\", got, quotaCurl)\n\t}\n\tif !items[0].QuotaEnabled {\n\t\tt.Fatalf(\"QuotaEnabled = false, want true\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsProxyURL(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"direct\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"proxy-url\":\"socks5://127.0.0.1:7890\"`) {\n\t\t\t\t\tt.Fatalf(\"proxy url not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"socks5://127.0.0.1:7890\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif got := items[0].ProxyURL; got != \"socks5://127.0.0.1:7890\" {\n\t\tt.Fatalf(\"ProxyURL = %q, want socks5://127.0.0.1:7890\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsModels(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"models\":[{\"name\":\"mimo-v2.5-pro\",\"alias\":\"claude-sonnet-4-6\"}]`) {\n\t\t\t\t\tt.Fatalf(\"models not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tModels: []OpenAICompatibleModel{\n\t\t\t{Name: \"mimo-v2.5-pro\", Alias: \"claude-sonnet-4-6\"},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || len(items[0].Models) != 1 {\n\t\tt.Fatalf(\"expected one persisted model, got %#v\", items)\n\t}\n\tif got := items[0].Models[0]; got.Name != \"mimo-v2.5-pro\" || got.Alias != \"claude-sonnet-4-6\" {\n\t\tt.Fatalf(\"unexpected model: %#v\", got)\n\t}\n}\n\nfunc TestDeleteCodexAPIKeyAcceptsDerivedConfigIDForStableLocalRecord(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif got := strings.TrimSpace(string(payload)); got != \"[]\" {\n\t\t\t\t\tt.Fatalf(\"expected empty sidecar sync payload, got %s\", got)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tderivedID := codexAPIKeyConfigIdentityFromInput(item)\n\tif err := app.DeleteCodexAPIKey(derivedID); err != nil {\n\t\tt.Fatalf(\"DeleteCodexAPIKey: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 0 {\n\t\tt.Fatalf(\"expected store to be empty, got %#v\", items)\n\t}\n}\n"}, "a16f5b4b01d671610fe6092cd7d9c66585a5b7a6": {"t": 1785759475.2535112, "n": 15674, "h": "5e1a293c39211eff", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 1, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/priority\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsCodexAPIKey(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(codexAPIKeyAssetIDFromInput(items[0]), true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || !items[0].Disabled {\n\t\tt.Fatalf(\"expected stored codex key to be disabled, got %#v\", items)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\taccount := testOpenAICompatibleAccount(\"acct_deepseek\", \"deepseek\", 0, false, \"https://api.deepseek.com/v1\", \"\", []cliproxyapi.OpenAICompatibleAPIKeyEntry{{APIKey: \"sk-old\"}}, nil, nil)\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/accounts\" {\n\t\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PATCH\" && path == \"/v0/management/accounts/acct_deepseek/status\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar update struct {\n\t\t\t\t\t\tDisabled bool `json:\"disabled\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal(payload, &update); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !update.Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", update)\n\t\t\t\t\t}\n\t\t\t\t\treturn testAccountResponse(t, account), 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"openai-compatible:deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar creates []cliproxyapi.AccountWriteRequest\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"POST\" || path != \"/v0/management/accounts\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar item cliproxyapi.AccountWriteRequest\n\t\t\t\tif err := json.Unmarshal(payload, &item); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tcreates = append(creates, item)\n\t\t\t\treturn []byte(`{\"account_key\":\"acct_00000000-0000-4000-8000-000000000001\",\"kind\":\"codex-api-key\",\"title\":\"created\",\"provider\":\"codex\",\"codex_api_key\":{\"api_key\":\"sk-test-duplicate\",\"base_url\":\"https://api.openai.com/v1\",\"prefix\":\"team-a\",\"websockets\":true}}`), 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\tif len(creates) != 2 {\n\t\tt.Fatalf(\"expected 2 sidecar account creates, got %#v\", creates)\n\t}\n\tif creates[0].Title != \"Primary\" || creates[1].Title != \"Copied\" {\n\t\tt.Fatalf(\"unexpected create titles: %#v\", creates)\n\t}\n\tfor _, item := range creates {\n\t\tif item.Kind != cliproxyapi.AccountKindCodexAPIKey || item.CodexAPIKey == nil {\n\t\t\tt.Fatalf(\"unexpected account write: %#v\", item)\n\t\t}\n\t\tif item.CodexAPIKey.APIKey != \"sk-test-duplicate\" || item.CodexAPIKey.BaseURL != \"https://api.openai.com/v1\" || item.CodexAPIKey.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsQuotaCurl(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif strings.Contains(string(payload), \"quota-curl\") || strings.Contains(string(payload), \"quota-enabled\") {\n\t\t\t\t\tt.Fatalf(\"quota curl must stay local and not sync to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tconst quotaCurl = `curl -sS \"https://quota.example.com/api/codex/usage\" -H \"Authorization: Bearer {{apiKey}}\"`\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tQuotaCurl: quotaCurl,\n\t\tQuotaEnabled: true,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].QuotaCurl; got != quotaCurl {\n\t\tt.Fatalf(\"QuotaCurl = %q, want %q\", got, quotaCurl)\n\t}\n\tif !items[0].QuotaEnabled {\n\t\tt.Fatalf(\"QuotaEnabled = false, want true\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsProxyURL(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"direct\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"proxy-url\":\"socks5://127.0.0.1:7890\"`) {\n\t\t\t\t\tt.Fatalf(\"proxy url not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"socks5://127.0.0.1:7890\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif got := items[0].ProxyURL; got != \"socks5://127.0.0.1:7890\" {\n\t\tt.Fatalf(\"ProxyURL = %q, want socks5://127.0.0.1:7890\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsModels(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"models\":[{\"name\":\"mimo-v2.5-pro\",\"alias\":\"claude-sonnet-4-6\"}]`) {\n\t\t\t\t\tt.Fatalf(\"models not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tModels: []OpenAICompatibleModel{\n\t\t\t{Name: \"mimo-v2.5-pro\", Alias: \"claude-sonnet-4-6\"},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || len(items[0].Models) != 1 {\n\t\tt.Fatalf(\"expected one persisted model, got %#v\", items)\n\t}\n\tif got := items[0].Models[0]; got.Name != \"mimo-v2.5-pro\" || got.Alias != \"claude-sonnet-4-6\" {\n\t\tt.Fatalf(\"unexpected model: %#v\", got)\n\t}\n}\n\nfunc TestDeleteCodexAPIKeyAcceptsDerivedConfigIDForStableLocalRecord(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif got := strings.TrimSpace(string(payload)); got != \"[]\" {\n\t\t\t\t\tt.Fatalf(\"expected empty sidecar sync payload, got %s\", got)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tderivedID := codexAPIKeyConfigIdentityFromInput(item)\n\tif err := app.DeleteCodexAPIKey(derivedID); err != nil {\n\t\tt.Fatalf(\"DeleteCodexAPIKey: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 0 {\n\t\tt.Fatalf(\"expected store to be empty, got %#v\", items)\n\t}\n}\n"}, "8798555b751a2082b983215bebeb76fa139a9f2f": {"t": 1785759476.7381763, "n": 15420, "h": "e81198aba4ff4bb5", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"priority\":1,\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsCodexAPIKey(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(codexAPIKeyAssetIDFromInput(items[0]), true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || !items[0].Disabled {\n\t\tt.Fatalf(\"expected stored codex key to be disabled, got %#v\", items)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar items []cliproxyapi.OpenAICompatibleProvider\n\t\t\t\t\tif err := json.Unmarshal(payload, &items); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif len(items) != 1 || !items[0].Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", items)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"openai-compatible:deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestCreateCodexAPIKeyAllowsDuplicateConfigAsSeparateAccounts(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tvar syncPayloads [][]cliproxyapi.CodexAPIKeyInput\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tvar items []cliproxyapi.CodexAPIKeyInput\n\t\t\t\tif err := json.Unmarshal(payload, &items); err != nil {\n\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t}\n\t\t\t\tsyncPayloads = append(syncPayloads, items)\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tinput := CreateCodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-duplicate\",\n\t\tLabel: \"Primary\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"first CreateCodexAPIKey: %v\", err)\n\t}\n\tinput.Label = \"Copied\"\n\tif err := app.CreateCodexAPIKey(input); err != nil {\n\t\tt.Fatalf(\"second CreateCodexAPIKey with same config: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 2 {\n\t\tt.Fatalf(\"expected 2 stored duplicate config accounts, got %d: %#v\", len(items), items)\n\t}\n\tif items[0].LocalID == \"\" || items[1].LocalID == \"\" || items[0].LocalID == items[1].LocalID {\n\t\tt.Fatalf(\"expected duplicate config accounts to have distinct local ids, got %#v\", items)\n\t}\n\tfor _, item := range items {\n\t\tif item.APIKey != \"sk-test-duplicate\" || item.BaseURL != \"https://api.openai.com/v1\" || item.Prefix != \"team-a\" {\n\t\t\tt.Fatalf(\"unexpected duplicate item config: %#v\", item)\n\t\t}\n\t}\n\tif len(syncPayloads) != 2 || len(syncPayloads[1]) != 2 {\n\t\tt.Fatalf(\"expected second sidecar sync to contain both copied accounts, got %#v\", syncPayloads)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsQuotaCurl(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif strings.Contains(string(payload), \"quota-curl\") || strings.Contains(string(payload), \"quota-enabled\") {\n\t\t\t\t\tt.Fatalf(\"quota curl must stay local and not sync to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tconst quotaCurl = `curl -sS \"https://quota.example.com/api/codex/usage\" -H \"Authorization: Bearer {{apiKey}}\"`\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tQuotaCurl: quotaCurl,\n\t\tQuotaEnabled: true,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].QuotaCurl; got != quotaCurl {\n\t\tt.Fatalf(\"QuotaCurl = %q, want %q\", got, quotaCurl)\n\t}\n\tif !items[0].QuotaEnabled {\n\t\tt.Fatalf(\"QuotaEnabled = false, want true\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsProxyURL(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"direct\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"proxy-url\":\"socks5://127.0.0.1:7890\"`) {\n\t\t\t\t\tt.Fatalf(\"proxy url not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"socks5://127.0.0.1:7890\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif got := items[0].ProxyURL; got != \"socks5://127.0.0.1:7890\" {\n\t\tt.Fatalf(\"ProxyURL = %q, want socks5://127.0.0.1:7890\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsModels(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"models\":[{\"name\":\"mimo-v2.5-pro\",\"alias\":\"claude-sonnet-4-6\"}]`) {\n\t\t\t\t\tt.Fatalf(\"models not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tModels: []OpenAICompatibleModel{\n\t\t\t{Name: \"mimo-v2.5-pro\", Alias: \"claude-sonnet-4-6\"},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || len(items[0].Models) != 1 {\n\t\tt.Fatalf(\"expected one persisted model, got %#v\", items)\n\t}\n\tif got := items[0].Models[0]; got.Name != \"mimo-v2.5-pro\" || got.Alias != \"claude-sonnet-4-6\" {\n\t\tt.Fatalf(\"unexpected model: %#v\", got)\n\t}\n}\n\nfunc TestDeleteCodexAPIKeyAcceptsDerivedConfigIDForStableLocalRecord(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif got := strings.TrimSpace(string(payload)); got != \"[]\" {\n\t\t\t\t\tt.Fatalf(\"expected empty sidecar sync payload, got %s\", got)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tderivedID := codexAPIKeyConfigIdentityFromInput(item)\n\tif err := app.DeleteCodexAPIKey(derivedID); err != nil {\n\t\tt.Fatalf(\"DeleteCodexAPIKey: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 0 {\n\t\tt.Fatalf(\"expected store to be empty, got %#v\", items)\n\t}\n}\n"}, "43720c5f1a4e9f30fdf4f9d4e81996b210fbbd78": {"t": 1785759478.2149193, "n": 13400, "h": "6bfdc16e24021ccc", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"priority\":1,\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsCodexAPIKey(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(codexAPIKeyAssetIDFromInput(items[0]), true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || !items[0].Disabled {\n\t\tt.Fatalf(\"expected stored codex key to be disabled, got %#v\", items)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar items []cliproxyapi.OpenAICompatibleProvider\n\t\t\t\t\tif err := json.Unmarshal(payload, &items); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif len(items) != 1 || !items[0].Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", items)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"openai-compatible:deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsQuotaCurl(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif strings.Contains(string(payload), \"quota-curl\") || strings.Contains(string(payload), \"quota-enabled\") {\n\t\t\t\t\tt.Fatalf(\"quota curl must stay local and not sync to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tconst quotaCurl = `curl -sS \"https://quota.example.com/api/codex/usage\" -H \"Authorization: Bearer {{apiKey}}\"`\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tQuotaCurl: quotaCurl,\n\t\tQuotaEnabled: true,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].QuotaCurl; got != quotaCurl {\n\t\tt.Fatalf(\"QuotaCurl = %q, want %q\", got, quotaCurl)\n\t}\n\tif !items[0].QuotaEnabled {\n\t\tt.Fatalf(\"QuotaEnabled = false, want true\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsProxyURL(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"direct\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"proxy-url\":\"socks5://127.0.0.1:7890\"`) {\n\t\t\t\t\tt.Fatalf(\"proxy url not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"socks5://127.0.0.1:7890\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif got := items[0].ProxyURL; got != \"socks5://127.0.0.1:7890\" {\n\t\tt.Fatalf(\"ProxyURL = %q, want socks5://127.0.0.1:7890\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsModels(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"models\":[{\"name\":\"mimo-v2.5-pro\",\"alias\":\"claude-sonnet-4-6\"}]`) {\n\t\t\t\t\tt.Fatalf(\"models not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tModels: []OpenAICompatibleModel{\n\t\t\t{Name: \"mimo-v2.5-pro\", Alias: \"claude-sonnet-4-6\"},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || len(items[0].Models) != 1 {\n\t\tt.Fatalf(\"expected one persisted model, got %#v\", items)\n\t}\n\tif got := items[0].Models[0]; got.Name != \"mimo-v2.5-pro\" || got.Alias != \"claude-sonnet-4-6\" {\n\t\tt.Fatalf(\"unexpected model: %#v\", got)\n\t}\n}\n\nfunc TestDeleteCodexAPIKeyAcceptsDerivedConfigIDForStableLocalRecord(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif got := strings.TrimSpace(string(payload)); got != \"[]\" {\n\t\t\t\t\tt.Fatalf(\"expected empty sidecar sync payload, got %s\", got)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tderivedID := codexAPIKeyConfigIdentityFromInput(item)\n\tif err := app.DeleteCodexAPIKey(derivedID); err != nil {\n\t\tt.Fatalf(\"DeleteCodexAPIKey: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 0 {\n\t\tt.Fatalf(\"expected store to be empty, got %#v\", items)\n\t}\n}\n"}, "8057be7da252981140fe0861cdd8f8fab297e49d": {"t": 1785759479.5393074, "n": 11679, "h": "8bc4fb0ed6fb1f71", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"priority\":1,\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsCodexAPIKey(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(codexAPIKeyAssetIDFromInput(items[0]), true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || !items[0].Disabled {\n\t\tt.Fatalf(\"expected stored codex key to be disabled, got %#v\", items)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar items []cliproxyapi.OpenAICompatibleProvider\n\t\t\t\t\tif err := json.Unmarshal(payload, &items); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif len(items) != 1 || !items[0].Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", items)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"openai-compatible:deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsQuotaCurl(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif strings.Contains(string(payload), \"quota-curl\") || strings.Contains(string(payload), \"quota-enabled\") {\n\t\t\t\t\tt.Fatalf(\"quota curl must stay local and not sync to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tconst quotaCurl = `curl -sS \"https://quota.example.com/api/codex/usage\" -H \"Authorization: Bearer {{apiKey}}\"`\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tQuotaCurl: quotaCurl,\n\t\tQuotaEnabled: true,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].QuotaCurl; got != quotaCurl {\n\t\tt.Fatalf(\"QuotaCurl = %q, want %q\", got, quotaCurl)\n\t}\n\tif !items[0].QuotaEnabled {\n\t\tt.Fatalf(\"QuotaEnabled = false, want true\")\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsProxyURL(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"direct\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"proxy-url\":\"socks5://127.0.0.1:7890\"`) {\n\t\t\t\t\tt.Fatalf(\"proxy url not synced to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tProxyURL: \"socks5://127.0.0.1:7890\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif got := items[0].ProxyURL; got != \"socks5://127.0.0.1:7890\" {\n\t\tt.Fatalf(\"ProxyURL = %q, want socks5://127.0.0.1:7890\", got)\n\t}\n}\n\nfunc TestDeleteCodexAPIKeyAcceptsDerivedConfigIDForStableLocalRecord(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif got := strings.TrimSpace(string(payload)); got != \"[]\" {\n\t\t\t\t\tt.Fatalf(\"expected empty sidecar sync payload, got %s\", got)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tderivedID := codexAPIKeyConfigIdentityFromInput(item)\n\tif err := app.DeleteCodexAPIKey(derivedID); err != nil {\n\t\tt.Fatalf(\"DeleteCodexAPIKey: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 0 {\n\t\tt.Fatalf(\"expected store to be empty, got %#v\", items)\n\t}\n}\n"}, "d68cd13cbf8e2eb47283bf5719a015d3bc012875": {"t": 1785759480.8602076, "n": 10131, "h": "172d48121fb2850d", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"priority\":1,\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsCodexAPIKey(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(codexAPIKeyAssetIDFromInput(items[0]), true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || !items[0].Disabled {\n\t\tt.Fatalf(\"expected stored codex key to be disabled, got %#v\", items)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar items []cliproxyapi.OpenAICompatibleProvider\n\t\t\t\t\tif err := json.Unmarshal(payload, &items); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif len(items) != 1 || !items[0].Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", items)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"openai-compatible:deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPersistsQuotaCurl(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif strings.Contains(string(payload), \"quota-curl\") || strings.Contains(string(payload), \"quota-enabled\") {\n\t\t\t\t\tt.Fatalf(\"quota curl must stay local and not sync to sidecar: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tconst quotaCurl = `curl -sS \"https://quota.example.com/api/codex/usage\" -H \"Authorization: Bearer {{apiKey}}\"`\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tQuotaCurl: quotaCurl,\n\t\tQuotaEnabled: true,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].QuotaCurl; got != quotaCurl {\n\t\tt.Fatalf(\"QuotaCurl = %q, want %q\", got, quotaCurl)\n\t}\n\tif !items[0].QuotaEnabled {\n\t\tt.Fatalf(\"QuotaEnabled = false, want true\")\n\t}\n}\n\nfunc TestDeleteCodexAPIKeyAcceptsDerivedConfigIDForStableLocalRecord(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif got := strings.TrimSpace(string(payload)); got != \"[]\" {\n\t\t\t\t\tt.Fatalf(\"expected empty sidecar sync payload, got %s\", got)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tderivedID := codexAPIKeyConfigIdentityFromInput(item)\n\tif err := app.DeleteCodexAPIKey(derivedID); err != nil {\n\t\tt.Fatalf(\"DeleteCodexAPIKey: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 0 {\n\t\tt.Fatalf(\"expected store to be empty, got %#v\", items)\n\t}\n}\n"}, "4116a0e705a27c354467eb480dfcd3961f583af5": {"t": 1785759482.0231504, "n": 8299, "h": "3960e6e682fd6145", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"priority\":1,\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsCodexAPIKey(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(codexAPIKeyAssetIDFromInput(items[0]), true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || !items[0].Disabled {\n\t\tt.Fatalf(\"expected stored codex key to be disabled, got %#v\", items)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar items []cliproxyapi.OpenAICompatibleProvider\n\t\t\t\t\tif err := json.Unmarshal(payload, &items); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif len(items) != 1 || !items[0].Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", items)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"openai-compatible:deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n\nfunc TestDeleteCodexAPIKeyAcceptsDerivedConfigIDForStableLocalRecord(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif got := strings.TrimSpace(string(payload)); got != \"[]\" {\n\t\t\t\t\tt.Fatalf(\"expected empty sidecar sync payload, got %s\", got)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tderivedID := codexAPIKeyConfigIdentityFromInput(item)\n\tif err := app.DeleteCodexAPIKey(derivedID); err != nil {\n\t\tt.Fatalf(\"DeleteCodexAPIKey: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 0 {\n\t\tt.Fatalf(\"expected store to be empty, got %#v\", items)\n\t}\n}\n"}, "3133b4168464e6e1957050cffcd1b19e6d5ad4d0": {"t": 1785759483.4007275, "n": 6929, "h": "f47a4318fe657dda", "c": "package wailsapp\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(items[0]),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"priority\":1,\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsCodexAPIKey(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"disabled\":true`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(codexAPIKeyAssetIDFromInput(items[0]), true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n\n\titems, err = loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 || !items[0].Disabled {\n\t\tt.Fatalf(\"expected stored codex key to be disabled, got %#v\", items)\n\t}\n}\n\nfunc TestSetAccountDisabledSupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tvar items []cliproxyapi.OpenAICompatibleProvider\n\t\t\t\t\tif err := json.Unmarshal(payload, &items); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"unmarshal payload: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif len(items) != 1 || !items[0].Disabled {\n\t\t\t\t\t\tt.Fatalf(\"expected provider to be disabled, got %#v\", items)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.SetAccountDisabled(\"openai-compatible:deepseek\", true); err != nil {\n\t\tt.Fatalf(\"SetAccountDisabled: %v\", err)\n\t}\n}\n\nfunc TestUpdateCodexAPIKeyConfigPreservesStableID(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tLocalID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\tPrefix: \"team-a\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method != \"PUT\" || path != \"/v0/management/codex-api-key\" {\n\t\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\t}\n\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(string(payload), `\"api-key\":\"sk-test-2222\"`) {\n\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t}\n\t\t\t\treturn nil, 200, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateCodexAPIKeyConfig(UpdateCodexAPIKeyConfigInput{\n\t\tID: \"codex-api-key:stable-001\",\n\t\tAPIKey: \"sk-test-2222\",\n\t\tBaseURL: \"https://api.example.com/v2\",\n\t\tPrefix: \"team-b\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyConfig: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif got := items[0].LocalID; got != \"codex-api-key:stable-001\" {\n\t\tt.Fatalf(\"LocalID = %q, want codex-api-key:stable-001\", got)\n\t}\n\tif got := items[0].APIKey; got != \"sk-test-2222\" {\n\t\tt.Fatalf(\"APIKey = %q, want sk-test-2222\", got)\n\t}\n\tif got := items[0].BaseURL; got != \"https://api.example.com/v2\" {\n\t\tt.Fatalf(\"BaseURL = %q, want https://api.example.com/v2\", got)\n\t}\n\tif got := items[0].Prefix; got != \"team-b\" {\n\t\tt.Fatalf(\"Prefix = %q, want team-b\", got)\n\t}\n}\n"}, "bb3a6fe1285b767b9c6944289e1f9e6c1a9a8666": {"t": 1785759484.795601, "n": 2117, "h": "b7dbc878abb71ca2", "c": "package wailsapp\n\nimport (\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestUpdateCodexAPIKeyLabelPersistsToStore(t *testing.T) {\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\titem := cliproxyapi.CodexAPIKeyInput{\n\t\tAPIKey: \"sk-test-1111\",\n\t\tBaseURL: \"https://api.openai.com/v1\",\n\t}\n\tif err := persistCodexAPIKeySet([]cliproxyapi.CodexAPIKeyInput{item}); err != nil {\n\t\tt.Fatalf(\"persistCodexAPIKeySet: %v\", err)\n\t}\n\n\tapp := &App{}\n\tif err := app.UpdateCodexAPIKeyLabel(UpdateCodexAPIKeyLabelInput{\n\t\tID: codexAPIKeyAssetIDFromInput(item),\n\t\tLabel: \"PRIMARY PROD KEY\",\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateCodexAPIKeyLabel: %v\", err)\n\t}\n\n\titems, err := loadStoredCodexAPIKeys()\n\tif err != nil {\n\t\tt.Fatalf(\"loadStoredCodexAPIKeys: %v\", err)\n\t}\n\tif len(items) != 1 {\n\t\tt.Fatalf(\"expected 1 item, got %d\", len(items))\n\t}\n\tif items[0].Label != \"PRIMARY PROD KEY\" {\n\t\tt.Fatalf(\"Label = %q, want PRIMARY PROD KEY\", items[0].Label)\n\t}\n}\n\nfunc TestUpdateAccountPrioritySupportsOpenAICompatibleProvider(t *testing.T) {\n\tapp := &App{\n\t\tmanagementAPI: func() *cliproxyapi.Client {\n\t\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\t\tif method == \"GET\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\treturn []byte(`{\"openai-compatibility\":[{\"name\":\"deepseek\",\"priority\":1,\"base-url\":\"https://api.deepseek.com/v1\",\"api-key-entries\":[{\"api-key\":\"sk-old\"}]}]}`), 200, nil\n\t\t\t\t}\n\t\t\t\tif method == \"PUT\" && path == \"/v0/management/openai-compatibility\" {\n\t\t\t\t\tpayload, err := io.ReadAll(body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"read body: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.Contains(string(payload), `\"priority\":5`) {\n\t\t\t\t\t\tt.Fatalf(\"unexpected payload: %s\", payload)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, 200, nil\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\t\treturn nil, 0, nil\n\t\t\t})\n\t\t},\n\t}\n\n\tif err := app.UpdateAccountPriority(UpdateAccountPriorityInput{\n\t\tID: \"openai-compatible:deepseek\",\n\t\tPriority: 5,\n\t}); err != nil {\n\t\tt.Fatalf(\"UpdateAccountPriority: %v\", err)\n\t}\n}\n"}}, "internal/wailsapp/relay_model_account_cache_test.go": {"6a6556121411b7445b1e0e28d0b7af6920236b59": {"t": 1785746086.4191065, "n": 19098, "h": "1fd934f22e1f63f8", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestRelayModelAccountCacheRoundTrip(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\n\tsnapshots := []relayModelAccountSnapshot{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tKind: \"openai-compatible\",\n\t\t\tProviderName: \"DeepSeek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\", Alias: \"deepseek-v4-flash\", SupportedReasoningEfforts: []string{\"low\", \"high\"}, DefaultReasoningEffort: \"high\"},\n\t\t\t\t{Name: \"deepseek-v4-pro\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err := saveRelayModelAccountCache(snapshots); err != nil {\n\t\tt.Fatalf(\"saveRelayModelAccountCache returned error: %v\", err)\n\t}\n\n\tpath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath returned error: %v\", err)\n\t}\n\tif filepath.Dir(path) != filepath.Join(home, \".config\", \"gettokens-data\", \"codex-model-account-cache\") {\n\t\tt.Fatalf(\"unexpected cache path: %s\", path)\n\t}\n\n\tloaded, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"loadRelayModelAccountCache returned error: %v\", err)\n\t}\n\tif len(loaded) != 1 || loaded[0].AccountKey != \"acct_deepseek\" || len(loaded[0].Models) != 2 {\n\t\tt.Fatalf(\"unexpected loaded cache: %#v\", loaded)\n\t}\n\tif loaded[0].Models[0].Name != \"deepseek-v4-flash\" || loaded[0].Models[0].DefaultReasoningEffort != \"high\" {\n\t\tt.Fatalf(\"unexpected normalized model: %#v\", loaded[0].Models[0])\n\t}\n}\n\nfunc TestListRelaySupportedModelsUsesAccountCacheForActiveAccountWhenRemoteUnavailable(t *testing.T) {\n\tproviders := []OpenAICompatibleProvider{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tName: \"DeepSeek\",\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-deepseek\",\n\t\t},\n\t\t{\n\t\t\tAccountKey: \"acct_disabled\",\n\t\t\tName: \"Disabled DeepSeek\",\n\t\t\tDisabled: true,\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-disabled\",\n\t\t},\n\t}\n\tcached := map[string]relayModelAccountSnapshot{\n\t\t\"acct_deepseek\": {\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\"},\n\t\t\t},\n\t\t},\n\t\t\"acct_disabled\": {\n\t\t\tAccountKey: \"acct_disabled\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"should-not-appear\"},\n\t\t\t},\n\t\t},\n\t\t\"acct_deleted\": {\n\t\t\tAccountKey: \"acct_deleted\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deleted-should-not-appear\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(providers, nil, func(input FetchOpenAICompatibleProviderModelsInput) ([]OpenAICompatibleModel, error) {\n\t\treturn nil, os.ErrNotExist\n\t}, nil, []OpenAICompatibleModel{{Name: \"deepseek-sidecar-only\"}}, cached)\n\n\tif len(models) != 1 || models[0].Name != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"expected only active account cached model, got %#v\", models)\n\t}\n\tif len(snapshots) != 1 || snapshots[0].AccountKey != \"acct_deepseek\" || len(snapshots[0].Models) != 1 {\n\t\tt.Fatalf(\"unexpected snapshots: %#v\", snapshots)\n\t}\n}\n\nfunc TestListRelaySupportedModelsRefreshesAccountCacheWithLatestRemoteModels(t *testing.T) {\n\tproviders := []OpenAICompatibleProvider{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tName: \"DeepSeek\",\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-deepseek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(providers, nil, func(input FetchOpenAICompatibleProviderModelsInput) ([]OpenAICompatibleModel, error) {\n\t\treturn []OpenAICompatibleModel{{Name: \"deepseek-v4-pro\"}}, nil\n\t}, nil, nil, nil)\n\n\tif len(models) != 2 || models[0].Name != \"deepseek-v4-flash\" || models[1].Name != \"deepseek-v4-pro\" {\n\t\tt.Fatalf(\"unexpected refreshed models: %#v\", models)\n\t}\n\tif len(snapshots) != 1 || len(snapshots[0].Models) != 2 {\n\t\tt.Fatalf(\"expected latest per-account snapshot, got %#v\", snapshots)\n\t}\n}\n\nfunc TestLoadRelaySupportedModelsFromAccountCacheIncludesLocalCodexCache(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif err := os.MkdirAll(codexHome, 0700); err != nil {\n\t\tt.Fatalf(\"mkdir codex home: %v\", err)\n\t}\n\tif err := os.WriteFile(filepath.Join(codexHome, \"models_cache.json\"), []byte(`{\"models\":[{\"slug\":\"gpt-5.5\",\"display_name\":\"GPT-5.5\"}]}`), 0600); err != nil {\n\t\tt.Fatalf(\"write local codex cache: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tmodels, err := loadRelaySupportedModelsFromAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"loadRelaySupportedModelsFromAccountCache returned error: %v\", err)\n\t}\n\tif len(models) != 2 || models[0].Name != \"deepseek-v4-flash\" || models[1].Name != \"gpt-5.5\" {\n\t\tt.Fatalf(\"unexpected cached startup models: %#v\", models)\n\t}\n}\n\nfunc TestCodexAPIKeyAccountCacheUsesLocalID(t *testing.T) {\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(nil, []cliproxyapi.CodexAPIKey{{\n\t\tLocalID: \"acct_codex_key\",\n\t\tModels: []cliproxyapi.CodexModel{{Name: \"mimo-v2-pro\"}},\n\t}}, nil, nil, nil, nil)\n\tif len(models) != 1 || models[0].Name != \"mimo-v2-pro\" {\n\t\tt.Fatalf(\"unexpected codex key models: %#v\", models)\n\t}\n\tif len(snapshots) != 1 || snapshots[0].AccountKey != \"acct_codex_key\" || snapshots[0].Kind != \"codex-api-key\" {\n\t\tt.Fatalf(\"unexpected codex key snapshot: %#v\", snapshots)\n\t}\n}\n\nfunc TestApplyPersistedCodexModelCatalogCacheSnapshotWritesCatalogBeforeSidecarReady(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := applyPersistedCodexModelCatalogCacheSnapshot(); err != nil {\n\t\tt.Fatalf(\"applyPersistedCodexModelCatalogCacheSnapshot returned error: %v\", err)\n\t}\n\n\tcatalogBody, err := os.ReadFile(filepath.Join(codexHome, gettokensCodexModelCatalogFilename))\n\tif err != nil {\n\t\tt.Fatalf(\"read model catalog: %v\", err)\n\t}\n\tif !strings.Contains(string(catalogBody), \"deepseek-v4-flash\") {\n\t\tt.Fatalf(\"catalog does not contain cached DeepSeek model: %s\", catalogBody)\n\t}\n\tconfigBody, err := os.ReadFile(filepath.Join(codexHome, \"config.toml\"))\n\tif err != nil {\n\t\tt.Fatalf(\"read codex config: %v\", err)\n\t}\n\tif !strings.Contains(string(configBody), \"model_catalog_json\") {\n\t\tt.Fatalf(\"config does not contain model_catalog_json pointer: %s\", configBody)\n\t}\n}\n\nfunc TestApplyPersistedCodexModelCatalogCacheSnapshotSkipsWhenDisabled(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tt.Setenv(\"CODEX_HOME\", filepath.Join(home, \".codex\"))\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: false}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := applyPersistedCodexModelCatalogCacheSnapshot(); err != nil {\n\t\tt.Fatalf(\"apply disabled setting returned error: %v\", err)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".codex\", gettokensCodexModelCatalogFilename)); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"expected no catalog when sync disabled, stat err=%v\", err)\n\t}\n}\n\nfunc TestRefreshCodexModelCatalogAfterAccountMutationUpdatesCacheAndCatalog(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\n\taccount := testOpenAICompatibleAccount(\n\t\t\"acct_deepseek\",\n\t\t\"DeepSeek\",\n\t\t0,\n\t\tfalse,\n\t\t\"https://api.deepseek.com/v1\",\n\t\t\"\",\n\t\tnil,\n\t\tnil,\n\t\t[]cliproxyapi.OpenAICompatibleModel{{Name: \"deepseek-v4-pro\"}},\n\t)\n\tapp := &App{ctx: context.Background()}\n\tapp.managementAPI = func() *cliproxyapi.Client {\n\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\tif method == http.MethodGet && path == \"/v0/management/accounts\" {\n\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t}\n\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\treturn nil, 0, nil\n\t\t})\n\t}\n\n\tif err := app.refreshCodexModelCatalogAfterAccountMutation(); err != nil {\n\t\tt.Fatalf(\"refreshCodexModelCatalogAfterAccountMutation returned error: %v\", err)\n\t}\n\n\tcached, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"load cache: %v\", err)\n\t}\n\tif len(cached) != 1 || cached[0].AccountKey != \"acct_deepseek\" || cached[0].Models[0].Name != \"deepseek-v4-pro\" {\n\t\tt.Fatalf(\"unexpected refreshed cache: %#v\", cached)\n\t}\n\tcatalogBody, err := os.ReadFile(filepath.Join(codexHome, gettokensCodexModelCatalogFilename))\n\tif err != nil {\n\t\tt.Fatalf(\"read model catalog: %v\", err)\n\t}\n\tif !strings.Contains(string(catalogBody), \"deepseek-v4-pro\") {\n\t\tt.Fatalf(\"catalog does not contain refreshed model: %s\", catalogBody)\n\t}\n}\n\nfunc TestRefreshCodexModelCatalogAfterAccountMutationClearsCatalogWhenNoActiveModels(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := os.MkdirAll(codexHome, 0700); err != nil {\n\t\tt.Fatalf(\"mkdir codex home: %v\", err)\n\t}\n\tif _, err := enableGetTokensCodexModelCatalogProjection([]OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}, false); err != nil {\n\t\tt.Fatalf(\"seed catalog: %v\", err)\n\t}\n\n\taccount := testOpenAICompatibleAccount(\n\t\t\"acct_deepseek\",\n\t\t\"DeepSeek\",\n\t\t0,\n\t\ttrue,\n\t\t\"https://api.deepseek.com/v1\",\n\t\t\"\",\n\t\tnil,\n\t\tnil,\n\t\t[]cliproxyapi.OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t)\n\tapp := &App{ctx: context.Background()}\n\tapp.managementAPI = func() *cliproxyapi.Client {\n\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\tif method == http.MethodGet && path == \"/v0/management/accounts\" {\n\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t}\n\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\treturn nil, 0, nil\n\t\t})\n\t}\n\n\tif err := app.refreshCodexModelCatalogAfterAccountMutation(); err != nil {\n\t\tt.Fatalf(\"refreshCodexModelCatalogAfterAccountMutation returned error: %v\", err)\n\t}\n\tconfigBody, err := os.ReadFile(filepath.Join(codexHome, \"config.toml\"))\n\tif err != nil {\n\t\tt.Fatalf(\"read codex config: %v\", err)\n\t}\n\tif strings.Contains(string(configBody), \"model_catalog_json\") {\n\t\tt.Fatalf(\"expected stale model_catalog_json pointer to be removed, got: %s\", configBody)\n\t}\n\tcached, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"load cache: %v\", err)\n\t}\n\tif len(cached) != 0 {\n\t\tt.Fatalf(\"disabled account should clear cache, got %#v\", cached)\n\t}\n}\n\nfunc TestPruneRelayModelAccountCacheEntriesRemovesDeletedAccountImmediately(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{\n\t\t{AccountKey: \"acct_keep\", Kind: \"openai-compatible\", Models: []OpenAICompatibleModel{{Name: \"gpt-5.5\"}}},\n\t\t{AccountKey: \"acct_delete\", Kind: \"openai-compatible\", Models: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}},\n\t}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := pruneRelayModelAccountCacheEntries(\"acct_delete\"); err != nil {\n\t\tt.Fatalf(\"pruneRelayModelAccountCacheEntries returned error: %v\", err)\n\t}\n\tcached, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"load cache: %v\", err)\n\t}\n\tif len(cached) != 1 || cached[0].AccountKey != \"acct_keep\" {\n\t\tt.Fatalf(\"unexpected pruned cache: %#v\", cached)\n\t}\n}\n\nfunc TestScheduleCodexModelCatalogRefreshDebouncesAndRunsOnce(t *testing.T) {\n\tapp := &App{ctx: context.Background()}\n\tapp.codexModelCatalogRefreshDebounce = time.Millisecond\n\tcalls := 0\n\tdone := make(chan struct{}, 1)\n\tapp.codexModelCatalogRefreshFunc = func() error {\n\t\tcalls++\n\t\tdone <- struct{}{}\n\t\treturn nil\n\t}\n\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for debounced refresh\")\n\t}\n\ttime.Sleep(20 * time.Millisecond)\n\tif calls != 1 {\n\t\tt.Fatalf(\"expected one debounced refresh, got %d\", calls)\n\t}\n}\n\nfunc TestScheduleCodexModelCatalogRefreshRunsPendingAfterInFlight(t *testing.T) {\n\tapp := &App{ctx: context.Background()}\n\tapp.codexModelCatalogRefreshDebounce = time.Millisecond\n\tstarted := make(chan struct{}, 2)\n\trelease := make(chan struct{})\n\tdone := make(chan struct{}, 2)\n\tcalls := 0\n\tapp.codexModelCatalogRefreshFunc = func() error {\n\t\tcalls++\n\t\tstarted <- struct{}{}\n\t\tif calls == 1 {\n\t\t\t<-release\n\t\t}\n\t\tdone <- struct{}{}\n\t\treturn nil\n\t}\n\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tselect {\n\tcase <-started:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for first refresh\")\n\t}\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tclose(release)\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for first completion\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for pending refresh\")\n\t}\n\tif calls != 2 {\n\t\tt.Fatalf(\"expected pending refresh to run once after in-flight refresh, got %d\", calls)\n\t}\n}\n\nfunc TestApplyGetTokensCodexModelCatalogProjectionDoesNotRewriteUnchangedCatalog(t *testing.T) {\n\tcodexHome := t.TempDir()\n\tmodels := []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}\n\tif _, _, _, _, err := applyGetTokensCodexModelCatalogProjection(\"\", codexHome, models, false); err != nil {\n\t\tt.Fatalf(\"first apply: %v\", err)\n\t}\n\tcatalogPath := filepath.Join(codexHome, gettokensCodexModelCatalogFilename)\n\tbefore, err := os.Stat(catalogPath)\n\tif err != nil {\n\t\tt.Fatalf(\"stat catalog: %v\", err)\n\t}\n\ttime.Sleep(10 * time.Millisecond)\n\tif _, _, _, _, err := applyGetTokensCodexModelCatalogProjection(\"\", codexHome, models, false); err != nil {\n\t\tt.Fatalf(\"second apply: %v\", err)\n\t}\n\tafter, err := os.Stat(catalogPath)\n\tif err != nil {\n\t\tt.Fatalf(\"stat catalog after: %v\", err)\n\t}\n\tif !after.ModTime().Equal(before.ModTime()) {\n\t\tt.Fatalf(\"unchanged catalog should not be rewritten: before=%s after=%s\", before.ModTime(), after.ModTime())\n\t}\n}\n\nfunc TestCodexModelCatalogDiagnosticsReportsCacheCatalogAndWarnings(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tProviderName: \"DeepSeek\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\tif _, err := enableGetTokensCodexModelCatalogProjection([]OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}, false); err != nil {\n\t\tt.Fatalf(\"enable catalog projection: %v\", err)\n\t}\n\n\tdiagnostics, err := GetCodexModelCatalogDiagnostics()\n\tif err != nil {\n\t\tt.Fatalf(\"GetCodexModelCatalogDiagnostics returned error: %v\", err)\n\t}\n\tif !diagnostics.SyncEnabled || !diagnostics.HasGetTokensCatalogPointer || diagnostics.CatalogModelCount != 1 {\n\t\tt.Fatalf(\"unexpected diagnostics summary: %#v\", diagnostics)\n\t}\n\tif diagnostics.CachedAccountCount != 1 || diagnostics.CachedModelCount != 1 {\n\t\tt.Fatalf(\"unexpected cache counts: %#v\", diagnostics)\n\t}\n\tif len(diagnostics.Models) != 1 || diagnostics.Models[0].Slug != \"deepseek-v4-flash\" || diagnostics.Models[0].SourceAccounts[0] != \"acct_deepseek\" {\n\t\tt.Fatalf(\"unexpected diagnostics models: %#v\", diagnostics.Models)\n\t}\n}\n\nfunc TestCodexModelCatalogDiagnosticsWarnsForExternalCatalogPointer(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif err := os.MkdirAll(codexHome, 0700); err != nil {\n\t\tt.Fatalf(\"mkdir codex home: %v\", err)\n\t}\n\tif err := os.WriteFile(filepath.Join(codexHome, \"config.toml\"), []byte(\"model_catalog_json = \\\"/tmp/external-catalog.json\\\"\\n\"), 0600); err != nil {\n\t\tt.Fatalf(\"write config: %v\", err)\n\t}\n\n\tdiagnostics, err := GetCodexModelCatalogDiagnostics()\n\tif err != nil {\n\t\tt.Fatalf(\"GetCodexModelCatalogDiagnostics returned error: %v\", err)\n\t}\n\tif diagnostics.HasGetTokensCatalogPointer {\n\t\tt.Fatalf(\"expected external pointer, got %#v\", diagnostics)\n\t}\n\tjoined := strings.Join(diagnostics.Warnings, \"\\n\")\n\tif !strings.Contains(joined, \"外部 model_catalog_json\") {\n\t\tt.Fatalf(\"expected external pointer warning, got %#v\", diagnostics.Warnings)\n\t}\n}\n\nfunc TestRelayModelCatalogTraceWrittenFromAccountSnapshots(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tt.Setenv(\"CODEX_HOME\", filepath.Join(home, \".codex\"))\n\taccount := testOpenAICompatibleAccount(\n\t\t\"acct_deepseek\",\n\t\t\"DeepSeek\",\n\t\t0,\n\t\tfalse,\n\t\t\"https://api.deepseek.com/v1\",\n\t\t\"\",\n\t\tnil,\n\t\tnil,\n\t\t[]cliproxyapi.OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t)\n\tapp := &App{}\n\tapp.managementAPI = func() *cliproxyapi.Client {\n\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\tif method == http.MethodGet && path == \"/v0/management/accounts\" {\n\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t}\n\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\treturn nil, 0, nil\n\t\t})\n\t}\n\n\tmodels, err := app.ListRelaySupportedModels()\n\tif err != nil {\n\t\tt.Fatalf(\"ListRelaySupportedModels returned error: %v\", err)\n\t}\n\tif len(models) != 1 || models[0].Name != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"unexpected models: %#v\", models)\n\t}\n\ttrace, err := loadRelayModelCatalogTrace()\n\tif err != nil {\n\t\tt.Fatalf(\"load trace: %v\", err)\n\t}\n\tif len(trace.Accounts) != 1 || trace.Accounts[0].AccountKey != \"acct_deepseek\" {\n\t\tt.Fatalf(\"unexpected trace accounts: %#v\", trace)\n\t}\n\tif len(trace.Models) != 1 || trace.Models[0].Slug != \"deepseek-v4-flash\" || trace.Models[0].SourceAccounts[0] != \"acct_deepseek\" {\n\t\tt.Fatalf(\"unexpected trace models: %#v\", trace)\n\t}\n}\n"}, "76a7226c847507681955f5fb2f35b558173018dc": {"t": 1785759456.7321877, "n": 19098, "h": "1fd934f22e1f63f8", "c": "package wailsapp\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestRelayModelAccountCacheRoundTrip(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\n\tsnapshots := []relayModelAccountSnapshot{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tKind: \"openai-compatible\",\n\t\t\tProviderName: \"DeepSeek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\", Alias: \"deepseek-v4-flash\", SupportedReasoningEfforts: []string{\"low\", \"high\"}, DefaultReasoningEffort: \"high\"},\n\t\t\t\t{Name: \"deepseek-v4-pro\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err := saveRelayModelAccountCache(snapshots); err != nil {\n\t\tt.Fatalf(\"saveRelayModelAccountCache returned error: %v\", err)\n\t}\n\n\tpath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath returned error: %v\", err)\n\t}\n\tif filepath.Dir(path) != filepath.Join(home, \".config\", \"gettokens-data\", \"codex-model-account-cache\") {\n\t\tt.Fatalf(\"unexpected cache path: %s\", path)\n\t}\n\n\tloaded, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"loadRelayModelAccountCache returned error: %v\", err)\n\t}\n\tif len(loaded) != 1 || loaded[0].AccountKey != \"acct_deepseek\" || len(loaded[0].Models) != 2 {\n\t\tt.Fatalf(\"unexpected loaded cache: %#v\", loaded)\n\t}\n\tif loaded[0].Models[0].Name != \"deepseek-v4-flash\" || loaded[0].Models[0].DefaultReasoningEffort != \"high\" {\n\t\tt.Fatalf(\"unexpected normalized model: %#v\", loaded[0].Models[0])\n\t}\n}\n\nfunc TestListRelaySupportedModelsUsesAccountCacheForActiveAccountWhenRemoteUnavailable(t *testing.T) {\n\tproviders := []OpenAICompatibleProvider{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tName: \"DeepSeek\",\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-deepseek\",\n\t\t},\n\t\t{\n\t\t\tAccountKey: \"acct_disabled\",\n\t\t\tName: \"Disabled DeepSeek\",\n\t\t\tDisabled: true,\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-disabled\",\n\t\t},\n\t}\n\tcached := map[string]relayModelAccountSnapshot{\n\t\t\"acct_deepseek\": {\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\"},\n\t\t\t},\n\t\t},\n\t\t\"acct_disabled\": {\n\t\t\tAccountKey: \"acct_disabled\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"should-not-appear\"},\n\t\t\t},\n\t\t},\n\t\t\"acct_deleted\": {\n\t\t\tAccountKey: \"acct_deleted\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deleted-should-not-appear\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(providers, nil, func(input FetchOpenAICompatibleProviderModelsInput) ([]OpenAICompatibleModel, error) {\n\t\treturn nil, os.ErrNotExist\n\t}, nil, []OpenAICompatibleModel{{Name: \"deepseek-sidecar-only\"}}, cached)\n\n\tif len(models) != 1 || models[0].Name != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"expected only active account cached model, got %#v\", models)\n\t}\n\tif len(snapshots) != 1 || snapshots[0].AccountKey != \"acct_deepseek\" || len(snapshots[0].Models) != 1 {\n\t\tt.Fatalf(\"unexpected snapshots: %#v\", snapshots)\n\t}\n}\n\nfunc TestListRelaySupportedModelsRefreshesAccountCacheWithLatestRemoteModels(t *testing.T) {\n\tproviders := []OpenAICompatibleProvider{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tName: \"DeepSeek\",\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-deepseek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(providers, nil, func(input FetchOpenAICompatibleProviderModelsInput) ([]OpenAICompatibleModel, error) {\n\t\treturn []OpenAICompatibleModel{{Name: \"deepseek-v4-pro\"}}, nil\n\t}, nil, nil, nil)\n\n\tif len(models) != 2 || models[0].Name != \"deepseek-v4-flash\" || models[1].Name != \"deepseek-v4-pro\" {\n\t\tt.Fatalf(\"unexpected refreshed models: %#v\", models)\n\t}\n\tif len(snapshots) != 1 || len(snapshots[0].Models) != 2 {\n\t\tt.Fatalf(\"expected latest per-account snapshot, got %#v\", snapshots)\n\t}\n}\n\nfunc TestLoadRelaySupportedModelsFromAccountCacheIncludesLocalCodexCache(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif err := os.MkdirAll(codexHome, 0700); err != nil {\n\t\tt.Fatalf(\"mkdir codex home: %v\", err)\n\t}\n\tif err := os.WriteFile(filepath.Join(codexHome, \"models_cache.json\"), []byte(`{\"models\":[{\"slug\":\"gpt-5.5\",\"display_name\":\"GPT-5.5\"}]}`), 0600); err != nil {\n\t\tt.Fatalf(\"write local codex cache: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tmodels, err := loadRelaySupportedModelsFromAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"loadRelaySupportedModelsFromAccountCache returned error: %v\", err)\n\t}\n\tif len(models) != 2 || models[0].Name != \"deepseek-v4-flash\" || models[1].Name != \"gpt-5.5\" {\n\t\tt.Fatalf(\"unexpected cached startup models: %#v\", models)\n\t}\n}\n\nfunc TestCodexAPIKeyAccountCacheUsesLocalID(t *testing.T) {\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(nil, []cliproxyapi.CodexAPIKey{{\n\t\tLocalID: \"acct_codex_key\",\n\t\tModels: []cliproxyapi.CodexModel{{Name: \"mimo-v2-pro\"}},\n\t}}, nil, nil, nil, nil)\n\tif len(models) != 1 || models[0].Name != \"mimo-v2-pro\" {\n\t\tt.Fatalf(\"unexpected codex key models: %#v\", models)\n\t}\n\tif len(snapshots) != 1 || snapshots[0].AccountKey != \"acct_codex_key\" || snapshots[0].Kind != \"codex-api-key\" {\n\t\tt.Fatalf(\"unexpected codex key snapshot: %#v\", snapshots)\n\t}\n}\n\nfunc TestApplyPersistedCodexModelCatalogCacheSnapshotWritesCatalogBeforeSidecarReady(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := applyPersistedCodexModelCatalogCacheSnapshot(); err != nil {\n\t\tt.Fatalf(\"applyPersistedCodexModelCatalogCacheSnapshot returned error: %v\", err)\n\t}\n\n\tcatalogBody, err := os.ReadFile(filepath.Join(codexHome, gettokensCodexModelCatalogFilename))\n\tif err != nil {\n\t\tt.Fatalf(\"read model catalog: %v\", err)\n\t}\n\tif !strings.Contains(string(catalogBody), \"deepseek-v4-flash\") {\n\t\tt.Fatalf(\"catalog does not contain cached DeepSeek model: %s\", catalogBody)\n\t}\n\tconfigBody, err := os.ReadFile(filepath.Join(codexHome, \"config.toml\"))\n\tif err != nil {\n\t\tt.Fatalf(\"read codex config: %v\", err)\n\t}\n\tif !strings.Contains(string(configBody), \"model_catalog_json\") {\n\t\tt.Fatalf(\"config does not contain model_catalog_json pointer: %s\", configBody)\n\t}\n}\n\nfunc TestApplyPersistedCodexModelCatalogCacheSnapshotSkipsWhenDisabled(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tt.Setenv(\"CODEX_HOME\", filepath.Join(home, \".codex\"))\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: false}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := applyPersistedCodexModelCatalogCacheSnapshot(); err != nil {\n\t\tt.Fatalf(\"apply disabled setting returned error: %v\", err)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".codex\", gettokensCodexModelCatalogFilename)); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"expected no catalog when sync disabled, stat err=%v\", err)\n\t}\n}\n\nfunc TestRefreshCodexModelCatalogAfterAccountMutationUpdatesCacheAndCatalog(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\n\taccount := testOpenAICompatibleAccount(\n\t\t\"acct_deepseek\",\n\t\t\"DeepSeek\",\n\t\t0,\n\t\tfalse,\n\t\t\"https://api.deepseek.com/v1\",\n\t\t\"\",\n\t\tnil,\n\t\tnil,\n\t\t[]cliproxyapi.OpenAICompatibleModel{{Name: \"deepseek-v4-pro\"}},\n\t)\n\tapp := &App{ctx: context.Background()}\n\tapp.managementAPI = func() *cliproxyapi.Client {\n\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\tif method == http.MethodGet && path == \"/v0/management/accounts\" {\n\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t}\n\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\treturn nil, 0, nil\n\t\t})\n\t}\n\n\tif err := app.refreshCodexModelCatalogAfterAccountMutation(); err != nil {\n\t\tt.Fatalf(\"refreshCodexModelCatalogAfterAccountMutation returned error: %v\", err)\n\t}\n\n\tcached, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"load cache: %v\", err)\n\t}\n\tif len(cached) != 1 || cached[0].AccountKey != \"acct_deepseek\" || cached[0].Models[0].Name != \"deepseek-v4-pro\" {\n\t\tt.Fatalf(\"unexpected refreshed cache: %#v\", cached)\n\t}\n\tcatalogBody, err := os.ReadFile(filepath.Join(codexHome, gettokensCodexModelCatalogFilename))\n\tif err != nil {\n\t\tt.Fatalf(\"read model catalog: %v\", err)\n\t}\n\tif !strings.Contains(string(catalogBody), \"deepseek-v4-pro\") {\n\t\tt.Fatalf(\"catalog does not contain refreshed model: %s\", catalogBody)\n\t}\n}\n\nfunc TestRefreshCodexModelCatalogAfterAccountMutationClearsCatalogWhenNoActiveModels(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := os.MkdirAll(codexHome, 0700); err != nil {\n\t\tt.Fatalf(\"mkdir codex home: %v\", err)\n\t}\n\tif _, err := enableGetTokensCodexModelCatalogProjection([]OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}, false); err != nil {\n\t\tt.Fatalf(\"seed catalog: %v\", err)\n\t}\n\n\taccount := testOpenAICompatibleAccount(\n\t\t\"acct_deepseek\",\n\t\t\"DeepSeek\",\n\t\t0,\n\t\ttrue,\n\t\t\"https://api.deepseek.com/v1\",\n\t\t\"\",\n\t\tnil,\n\t\tnil,\n\t\t[]cliproxyapi.OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t)\n\tapp := &App{ctx: context.Background()}\n\tapp.managementAPI = func() *cliproxyapi.Client {\n\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\tif method == http.MethodGet && path == \"/v0/management/accounts\" {\n\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t}\n\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\treturn nil, 0, nil\n\t\t})\n\t}\n\n\tif err := app.refreshCodexModelCatalogAfterAccountMutation(); err != nil {\n\t\tt.Fatalf(\"refreshCodexModelCatalogAfterAccountMutation returned error: %v\", err)\n\t}\n\tconfigBody, err := os.ReadFile(filepath.Join(codexHome, \"config.toml\"))\n\tif err != nil {\n\t\tt.Fatalf(\"read codex config: %v\", err)\n\t}\n\tif strings.Contains(string(configBody), \"model_catalog_json\") {\n\t\tt.Fatalf(\"expected stale model_catalog_json pointer to be removed, got: %s\", configBody)\n\t}\n\tcached, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"load cache: %v\", err)\n\t}\n\tif len(cached) != 0 {\n\t\tt.Fatalf(\"disabled account should clear cache, got %#v\", cached)\n\t}\n}\n\nfunc TestPruneRelayModelAccountCacheEntriesRemovesDeletedAccountImmediately(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{\n\t\t{AccountKey: \"acct_keep\", Kind: \"openai-compatible\", Models: []OpenAICompatibleModel{{Name: \"gpt-5.5\"}}},\n\t\t{AccountKey: \"acct_delete\", Kind: \"openai-compatible\", Models: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}},\n\t}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := pruneRelayModelAccountCacheEntries(\"acct_delete\"); err != nil {\n\t\tt.Fatalf(\"pruneRelayModelAccountCacheEntries returned error: %v\", err)\n\t}\n\tcached, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"load cache: %v\", err)\n\t}\n\tif len(cached) != 1 || cached[0].AccountKey != \"acct_keep\" {\n\t\tt.Fatalf(\"unexpected pruned cache: %#v\", cached)\n\t}\n}\n\nfunc TestScheduleCodexModelCatalogRefreshDebouncesAndRunsOnce(t *testing.T) {\n\tapp := &App{ctx: context.Background()}\n\tapp.codexModelCatalogRefreshDebounce = time.Millisecond\n\tcalls := 0\n\tdone := make(chan struct{}, 1)\n\tapp.codexModelCatalogRefreshFunc = func() error {\n\t\tcalls++\n\t\tdone <- struct{}{}\n\t\treturn nil\n\t}\n\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for debounced refresh\")\n\t}\n\ttime.Sleep(20 * time.Millisecond)\n\tif calls != 1 {\n\t\tt.Fatalf(\"expected one debounced refresh, got %d\", calls)\n\t}\n}\n\nfunc TestScheduleCodexModelCatalogRefreshRunsPendingAfterInFlight(t *testing.T) {\n\tapp := &App{ctx: context.Background()}\n\tapp.codexModelCatalogRefreshDebounce = time.Millisecond\n\tstarted := make(chan struct{}, 2)\n\trelease := make(chan struct{})\n\tdone := make(chan struct{}, 2)\n\tcalls := 0\n\tapp.codexModelCatalogRefreshFunc = func() error {\n\t\tcalls++\n\t\tstarted <- struct{}{}\n\t\tif calls == 1 {\n\t\t\t<-release\n\t\t}\n\t\tdone <- struct{}{}\n\t\treturn nil\n\t}\n\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tselect {\n\tcase <-started:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for first refresh\")\n\t}\n\tapp.scheduleCodexModelCatalogRefreshAfterAccountMutation()\n\tclose(release)\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for first completion\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for pending refresh\")\n\t}\n\tif calls != 2 {\n\t\tt.Fatalf(\"expected pending refresh to run once after in-flight refresh, got %d\", calls)\n\t}\n}\n\nfunc TestApplyGetTokensCodexModelCatalogProjectionDoesNotRewriteUnchangedCatalog(t *testing.T) {\n\tcodexHome := t.TempDir()\n\tmodels := []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}\n\tif _, _, _, _, err := applyGetTokensCodexModelCatalogProjection(\"\", codexHome, models, false); err != nil {\n\t\tt.Fatalf(\"first apply: %v\", err)\n\t}\n\tcatalogPath := filepath.Join(codexHome, gettokensCodexModelCatalogFilename)\n\tbefore, err := os.Stat(catalogPath)\n\tif err != nil {\n\t\tt.Fatalf(\"stat catalog: %v\", err)\n\t}\n\ttime.Sleep(10 * time.Millisecond)\n\tif _, _, _, _, err := applyGetTokensCodexModelCatalogProjection(\"\", codexHome, models, false); err != nil {\n\t\tt.Fatalf(\"second apply: %v\", err)\n\t}\n\tafter, err := os.Stat(catalogPath)\n\tif err != nil {\n\t\tt.Fatalf(\"stat catalog after: %v\", err)\n\t}\n\tif !after.ModTime().Equal(before.ModTime()) {\n\t\tt.Fatalf(\"unchanged catalog should not be rewritten: before=%s after=%s\", before.ModTime(), after.ModTime())\n\t}\n}\n\nfunc TestCodexModelCatalogDiagnosticsReportsCacheCatalogAndWarnings(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tProviderName: \"DeepSeek\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\tif _, err := enableGetTokensCodexModelCatalogProjection([]OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}}, false); err != nil {\n\t\tt.Fatalf(\"enable catalog projection: %v\", err)\n\t}\n\n\tdiagnostics, err := GetCodexModelCatalogDiagnostics()\n\tif err != nil {\n\t\tt.Fatalf(\"GetCodexModelCatalogDiagnostics returned error: %v\", err)\n\t}\n\tif !diagnostics.SyncEnabled || !diagnostics.HasGetTokensCatalogPointer || diagnostics.CatalogModelCount != 1 {\n\t\tt.Fatalf(\"unexpected diagnostics summary: %#v\", diagnostics)\n\t}\n\tif diagnostics.CachedAccountCount != 1 || diagnostics.CachedModelCount != 1 {\n\t\tt.Fatalf(\"unexpected cache counts: %#v\", diagnostics)\n\t}\n\tif len(diagnostics.Models) != 1 || diagnostics.Models[0].Slug != \"deepseek-v4-flash\" || diagnostics.Models[0].SourceAccounts[0] != \"acct_deepseek\" {\n\t\tt.Fatalf(\"unexpected diagnostics models: %#v\", diagnostics.Models)\n\t}\n}\n\nfunc TestCodexModelCatalogDiagnosticsWarnsForExternalCatalogPointer(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif err := os.MkdirAll(codexHome, 0700); err != nil {\n\t\tt.Fatalf(\"mkdir codex home: %v\", err)\n\t}\n\tif err := os.WriteFile(filepath.Join(codexHome, \"config.toml\"), []byte(\"model_catalog_json = \\\"/tmp/external-catalog.json\\\"\\n\"), 0600); err != nil {\n\t\tt.Fatalf(\"write config: %v\", err)\n\t}\n\n\tdiagnostics, err := GetCodexModelCatalogDiagnostics()\n\tif err != nil {\n\t\tt.Fatalf(\"GetCodexModelCatalogDiagnostics returned error: %v\", err)\n\t}\n\tif diagnostics.HasGetTokensCatalogPointer {\n\t\tt.Fatalf(\"expected external pointer, got %#v\", diagnostics)\n\t}\n\tjoined := strings.Join(diagnostics.Warnings, \"\\n\")\n\tif !strings.Contains(joined, \"外部 model_catalog_json\") {\n\t\tt.Fatalf(\"expected external pointer warning, got %#v\", diagnostics.Warnings)\n\t}\n}\n\nfunc TestRelayModelCatalogTraceWrittenFromAccountSnapshots(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tt.Setenv(\"CODEX_HOME\", filepath.Join(home, \".codex\"))\n\taccount := testOpenAICompatibleAccount(\n\t\t\"acct_deepseek\",\n\t\t\"DeepSeek\",\n\t\t0,\n\t\tfalse,\n\t\t\"https://api.deepseek.com/v1\",\n\t\t\"\",\n\t\tnil,\n\t\tnil,\n\t\t[]cliproxyapi.OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t)\n\tapp := &App{}\n\tapp.managementAPI = func() *cliproxyapi.Client {\n\t\treturn cliproxyapi.New(func(method string, path string, query url.Values, body io.Reader, contentType string) ([]byte, int, error) {\n\t\t\tif method == http.MethodGet && path == \"/v0/management/accounts\" {\n\t\t\t\treturn testAccountsResponse(t, account), 200, nil\n\t\t\t}\n\t\t\tt.Fatalf(\"unexpected request: %s %s\", method, path)\n\t\t\treturn nil, 0, nil\n\t\t})\n\t}\n\n\tmodels, err := app.ListRelaySupportedModels()\n\tif err != nil {\n\t\tt.Fatalf(\"ListRelaySupportedModels returned error: %v\", err)\n\t}\n\tif len(models) != 1 || models[0].Name != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"unexpected models: %#v\", models)\n\t}\n\ttrace, err := loadRelayModelCatalogTrace()\n\tif err != nil {\n\t\tt.Fatalf(\"load trace: %v\", err)\n\t}\n\tif len(trace.Accounts) != 1 || trace.Accounts[0].AccountKey != \"acct_deepseek\" {\n\t\tt.Fatalf(\"unexpected trace accounts: %#v\", trace)\n\t}\n\tif len(trace.Models) != 1 || trace.Models[0].Slug != \"deepseek-v4-flash\" || trace.Models[0].SourceAccounts[0] != \"acct_deepseek\" {\n\t\tt.Fatalf(\"unexpected trace models: %#v\", trace)\n\t}\n}\n"}, "52325bbc68a8412d2637fa20fab6213af99cb02f": {"t": 1785759458.6357431, "n": 7864, "h": "d20056a9c8887d11", "c": "package wailsapp\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/linhay/gettokens/internal/cliproxyapi\"\n)\n\nfunc TestRelayModelAccountCacheRoundTrip(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\n\tsnapshots := []relayModelAccountSnapshot{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tKind: \"openai-compatible\",\n\t\t\tProviderName: \"DeepSeek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\", Alias: \"deepseek-v4-flash\", SupportedReasoningEfforts: []string{\"low\", \"high\"}, DefaultReasoningEffort: \"high\"},\n\t\t\t\t{Name: \"deepseek-v4-pro\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err := saveRelayModelAccountCache(snapshots); err != nil {\n\t\tt.Fatalf(\"saveRelayModelAccountCache returned error: %v\", err)\n\t}\n\n\tpath, err := relayModelAccountCachePath()\n\tif err != nil {\n\t\tt.Fatalf(\"relayModelAccountCachePath returned error: %v\", err)\n\t}\n\tif filepath.Dir(path) != filepath.Join(home, \".config\", \"gettokens-data\", \"codex-model-account-cache\") {\n\t\tt.Fatalf(\"unexpected cache path: %s\", path)\n\t}\n\n\tloaded, err := loadRelayModelAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"loadRelayModelAccountCache returned error: %v\", err)\n\t}\n\tif len(loaded) != 1 || loaded[0].AccountKey != \"acct_deepseek\" || len(loaded[0].Models) != 2 {\n\t\tt.Fatalf(\"unexpected loaded cache: %#v\", loaded)\n\t}\n\tif loaded[0].Models[0].Name != \"deepseek-v4-flash\" || loaded[0].Models[0].DefaultReasoningEffort != \"high\" {\n\t\tt.Fatalf(\"unexpected normalized model: %#v\", loaded[0].Models[0])\n\t}\n}\n\nfunc TestListRelaySupportedModelsUsesAccountCacheForActiveAccountWhenRemoteUnavailable(t *testing.T) {\n\tproviders := []OpenAICompatibleProvider{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tName: \"DeepSeek\",\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-deepseek\",\n\t\t},\n\t\t{\n\t\t\tAccountKey: \"acct_disabled\",\n\t\t\tName: \"Disabled DeepSeek\",\n\t\t\tDisabled: true,\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-disabled\",\n\t\t},\n\t}\n\tcached := map[string]relayModelAccountSnapshot{\n\t\t\"acct_deepseek\": {\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\"},\n\t\t\t},\n\t\t},\n\t\t\"acct_disabled\": {\n\t\t\tAccountKey: \"acct_disabled\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"should-not-appear\"},\n\t\t\t},\n\t\t},\n\t\t\"acct_deleted\": {\n\t\t\tAccountKey: \"acct_deleted\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deleted-should-not-appear\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(providers, nil, func(input FetchOpenAICompatibleProviderModelsInput) ([]OpenAICompatibleModel, error) {\n\t\treturn nil, os.ErrNotExist\n\t}, nil, []OpenAICompatibleModel{{Name: \"deepseek-sidecar-only\"}}, cached)\n\n\tif len(models) != 1 || models[0].Name != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"expected only active account cached model, got %#v\", models)\n\t}\n\tif len(snapshots) != 1 || snapshots[0].AccountKey != \"acct_deepseek\" || len(snapshots[0].Models) != 1 {\n\t\tt.Fatalf(\"unexpected snapshots: %#v\", snapshots)\n\t}\n}\n\nfunc TestListRelaySupportedModelsRefreshesAccountCacheWithLatestRemoteModels(t *testing.T) {\n\tproviders := []OpenAICompatibleProvider{\n\t\t{\n\t\t\tAccountKey: \"acct_deepseek\",\n\t\t\tName: \"DeepSeek\",\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tAPIKey: \"sk-deepseek\",\n\t\t\tModels: []OpenAICompatibleModel{\n\t\t\t\t{Name: \"deepseek-v4-flash\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(providers, nil, func(input FetchOpenAICompatibleProviderModelsInput) ([]OpenAICompatibleModel, error) {\n\t\treturn []OpenAICompatibleModel{{Name: \"deepseek-v4-pro\"}}, nil\n\t}, nil, nil, nil)\n\n\tif len(models) != 2 || models[0].Name != \"deepseek-v4-flash\" || models[1].Name != \"deepseek-v4-pro\" {\n\t\tt.Fatalf(\"unexpected refreshed models: %#v\", models)\n\t}\n\tif len(snapshots) != 1 || len(snapshots[0].Models) != 2 {\n\t\tt.Fatalf(\"expected latest per-account snapshot, got %#v\", snapshots)\n\t}\n}\n\nfunc TestLoadRelaySupportedModelsFromAccountCacheIncludesLocalCodexCache(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif err := os.MkdirAll(codexHome, 0700); err != nil {\n\t\tt.Fatalf(\"mkdir codex home: %v\", err)\n\t}\n\tif err := os.WriteFile(filepath.Join(codexHome, \"models_cache.json\"), []byte(`{\"models\":[{\"slug\":\"gpt-5.5\",\"display_name\":\"GPT-5.5\"}]}`), 0600); err != nil {\n\t\tt.Fatalf(\"write local codex cache: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tmodels, err := loadRelaySupportedModelsFromAccountCache()\n\tif err != nil {\n\t\tt.Fatalf(\"loadRelaySupportedModelsFromAccountCache returned error: %v\", err)\n\t}\n\tif len(models) != 2 || models[0].Name != \"deepseek-v4-flash\" || models[1].Name != \"gpt-5.5\" {\n\t\tt.Fatalf(\"unexpected cached startup models: %#v\", models)\n\t}\n}\n\nfunc TestCodexAPIKeyAccountCacheUsesLocalID(t *testing.T) {\n\tmodels, snapshots := listRelaySupportedModelsWithAccountSnapshots(nil, []cliproxyapi.CodexAPIKey{{\n\t\tLocalID: \"acct_codex_key\",\n\t\tModels: []cliproxyapi.CodexModel{{Name: \"mimo-v2-pro\"}},\n\t}}, nil, nil, nil, nil)\n\tif len(models) != 1 || models[0].Name != \"mimo-v2-pro\" {\n\t\tt.Fatalf(\"unexpected codex key models: %#v\", models)\n\t}\n\tif len(snapshots) != 1 || snapshots[0].AccountKey != \"acct_codex_key\" || snapshots[0].Kind != \"codex-api-key\" {\n\t\tt.Fatalf(\"unexpected codex key snapshot: %#v\", snapshots)\n\t}\n}\n\nfunc TestApplyPersistedCodexModelCatalogCacheSnapshotWritesCatalogBeforeSidecarReady(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tcodexHome := filepath.Join(home, \".codex\")\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: true}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := applyPersistedCodexModelCatalogCacheSnapshot(); err != nil {\n\t\tt.Fatalf(\"applyPersistedCodexModelCatalogCacheSnapshot returned error: %v\", err)\n\t}\n\n\tcatalogBody, err := os.ReadFile(filepath.Join(codexHome, gettokensCodexModelCatalogFilename))\n\tif err != nil {\n\t\tt.Fatalf(\"read model catalog: %v\", err)\n\t}\n\tif !strings.Contains(string(catalogBody), \"deepseek-v4-flash\") {\n\t\tt.Fatalf(\"catalog does not contain cached DeepSeek model: %s\", catalogBody)\n\t}\n\tconfigBody, err := os.ReadFile(filepath.Join(codexHome, \"config.toml\"))\n\tif err != nil {\n\t\tt.Fatalf(\"read codex config: %v\", err)\n\t}\n\tif !strings.Contains(string(configBody), \"model_catalog_json\") {\n\t\tt.Fatalf(\"config does not contain model_catalog_json pointer: %s\", configBody)\n\t}\n}\n\nfunc TestApplyPersistedCodexModelCatalogCacheSnapshotSkipsWhenDisabled(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tt.Setenv(\"CODEX_HOME\", filepath.Join(home, \".codex\"))\n\tif _, err := saveAppRuntimeSettings(AppRuntimeSettings{CodexModelCatalogSyncEnabled: false}); err != nil {\n\t\tt.Fatalf(\"save settings: %v\", err)\n\t}\n\tif err := saveRelayModelAccountCache([]relayModelAccountSnapshot{{\n\t\tAccountKey: \"acct_deepseek\",\n\t\tKind: \"openai-compatible\",\n\t\tModels: []OpenAICompatibleModel{{Name: \"deepseek-v4-flash\"}},\n\t}}); err != nil {\n\t\tt.Fatalf(\"save cache: %v\", err)\n\t}\n\n\tif err := applyPersistedCodexModelCatalogCacheSnapshot(); err != nil {\n\t\tt.Fatalf(\"apply disabled setting returned error: %v\", err)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".codex\", gettokensCodexModelCatalogFilename)); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"expected no catalog when sync disabled, stat err=%v\", err)\n\t}\n}\n"}}, "docs-linhay/spaces/20260528-cc-switch-one-click-import/plans/20260528-gettokens-deeplink-account-codex-config-design.md": {"6a6556121411b7445b1e0e28d0b7af6920236b59": {"t": 1785746410.5076606, "n": 35619, "h": "304a46ad79111cc9", "c": "# GetTokens deep link 账号与 Codex 配置导入设计\n\n## 设计目标\n\n设计 `gettokens://` deep link,让外部页面、厂商文档或团队配置可以把账号和 Codex 本地配置带入 GetTokens。设计必须满足两个前提:\n\n1. Deep link 只表达“导入意图”,不直接落盘。\n2. 最终写入必须复用现有账号、sidecar 和 Codex local apply 服务层,不能绕过既有校验去写 `auth.json`、`config.toml` 或 CLIProxyAPI 配置。\n\n## 现有边界\n\n### 账号服务\n\n当前账号列表由三类来源组成:\n\n| 来源 | 账号 ID | 现有创建/写入入口 |\n| --- | --- | --- |\n| Codex OAuth / auth file | `auth-file:<name>` | `UploadAuthFiles(files)` 经 sidecar `/v0/management/auth-files` 上传 |\n| Codex API key | `codex-api-key:<local-id>` 或指纹 ID | `CreateCodexAPIKey(input)` 写入本地 `~/.config/gettokens-data/codex-api-keys/*.json` 并同步 sidecar |\n| OpenAI-compatible provider | `openai-compatible:<name>` | `CreateOpenAICompatibleProvider(input)` 经 CLIProxyAPI Management API 写入 provider |\n\n账号列表展示和路由语义已经区分 `auth-file`、`codex-api-key`、`openai-compatible`,后续 deep link 必须沿用这个分层。\n\n### Codex 本地配置\n\nCodex local apply 已有三类语义:\n\n1. API Key 模式:写 `CODEX_HOME/auth.json` 与 `CODEX_HOME/config.toml`。\n2. OAuth / auth-file 模式:写所选账号 OAuth `auth.json`,让 Codex 走 ChatGPT/Codex OAuth backend。\n3. 保留 ChatGPT 登录态模式:只 patch `config.toml` 的 custom provider 字段。\n\n`GetLocalCodexModelProviderState()` 能读取当前 `model_provider`、当前模型和已存在 provider;`mergeRelayCodexConfigToml()` 已经实现保留式 TOML patch。Deep link 不应新增完整覆盖 `config.toml` 的入口。\n\n## 方案比较\n\n### 方案 A:只支持账号 deep link\n\n只让 `gettokens://v1/import?channel=codex&resource=account...` 创建账号,不触碰 Codex 本地配置。\n\n- 成本:低。\n- 风险:低。\n- 问题:用户仍需手动进入 Codex local apply,无法满足“直接导入账号和 codex 配置”。\n\n### 方案 B:账号与 Codex 配置分资源导入\n\n支持 `resource=account` 和 `resource=codex-config` 两类 deep link。账号导入创建账号;Codex 配置导入只生成 local apply 草稿和 diff,确认后调用现有 local apply。\n\n- 成本:中。\n- 风险:中。\n- 依赖:现有账号创建、Codex provider state、local apply diff / apply。\n- 问题:外部平台想“一条链接完成账号和配置”时体验不够顺。\n\n### 方案 C:账号、Codex 配置、组合导入三层协议\n\n在方案 B 基础上增加 `resource=codex-setup`,一条链接可以同时携带账号草稿和 Codex 配置草稿。确认页分成两个步骤:先创建账号,再应用 Codex 配置;任一步失败都不吞掉错误。\n\n- 成本:中高。\n- 风险:可控,但需要更完整的测试矩阵。\n- 依赖:同方案 B,并额外需要导入事务/回滚提示设计。\n- 推荐:采用。它满足外部一键配置场景,同时保留账号和 Codex 配置的独立确认边界。\n\n攻击角度校验:\n\n| 角度 | 结论 |\n| --- | --- |\n| 依赖失败 | sidecar 未 ready 时只允许保存账号草稿或展示待执行,不写 Codex 配置;Codex local apply 失败不删除已创建账号 |\n| 数据量放大 | deep link 只支持单账号、单 Codex 配置;批量导入改走 JSON 文件导入 |\n| 回滚成本 | 账号创建和 Codex 配置写入分步展示,Codex 配置写入前有 diff;账号误导入可用现有删除入口 |\n| 前提崩塌 | 如果系统未注册 URL scheme,仍可提供“粘贴 deep link”入口复用同一 parser |\n\n## 推荐方案\n\n采用方案 C:三类 resource,共用 parser 和确认弹窗。\n\n```text\ngettokens://v1/import?channel=codex&resource=account&...\ngettokens://v1/import?channel=codex&resource=codex-config&...\ngettokens://v1/import?channel=codex&resource=codex-setup&...\n```\n\n### 不做的事\n\n1. 不支持远程 `configUrl`。\n2. 不支持导入 usage script 或任意可执行脚本。\n3. 不支持批量账号导入。\n4. 不支持无确认自动启用、自动切换 Codex 当前账号。\n5. 不支持完整替换 `CODEX_HOME/config.toml`。\n6. 不支持把任意 TOML 片段拼进用户配置;只允许受控字段 patch。\n\n## 协议设计\n\n### 通用参数\n\n| 参数 | 必填 | 说明 |\n| --- | --- | --- |\n| `channel` | 是 | 固定值 `codex`,明确这是 Codex 账号 / 配置导入,不与其他产品线混淆 |\n| `resource` | 是 | `account` / `codex-config` / `codex-setup` |\n| `version` | 否 | URL host 已表达 `v1`,query 中可省略 |\n| `source` | 否 | 来源标识,用于确认页展示,不参与信任判断 |\n| `nonce` | 否 | 外部生成的去重标识;同 nonce 重复点击时提示重复 |\n| `config` | 否 | Base64URL 编码 JSON;复杂字段优先放这里 |\n| `enabled` | 否 | 只表示账号创建后是否启用;默认 `false` |\n| `apply` | 否 | 是否在确认后继续应用 Codex 配置;默认 `false` |\n\n所有 query 参数和 `config` 合并时,query 参数优先级更高。`config` 只允许 JSON,使用 Base64URL UTF-8 编码,不支持 TOML、YAML 或远程 URL。\n\n`config` 的 canonical 形式不是一组散落字段,而是一个 `documents[]` 补丁包。`account` / `codexConfig` 这类对象只是便捷语法,最终都要编译成对 `auth.json`、`config.toml` 或未来扩展文件的文档级 patch。\n\n### 字段覆盖语义\n\nCodex config patch 统一采用 presence 语义:\n\n1. 字段在 query、`codexConfig` 或 `documents[]` operation 中显式出现时,才覆盖对应的本地字段。\n2. 字段未出现时,保留用户现有 `auth.json` / `config.toml` 内容,不用默认值回填。\n3. `false`、空字符串以外的零值都不能被当作“未出现”;例如 `supportsWebsockets=false` 和 `requires_openai_auth=false` 必须写入 `false`。\n4. `providerID` 只作为身份值;本机已有显式 `model_provider` 时不改 root `model_provider`,只把显式字段 patch 到当前激活 provider section。\n5. 手动“应用模板到 Codex”不是 deep link patch,它会把当前表单值作为显式字段提交,保持原有一键应用体验。\n\n## 通用文件改写格式\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-xxx\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" }\n ]\n }\n ]\n}\n```\n\n### 文档对象字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `target` | 是 | 目标文件名,如 `auth.json`、`config.toml` |\n| `format` | 是 | `json` / `toml` / `text` |\n| `mode` | 否 | `merge` / `patch` / `replace`,默认由格式推断 |\n| `operations` | 是 | 按顺序执行的改写操作 |\n| `preserveUnknown` | 否 | 是否保留未知字段,默认 `true` |\n| `backup` | 否 | 是否在写入前保留备份,默认 `true` |\n\n### 操作对象字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `op` | 是 | `set` / `delete` / `append` / `merge` |\n| `path` | 是 | `json` 使用 JSON Pointer,`toml` 使用 dotted path |\n| `value` | 否 | 新值,JSON value 形式 |\n| `valueEncoding` | 否 | `plain` / `base64`,默认 `plain` |\n| `allowCreate` | 否 | 不存在时是否创建路径,默认 `true` |\n\n### 目标文件约定\n\n| target | 说明 |\n| --- | --- |\n| `auth.json` | Codex auth file,适合 `replace` 或 root-level `set`/`merge` |\n| `config.toml` | Codex 本地配置,适合 root key 和 `model_providers.<id>` patch |\n| `settings.json` | 未来可扩展目标,当前不开放 |\n\n### 推荐路径语义\n\n| 文件 | 路径语义 |\n| --- | --- |\n| JSON 文件 | JSON Pointer,如 `/user/email`、`/tokens/access_token` |\n| TOML 文件 | dotted path,如 `model_provider`、`model_providers.team-relay.base_url` |\n\n### 典型操作\n\n| 操作 | 语义 |\n| --- | --- |\n| `set` | 设置或覆盖路径值 |\n| `delete` | 删除路径 |\n| `append` | 向数组追加单项 |\n| `merge` | 对对象/表执行深合并,保留未提及字段 |\n\n## 字段总表\n\n### 1. 通用字段\n\n| 字段 | 必填 | 适用资源 | 说明 |\n| --- | --- | --- | --- |\n| `channel` | 是 | 全部 | 固定值 `codex`,用于明确这是 Codex 导入链路 |\n| `resource` | 是 | 全部 | `account` / `codex-config` / `codex-setup` |\n| `config` | 否 | 全部 | Base64URL JSON,优先承载复杂对象 |\n| `source` | 否 | 全部 | 来源标识,仅用于确认页展示 |\n| `nonce` | 否 | 全部 | 去重标识 |\n| `enabled` | 否 | account | 账号导入后是否启用,默认 `false` |\n| `apply` | 否 | codex-config / codex-setup | 是否在确认后继续应用 Codex 配置,默认 `false` |\n\n### 2. account 资源字段\n\n#### 2.1 `accountType = codex-api-key`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `CreateCodexAPIKey` | 固定值 `codex-api-key` |\n| `label` | 否 | `CreateCodexAPIKeyInput.Label` | 显示名 |\n| `apiKey` | 是 | `CreateCodexAPIKeyInput.APIKey` | 账号 API key |\n| `baseUrl` | 是 | `CreateCodexAPIKeyInput.BaseURL` | API base URL |\n| `prefix` | 否 | `CreateCodexAPIKeyInput.Prefix` | 前缀 |\n| `proxyUrl` | 否 | `CreateCodexAPIKeyInput.ProxyURL` | 代理 URL |\n| `models` | 否 | `CreateCodexAPIKeyInput.Models` | 模型映射,放在 `config` 中更合适 |\n| `formatBaseUrls` | 否 | `CreateCodexAPIKeyInput.FormatBaseURLs` | 格式级 base URL 映射 |\n| `quotaCurl` | 否 | `CreateCodexAPIKeyInput.QuotaCurl` | 用量查询 curl,仅建议本地生成,不建议外链 |\n| `billingCurl` | 否 | `CreateCodexAPIKeyInput.BillingCurl` | 账单 curl,仅建议本地生成,不建议外链 |\n| `quotaEnabled` | 否 | `CreateCodexAPIKeyInput.QuotaEnabled` | 是否启用用量查询 |\n| `billingEnabled` | 否 | `CreateCodexAPIKeyInput.BillingEnabled` | 是否启用账单查询 |\n\n#### 2.2 `accountType = openai-compatible`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `CreateOpenAICompatibleProvider` | 固定值 `openai-compatible` |\n| `name` | 是 | `CreateOpenAICompatibleProviderInput.Name` | provider 名称 |\n| `apiKey` | 是 | `CreateOpenAICompatibleProviderInput.APIKey` | 首个 API key |\n| `baseUrl` | 是 | `CreateOpenAICompatibleProviderInput.BaseURL` | provider base URL |\n| `prefix` | 否 | `CreateOpenAICompatibleProviderInput.Prefix` | provider 前缀 |\n| `models` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 模型映射 |\n| `apiKeys` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 多 key 账号 |\n| `proxyUrl` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 代理 URL |\n| `headers` | 否 | 首期不支持 | 需要手动补充,避免外链注入 |\n\n#### 2.3 `accountType = auth-file`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `UploadAuthFiles` | 固定值 `auth-file` |\n| `name` | 是 | `UploadAuthFiles` | 文件名 |\n| `config.documents[]` | 是 | `UploadAuthFiles` | 目标 `auth.json` 的补丁包,建议用 `merge` 或 `replace` |\n| `enabled` | 否 | 仅在预览层解释 | auth file 本身不是启停字段 |\n\n### 3. codex-config 资源字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `mode` | 是 | `api-key` / `oauth-auth-file` / `preserve-chatgpt-provider` |\n| `config.documents[]` | 是 | 至少包含 `auth.json` 或 `config.toml` 之一的补丁包 |\n| `model` | 否 | root `model`,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `reasoningEffort` | 否 | root `model_reasoning_effort`,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `providerID` | 否 | 当前无激活 provider 时的创建值;若已有激活值则保持原值,仅 patch 当前 section |\n| `providerName` | 否 | provider 显示名,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `baseUrl` | 按 mode | custom provider base URL,可写入 `auth.json` 或 `config.toml` |\n| `apiKey` | `api-key` 必填 | 写入 `auth.json` 或 bearer token 草稿 |\n| `accountRef` | 否 | `auth-file:<name>` / `codex-api-key:<id>` / `openai-compatible:<name>` |\n| `supportsWebsockets` | 否 | patch `supports_websockets`;遵循 presence 语义,`false` 也是显式写入 |\n| `providerScope` | 否 | `current-active` / `create-new`,显式标识沿用当前激活 provider 还是创建新 provider |\n| `providerCompatibility` | 否 | 预览结果 | `compatible` / `blocked_builtin_openai` / `missing_chatgpt_auth` / `missing_provider_section` |\n| `providerRewriteMode` | 否 | 预览结果 | `keep-current` / `patch-current` / `create-new` |\n\n### 4. codex-setup 资源字段\n\n`codex-setup` 主要通过 `config` 传入两块对象:\n\n| 对象 | 字段 |\n| --- | --- |\n| `account` | `accountType`, `name`, `label`, `apiKey`, `apiKeys`, `baseUrl`, `prefix`, `proxyUrl`, `models`, `formatBaseUrls`, `quotaCurl`, `billingCurl`, `authFile` |\n| `codexConfig` | `mode`, `model`, `reasoningEffort`, `providerID`, `providerName`, `providerScope`, `baseUrl`, `apiKey`, `supportsWebsockets`, `accountRef`, `providerCompatibility`, `providerRewriteMode` |\n\n`codex-setup` 额外支持通用字段 `apply`、`enabled`、`source`、`nonce`,但不会新增自己的字段池。\n\n### 5. 明确不支持的字段\n\n| 字段 | 状态 | 说明 |\n| --- | --- | --- |\n| `configUrl` | 不支持 | 不做远程拉取 |\n| `usageScript` | 不支持 | 不导入可执行脚本 |\n| `headers`(openai-compatible 首期) | 不支持 | 防止外链注入请求头 |\n| 批量账号数组 | 不支持 | 只做单账号导入 |\n| 任意 TOML 片段 | 不支持 | 只允许受控 patch |\n| 自动启用 / 自动切换当前账号 | 不支持 | 必须确认后再做 |\n\n### resource=account\n\n账号导入支持三种 `accountType`:\n\n| accountType | 用途 | 最终服务入口 |\n| --- | --- | --- |\n| `codex-api-key` | 创建 GetTokens 本地 Codex API key 账号 | `CreateCodexAPIKey` |\n| `openai-compatible` | 创建 CLIProxyAPI OpenAI-compatible provider | `CreateOpenAICompatibleProvider`,后续增强可走 `UpdateOpenAICompatibleProvider` 支持 models/headers |\n| `auth-file` | 导入 Codex OAuth auth.json | `UploadAuthFiles` |\n\n#### Codex API key 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=codex-api-key&label=Team%20Relay&apiKey=sk-xxx&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&model=gpt-5-codex\n```\n\n字段:\n\n| 字段 | 必填 | 映射 |\n| --- | --- | --- |\n| `label` | 否 | `CreateCodexAPIKeyInput.Label` |\n| `apiKey` | 是 | `CreateCodexAPIKeyInput.APIKey` |\n| `baseUrl` | 是 | `CreateCodexAPIKeyInput.BaseURL` |\n| `prefix` | 否 | `CreateCodexAPIKeyInput.Prefix` |\n| `proxyUrl` | 否 | `CreateCodexAPIKeyInput.ProxyURL` |\n| `models` | 否 | `CreateCodexAPIKeyInput.Models`,从 `config` JSON 传数组 |\n| `formatBaseUrls` | 否 | `CreateCodexAPIKeyInput.FormatBaseURLs` |\n| `quotaCurl` / `billingCurl` | 否 | 默认只预览,不建议外链携带 |\n\n#### OpenAI-compatible 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=openai-compatible&name=deepseek&apiKey=sk-xxx&baseUrl=https%3A%2F%2Fapi.deepseek.com%2Fv1\n```\n\n字段:\n\n| 字段 | 必填 | 映射 |\n| --- | --- | --- |\n| `name` | 是 | provider name,必须唯一 |\n| `apiKey` | 是 | 首个 API key |\n| `baseUrl` | 是 | provider base URL |\n| `prefix` | 否 | provider prefix |\n| `models` | 否 | 需要增强 root `CreateOpenAICompatibleProvider`,否则首期只能创建后再提示用户补模型 |\n| `headers` | 否 | 首期不允许 deep link 写入,避免外链注入 header |\n\n#### Auth file 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=auth-file&name=team-codex-auth.json&config=<base64url-json>\n```\n\n`config` JSON:\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"chatgpt\" },\n { \"op\": \"set\", \"path\": \"/user/email\", \"value\": \"team@example.com\" }\n ]\n }\n ]\n}\n```\n\nauth-file 导入风险最高,确认页必须展示 email / plan / token 字段摘要,不展示完整 token。默认不支持从普通 query 参数直接传 `authJson`,只允许 `config`。\n\n### resource=codex-config\n\nCodex 配置导入只表达受控 patch,不表达整份 TOML。推荐把 `auth.json` 和 `config.toml` 都放进同一个 `documents[]` bundle。\n\n示例:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=api-key&model=gpt-5-codex&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&apiKey=sk-xxx&providerID=team-relay\n```\n\n字段:\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `mode` | 是 | `api-key` / `oauth-auth-file` / `preserve-chatgpt-provider` |\n| `config.documents[]` | 是 | 目标 `auth.json` / `config.toml` 的补丁包 |\n| `model` | 否 | root `model`,可由 `config.toml` 文档表示 |\n| `reasoningEffort` | 否 | root `model_reasoning_effort`,可由 `config.toml` 文档表示 |\n| `providerID` | 否 | 目标 provider;默认读取当前 `model_provider`,`mode=preserve-chatgpt-provider` 时不得为 `openai` |\n| `providerName` | 否 | `[model_providers.<id>].name` |\n| `baseUrl` | 按模式 | custom provider base URL,可写入 `config.toml` 文档 |\n| `apiKey` | `api-key` 必填 | 只用于 auth 写入或 bearer token 草稿,可写入 `auth.json` 文档 |\n| `supportsWebsockets` | 否 | patch `supports_websockets`;遵循 presence 语义,`false` 也是显式写入 |\n| `accountRef` | 否 | 指向刚导入或已有账号,如 `codex-api-key:<id>` |\n\n模式语义:\n\n| mode | 写入策略 |\n| --- | --- |\n| `api-key` | 生成 Codex API key local apply 草稿,写 `auth.json` 的 `OPENAI_API_KEY`,并 patch `config.toml` |\n| `oauth-auth-file` | 选择已有或刚导入的 `auth-file`,写 OAuth tokens,让 Codex 走 ChatGPT/Codex OAuth backend |\n| `preserve-chatgpt-provider` | 不碰 `auth.json` tokens,只 patch custom provider;需要 preflight 确认本机有可保留 ChatGPT 登录态 |\n\n`codex-config` 的确认页必须先调用 `GetLocalCodexModelProviderState()`,展示当前 provider。默认 patch 当前 provider,不默认创建 `gettokens` provider;如果本地已有激活的 `model_provider`,deep link 不得改写它,只能补齐当前 provider 的 section。\n\n`providerScope` 是这个场景的显式标识:\n\n1. `providerScope=current-active` 表示沿用当前激活 `model_provider`,只 patch 该 provider 的 section。\n2. `providerScope=create-new` 表示创建新的 provider,并在没有当前激活 provider 时写入新的 `model_provider`。\n3. 当 `providerScope=current-active` 但当前没有激活值时,确认页可退回到 `create-new`,但必须明确提示是降级执行。\n4. `providerID` 只作为具体 provider 的身份值,不再承担“是否改写当前激活值”的语义。\n\n### provider 改写与 status 页对齐\n\n`/#frame=status` 里的 provider 语义,和 deep link 的 provider 改写语义必须一致:\n\n1. provider 是本地工作台偏好,不是 sidecar 的单一真相。\n2. 默认优先保留当前 `model_provider` 的连续性;是否沿用当前激活值由 `providerScope=current-active` 显式表达。\n3. 只有 `providerScope=create-new`,或者当前没有激活值且已降级确认时,才允许新建 `model_provider` / `[model_providers.<id>]`。\n4. `openai` 只作为内置连续性保留,不作为 `preserve-chatgpt-provider` 的目标 provider id。\n5. 当本地 `config.toml` 已存在当前激活 provider section 时,deep link 只 patch 当前 section,不静默清空其他 provider section。\n6. 当当前 provider section 缺失但符合兼容性要求时,确认页可以提示“将补齐当前 provider section”;只有 `providerScope=create-new` 时才提示“将创建新的 provider”。\n7. 若当前 provider 不存在、为 builtin `openai`,或与 preserve 模式冲突,则预览阶段必须给出和 status 页一致的阻断/警告文案。\n8. 所有受控字段都采用 presence 语义:显式出现才覆盖,未出现就保留;`supports_websockets=false`、`requires_openai_auth=false` 这类布尔 false 也必须写入。\n\n兼容性结果沿用状态页的心智,建议至少分成:\n\n| 结果 | 含义 |\n| --- | --- |\n| `compatible` | 可以按当前 provider 连续性应用 |\n| `blocked_builtin_openai` | 目标 provider 或模式要求与内置 `openai` 冲突 |\n| `missing_chatgpt_auth` | preserve 场景下本机 ChatGPT auth 不存在或无效 |\n| `missing_provider_section` | 需要写入的 provider section 还不存在 |\n\n### resource=codex-setup\n\n组合导入用于“一条链接导入账号并配置 Codex”。推荐把复杂内容放在 `config` JSON。\n\n```json\n{\n \"account\": {\n \"accountType\": \"codex-api-key\",\n \"label\": \"Team Relay\",\n \"apiKey\": \"sk-xxx\",\n \"baseUrl\": \"https://api.example.com/v1\",\n \"models\": [\n { \"name\": \"gpt-5-codex\", \"alias\": \"gpt-5-codex\" }\n ]\n },\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"replace\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"apikey\" },\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-xxx\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n执行顺序:\n\n1. Parse:只解析和校验,不写入。\n2. Preview:展示账号草稿、Codex config diff、敏感字段脱敏。\n3. Confirm account:创建账号。\n4. Resolve accountRef:把刚创建账号的 ID 注入 Codex config 草稿或后续文档 patch。\n5. Confirm/apply Codex config:调用现有 local apply。\n6. Refresh:刷新账号列表、Codex channel routing、Codex config snapshot。\n\n如果账号创建成功但 Codex config apply 失败,不回滚账号;确认页展示“账号已导入,Codex 配置未应用”的明确状态。\n\n## 安全策略\n\n1. 日志中 deep link 永远只记录 redacted URL,不记录 `apiKey`、`config`、token、header。\n2. UI 中 API key 只展示前 4 位和后 4 位,中间脱敏。\n3. `config` 大小限制建议 32 KB;超出要求改用文件导入。\n4. URL scheme handler 和“粘贴链接导入”共用 parser,避免两套解析差异。\n5. 所有写入前必须展示 diff 或字段级 change summary。\n6. `enabled=true` 不代表切换当前 Codex 本地配置;只代表账号创建后是否在 GetTokens/sidecar 中禁用。\n7. `apply=true` 只让确认页默认勾选“继续应用 Codex 配置”,不能跳过确认。\n8. 不允许 deep link 写入 arbitrary headers;OpenAI-compatible headers 首期只展示“需要用户手动补充”。\n\n## UI 交互\n\n### DeepLinkImportDialog\n\n`DeepLinkImportDialog` 不再新建一套独立确认 UI,而是作为 thin adapter 复用现有“应用模板到 Codex”确认页。deep link 解析和 preview 只负责把外部导入意图转换成现有 `AccountLocalCliApplyConfirm` 能消费的 Codex local apply 草稿,并补充来源、resource、providerScope 等 deep link 元信息。\n\n复用边界:\n\n| resource | UI 入口 | 说明 |\n| --- | --- | --- |\n| `account` | 账号导入确认 | 只创建账号,不进入“应用模板到 Codex” |\n| `codex-config` | 直接打开“应用模板到 Codex” | deep link 转成 Codex local apply 草稿 |\n| `codex-setup` | 账号草稿摘要 + “应用模板到 Codex” | 先确认账号草稿,再沿用同一 diff / apply 页面 |\n\n必须复用的现有能力:\n\n1. `ModalFrame` 的窗口外壳、遮罩、header/footer 分区。\n2. `AccountLocalCliApplyConfirm` 的标题、summary badge、文件列表、diff 预览和 footer 状态。\n3. `StatusSnippetPanel` / `buildCodexLocalApplyDiff` 的文件预览。\n4. `/#frame=status` 已有 preflight / provider 兼容判断。\n\n`codex-config` 单独导入时不展示“只导入账号”。\n\n视觉稿入口:[../deep-link-import-modal-design.html](../deep-link-import-modal-design.html)。\n\n### Modal 卡片结构\n\n入口卡片按“应用模板到 Codex”原页面组织,不再采用新的 parse rail + draft panels 双栏结构:\n\n1. Header summary:保留 `FILE PREVIEW CONFIRM` 和“应用模板到 Codex”标题;在现有 summary badge 里增加 `外部来源`、`resource`、`providerScope`、`providerRewriteMode`。\n2. 左侧栏:继续展示文件列表;`codex-setup` 额外在文件列表上方插入一个紧凑账号草稿摘要,避免新建完整账号导入页面。\n3. 右侧区域:继续展示 provider preflight 结果和文件 diff;`providerScope=current-active` 时必须突出“current user provider preserved”。\n4. Footer:保留现有取消 / 确认应用节奏;`codex-setup` 增加“只导入账号”,`codex-config` 不展示该按钮。\n\n### Modal 状态\n\n| 状态 | 视觉处理 | 说明 |\n| --- | --- | --- |\n| `preview-ready` | 绿色状态条 | 解析成功,等待用户确认 |\n| `warning` | 黄色状态条和 warning badge | 需要注意重名、兼容性降级、缺失 provider section |\n| `blocked` | 黄色或红色 warning card | 不能继续写入,比如 preserve 场景缺失 ChatGPT auth |\n| `preview-only` | footer 说明 + 按钮降级 | 浏览器 preview 或无 Wails runtime 时只读展示 |\n\n### Modal 文案原则\n\n1. 标题沿用“应用模板到 Codex”,避免用户误以为这是另一套写入链路。\n2. `providerID` 永远写成身份值,不要写成“默认值”或“当前默认 provider”。\n3. `current-active` 场景必须明确写出“保留 model_provider”。\n4. `create-new` 场景必须明确写出“仅在没有激活值时创建新 provider”。\n5. API key、token、auth 字段只展示脱敏值,diff 里也只展示脱敏值。\n\n### 浏览器 preview\n\n普通浏览器没有 Wails runtime 时可以解析示例链接并展示确认状态,但所有写入按钮进入 preview-only 状态,不调用真实绑定。\n\n## 后端设计\n\n新增 Wails-facing 方法建议:\n\n| 方法 | 作用 |\n| --- | --- |\n| `ParseDeepLink(url string)` | 解析 `gettokens://` URL,返回结构化 request |\n| `PreviewDeepLinkImport(request)` | 合并 config、读取现有账号/Codex 状态、生成确认模型和 diff |\n| `ApplyDeepLinkImport(request, options)` | 按确认选项执行账号导入和 Codex local apply |\n\nWails 绑定要求同步 root `app.go`、`app_types.go`、`app_mappers.go` 和 `frontend/wailsjs`。\n\n内部模块建议:\n\n```text\ninternal/wailsapp/deeplink.go\ninternal/wailsapp/deeplink_parser.go\ninternal/wailsapp/deeplink_preview.go\ninternal/wailsapp/deeplink_apply.go\n```\n\n纯解析函数必须不依赖 App,方便单元测试。\n\n## 数据结构草案\n\n```go\ntype DeepLinkImportRequest struct {\n Channel string\n Version string\n Resource string\n Source string\n Nonce string\n Account *DeepLinkAccountDraft\n CodexConfig *DeepLinkCodexConfigDraft\n}\n\ntype DeepLinkAccountDraft struct {\n AccountType string\n Name string\n Label string\n APIKey string\n APIKeys []string\n BaseURL string\n Prefix string\n ProxyURL string\n Models []OpenAICompatibleModel\n FormatBaseURLs map[string]string\n AuthFileName string\n AuthFileJSON string\n Enabled bool\n}\n\ntype DeepLinkCodexConfigDraft struct {\n Mode string\n AccountRef string\n Model string\n ReasoningEffort string\n ProviderID string\n ProviderName string\n ProviderScope string\n BaseURL string\n APIKey string\n SupportsWebsockets bool\n Apply bool\n}\n```\n\n## BDD 验收\n\n### 场景 1:导入 Codex API key 账号\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=codex-api-key`\nWhen 用户确认导入账号\nThen GetTokens 调用 `CreateCodexAPIKey`\nAnd 新账号出现在账号列表\nAnd 未确认 Codex 配置前不写 `CODEX_HOME/config.toml`\n\n### 场景 2:导入 OpenAI-compatible provider\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=openai-compatible`\nWhen provider name 不存在且用户确认\nThen GetTokens 调用 `CreateOpenAICompatibleProvider`\nAnd 账号列表出现 `openai-compatible:<name>`\n\n### 场景 3:导入 auth-file\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=auth-file&config=<base64url-json>`\nWhen 用户确认\nThen GetTokens 归一化 auth file 内容并调用 `UploadAuthFiles`\nAnd 确认页不显示完整 token\n\n### 场景 4:预览 Codex config patch\n\nGiven deep link 包含 `channel=codex&resource=codex-config`\nWhen 确认弹窗打开\nThen GetTokens 读取当前 `CODEX_HOME/config.toml`\nAnd 展示当前 provider、目标 provider 和 diff\nAnd 不做完整文件覆盖\n\n### 场景 5:provider 兼容性与 status 页一致\n\nGiven deep link 需要改写 provider 且 `/#frame=status` 当前显示的 provider 目录可用\nWhen 目标 provider 为 `openai` 或本地 provider section 缺失\nThen 预览阶段返回与 status 页一致的兼容性结果\nAnd `openai` 在 preserve 场景下被阻断\nAnd 只在兼容时才允许继续写入 `model_provider` 与 `[model_providers.<id>]`\n\n### 场景 6:组合导入\n\nGiven deep link 包含 `channel=codex&resource=codex-setup`\nWhen 用户点击确认\nThen 系统先创建账号\nAnd 再把创建出的账号 ID 作为 `accountRef` 生成 Codex local apply 草稿\nAnd Codex config apply 失败时保留已创建账号并展示失败原因\n\n### 场景 7:重复和冲突\n\nGiven deep link 要创建的 provider name 已存在\nWhen 用户预览导入\nThen 弹窗提示重名\nAnd 默认不覆盖已有账号\nAnd 用户只能选择取消或另存为新名称\n\n### 场景 8:敏感信息保护\n\nGiven deep link 带有 `apiKey` 或 auth tokens\nWhen 后端记录日志或前端展示确认页\nThen 日志不包含明文密钥\nAnd UI 默认脱敏\n\n### 场景 9:无 Wails preview\n\nGiven 浏览器 preview 环境没有 Wails runtime\nWhen 打开 deep link 预览\nThen 页面能展示解析结果和确认布局\nAnd 写入按钮不可执行\n\n### 场景 10:字段 presence 覆盖\n\nGiven deep link 只提供部分 `auth.json` / `config.toml` 字段\nWhen 用户确认应用 Codex 配置\nThen 显式提供的字段覆盖本地配置\nAnd 未提供的字段保留用户现有值\nAnd `false` 被视为显式值,例如 `supports_websockets=false` 与 `requires_openai_auth=false` 都写入 false\n\n## 测试计划\n\n### Go 单元测试\n\n1. `TestParseDeepLinkImportMergesConfigAndQueryWins`\n2. `TestParseDeepLinkImportRejectsUnsupportedFieldsAndNonCodexChannel`\n3. `TestPreviewDeepLinkImportRedactsURLAndKeepsCurrentActiveProvider`\n4. `TestPreviewDeepLinkImportCreatesProviderOnlyWhenNoExplicitActiveProvider`\n5. `TestPreviewDeepLinkImportBuildsCodexConfigFromDocuments`\n6. `TestPreviewDeepLinkImportPreservesUnspecifiedConfigTomlFields`\n7. `TestApplyDeepLinkImportCodexSetupReportsPartialSuccess`\n8. `TestApplyRelayServiceConfigToLocalV2WritesExplicitSupportsWebsocketsFalse`\n9. `TestApplyRelayServiceConfigToLocalV2PreservesSupportsWebsocketsWhenUnset`\n\n### 前端单测\n\n1. 解析后的确认模型能正确渲染账号类型。\n2. API key 和 token 字段脱敏。\n3. `codex-setup` 在账号重名时禁用主确认按钮。\n4. preview-only 环境不调用 Wails 写入。\n\n### 集成验证\n\n1. macOS 安装包注册 `gettokens://` scheme 后可唤起 app。\n2. sidecar ready 时组合导入能创建账号并刷新账号列表。\n3. Codex local apply diff 与实际写入一致。\n\n## 测试样例\n\n### 1. Codex API key 账号导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=codex-api-key&label=Team%20Relay&apiKey=sk-test-123456&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&model=gpt-5-codex\n```\n\nPatch A(本机已有激活 `model_provider`,不改用户默认值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.supports_websockets\", \"value\": false }\n ]\n }\n ]\n}\n```\n\nPatch B(本机没有激活 `model_provider`,才新建):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 2. OpenAI-compatible provider 导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=openai-compatible&name=deepseek&apiKey=sk-test-abc&baseUrl=https%3A%2F%2Fapi.deepseek.com%2Fv1\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 3. auth-file 导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=auth-file&name=team-codex-auth.json&config=eyJkb2N1bWVudHMiOlt7InRhcmdldCI6ImF1dGguanNvbiIsImZvcm1hdCI6Impzb24iLCJtb2RlIjoibWVyZ2UiLCJvcGVyYXRpb25zIjpbeyJvcCI6InNldCIsInBhdGgiOiIvYXV0aF9tb2RlIiwidmFsdWUiOiJjaGF0Z3B0In0seyJvcCI6InNldCIsInBhdGgiOiIvdXNlci9lbWFpbCIsInZhbHVlIjoidGVhbUBleGFtcGxlLmNvbSJ9XX1dfQ\n```\n\nPatch:\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"chatgpt\" },\n { \"op\": \"set\", \"path\": \"/user/email\", \"value\": \"team@example.com\" }\n ]\n }\n ]\n}\n```\n\n### 4. Codex config 仅改写当前 provider\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=api-key&model=gpt-5-codex&reasoningEffort=high&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&apiKey=sk-test-123456&providerID=team-relay&providerName=Team%20Relay&providerScope=current-active\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 5. preserve ChatGPT 登录态\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=preserve-chatgpt-provider&model=gpt-5-codex&reasoningEffort=medium&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&providerID=relay-chatgpt-preserve&providerName=Relay%20Preserve&providerScope=current-active\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"medium\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.experimental_bearer_token\", \"value\": \"<preserved-token>\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"medium\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"relay-chatgpt-preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.experimental_bearer_token\", \"value\": \"<preserved-token>\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 6. 组合导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-setup&config=eyJhY2NvdW50Ijp7ImFjY291bnRUeXBlIjoiY29kZXgtYXBpLWtleSIsImxhYmVsIjoiVGVhbSBSZWxheSIsImFwaUtleSI6InNrLXRlc3QtMTIzNDU2IiwiYmFzZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3YxIiwibW9kZWxzIjpbeyJuYW1lIjoiZ3B0LTUuNCIsImFsaWFzIjoiZ3B0LTUuNCJ9XX0sImNvZGV4Q29uZmlnIjp7Im1vZGUiOiJhcGkta2V5IiwibW9kZWwiOiJncHQtNS1jb2RleCIsInJlYXNvbmluZ0VmZm9ydCI6ImhpZ2giLCJwcm92aWRlcklEIjoidGVhbS1yZWxheSIsInByb3ZpZGVyTmFtZSI6IlRlYW0gUmVsYXkiLCJwcm92aWRlclNjb3BlIjoiY3VycmVudC1hY3RpdmUiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vdjEiLCJhcGlLZXkiOiJzay10ZXN0LTEyMzQ1NiIsInN1cHBvcnRzV2Vic29ja2V0cyI6dHJ1ZX19\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n## 实施顺序\n\n1. 先做 parser 和 Go 单元测试。\n2. 增加 preview DTO,只返回脱敏确认模型,不写入。\n3. 接前端确认弹窗和 preview-only 状态。\n4. 接账号导入 apply。\n5. 接 Codex config local apply。\n6. 最后注册 macOS URL scheme 并做桌面验收。\n\n## 待确认决策\n\n1. URL scheme 使用 `gettokens://`,还是兼容旧名/产品名别名。推荐只注册 `gettokens://`。\n2. auth-file 是否进入首期。推荐首期只做 `codex-api-key` 和 `openai-compatible`,auth-file 保留 parser 设计但不开放外链入口。\n3. OpenAI-compatible 创建时是否同步支持 models/headers。推荐首期支持 models,不支持 headers。\n"}, "f22688eb193278693ba7cdffa50b9eac24889cd2": {"t": 1785759458.4501712, "n": 35619, "h": "304a46ad79111cc9", "c": "# GetTokens deep link 账号与 Codex 配置导入设计\n\n## 设计目标\n\n设计 `gettokens://` deep link,让外部页面、厂商文档或团队配置可以把账号和 Codex 本地配置带入 GetTokens。设计必须满足两个前提:\n\n1. Deep link 只表达“导入意图”,不直接落盘。\n2. 最终写入必须复用现有账号、sidecar 和 Codex local apply 服务层,不能绕过既有校验去写 `auth.json`、`config.toml` 或 CLIProxyAPI 配置。\n\n## 现有边界\n\n### 账号服务\n\n当前账号列表由三类来源组成:\n\n| 来源 | 账号 ID | 现有创建/写入入口 |\n| --- | --- | --- |\n| Codex OAuth / auth file | `auth-file:<name>` | `UploadAuthFiles(files)` 经 sidecar `/v0/management/auth-files` 上传 |\n| Codex API key | `codex-api-key:<local-id>` 或指纹 ID | `CreateCodexAPIKey(input)` 写入本地 `~/.config/gettokens-data/codex-api-keys/*.json` 并同步 sidecar |\n| OpenAI-compatible provider | `openai-compatible:<name>` | `CreateOpenAICompatibleProvider(input)` 经 CLIProxyAPI Management API 写入 provider |\n\n账号列表展示和路由语义已经区分 `auth-file`、`codex-api-key`、`openai-compatible`,后续 deep link 必须沿用这个分层。\n\n### Codex 本地配置\n\nCodex local apply 已有三类语义:\n\n1. API Key 模式:写 `CODEX_HOME/auth.json` 与 `CODEX_HOME/config.toml`。\n2. OAuth / auth-file 模式:写所选账号 OAuth `auth.json`,让 Codex 走 ChatGPT/Codex OAuth backend。\n3. 保留 ChatGPT 登录态模式:只 patch `config.toml` 的 custom provider 字段。\n\n`GetLocalCodexModelProviderState()` 能读取当前 `model_provider`、当前模型和已存在 provider;`mergeRelayCodexConfigToml()` 已经实现保留式 TOML patch。Deep link 不应新增完整覆盖 `config.toml` 的入口。\n\n## 方案比较\n\n### 方案 A:只支持账号 deep link\n\n只让 `gettokens://v1/import?channel=codex&resource=account...` 创建账号,不触碰 Codex 本地配置。\n\n- 成本:低。\n- 风险:低。\n- 问题:用户仍需手动进入 Codex local apply,无法满足“直接导入账号和 codex 配置”。\n\n### 方案 B:账号与 Codex 配置分资源导入\n\n支持 `resource=account` 和 `resource=codex-config` 两类 deep link。账号导入创建账号;Codex 配置导入只生成 local apply 草稿和 diff,确认后调用现有 local apply。\n\n- 成本:中。\n- 风险:中。\n- 依赖:现有账号创建、Codex provider state、local apply diff / apply。\n- 问题:外部平台想“一条链接完成账号和配置”时体验不够顺。\n\n### 方案 C:账号、Codex 配置、组合导入三层协议\n\n在方案 B 基础上增加 `resource=codex-setup`,一条链接可以同时携带账号草稿和 Codex 配置草稿。确认页分成两个步骤:先创建账号,再应用 Codex 配置;任一步失败都不吞掉错误。\n\n- 成本:中高。\n- 风险:可控,但需要更完整的测试矩阵。\n- 依赖:同方案 B,并额外需要导入事务/回滚提示设计。\n- 推荐:采用。它满足外部一键配置场景,同时保留账号和 Codex 配置的独立确认边界。\n\n攻击角度校验:\n\n| 角度 | 结论 |\n| --- | --- |\n| 依赖失败 | sidecar 未 ready 时只允许保存账号草稿或展示待执行,不写 Codex 配置;Codex local apply 失败不删除已创建账号 |\n| 数据量放大 | deep link 只支持单账号、单 Codex 配置;批量导入改走 JSON 文件导入 |\n| 回滚成本 | 账号创建和 Codex 配置写入分步展示,Codex 配置写入前有 diff;账号误导入可用现有删除入口 |\n| 前提崩塌 | 如果系统未注册 URL scheme,仍可提供“粘贴 deep link”入口复用同一 parser |\n\n## 推荐方案\n\n采用方案 C:三类 resource,共用 parser 和确认弹窗。\n\n```text\ngettokens://v1/import?channel=codex&resource=account&...\ngettokens://v1/import?channel=codex&resource=codex-config&...\ngettokens://v1/import?channel=codex&resource=codex-setup&...\n```\n\n### 不做的事\n\n1. 不支持远程 `configUrl`。\n2. 不支持导入 usage script 或任意可执行脚本。\n3. 不支持批量账号导入。\n4. 不支持无确认自动启用、自动切换 Codex 当前账号。\n5. 不支持完整替换 `CODEX_HOME/config.toml`。\n6. 不支持把任意 TOML 片段拼进用户配置;只允许受控字段 patch。\n\n## 协议设计\n\n### 通用参数\n\n| 参数 | 必填 | 说明 |\n| --- | --- | --- |\n| `channel` | 是 | 固定值 `codex`,明确这是 Codex 账号 / 配置导入,不与其他产品线混淆 |\n| `resource` | 是 | `account` / `codex-config` / `codex-setup` |\n| `version` | 否 | URL host 已表达 `v1`,query 中可省略 |\n| `source` | 否 | 来源标识,用于确认页展示,不参与信任判断 |\n| `nonce` | 否 | 外部生成的去重标识;同 nonce 重复点击时提示重复 |\n| `config` | 否 | Base64URL 编码 JSON;复杂字段优先放这里 |\n| `enabled` | 否 | 只表示账号创建后是否启用;默认 `false` |\n| `apply` | 否 | 是否在确认后继续应用 Codex 配置;默认 `false` |\n\n所有 query 参数和 `config` 合并时,query 参数优先级更高。`config` 只允许 JSON,使用 Base64URL UTF-8 编码,不支持 TOML、YAML 或远程 URL。\n\n`config` 的 canonical 形式不是一组散落字段,而是一个 `documents[]` 补丁包。`account` / `codexConfig` 这类对象只是便捷语法,最终都要编译成对 `auth.json`、`config.toml` 或未来扩展文件的文档级 patch。\n\n### 字段覆盖语义\n\nCodex config patch 统一采用 presence 语义:\n\n1. 字段在 query、`codexConfig` 或 `documents[]` operation 中显式出现时,才覆盖对应的本地字段。\n2. 字段未出现时,保留用户现有 `auth.json` / `config.toml` 内容,不用默认值回填。\n3. `false`、空字符串以外的零值都不能被当作“未出现”;例如 `supportsWebsockets=false` 和 `requires_openai_auth=false` 必须写入 `false`。\n4. `providerID` 只作为身份值;本机已有显式 `model_provider` 时不改 root `model_provider`,只把显式字段 patch 到当前激活 provider section。\n5. 手动“应用模板到 Codex”不是 deep link patch,它会把当前表单值作为显式字段提交,保持原有一键应用体验。\n\n## 通用文件改写格式\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-xxx\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" }\n ]\n }\n ]\n}\n```\n\n### 文档对象字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `target` | 是 | 目标文件名,如 `auth.json`、`config.toml` |\n| `format` | 是 | `json` / `toml` / `text` |\n| `mode` | 否 | `merge` / `patch` / `replace`,默认由格式推断 |\n| `operations` | 是 | 按顺序执行的改写操作 |\n| `preserveUnknown` | 否 | 是否保留未知字段,默认 `true` |\n| `backup` | 否 | 是否在写入前保留备份,默认 `true` |\n\n### 操作对象字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `op` | 是 | `set` / `delete` / `append` / `merge` |\n| `path` | 是 | `json` 使用 JSON Pointer,`toml` 使用 dotted path |\n| `value` | 否 | 新值,JSON value 形式 |\n| `valueEncoding` | 否 | `plain` / `base64`,默认 `plain` |\n| `allowCreate` | 否 | 不存在时是否创建路径,默认 `true` |\n\n### 目标文件约定\n\n| target | 说明 |\n| --- | --- |\n| `auth.json` | Codex auth file,适合 `replace` 或 root-level `set`/`merge` |\n| `config.toml` | Codex 本地配置,适合 root key 和 `model_providers.<id>` patch |\n| `settings.json` | 未来可扩展目标,当前不开放 |\n\n### 推荐路径语义\n\n| 文件 | 路径语义 |\n| --- | --- |\n| JSON 文件 | JSON Pointer,如 `/user/email`、`/tokens/access_token` |\n| TOML 文件 | dotted path,如 `model_provider`、`model_providers.team-relay.base_url` |\n\n### 典型操作\n\n| 操作 | 语义 |\n| --- | --- |\n| `set` | 设置或覆盖路径值 |\n| `delete` | 删除路径 |\n| `append` | 向数组追加单项 |\n| `merge` | 对对象/表执行深合并,保留未提及字段 |\n\n## 字段总表\n\n### 1. 通用字段\n\n| 字段 | 必填 | 适用资源 | 说明 |\n| --- | --- | --- | --- |\n| `channel` | 是 | 全部 | 固定值 `codex`,用于明确这是 Codex 导入链路 |\n| `resource` | 是 | 全部 | `account` / `codex-config` / `codex-setup` |\n| `config` | 否 | 全部 | Base64URL JSON,优先承载复杂对象 |\n| `source` | 否 | 全部 | 来源标识,仅用于确认页展示 |\n| `nonce` | 否 | 全部 | 去重标识 |\n| `enabled` | 否 | account | 账号导入后是否启用,默认 `false` |\n| `apply` | 否 | codex-config / codex-setup | 是否在确认后继续应用 Codex 配置,默认 `false` |\n\n### 2. account 资源字段\n\n#### 2.1 `accountType = codex-api-key`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `CreateCodexAPIKey` | 固定值 `codex-api-key` |\n| `label` | 否 | `CreateCodexAPIKeyInput.Label` | 显示名 |\n| `apiKey` | 是 | `CreateCodexAPIKeyInput.APIKey` | 账号 API key |\n| `baseUrl` | 是 | `CreateCodexAPIKeyInput.BaseURL` | API base URL |\n| `prefix` | 否 | `CreateCodexAPIKeyInput.Prefix` | 前缀 |\n| `proxyUrl` | 否 | `CreateCodexAPIKeyInput.ProxyURL` | 代理 URL |\n| `models` | 否 | `CreateCodexAPIKeyInput.Models` | 模型映射,放在 `config` 中更合适 |\n| `formatBaseUrls` | 否 | `CreateCodexAPIKeyInput.FormatBaseURLs` | 格式级 base URL 映射 |\n| `quotaCurl` | 否 | `CreateCodexAPIKeyInput.QuotaCurl` | 用量查询 curl,仅建议本地生成,不建议外链 |\n| `billingCurl` | 否 | `CreateCodexAPIKeyInput.BillingCurl` | 账单 curl,仅建议本地生成,不建议外链 |\n| `quotaEnabled` | 否 | `CreateCodexAPIKeyInput.QuotaEnabled` | 是否启用用量查询 |\n| `billingEnabled` | 否 | `CreateCodexAPIKeyInput.BillingEnabled` | 是否启用账单查询 |\n\n#### 2.2 `accountType = openai-compatible`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `CreateOpenAICompatibleProvider` | 固定值 `openai-compatible` |\n| `name` | 是 | `CreateOpenAICompatibleProviderInput.Name` | provider 名称 |\n| `apiKey` | 是 | `CreateOpenAICompatibleProviderInput.APIKey` | 首个 API key |\n| `baseUrl` | 是 | `CreateOpenAICompatibleProviderInput.BaseURL` | provider base URL |\n| `prefix` | 否 | `CreateOpenAICompatibleProviderInput.Prefix` | provider 前缀 |\n| `models` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 模型映射 |\n| `apiKeys` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 多 key 账号 |\n| `proxyUrl` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 代理 URL |\n| `headers` | 否 | 首期不支持 | 需要手动补充,避免外链注入 |\n\n#### 2.3 `accountType = auth-file`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `UploadAuthFiles` | 固定值 `auth-file` |\n| `name` | 是 | `UploadAuthFiles` | 文件名 |\n| `config.documents[]` | 是 | `UploadAuthFiles` | 目标 `auth.json` 的补丁包,建议用 `merge` 或 `replace` |\n| `enabled` | 否 | 仅在预览层解释 | auth file 本身不是启停字段 |\n\n### 3. codex-config 资源字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `mode` | 是 | `api-key` / `oauth-auth-file` / `preserve-chatgpt-provider` |\n| `config.documents[]` | 是 | 至少包含 `auth.json` 或 `config.toml` 之一的补丁包 |\n| `model` | 否 | root `model`,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `reasoningEffort` | 否 | root `model_reasoning_effort`,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `providerID` | 否 | 当前无激活 provider 时的创建值;若已有激活值则保持原值,仅 patch 当前 section |\n| `providerName` | 否 | provider 显示名,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `baseUrl` | 按 mode | custom provider base URL,可写入 `auth.json` 或 `config.toml` |\n| `apiKey` | `api-key` 必填 | 写入 `auth.json` 或 bearer token 草稿 |\n| `accountRef` | 否 | `auth-file:<name>` / `codex-api-key:<id>` / `openai-compatible:<name>` |\n| `supportsWebsockets` | 否 | patch `supports_websockets`;遵循 presence 语义,`false` 也是显式写入 |\n| `providerScope` | 否 | `current-active` / `create-new`,显式标识沿用当前激活 provider 还是创建新 provider |\n| `providerCompatibility` | 否 | 预览结果 | `compatible` / `blocked_builtin_openai` / `missing_chatgpt_auth` / `missing_provider_section` |\n| `providerRewriteMode` | 否 | 预览结果 | `keep-current` / `patch-current` / `create-new` |\n\n### 4. codex-setup 资源字段\n\n`codex-setup` 主要通过 `config` 传入两块对象:\n\n| 对象 | 字段 |\n| --- | --- |\n| `account` | `accountType`, `name`, `label`, `apiKey`, `apiKeys`, `baseUrl`, `prefix`, `proxyUrl`, `models`, `formatBaseUrls`, `quotaCurl`, `billingCurl`, `authFile` |\n| `codexConfig` | `mode`, `model`, `reasoningEffort`, `providerID`, `providerName`, `providerScope`, `baseUrl`, `apiKey`, `supportsWebsockets`, `accountRef`, `providerCompatibility`, `providerRewriteMode` |\n\n`codex-setup` 额外支持通用字段 `apply`、`enabled`、`source`、`nonce`,但不会新增自己的字段池。\n\n### 5. 明确不支持的字段\n\n| 字段 | 状态 | 说明 |\n| --- | --- | --- |\n| `configUrl` | 不支持 | 不做远程拉取 |\n| `usageScript` | 不支持 | 不导入可执行脚本 |\n| `headers`(openai-compatible 首期) | 不支持 | 防止外链注入请求头 |\n| 批量账号数组 | 不支持 | 只做单账号导入 |\n| 任意 TOML 片段 | 不支持 | 只允许受控 patch |\n| 自动启用 / 自动切换当前账号 | 不支持 | 必须确认后再做 |\n\n### resource=account\n\n账号导入支持三种 `accountType`:\n\n| accountType | 用途 | 最终服务入口 |\n| --- | --- | --- |\n| `codex-api-key` | 创建 GetTokens 本地 Codex API key 账号 | `CreateCodexAPIKey` |\n| `openai-compatible` | 创建 CLIProxyAPI OpenAI-compatible provider | `CreateOpenAICompatibleProvider`,后续增强可走 `UpdateOpenAICompatibleProvider` 支持 models/headers |\n| `auth-file` | 导入 Codex OAuth auth.json | `UploadAuthFiles` |\n\n#### Codex API key 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=codex-api-key&label=Team%20Relay&apiKey=sk-xxx&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&model=gpt-5-codex\n```\n\n字段:\n\n| 字段 | 必填 | 映射 |\n| --- | --- | --- |\n| `label` | 否 | `CreateCodexAPIKeyInput.Label` |\n| `apiKey` | 是 | `CreateCodexAPIKeyInput.APIKey` |\n| `baseUrl` | 是 | `CreateCodexAPIKeyInput.BaseURL` |\n| `prefix` | 否 | `CreateCodexAPIKeyInput.Prefix` |\n| `proxyUrl` | 否 | `CreateCodexAPIKeyInput.ProxyURL` |\n| `models` | 否 | `CreateCodexAPIKeyInput.Models`,从 `config` JSON 传数组 |\n| `formatBaseUrls` | 否 | `CreateCodexAPIKeyInput.FormatBaseURLs` |\n| `quotaCurl` / `billingCurl` | 否 | 默认只预览,不建议外链携带 |\n\n#### OpenAI-compatible 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=openai-compatible&name=deepseek&apiKey=sk-xxx&baseUrl=https%3A%2F%2Fapi.deepseek.com%2Fv1\n```\n\n字段:\n\n| 字段 | 必填 | 映射 |\n| --- | --- | --- |\n| `name` | 是 | provider name,必须唯一 |\n| `apiKey` | 是 | 首个 API key |\n| `baseUrl` | 是 | provider base URL |\n| `prefix` | 否 | provider prefix |\n| `models` | 否 | 需要增强 root `CreateOpenAICompatibleProvider`,否则首期只能创建后再提示用户补模型 |\n| `headers` | 否 | 首期不允许 deep link 写入,避免外链注入 header |\n\n#### Auth file 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=auth-file&name=team-codex-auth.json&config=<base64url-json>\n```\n\n`config` JSON:\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"chatgpt\" },\n { \"op\": \"set\", \"path\": \"/user/email\", \"value\": \"team@example.com\" }\n ]\n }\n ]\n}\n```\n\nauth-file 导入风险最高,确认页必须展示 email / plan / token 字段摘要,不展示完整 token。默认不支持从普通 query 参数直接传 `authJson`,只允许 `config`。\n\n### resource=codex-config\n\nCodex 配置导入只表达受控 patch,不表达整份 TOML。推荐把 `auth.json` 和 `config.toml` 都放进同一个 `documents[]` bundle。\n\n示例:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=api-key&model=gpt-5-codex&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&apiKey=sk-xxx&providerID=team-relay\n```\n\n字段:\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `mode` | 是 | `api-key` / `oauth-auth-file` / `preserve-chatgpt-provider` |\n| `config.documents[]` | 是 | 目标 `auth.json` / `config.toml` 的补丁包 |\n| `model` | 否 | root `model`,可由 `config.toml` 文档表示 |\n| `reasoningEffort` | 否 | root `model_reasoning_effort`,可由 `config.toml` 文档表示 |\n| `providerID` | 否 | 目标 provider;默认读取当前 `model_provider`,`mode=preserve-chatgpt-provider` 时不得为 `openai` |\n| `providerName` | 否 | `[model_providers.<id>].name` |\n| `baseUrl` | 按模式 | custom provider base URL,可写入 `config.toml` 文档 |\n| `apiKey` | `api-key` 必填 | 只用于 auth 写入或 bearer token 草稿,可写入 `auth.json` 文档 |\n| `supportsWebsockets` | 否 | patch `supports_websockets`;遵循 presence 语义,`false` 也是显式写入 |\n| `accountRef` | 否 | 指向刚导入或已有账号,如 `codex-api-key:<id>` |\n\n模式语义:\n\n| mode | 写入策略 |\n| --- | --- |\n| `api-key` | 生成 Codex API key local apply 草稿,写 `auth.json` 的 `OPENAI_API_KEY`,并 patch `config.toml` |\n| `oauth-auth-file` | 选择已有或刚导入的 `auth-file`,写 OAuth tokens,让 Codex 走 ChatGPT/Codex OAuth backend |\n| `preserve-chatgpt-provider` | 不碰 `auth.json` tokens,只 patch custom provider;需要 preflight 确认本机有可保留 ChatGPT 登录态 |\n\n`codex-config` 的确认页必须先调用 `GetLocalCodexModelProviderState()`,展示当前 provider。默认 patch 当前 provider,不默认创建 `gettokens` provider;如果本地已有激活的 `model_provider`,deep link 不得改写它,只能补齐当前 provider 的 section。\n\n`providerScope` 是这个场景的显式标识:\n\n1. `providerScope=current-active` 表示沿用当前激活 `model_provider`,只 patch 该 provider 的 section。\n2. `providerScope=create-new` 表示创建新的 provider,并在没有当前激活 provider 时写入新的 `model_provider`。\n3. 当 `providerScope=current-active` 但当前没有激活值时,确认页可退回到 `create-new`,但必须明确提示是降级执行。\n4. `providerID` 只作为具体 provider 的身份值,不再承担“是否改写当前激活值”的语义。\n\n### provider 改写与 status 页对齐\n\n`/#frame=status` 里的 provider 语义,和 deep link 的 provider 改写语义必须一致:\n\n1. provider 是本地工作台偏好,不是 sidecar 的单一真相。\n2. 默认优先保留当前 `model_provider` 的连续性;是否沿用当前激活值由 `providerScope=current-active` 显式表达。\n3. 只有 `providerScope=create-new`,或者当前没有激活值且已降级确认时,才允许新建 `model_provider` / `[model_providers.<id>]`。\n4. `openai` 只作为内置连续性保留,不作为 `preserve-chatgpt-provider` 的目标 provider id。\n5. 当本地 `config.toml` 已存在当前激活 provider section 时,deep link 只 patch 当前 section,不静默清空其他 provider section。\n6. 当当前 provider section 缺失但符合兼容性要求时,确认页可以提示“将补齐当前 provider section”;只有 `providerScope=create-new` 时才提示“将创建新的 provider”。\n7. 若当前 provider 不存在、为 builtin `openai`,或与 preserve 模式冲突,则预览阶段必须给出和 status 页一致的阻断/警告文案。\n8. 所有受控字段都采用 presence 语义:显式出现才覆盖,未出现就保留;`supports_websockets=false`、`requires_openai_auth=false` 这类布尔 false 也必须写入。\n\n兼容性结果沿用状态页的心智,建议至少分成:\n\n| 结果 | 含义 |\n| --- | --- |\n| `compatible` | 可以按当前 provider 连续性应用 |\n| `blocked_builtin_openai` | 目标 provider 或模式要求与内置 `openai` 冲突 |\n| `missing_chatgpt_auth` | preserve 场景下本机 ChatGPT auth 不存在或无效 |\n| `missing_provider_section` | 需要写入的 provider section 还不存在 |\n\n### resource=codex-setup\n\n组合导入用于“一条链接导入账号并配置 Codex”。推荐把复杂内容放在 `config` JSON。\n\n```json\n{\n \"account\": {\n \"accountType\": \"codex-api-key\",\n \"label\": \"Team Relay\",\n \"apiKey\": \"sk-xxx\",\n \"baseUrl\": \"https://api.example.com/v1\",\n \"models\": [\n { \"name\": \"gpt-5-codex\", \"alias\": \"gpt-5-codex\" }\n ]\n },\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"replace\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"apikey\" },\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-xxx\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n执行顺序:\n\n1. Parse:只解析和校验,不写入。\n2. Preview:展示账号草稿、Codex config diff、敏感字段脱敏。\n3. Confirm account:创建账号。\n4. Resolve accountRef:把刚创建账号的 ID 注入 Codex config 草稿或后续文档 patch。\n5. Confirm/apply Codex config:调用现有 local apply。\n6. Refresh:刷新账号列表、Codex channel routing、Codex config snapshot。\n\n如果账号创建成功但 Codex config apply 失败,不回滚账号;确认页展示“账号已导入,Codex 配置未应用”的明确状态。\n\n## 安全策略\n\n1. 日志中 deep link 永远只记录 redacted URL,不记录 `apiKey`、`config`、token、header。\n2. UI 中 API key 只展示前 4 位和后 4 位,中间脱敏。\n3. `config` 大小限制建议 32 KB;超出要求改用文件导入。\n4. URL scheme handler 和“粘贴链接导入”共用 parser,避免两套解析差异。\n5. 所有写入前必须展示 diff 或字段级 change summary。\n6. `enabled=true` 不代表切换当前 Codex 本地配置;只代表账号创建后是否在 GetTokens/sidecar 中禁用。\n7. `apply=true` 只让确认页默认勾选“继续应用 Codex 配置”,不能跳过确认。\n8. 不允许 deep link 写入 arbitrary headers;OpenAI-compatible headers 首期只展示“需要用户手动补充”。\n\n## UI 交互\n\n### DeepLinkImportDialog\n\n`DeepLinkImportDialog` 不再新建一套独立确认 UI,而是作为 thin adapter 复用现有“应用模板到 Codex”确认页。deep link 解析和 preview 只负责把外部导入意图转换成现有 `AccountLocalCliApplyConfirm` 能消费的 Codex local apply 草稿,并补充来源、resource、providerScope 等 deep link 元信息。\n\n复用边界:\n\n| resource | UI 入口 | 说明 |\n| --- | --- | --- |\n| `account` | 账号导入确认 | 只创建账号,不进入“应用模板到 Codex” |\n| `codex-config` | 直接打开“应用模板到 Codex” | deep link 转成 Codex local apply 草稿 |\n| `codex-setup` | 账号草稿摘要 + “应用模板到 Codex” | 先确认账号草稿,再沿用同一 diff / apply 页面 |\n\n必须复用的现有能力:\n\n1. `ModalFrame` 的窗口外壳、遮罩、header/footer 分区。\n2. `AccountLocalCliApplyConfirm` 的标题、summary badge、文件列表、diff 预览和 footer 状态。\n3. `StatusSnippetPanel` / `buildCodexLocalApplyDiff` 的文件预览。\n4. `/#frame=status` 已有 preflight / provider 兼容判断。\n\n`codex-config` 单独导入时不展示“只导入账号”。\n\n视觉稿入口:[../deep-link-import-modal-design.html](../deep-link-import-modal-design.html)。\n\n### Modal 卡片结构\n\n入口卡片按“应用模板到 Codex”原页面组织,不再采用新的 parse rail + draft panels 双栏结构:\n\n1. Header summary:保留 `FILE PREVIEW CONFIRM` 和“应用模板到 Codex”标题;在现有 summary badge 里增加 `外部来源`、`resource`、`providerScope`、`providerRewriteMode`。\n2. 左侧栏:继续展示文件列表;`codex-setup` 额外在文件列表上方插入一个紧凑账号草稿摘要,避免新建完整账号导入页面。\n3. 右侧区域:继续展示 provider preflight 结果和文件 diff;`providerScope=current-active` 时必须突出“current user provider preserved”。\n4. Footer:保留现有取消 / 确认应用节奏;`codex-setup` 增加“只导入账号”,`codex-config` 不展示该按钮。\n\n### Modal 状态\n\n| 状态 | 视觉处理 | 说明 |\n| --- | --- | --- |\n| `preview-ready` | 绿色状态条 | 解析成功,等待用户确认 |\n| `warning` | 黄色状态条和 warning badge | 需要注意重名、兼容性降级、缺失 provider section |\n| `blocked` | 黄色或红色 warning card | 不能继续写入,比如 preserve 场景缺失 ChatGPT auth |\n| `preview-only` | footer 说明 + 按钮降级 | 浏览器 preview 或无 Wails runtime 时只读展示 |\n\n### Modal 文案原则\n\n1. 标题沿用“应用模板到 Codex”,避免用户误以为这是另一套写入链路。\n2. `providerID` 永远写成身份值,不要写成“默认值”或“当前默认 provider”。\n3. `current-active` 场景必须明确写出“保留 model_provider”。\n4. `create-new` 场景必须明确写出“仅在没有激活值时创建新 provider”。\n5. API key、token、auth 字段只展示脱敏值,diff 里也只展示脱敏值。\n\n### 浏览器 preview\n\n普通浏览器没有 Wails runtime 时可以解析示例链接并展示确认状态,但所有写入按钮进入 preview-only 状态,不调用真实绑定。\n\n## 后端设计\n\n新增 Wails-facing 方法建议:\n\n| 方法 | 作用 |\n| --- | --- |\n| `ParseDeepLink(url string)` | 解析 `gettokens://` URL,返回结构化 request |\n| `PreviewDeepLinkImport(request)` | 合并 config、读取现有账号/Codex 状态、生成确认模型和 diff |\n| `ApplyDeepLinkImport(request, options)` | 按确认选项执行账号导入和 Codex local apply |\n\nWails 绑定要求同步 root `app.go`、`app_types.go`、`app_mappers.go` 和 `frontend/wailsjs`。\n\n内部模块建议:\n\n```text\ninternal/wailsapp/deeplink.go\ninternal/wailsapp/deeplink_parser.go\ninternal/wailsapp/deeplink_preview.go\ninternal/wailsapp/deeplink_apply.go\n```\n\n纯解析函数必须不依赖 App,方便单元测试。\n\n## 数据结构草案\n\n```go\ntype DeepLinkImportRequest struct {\n Channel string\n Version string\n Resource string\n Source string\n Nonce string\n Account *DeepLinkAccountDraft\n CodexConfig *DeepLinkCodexConfigDraft\n}\n\ntype DeepLinkAccountDraft struct {\n AccountType string\n Name string\n Label string\n APIKey string\n APIKeys []string\n BaseURL string\n Prefix string\n ProxyURL string\n Models []OpenAICompatibleModel\n FormatBaseURLs map[string]string\n AuthFileName string\n AuthFileJSON string\n Enabled bool\n}\n\ntype DeepLinkCodexConfigDraft struct {\n Mode string\n AccountRef string\n Model string\n ReasoningEffort string\n ProviderID string\n ProviderName string\n ProviderScope string\n BaseURL string\n APIKey string\n SupportsWebsockets bool\n Apply bool\n}\n```\n\n## BDD 验收\n\n### 场景 1:导入 Codex API key 账号\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=codex-api-key`\nWhen 用户确认导入账号\nThen GetTokens 调用 `CreateCodexAPIKey`\nAnd 新账号出现在账号列表\nAnd 未确认 Codex 配置前不写 `CODEX_HOME/config.toml`\n\n### 场景 2:导入 OpenAI-compatible provider\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=openai-compatible`\nWhen provider name 不存在且用户确认\nThen GetTokens 调用 `CreateOpenAICompatibleProvider`\nAnd 账号列表出现 `openai-compatible:<name>`\n\n### 场景 3:导入 auth-file\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=auth-file&config=<base64url-json>`\nWhen 用户确认\nThen GetTokens 归一化 auth file 内容并调用 `UploadAuthFiles`\nAnd 确认页不显示完整 token\n\n### 场景 4:预览 Codex config patch\n\nGiven deep link 包含 `channel=codex&resource=codex-config`\nWhen 确认弹窗打开\nThen GetTokens 读取当前 `CODEX_HOME/config.toml`\nAnd 展示当前 provider、目标 provider 和 diff\nAnd 不做完整文件覆盖\n\n### 场景 5:provider 兼容性与 status 页一致\n\nGiven deep link 需要改写 provider 且 `/#frame=status` 当前显示的 provider 目录可用\nWhen 目标 provider 为 `openai` 或本地 provider section 缺失\nThen 预览阶段返回与 status 页一致的兼容性结果\nAnd `openai` 在 preserve 场景下被阻断\nAnd 只在兼容时才允许继续写入 `model_provider` 与 `[model_providers.<id>]`\n\n### 场景 6:组合导入\n\nGiven deep link 包含 `channel=codex&resource=codex-setup`\nWhen 用户点击确认\nThen 系统先创建账号\nAnd 再把创建出的账号 ID 作为 `accountRef` 生成 Codex local apply 草稿\nAnd Codex config apply 失败时保留已创建账号并展示失败原因\n\n### 场景 7:重复和冲突\n\nGiven deep link 要创建的 provider name 已存在\nWhen 用户预览导入\nThen 弹窗提示重名\nAnd 默认不覆盖已有账号\nAnd 用户只能选择取消或另存为新名称\n\n### 场景 8:敏感信息保护\n\nGiven deep link 带有 `apiKey` 或 auth tokens\nWhen 后端记录日志或前端展示确认页\nThen 日志不包含明文密钥\nAnd UI 默认脱敏\n\n### 场景 9:无 Wails preview\n\nGiven 浏览器 preview 环境没有 Wails runtime\nWhen 打开 deep link 预览\nThen 页面能展示解析结果和确认布局\nAnd 写入按钮不可执行\n\n### 场景 10:字段 presence 覆盖\n\nGiven deep link 只提供部分 `auth.json` / `config.toml` 字段\nWhen 用户确认应用 Codex 配置\nThen 显式提供的字段覆盖本地配置\nAnd 未提供的字段保留用户现有值\nAnd `false` 被视为显式值,例如 `supports_websockets=false` 与 `requires_openai_auth=false` 都写入 false\n\n## 测试计划\n\n### Go 单元测试\n\n1. `TestParseDeepLinkImportMergesConfigAndQueryWins`\n2. `TestParseDeepLinkImportRejectsUnsupportedFieldsAndNonCodexChannel`\n3. `TestPreviewDeepLinkImportRedactsURLAndKeepsCurrentActiveProvider`\n4. `TestPreviewDeepLinkImportCreatesProviderOnlyWhenNoExplicitActiveProvider`\n5. `TestPreviewDeepLinkImportBuildsCodexConfigFromDocuments`\n6. `TestPreviewDeepLinkImportPreservesUnspecifiedConfigTomlFields`\n7. `TestApplyDeepLinkImportCodexSetupReportsPartialSuccess`\n8. `TestApplyRelayServiceConfigToLocalV2WritesExplicitSupportsWebsocketsFalse`\n9. `TestApplyRelayServiceConfigToLocalV2PreservesSupportsWebsocketsWhenUnset`\n\n### 前端单测\n\n1. 解析后的确认模型能正确渲染账号类型。\n2. API key 和 token 字段脱敏。\n3. `codex-setup` 在账号重名时禁用主确认按钮。\n4. preview-only 环境不调用 Wails 写入。\n\n### 集成验证\n\n1. macOS 安装包注册 `gettokens://` scheme 后可唤起 app。\n2. sidecar ready 时组合导入能创建账号并刷新账号列表。\n3. Codex local apply diff 与实际写入一致。\n\n## 测试样例\n\n### 1. Codex API key 账号导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=codex-api-key&label=Team%20Relay&apiKey=sk-test-123456&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&model=gpt-5-codex\n```\n\nPatch A(本机已有激活 `model_provider`,不改用户默认值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.supports_websockets\", \"value\": false }\n ]\n }\n ]\n}\n```\n\nPatch B(本机没有激活 `model_provider`,才新建):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 2. OpenAI-compatible provider 导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=openai-compatible&name=deepseek&apiKey=sk-test-abc&baseUrl=https%3A%2F%2Fapi.deepseek.com%2Fv1\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 3. auth-file 导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=auth-file&name=team-codex-auth.json&config=eyJkb2N1bWVudHMiOlt7InRhcmdldCI6ImF1dGguanNvbiIsImZvcm1hdCI6Impzb24iLCJtb2RlIjoibWVyZ2UiLCJvcGVyYXRpb25zIjpbeyJvcCI6InNldCIsInBhdGgiOiIvYXV0aF9tb2RlIiwidmFsdWUiOiJjaGF0Z3B0In0seyJvcCI6InNldCIsInBhdGgiOiIvdXNlci9lbWFpbCIsInZhbHVlIjoidGVhbUBleGFtcGxlLmNvbSJ9XX1dfQ\n```\n\nPatch:\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"chatgpt\" },\n { \"op\": \"set\", \"path\": \"/user/email\", \"value\": \"team@example.com\" }\n ]\n }\n ]\n}\n```\n\n### 4. Codex config 仅改写当前 provider\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=api-key&model=gpt-5-codex&reasoningEffort=high&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&apiKey=sk-test-123456&providerID=team-relay&providerName=Team%20Relay&providerScope=current-active\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 5. preserve ChatGPT 登录态\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=preserve-chatgpt-provider&model=gpt-5-codex&reasoningEffort=medium&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&providerID=relay-chatgpt-preserve&providerName=Relay%20Preserve&providerScope=current-active\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"medium\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.experimental_bearer_token\", \"value\": \"<preserved-token>\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"medium\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"relay-chatgpt-preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.experimental_bearer_token\", \"value\": \"<preserved-token>\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 6. 组合导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-setup&config=eyJhY2NvdW50Ijp7ImFjY291bnRUeXBlIjoiY29kZXgtYXBpLWtleSIsImxhYmVsIjoiVGVhbSBSZWxheSIsImFwaUtleSI6InNrLXRlc3QtMTIzNDU2IiwiYmFzZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3YxIiwibW9kZWxzIjpbeyJuYW1lIjoiZ3B0LTUuNCIsImFsaWFzIjoiZ3B0LTUuNCJ9XX0sImNvZGV4Q29uZmlnIjp7Im1vZGUiOiJhcGkta2V5IiwibW9kZWwiOiJncHQtNS1jb2RleCIsInJlYXNvbmluZ0VmZm9ydCI6ImhpZ2giLCJwcm92aWRlcklEIjoidGVhbS1yZWxheSIsInByb3ZpZGVyTmFtZSI6IlRlYW0gUmVsYXkiLCJwcm92aWRlclNjb3BlIjoiY3VycmVudC1hY3RpdmUiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vdjEiLCJhcGlLZXkiOiJzay10ZXN0LTEyMzQ1NiIsInN1cHBvcnRzV2Vic29ja2V0cyI6dHJ1ZX19\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n## 实施顺序\n\n1. 先做 parser 和 Go 单元测试。\n2. 增加 preview DTO,只返回脱敏确认模型,不写入。\n3. 接前端确认弹窗和 preview-only 状态。\n4. 接账号导入 apply。\n5. 接 Codex config local apply。\n6. 最后注册 macOS URL scheme 并做桌面验收。\n\n## 待确认决策\n\n1. URL scheme 使用 `gettokens://`,还是兼容旧名/产品名别名。推荐只注册 `gettokens://`。\n2. auth-file 是否进入首期。推荐首期只做 `codex-api-key` 和 `openai-compatible`,auth-file 保留 parser 设计但不开放外链入口。\n3. OpenAI-compatible 创建时是否同步支持 models/headers。推荐首期支持 models,不支持 headers。\n"}, "f107f3039d85dcb737dc81c75b09ae7c6cd2ab23": {"t": 1785759460.1214192, "n": 34369, "h": "8c40828a3b787f81", "c": "# GetTokens deep link 账号与 Codex 配置导入设计\n\n## 设计目标\n\n设计 `gettokens://` deep link,让外部页面、厂商文档或团队配置可以把账号和 Codex 本地配置带入 GetTokens。设计必须满足两个前提:\n\n1. Deep link 只表达“导入意图”,不直接落盘。\n2. 最终写入必须复用现有账号、sidecar 和 Codex local apply 服务层,不能绕过既有校验去写 `auth.json`、`config.toml` 或 CLIProxyAPI 配置。\n\n## 现有边界\n\n### 账号服务\n\n当前账号列表由三类来源组成:\n\n| 来源 | 账号 ID | 现有创建/写入入口 |\n| --- | --- | --- |\n| Codex OAuth / auth file | `auth-file:<name>` | `UploadAuthFiles(files)` 经 sidecar `/v0/management/auth-files` 上传 |\n| Codex API key | `codex-api-key:<local-id>` 或指纹 ID | `CreateCodexAPIKey(input)` 写入本地 `~/.config/gettokens-data/codex-api-keys/*.json` 并同步 sidecar |\n| OpenAI-compatible provider | `openai-compatible:<name>` | `CreateOpenAICompatibleProvider(input)` 经 CLIProxyAPI Management API 写入 provider |\n\n账号列表展示和路由语义已经区分 `auth-file`、`codex-api-key`、`openai-compatible`,后续 deep link 必须沿用这个分层。\n\n### Codex 本地配置\n\nCodex local apply 已有三类语义:\n\n1. API Key 模式:写 `CODEX_HOME/auth.json` 与 `CODEX_HOME/config.toml`。\n2. OAuth / auth-file 模式:写所选账号 OAuth `auth.json`,让 Codex 走 ChatGPT/Codex OAuth backend。\n3. 保留 ChatGPT 登录态模式:只 patch `config.toml` 的 custom provider 字段。\n\n`GetLocalCodexModelProviderState()` 能读取当前 `model_provider`、当前模型和已存在 provider;`mergeRelayCodexConfigToml()` 已经实现保留式 TOML patch。Deep link 不应新增完整覆盖 `config.toml` 的入口。\n\n## 方案比较\n\n### 方案 A:只支持账号 deep link\n\n只让 `gettokens://v1/import?channel=codex&resource=account...` 创建账号,不触碰 Codex 本地配置。\n\n- 成本:低。\n- 风险:低。\n- 问题:用户仍需手动进入 Codex local apply,无法满足“直接导入账号和 codex 配置”。\n\n### 方案 B:账号与 Codex 配置分资源导入\n\n支持 `resource=account` 和 `resource=codex-config` 两类 deep link。账号导入创建账号;Codex 配置导入只生成 local apply 草稿和 diff,确认后调用现有 local apply。\n\n- 成本:中。\n- 风险:中。\n- 依赖:现有账号创建、Codex provider state、local apply diff / apply。\n- 问题:外部平台想“一条链接完成账号和配置”时体验不够顺。\n\n### 方案 C:账号、Codex 配置、组合导入三层协议\n\n在方案 B 基础上增加 `resource=codex-setup`,一条链接可以同时携带账号草稿和 Codex 配置草稿。确认页分成两个步骤:先创建账号,再应用 Codex 配置;任一步失败都不吞掉错误。\n\n- 成本:中高。\n- 风险:可控,但需要更完整的测试矩阵。\n- 依赖:同方案 B,并额外需要导入事务/回滚提示设计。\n- 推荐:采用。它满足外部一键配置场景,同时保留账号和 Codex 配置的独立确认边界。\n\n攻击角度校验:\n\n| 角度 | 结论 |\n| --- | --- |\n| 依赖失败 | sidecar 未 ready 时只允许保存账号草稿或展示待执行,不写 Codex 配置;Codex local apply 失败不删除已创建账号 |\n| 数据量放大 | deep link 只支持单账号、单 Codex 配置;批量导入改走 JSON 文件导入 |\n| 回滚成本 | 账号创建和 Codex 配置写入分步展示,Codex 配置写入前有 diff;账号误导入可用现有删除入口 |\n| 前提崩塌 | 如果系统未注册 URL scheme,仍可提供“粘贴 deep link”入口复用同一 parser |\n\n## 推荐方案\n\n采用方案 C:三类 resource,共用 parser 和确认弹窗。\n\n```text\ngettokens://v1/import?channel=codex&resource=account&...\ngettokens://v1/import?channel=codex&resource=codex-config&...\ngettokens://v1/import?channel=codex&resource=codex-setup&...\n```\n\n### 不做的事\n\n1. 不支持远程 `configUrl`。\n2. 不支持导入 usage script 或任意可执行脚本。\n3. 不支持批量账号导入。\n4. 不支持无确认自动启用、自动切换 Codex 当前账号。\n5. 不支持完整替换 `CODEX_HOME/config.toml`。\n6. 不支持把任意 TOML 片段拼进用户配置;只允许受控字段 patch。\n\n## 协议设计\n\n### 通用参数\n\n| 参数 | 必填 | 说明 |\n| --- | --- | --- |\n| `channel` | 是 | 固定值 `codex`,明确这是 Codex 账号 / 配置导入,不与其他产品线混淆 |\n| `resource` | 是 | `account` / `codex-config` / `codex-setup` |\n| `version` | 否 | URL host 已表达 `v1`,query 中可省略 |\n| `source` | 否 | 来源标识,用于确认页展示,不参与信任判断 |\n| `nonce` | 否 | 外部生成的去重标识;同 nonce 重复点击时提示重复 |\n| `config` | 否 | Base64URL 编码 JSON;复杂字段优先放这里 |\n| `enabled` | 否 | 只表示账号创建后是否启用;默认 `false` |\n| `apply` | 否 | 是否在确认后继续应用 Codex 配置;默认 `false` |\n\n所有 query 参数和 `config` 合并时,query 参数优先级更高。`config` 只允许 JSON,使用 Base64URL UTF-8 编码,不支持 TOML、YAML 或远程 URL。\n\n`config` 的 canonical 形式不是一组散落字段,而是一个 `documents[]` 补丁包。`account` / `codexConfig` 这类对象只是便捷语法,最终都要编译成对 `auth.json`、`config.toml` 或未来扩展文件的文档级 patch。\n\n## 通用文件改写格式\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-xxx\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" }\n ]\n }\n ]\n}\n```\n\n### 文档对象字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `target` | 是 | 目标文件名,如 `auth.json`、`config.toml` |\n| `format` | 是 | `json` / `toml` / `text` |\n| `mode` | 否 | `merge` / `patch` / `replace`,默认由格式推断 |\n| `operations` | 是 | 按顺序执行的改写操作 |\n| `preserveUnknown` | 否 | 是否保留未知字段,默认 `true` |\n| `backup` | 否 | 是否在写入前保留备份,默认 `true` |\n\n### 操作对象字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `op` | 是 | `set` / `delete` / `append` / `merge` |\n| `path` | 是 | `json` 使用 JSON Pointer,`toml` 使用 dotted path |\n| `value` | 否 | 新值,JSON value 形式 |\n| `valueEncoding` | 否 | `plain` / `base64`,默认 `plain` |\n| `allowCreate` | 否 | 不存在时是否创建路径,默认 `true` |\n\n### 目标文件约定\n\n| target | 说明 |\n| --- | --- |\n| `auth.json` | Codex auth file,适合 `replace` 或 root-level `set`/`merge` |\n| `config.toml` | Codex 本地配置,适合 root key 和 `model_providers.<id>` patch |\n| `settings.json` | 未来可扩展目标,当前不开放 |\n\n### 推荐路径语义\n\n| 文件 | 路径语义 |\n| --- | --- |\n| JSON 文件 | JSON Pointer,如 `/user/email`、`/tokens/access_token` |\n| TOML 文件 | dotted path,如 `model_provider`、`model_providers.team-relay.base_url` |\n\n### 典型操作\n\n| 操作 | 语义 |\n| --- | --- |\n| `set` | 设置或覆盖路径值 |\n| `delete` | 删除路径 |\n| `append` | 向数组追加单项 |\n| `merge` | 对对象/表执行深合并,保留未提及字段 |\n\n## 字段总表\n\n### 1. 通用字段\n\n| 字段 | 必填 | 适用资源 | 说明 |\n| --- | --- | --- | --- |\n| `channel` | 是 | 全部 | 固定值 `codex`,用于明确这是 Codex 导入链路 |\n| `resource` | 是 | 全部 | `account` / `codex-config` / `codex-setup` |\n| `config` | 否 | 全部 | Base64URL JSON,优先承载复杂对象 |\n| `source` | 否 | 全部 | 来源标识,仅用于确认页展示 |\n| `nonce` | 否 | 全部 | 去重标识 |\n| `enabled` | 否 | account | 账号导入后是否启用,默认 `false` |\n| `apply` | 否 | codex-config / codex-setup | 是否在确认后继续应用 Codex 配置,默认 `false` |\n\n### 2. account 资源字段\n\n#### 2.1 `accountType = codex-api-key`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `CreateCodexAPIKey` | 固定值 `codex-api-key` |\n| `label` | 否 | `CreateCodexAPIKeyInput.Label` | 显示名 |\n| `apiKey` | 是 | `CreateCodexAPIKeyInput.APIKey` | 账号 API key |\n| `baseUrl` | 是 | `CreateCodexAPIKeyInput.BaseURL` | API base URL |\n| `prefix` | 否 | `CreateCodexAPIKeyInput.Prefix` | 前缀 |\n| `proxyUrl` | 否 | `CreateCodexAPIKeyInput.ProxyURL` | 代理 URL |\n| `models` | 否 | `CreateCodexAPIKeyInput.Models` | 模型映射,放在 `config` 中更合适 |\n| `formatBaseUrls` | 否 | `CreateCodexAPIKeyInput.FormatBaseURLs` | 格式级 base URL 映射 |\n| `quotaCurl` | 否 | `CreateCodexAPIKeyInput.QuotaCurl` | 用量查询 curl,仅建议本地生成,不建议外链 |\n| `billingCurl` | 否 | `CreateCodexAPIKeyInput.BillingCurl` | 账单 curl,仅建议本地生成,不建议外链 |\n| `quotaEnabled` | 否 | `CreateCodexAPIKeyInput.QuotaEnabled` | 是否启用用量查询 |\n| `billingEnabled` | 否 | `CreateCodexAPIKeyInput.BillingEnabled` | 是否启用账单查询 |\n\n#### 2.2 `accountType = openai-compatible`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `CreateOpenAICompatibleProvider` | 固定值 `openai-compatible` |\n| `name` | 是 | `CreateOpenAICompatibleProviderInput.Name` | provider 名称 |\n| `apiKey` | 是 | `CreateOpenAICompatibleProviderInput.APIKey` | 首个 API key |\n| `baseUrl` | 是 | `CreateOpenAICompatibleProviderInput.BaseURL` | provider base URL |\n| `prefix` | 否 | `CreateOpenAICompatibleProviderInput.Prefix` | provider 前缀 |\n| `models` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 模型映射 |\n| `apiKeys` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 多 key 账号 |\n| `proxyUrl` | 否 | 需扩展 `CreateOpenAICompatibleProvider` | 代理 URL |\n| `headers` | 否 | 首期不支持 | 需要手动补充,避免外链注入 |\n\n#### 2.3 `accountType = auth-file`\n\n| 字段 | 必填 | 进入入口 | 说明 |\n| --- | --- | --- | --- |\n| `accountType` | 是 | `UploadAuthFiles` | 固定值 `auth-file` |\n| `name` | 是 | `UploadAuthFiles` | 文件名 |\n| `config.documents[]` | 是 | `UploadAuthFiles` | 目标 `auth.json` 的补丁包,建议用 `merge` 或 `replace` |\n| `enabled` | 否 | 仅在预览层解释 | auth file 本身不是启停字段 |\n\n### 3. codex-config 资源字段\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `mode` | 是 | `api-key` / `oauth-auth-file` / `preserve-chatgpt-provider` |\n| `config.documents[]` | 是 | 至少包含 `auth.json` 或 `config.toml` 之一的补丁包 |\n| `model` | 否 | root `model`,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `reasoningEffort` | 否 | root `model_reasoning_effort`,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `providerID` | 否 | 当前无激活 provider 时的创建值;若已有激活值则保持原值,仅 patch 当前 section |\n| `providerName` | 否 | provider 显示名,可作为 `config.toml` 文档中的 `set` 操作简写 |\n| `baseUrl` | 按 mode | custom provider base URL,可写入 `auth.json` 或 `config.toml` |\n| `apiKey` | `api-key` 必填 | 写入 `auth.json` 或 bearer token 草稿 |\n| `accountRef` | 否 | `auth-file:<name>` / `codex-api-key:<id>` / `openai-compatible:<name>` |\n| `supportsWebsockets` | 否 | patch `supports_websockets` |\n| `providerScope` | 否 | `current-active` / `create-new`,显式标识沿用当前激活 provider 还是创建新 provider |\n| `providerCompatibility` | 否 | 预览结果 | `compatible` / `blocked_builtin_openai` / `missing_chatgpt_auth` / `missing_provider_section` |\n| `providerRewriteMode` | 否 | 预览结果 | `keep-current` / `patch-current` / `create-new` |\n\n### 4. codex-setup 资源字段\n\n`codex-setup` 主要通过 `config` 传入两块对象:\n\n| 对象 | 字段 |\n| --- | --- |\n| `account` | `accountType`, `name`, `label`, `apiKey`, `apiKeys`, `baseUrl`, `prefix`, `proxyUrl`, `models`, `formatBaseUrls`, `quotaCurl`, `billingCurl`, `authFile` |\n| `codexConfig` | `mode`, `model`, `reasoningEffort`, `providerID`, `providerName`, `providerScope`, `baseUrl`, `apiKey`, `supportsWebsockets`, `accountRef`, `providerCompatibility`, `providerRewriteMode` |\n\n`codex-setup` 额外支持通用字段 `apply`、`enabled`、`source`、`nonce`,但不会新增自己的字段池。\n\n### 5. 明确不支持的字段\n\n| 字段 | 状态 | 说明 |\n| --- | --- | --- |\n| `configUrl` | 不支持 | 不做远程拉取 |\n| `usageScript` | 不支持 | 不导入可执行脚本 |\n| `headers`(openai-compatible 首期) | 不支持 | 防止外链注入请求头 |\n| 批量账号数组 | 不支持 | 只做单账号导入 |\n| 任意 TOML 片段 | 不支持 | 只允许受控 patch |\n| 自动启用 / 自动切换当前账号 | 不支持 | 必须确认后再做 |\n\n### resource=account\n\n账号导入支持三种 `accountType`:\n\n| accountType | 用途 | 最终服务入口 |\n| --- | --- | --- |\n| `codex-api-key` | 创建 GetTokens 本地 Codex API key 账号 | `CreateCodexAPIKey` |\n| `openai-compatible` | 创建 CLIProxyAPI OpenAI-compatible provider | `CreateOpenAICompatibleProvider`,后续增强可走 `UpdateOpenAICompatibleProvider` 支持 models/headers |\n| `auth-file` | 导入 Codex OAuth auth.json | `UploadAuthFiles` |\n\n#### Codex API key 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=codex-api-key&label=Team%20Relay&apiKey=sk-xxx&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&model=gpt-5-codex\n```\n\n字段:\n\n| 字段 | 必填 | 映射 |\n| --- | --- | --- |\n| `label` | 否 | `CreateCodexAPIKeyInput.Label` |\n| `apiKey` | 是 | `CreateCodexAPIKeyInput.APIKey` |\n| `baseUrl` | 是 | `CreateCodexAPIKeyInput.BaseURL` |\n| `prefix` | 否 | `CreateCodexAPIKeyInput.Prefix` |\n| `proxyUrl` | 否 | `CreateCodexAPIKeyInput.ProxyURL` |\n| `models` | 否 | `CreateCodexAPIKeyInput.Models`,从 `config` JSON 传数组 |\n| `formatBaseUrls` | 否 | `CreateCodexAPIKeyInput.FormatBaseURLs` |\n| `quotaCurl` / `billingCurl` | 否 | 默认只预览,不建议外链携带 |\n\n#### OpenAI-compatible 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=openai-compatible&name=deepseek&apiKey=sk-xxx&baseUrl=https%3A%2F%2Fapi.deepseek.com%2Fv1\n```\n\n字段:\n\n| 字段 | 必填 | 映射 |\n| --- | --- | --- |\n| `name` | 是 | provider name,必须唯一 |\n| `apiKey` | 是 | 首个 API key |\n| `baseUrl` | 是 | provider base URL |\n| `prefix` | 否 | provider prefix |\n| `models` | 否 | 需要增强 root `CreateOpenAICompatibleProvider`,否则首期只能创建后再提示用户补模型 |\n| `headers` | 否 | 首期不允许 deep link 写入,避免外链注入 header |\n\n#### Auth file 示例\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=auth-file&name=team-codex-auth.json&config=<base64url-json>\n```\n\n`config` JSON:\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"chatgpt\" },\n { \"op\": \"set\", \"path\": \"/user/email\", \"value\": \"team@example.com\" }\n ]\n }\n ]\n}\n```\n\nauth-file 导入风险最高,确认页必须展示 email / plan / token 字段摘要,不展示完整 token。默认不支持从普通 query 参数直接传 `authJson`,只允许 `config`。\n\n### resource=codex-config\n\nCodex 配置导入只表达受控 patch,不表达整份 TOML。推荐把 `auth.json` 和 `config.toml` 都放进同一个 `documents[]` bundle。\n\n示例:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=api-key&model=gpt-5-codex&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&apiKey=sk-xxx&providerID=team-relay\n```\n\n字段:\n\n| 字段 | 必填 | 说明 |\n| --- | --- | --- |\n| `mode` | 是 | `api-key` / `oauth-auth-file` / `preserve-chatgpt-provider` |\n| `config.documents[]` | 是 | 目标 `auth.json` / `config.toml` 的补丁包 |\n| `model` | 否 | root `model`,可由 `config.toml` 文档表示 |\n| `reasoningEffort` | 否 | root `model_reasoning_effort`,可由 `config.toml` 文档表示 |\n| `providerID` | 否 | 目标 provider;默认读取当前 `model_provider`,`mode=preserve-chatgpt-provider` 时不得为 `openai` |\n| `providerName` | 否 | `[model_providers.<id>].name` |\n| `baseUrl` | 按模式 | custom provider base URL,可写入 `config.toml` 文档 |\n| `apiKey` | `api-key` 必填 | 只用于 auth 写入或 bearer token 草稿,可写入 `auth.json` 文档 |\n| `supportsWebsockets` | 否 | patch `supports_websockets` |\n| `accountRef` | 否 | 指向刚导入或已有账号,如 `codex-api-key:<id>` |\n\n模式语义:\n\n| mode | 写入策略 |\n| --- | --- |\n| `api-key` | 生成 Codex API key local apply 草稿,写 `auth.json` 的 `OPENAI_API_KEY`,并 patch `config.toml` |\n| `oauth-auth-file` | 选择已有或刚导入的 `auth-file`,写 OAuth tokens,让 Codex 走 ChatGPT/Codex OAuth backend |\n| `preserve-chatgpt-provider` | 不碰 `auth.json` tokens,只 patch custom provider;需要 preflight 确认本机有可保留 ChatGPT 登录态 |\n\n`codex-config` 的确认页必须先调用 `GetLocalCodexModelProviderState()`,展示当前 provider。默认 patch 当前 provider,不默认创建 `gettokens` provider;如果本地已有激活的 `model_provider`,deep link 不得改写它,只能补齐当前 provider 的 section。\n\n`providerScope` 是这个场景的显式标识:\n\n1. `providerScope=current-active` 表示沿用当前激活 `model_provider`,只 patch 该 provider 的 section。\n2. `providerScope=create-new` 表示创建新的 provider,并在没有当前激活 provider 时写入新的 `model_provider`。\n3. 当 `providerScope=current-active` 但当前没有激活值时,确认页可退回到 `create-new`,但必须明确提示是降级执行。\n4. `providerID` 只作为具体 provider 的身份值,不再承担“是否改写当前激活值”的语义。\n\n### provider 改写与 status 页对齐\n\n`/#frame=status` 里的 provider 语义,和 deep link 的 provider 改写语义必须一致:\n\n1. provider 是本地工作台偏好,不是 sidecar 的单一真相。\n2. 默认优先保留当前 `model_provider` 的连续性;是否沿用当前激活值由 `providerScope=current-active` 显式表达。\n3. 只有 `providerScope=create-new`,或者当前没有激活值且已降级确认时,才允许新建 `model_provider` / `[model_providers.<id>]`。\n4. `openai` 只作为内置连续性保留,不作为 `preserve-chatgpt-provider` 的目标 provider id。\n5. 当本地 `config.toml` 已存在当前激活 provider section 时,deep link 只 patch 当前 section,不静默清空其他 provider section。\n6. 当当前 provider section 缺失但符合兼容性要求时,确认页可以提示“将补齐当前 provider section”;只有 `providerScope=create-new` 时才提示“将创建新的 provider”。\n7. 若当前 provider 不存在、为 builtin `openai`,或与 preserve 模式冲突,则预览阶段必须给出和 status 页一致的阻断/警告文案。\n\n兼容性结果沿用状态页的心智,建议至少分成:\n\n| 结果 | 含义 |\n| --- | --- |\n| `compatible` | 可以按当前 provider 连续性应用 |\n| `blocked_builtin_openai` | 目标 provider 或模式要求与内置 `openai` 冲突 |\n| `missing_chatgpt_auth` | preserve 场景下本机 ChatGPT auth 不存在或无效 |\n| `missing_provider_section` | 需要写入的 provider section 还不存在 |\n\n### resource=codex-setup\n\n组合导入用于“一条链接导入账号并配置 Codex”。推荐把复杂内容放在 `config` JSON。\n\n```json\n{\n \"account\": {\n \"accountType\": \"codex-api-key\",\n \"label\": \"Team Relay\",\n \"apiKey\": \"sk-xxx\",\n \"baseUrl\": \"https://api.example.com/v1\",\n \"models\": [\n { \"name\": \"gpt-5-codex\", \"alias\": \"gpt-5-codex\" }\n ]\n },\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"replace\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"apikey\" },\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-xxx\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n执行顺序:\n\n1. Parse:只解析和校验,不写入。\n2. Preview:展示账号草稿、Codex config diff、敏感字段脱敏。\n3. Confirm account:创建账号。\n4. Resolve accountRef:把刚创建账号的 ID 注入 Codex config 草稿或后续文档 patch。\n5. Confirm/apply Codex config:调用现有 local apply。\n6. Refresh:刷新账号列表、Codex channel routing、Codex config snapshot。\n\n如果账号创建成功但 Codex config apply 失败,不回滚账号;确认页展示“账号已导入,Codex 配置未应用”的明确状态。\n\n## 安全策略\n\n1. 日志中 deep link 永远只记录 redacted URL,不记录 `apiKey`、`config`、token、header。\n2. UI 中 API key 只展示前 4 位和后 4 位,中间脱敏。\n3. `config` 大小限制建议 32 KB;超出要求改用文件导入。\n4. URL scheme handler 和“粘贴链接导入”共用 parser,避免两套解析差异。\n5. 所有写入前必须展示 diff 或字段级 change summary。\n6. `enabled=true` 不代表切换当前 Codex 本地配置;只代表账号创建后是否在 GetTokens/sidecar 中禁用。\n7. `apply=true` 只让确认页默认勾选“继续应用 Codex 配置”,不能跳过确认。\n8. 不允许 deep link 写入 arbitrary headers;OpenAI-compatible headers 首期只展示“需要用户手动补充”。\n\n## UI 交互\n\n### DeepLinkImportDialog\n\n`DeepLinkImportDialog` 不再新建一套独立确认 UI,而是作为 thin adapter 复用现有“应用模板到 Codex”确认页。deep link 解析和 preview 只负责把外部导入意图转换成现有 `AccountLocalCliApplyConfirm` 能消费的 Codex local apply 草稿,并补充来源、resource、providerScope 等 deep link 元信息。\n\n复用边界:\n\n| resource | UI 入口 | 说明 |\n| --- | --- | --- |\n| `account` | 账号导入确认 | 只创建账号,不进入“应用模板到 Codex” |\n| `codex-config` | 直接打开“应用模板到 Codex” | deep link 转成 Codex local apply 草稿 |\n| `codex-setup` | 账号草稿摘要 + “应用模板到 Codex” | 先确认账号草稿,再沿用同一 diff / apply 页面 |\n\n必须复用的现有能力:\n\n1. `ModalFrame` 的窗口外壳、遮罩、header/footer 分区。\n2. `AccountLocalCliApplyConfirm` 的标题、summary badge、文件列表、diff 预览和 footer 状态。\n3. `StatusSnippetPanel` / `buildCodexLocalApplyDiff` 的文件预览。\n4. `/#frame=status` 已有 preflight / provider 兼容判断。\n\n`codex-config` 单独导入时不展示“只导入账号”。\n\n视觉稿入口:[../deep-link-import-modal-design.html](../deep-link-import-modal-design.html)。\n\n### Modal 卡片结构\n\n入口卡片按“应用模板到 Codex”原页面组织,不再采用新的 parse rail + draft panels 双栏结构:\n\n1. Header summary:保留 `FILE PREVIEW CONFIRM` 和“应用模板到 Codex”标题;在现有 summary badge 里增加 `外部来源`、`resource`、`providerScope`、`providerRewriteMode`。\n2. 左侧栏:继续展示文件列表;`codex-setup` 额外在文件列表上方插入一个紧凑账号草稿摘要,避免新建完整账号导入页面。\n3. 右侧区域:继续展示 provider preflight 结果和文件 diff;`providerScope=current-active` 时必须突出“current user provider preserved”。\n4. Footer:保留现有取消 / 确认应用节奏;`codex-setup` 增加“只导入账号”,`codex-config` 不展示该按钮。\n\n### Modal 状态\n\n| 状态 | 视觉处理 | 说明 |\n| --- | --- | --- |\n| `preview-ready` | 绿色状态条 | 解析成功,等待用户确认 |\n| `warning` | 黄色状态条和 warning badge | 需要注意重名、兼容性降级、缺失 provider section |\n| `blocked` | 黄色或红色 warning card | 不能继续写入,比如 preserve 场景缺失 ChatGPT auth |\n| `preview-only` | footer 说明 + 按钮降级 | 浏览器 preview 或无 Wails runtime 时只读展示 |\n\n### Modal 文案原则\n\n1. 标题沿用“应用模板到 Codex”,避免用户误以为这是另一套写入链路。\n2. `providerID` 永远写成身份值,不要写成“默认值”或“当前默认 provider”。\n3. `current-active` 场景必须明确写出“保留 model_provider”。\n4. `create-new` 场景必须明确写出“仅在没有激活值时创建新 provider”。\n5. API key、token、auth 字段只展示脱敏值,diff 里也只展示脱敏值。\n\n### 浏览器 preview\n\n普通浏览器没有 Wails runtime 时可以解析示例链接并展示确认状态,但所有写入按钮进入 preview-only 状态,不调用真实绑定。\n\n## 后端设计\n\n新增 Wails-facing 方法建议:\n\n| 方法 | 作用 |\n| --- | --- |\n| `ParseDeepLink(url string)` | 解析 `gettokens://` URL,返回结构化 request |\n| `PreviewDeepLinkImport(request)` | 合并 config、读取现有账号/Codex 状态、生成确认模型和 diff |\n| `ApplyDeepLinkImport(request, options)` | 按确认选项执行账号导入和 Codex local apply |\n\nWails 绑定要求同步 root `app.go`、`app_types.go`、`app_mappers.go` 和 `frontend/wailsjs`。\n\n内部模块建议:\n\n```text\ninternal/wailsapp/deeplink.go\ninternal/wailsapp/deeplink_parser.go\ninternal/wailsapp/deeplink_preview.go\ninternal/wailsapp/deeplink_apply.go\n```\n\n纯解析函数必须不依赖 App,方便单元测试。\n\n## 数据结构草案\n\n```go\ntype DeepLinkImportRequest struct {\n Channel string\n Version string\n Resource string\n Source string\n Nonce string\n Account *DeepLinkAccountDraft\n CodexConfig *DeepLinkCodexConfigDraft\n}\n\ntype DeepLinkAccountDraft struct {\n AccountType string\n Name string\n Label string\n APIKey string\n APIKeys []string\n BaseURL string\n Prefix string\n ProxyURL string\n Models []OpenAICompatibleModel\n FormatBaseURLs map[string]string\n AuthFileName string\n AuthFileJSON string\n Enabled bool\n}\n\ntype DeepLinkCodexConfigDraft struct {\n Mode string\n AccountRef string\n Model string\n ReasoningEffort string\n ProviderID string\n ProviderName string\n ProviderScope string\n BaseURL string\n APIKey string\n SupportsWebsockets bool\n Apply bool\n}\n```\n\n## BDD 验收\n\n### 场景 1:导入 Codex API key 账号\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=codex-api-key`\nWhen 用户确认导入账号\nThen GetTokens 调用 `CreateCodexAPIKey`\nAnd 新账号出现在账号列表\nAnd 未确认 Codex 配置前不写 `CODEX_HOME/config.toml`\n\n### 场景 2:导入 OpenAI-compatible provider\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=openai-compatible`\nWhen provider name 不存在且用户确认\nThen GetTokens 调用 `CreateOpenAICompatibleProvider`\nAnd 账号列表出现 `openai-compatible:<name>`\n\n### 场景 3:导入 auth-file\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=auth-file&config=<base64url-json>`\nWhen 用户确认\nThen GetTokens 归一化 auth file 内容并调用 `UploadAuthFiles`\nAnd 确认页不显示完整 token\n\n### 场景 4:预览 Codex config patch\n\nGiven deep link 包含 `channel=codex&resource=codex-config`\nWhen 确认弹窗打开\nThen GetTokens 读取当前 `CODEX_HOME/config.toml`\nAnd 展示当前 provider、目标 provider 和 diff\nAnd 不做完整文件覆盖\n\n### 场景 5:provider 兼容性与 status 页一致\n\nGiven deep link 需要改写 provider 且 `/#frame=status` 当前显示的 provider 目录可用\nWhen 目标 provider 为 `openai` 或本地 provider section 缺失\nThen 预览阶段返回与 status 页一致的兼容性结果\nAnd `openai` 在 preserve 场景下被阻断\nAnd 只在兼容时才允许继续写入 `model_provider` 与 `[model_providers.<id>]`\n\n### 场景 6:组合导入\n\nGiven deep link 包含 `channel=codex&resource=codex-setup`\nWhen 用户点击确认\nThen 系统先创建账号\nAnd 再把创建出的账号 ID 作为 `accountRef` 生成 Codex local apply 草稿\nAnd Codex config apply 失败时保留已创建账号并展示失败原因\n\n### 场景 7:重复和冲突\n\nGiven deep link 要创建的 provider name 已存在\nWhen 用户预览导入\nThen 弹窗提示重名\nAnd 默认不覆盖已有账号\nAnd 用户只能选择取消或另存为新名称\n\n### 场景 8:敏感信息保护\n\nGiven deep link 带有 `apiKey` 或 auth tokens\nWhen 后端记录日志或前端展示确认页\nThen 日志不包含明文密钥\nAnd UI 默认脱敏\n\n### 场景 9:无 Wails preview\n\nGiven 浏览器 preview 环境没有 Wails runtime\nWhen 打开 deep link 预览\nThen 页面能展示解析结果和确认布局\nAnd 写入按钮不可执行\n\n## 测试计划\n\n### Go 单元测试\n\n1. `TestParseDeepLinkAccountCodexAPIKey`\n2. `TestParseDeepLinkAccountOpenAICompatible`\n3. `TestParseDeepLinkAccountAuthFileFromConfig`\n4. `TestParseDeepLinkCodexConfigRejectsRemoteURL`\n5. `TestPreviewDeepLinkRedactsSecrets`\n6. `TestApplyDeepLinkCodexSetupKeepsAccountWhenConfigFails`\n7. `TestDeepLinkCodexConfigPatchesCurrentProviderByDefault`\n\n### 前端单测\n\n1. 解析后的确认模型能正确渲染账号类型。\n2. API key 和 token 字段脱敏。\n3. `codex-setup` 在账号重名时禁用主确认按钮。\n4. preview-only 环境不调用 Wails 写入。\n\n### 集成验证\n\n1. macOS 安装包注册 `gettokens://` scheme 后可唤起 app。\n2. sidecar ready 时组合导入能创建账号并刷新账号列表。\n3. Codex local apply diff 与实际写入一致。\n\n## 测试样例\n\n### 1. Codex API key 账号导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=codex-api-key&label=Team%20Relay&apiKey=sk-test-123456&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&model=gpt-5-codex\n```\n\nPatch A(本机已有激活 `model_provider`,不改用户默认值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(本机没有激活 `model_provider`,才新建):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 2. OpenAI-compatible provider 导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=openai-compatible&name=deepseek&apiKey=sk-test-abc&baseUrl=https%3A%2F%2Fapi.deepseek.com%2Fv1\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.deepseek.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 3. auth-file 导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=account&accountType=auth-file&name=team-codex-auth.json&config=eyJkb2N1bWVudHMiOlt7InRhcmdldCI6ImF1dGguanNvbiIsImZvcm1hdCI6Impzb24iLCJtb2RlIjoibWVyZ2UiLCJvcGVyYXRpb25zIjpbeyJvcCI6InNldCIsInBhdGgiOiIvYXV0aF9tb2RlIiwidmFsdWUiOiJjaGF0Z3B0In0seyJvcCI6InNldCIsInBhdGgiOiIvdXNlci9lbWFpbCIsInZhbHVlIjoidGVhbUBleGFtcGxlLmNvbSJ9XX1dfQ\n```\n\nPatch:\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/auth_mode\", \"value\": \"chatgpt\" },\n { \"op\": \"set\", \"path\": \"/user/email\", \"value\": \"team@example.com\" }\n ]\n }\n ]\n}\n```\n\n### 4. Codex config 仅改写当前 provider\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=api-key&model=gpt-5-codex&reasoningEffort=high&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&apiKey=sk-test-123456&providerID=team-relay&providerName=Team%20Relay&providerScope=current-active\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 5. preserve ChatGPT 登录态\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-config&mode=preserve-chatgpt-provider&model=gpt-5-codex&reasoningEffort=medium&baseUrl=https%3A%2F%2Fapi.example.com%2Fv1&providerID=relay-chatgpt-preserve&providerName=Relay%20Preserve&providerScope=current-active\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"medium\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.experimental_bearer_token\", \"value\": \"<preserved-token>\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"medium\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"relay-chatgpt-preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.experimental_bearer_token\", \"value\": \"<preserved-token>\" },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.relay-chatgpt-preserve.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n### 6. 组合导入\n\nURL:\n\n```text\ngettokens://v1/import?channel=codex&resource=codex-setup&config=eyJhY2NvdW50Ijp7ImFjY291bnRUeXBlIjoiY29kZXgtYXBpLWtleSIsImxhYmVsIjoiVGVhbSBSZWxheSIsImFwaUtleSI6InNrLXRlc3QtMTIzNDU2IiwiYmFzZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3YxIiwibW9kZWxzIjpbeyJuYW1lIjoiZ3B0LTUuNCIsImFsaWFzIjoiZ3B0LTUuNCJ9XX0sImNvZGV4Q29uZmlnIjp7Im1vZGUiOiJhcGkta2V5IiwibW9kZWwiOiJncHQtNS1jb2RleCIsInJlYXNvbmluZ0VmZm9ydCI6ImhpZ2giLCJwcm92aWRlcklEIjoidGVhbS1yZWxheSIsInByb3ZpZGVyTmFtZSI6IlRlYW0gUmVsYXkiLCJwcm92aWRlclNjb3BlIjoiY3VycmVudC1hY3RpdmUiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vdjEiLCJhcGlLZXkiOiJzay10ZXN0LTEyMzQ1NiIsInN1cHBvcnRzV2Vic29ja2V0cyI6dHJ1ZX19\n```\n\nPatch A(当前 provider 已存在,保持 `model_provider` 不变):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.<current-provider-id>.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\nPatch B(当前没有激活 provider,才创建新值):\n\n```json\n{\n \"documents\": [\n {\n \"target\": \"auth.json\",\n \"format\": \"json\",\n \"mode\": \"merge\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"/OPENAI_API_KEY\", \"value\": \"sk-test-123456\" }\n ]\n },\n {\n \"target\": \"config.toml\",\n \"format\": \"toml\",\n \"mode\": \"patch\",\n \"operations\": [\n { \"op\": \"set\", \"path\": \"model\", \"value\": \"gpt-5-codex\" },\n { \"op\": \"set\", \"path\": \"model_reasoning_effort\", \"value\": \"high\" },\n { \"op\": \"set\", \"path\": \"model_provider\", \"value\": \"team-relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers.team-relay.wire_api\", \"value\": \"responses\" }\n ]\n }\n ]\n}\n```\n\n## 实施顺序\n\n1. 先做 parser 和 Go 单元测试。\n2. 增加 preview DTO,只返回脱敏确认模型,不写入。\n3. 接前端确认弹窗和 preview-only 状态。\n4. 接账号导入 apply。\n5. 接 Codex config local apply。\n6. 最后注册 macOS URL scheme 并做桌面验收。\n\n## 待确认决策\n\n1. URL scheme 使用 `gettokens://`,还是兼容旧名/产品名别名。推荐只注册 `gettokens://`。\n2. auth-file 是否进入首期。推荐首期只做 `codex-api-key` 和 `openai-compatible`,auth-file 保留 parser 设计但不开放外链入口。\n3. OpenAI-compatible 创建时是否同步支持 models/headers。推荐首期支持 models,不支持 headers。\n"}}, "docs-linhay/spaces/20260529-account-detail-ui/account-detail-account-types-v26.html": {"6a6556121411b7445b1e0e28d0b7af6920236b59": {"t": 1785746423.876858, "n": 47135, "h": "860104a39f6fb032", "c": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>GetTokens · Account Detail v09 Palette Fix</title>\n <style>\n :root{--paper:#f9f9f9;--panel:#ffffff;--panel2:#f0f0f0;--ink:#000000;--muted:#666666;--line:#000000;--hair:rgba(0,0,0,.14);--red:#ef4444;--green:#16a34a;--blue:#1f6feb;--amber:#eab308;--violet:#6366f1;--index:214px;--mono:JetBrains Mono,IBM Plex Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--sans:JetBrains Mono,IBM Plex Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--shadow:4px 4px 0 #e5e5e5}\n *{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);min-width:1420px}.page{padding:28px}.k,.mono{font-family:var(--mono)}.k{font-size:11px;letter-spacing:.22em;text-transform:uppercase;font-weight:1000;color:var(--muted)}.cover{border:2px solid var(--line);background:var(--panel);box-shadow:var(--shadow);padding:18px;margin-bottom:16px;display:grid;grid-template-columns:1fr 430px;gap:18px;align-items:end}.cover h1{margin:6px 0 0;font-size:48px;line-height:.9;letter-spacing:-.055em;text-transform:uppercase;font-style:italic;font-weight:1000}.cover p{margin:10px 0 0;color:var(--muted);font-size:13px;line-height:1.45;font-weight:850;max-width:880px}.brief{border:2px solid var(--line);background:var(--panel2);padding:12px;font-size:12px;line-height:1.45;color:var(--muted);font-weight:850}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;margin-bottom:14px}.tabs,.states{display:flex;gap:8px;flex-wrap:wrap}.tab,.state{border:2px solid var(--line);background:var(--panel);padding:8px 10px;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;font-weight:1000;cursor:pointer}.tab[aria-pressed=true],.state[aria-pressed=true]{background:var(--ink);color:var(--panel)}.shell{border:2px solid var(--line);background:#000000;padding:10px;box-shadow:var(--shadow)}.app{height:852px;border:2px solid var(--line);background:rgba(0,0,0,.72);padding:22px;display:grid;place-items:center}.modal{width:min(1370px,calc(100vw - 88px));height:792px;border:2px solid var(--line);background:var(--panel);display:grid;grid-template-rows:74px minmax(0,1fr)72px;box-shadow:0 0 0 1px rgba(0,0,0,.08), 0 18px 48px rgba(0,0,0,.28)}\n\n /* v06 header: two-level context, no single-line long title clipping */\n .fixed-head{border-bottom:3px solid var(--line);background:var(--panel2);display:grid;grid-template-rows:44px auto}.head-bar{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 250px;border-bottom:2px solid var(--line)}.head-bar>div{border-right:2px solid var(--line);padding:9px 14px;display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0}.head-bar>div:last-child{border-right:0}.sys{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:1000;color:var(--muted);white-space:nowrap}.head-context{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 250px;align-items:stretch}.head-context>div{border-right:2px solid var(--line);padding:10px 14px;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:8px}.head-context>div:last-child{border-right:0}.account-name{font-size:22px;line-height:.95;letter-spacing:-.035em;text-transform:uppercase;font-style:italic;font-weight:1000;white-space:normal;overflow:visible}.type-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:0}.type-label{font-size:15px;line-height:1.15;font-weight:1000;text-transform:uppercase;font-style:italic;letter-spacing:-.015em;white-space:normal;overflow:visible}.head-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:2px solid var(--line);background:#ffffff}.metric{border-right:1px solid var(--line);padding:7px 9px;min-width:0}.metric:last-child{border-right:0}.metric b{display:block;font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);white-space:nowrap}.metric span{display:block;margin-top:4px;font-size:12px;font-weight:950;white-space:normal;overflow:visible;text-overflow:clip;line-height:1.18}.last-box{display:grid;gap:7px;align-content:start}.last-box .pair{display:flex;justify-content:space-between;gap:12px;font-size:12px;font-weight:950}.last-box .pair b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}\n\n .pill{display:inline-flex;align-items:center;min-height:23px;border:1.5px solid var(--line);background:#ffffff;padding:4px 8px;font-family:var(--mono);font-size:10px;font-weight:1000;letter-spacing:.09em;text-transform:uppercase;white-space:nowrap}.green{color:var(--green);border-color:var(--green)}.red{color:var(--red);border-color:var(--red)}.blue{color:var(--blue);border-color:var(--blue)}.amber{color:var(--amber);border-color:var(--amber)}.violet{color:var(--violet);border-color:var(--violet)}.button{border:2px solid var(--line);background:#ffffff;padding:8px 10px;font-family:var(--mono);font-size:10px;font-weight:1000;letter-spacing:.1em;text-transform:uppercase;cursor:pointer}.button.primary{background:var(--ink);color:var(--panel)}.button.danger{color:var(--red);border-color:var(--red)}.button:disabled{opacity:.45;cursor:not-allowed}.body{min-height:0;overflow:auto;background:var(--panel)}.band{border-bottom:2px solid var(--line);display:grid;grid-template-columns:var(--index) minmax(0,1fr);min-height:96px}.band.hide-codex-oauth{display:grid}.band-index{border-right:2px solid var(--line);background:#f9f9f9;padding:14px}.band-index .num{font-family:var(--mono);font-size:13px;letter-spacing:.16em;font-weight:1000;color:var(--muted)}.band-index h2{margin:9px 0 0;font-size:17px;line-height:1;text-transform:uppercase;font-style:italic}.band-content{padding:14px;min-width:0}.fields{display:grid;grid-template-columns:repeat(12,1fr);gap:10px}.field{display:grid;gap:6px;min-width:0}.span1{grid-column:span 1}.span3{grid-column:span 3}.span4{grid-column:span 4}.span5{grid-column:span 5}.span7{grid-column:span 7}.span12{grid-column:span 12}.label{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:1000;color:var(--muted)}.input,.textarea{width:100%;border:2px solid var(--line);background:#ffffff;padding:8px 9px;min-height:36px;font-family:var(--mono);font-size:12px;font-weight:850;outline:0}.textarea{min-height:72px;line-height:1.42;resize:vertical}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.hint{color:var(--muted);font-size:12px;line-height:1.35;font-weight:800}.module-actions{margin-top:10px;border-top:1px dashed var(--hair);padding-top:10px;display:flex;align-items:center;justify-content:space-between;gap:12px}.request-config .textarea{min-height:0;height:auto;overflow:hidden;resize:none;line-height:1.42}.request-split{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(360px,.65fr);gap:14px;align-items:start}.request-left{min-width:0}.request-right{min-width:0;border-left:2px solid var(--line);padding-left:14px}.request-actions{margin-top:10px;border-top:1px dashed var(--hair);padding-top:10px;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.quota-result{border:2px solid var(--line);background:var(--panel2);display:grid;grid-template-columns:1fr 1fr}.quota-result>div{border-right:1px solid var(--line);padding:9px 10px}.quota-result>div:last-child{border-right:0}.quota-result b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.quota-result span{display:block;margin-top:5px;font-size:12px;font-weight:900;line-height:1.3}.quota-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0;padding:0}.quota-pane,.billing-pane{padding:14px;min-width:0}.quota-pane{border-right:2px solid var(--line)}.quota-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.result-log{margin-top:10px;border:2px solid var(--line);background:var(--panel2);padding:9px 10px;min-height:58px}.result-log b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.result-log span{display:block;margin-top:5px;font-size:12px;font-weight:900;line-height:1.3}.table{border:2px solid var(--line);width:100%;border-collapse:collapse;background:#ffffff}.table th,.table td{border:1px solid var(--line);padding:8px 9px;text-align:left;font-size:12px;vertical-align:middle}.table th{background:var(--panel2);font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.table input{width:100%;border:0;outline:0;background:transparent;font-family:var(--mono);font-size:12px;font-weight:850}.model-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.model-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.model-item{border:2px solid var(--line);background:#ffffff;min-height:46px;padding:8px 9px;display:flex;align-items:center;justify-content:space-between;gap:10px}.model-item b{display:block;font-size:12px;font-weight:1000;line-height:1.1}.model-item span{display:block;margin-top:4px;color:var(--muted);font-size:11px;font-weight:850}.model-item.readonly{background:var(--panel2)}.verify-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:end}.verify-actions{align-self:end}.flat-status{border:2px solid var(--line);background:#ffffff;display:grid;grid-template-columns:repeat(4,1fr)}.flat-status>div{border-right:1px solid var(--line);padding:8px 9px;min-width:0}.flat-status>div:last-child{border-right:0}.flat-status b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.flat-status span{display:block;margin-top:5px;font-size:13px;font-weight:950;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.footer{border-top:2px solid var(--line);background:var(--panel2);display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;padding:12px 16px}.footer-msg{display:flex;gap:10px;align-items:center;min-width:0}.footer-msg strong{font-size:13px;text-transform:uppercase;letter-spacing:.07em}.footer-msg span{display:block;color:var(--muted);font-size:12px;font-weight:850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-only,.state-only{display:none}.type-compatible .t-compatible,.type-codex-api .t-codex-api,.type-codex-oauth .t-codex-oauth,.type-token-plan .t-token-plan{display:block}.type-compatible .ti-compatible,.type-codex-api .ti-codex-api,.type-codex-oauth .ti-codex-oauth,.type-token-plan .ti-token-plan{display:inline-flex}.type-compatible .tr-compatible,.type-codex-api .tr-codex-api,.type-codex-oauth .tr-codex-oauth,.type-token-plan .tr-token-plan{display:table-row}.state-ready .s-ready,.state-dirty .s-dirty,.state-loading .s-loading,.state-error .s-error{display:block}.state-ready .si-ready,.state-dirty .si-dirty,.state-loading .si-loading,.state-error .si-error{display:inline-flex}.hide-compatible,.hide-codex-api,.hide-codex-oauth,.hide-token-plan,.hide-ready,.hide-dirty,.hide-loading,.hide-error{display:block}.type-compatible .hide-compatible,.type-codex-api .hide-codex-api,.type-codex-oauth .hide-codex-oauth,.type-token-plan .hide-token-plan,.state-ready .hide-ready,.state-dirty .hide-dirty,.state-loading .hide-loading,.state-error .hide-error{display:none!important}.note{margin-top:16px;border:2px dashed var(--line);background:#ffffff;padding:12px;color:var(--muted);font-size:12px;line-height:1.5;font-weight:850}\n .compact-head{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 220px;grid-template-rows:1fr;border-bottom:3px solid var(--line);background:var(--panel2)}.compact-head>div{border-right:2px solid var(--line);padding:10px 14px;min-width:0}.compact-head>div:last-child{border-right:0}.compact-account{display:flex;align-items:center;justify-content:space-between;gap:10px}.compact-account .account-name{font-size:20px;margin:0;min-width:0}.compact-main{display:grid;grid-template-columns:280px minmax(0,1fr);gap:12px;align-items:center}.compact-main .type-line{margin:0}.compact-main .type-label{font-size:14px}.compact-main .head-metrics{border-width:1px}.compact-main .metric{padding:6px 8px}.compact-last{display:grid;align-content:center;gap:6px}.compact-last .pair{display:flex;justify-content:space-between;gap:10px;font-size:12px;font-weight:950}.compact-last .pair b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-card-mode{display:grid;grid-template-columns:1fr 1fr;gap:0;padding:0}.balance-card{min-width:0;padding:14px;background:var(--panel)}.balance-card:first-child{border-right:2px solid var(--line)}.balance-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;border-bottom:2px solid var(--line);padding-bottom:10px;margin-bottom:10px}.balance-card-head strong{display:block;margin-top:5px;font-size:18px;line-height:1;font-weight:1000;text-transform:uppercase;font-style:italic}.balance-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border:2px solid var(--line);background:#ffffff}.balance-stats>div{border-right:1px solid var(--line);padding:9px 10px;min-width:0}.balance-stats>div:last-child{border-right:0}.balance-stats b,.balance-log b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-stats span{display:block;margin-top:6px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.balance-log{margin-top:10px;border:2px solid var(--line);background:var(--panel2);padding:9px 10px;min-height:64px}.balance-log span{display:block;margin-top:6px;font-size:12px;font-weight:900;line-height:1.35}textarea[data-autosize]{height:auto;min-height:0}.account-type-line{align-items:baseline}.type-key{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:1000;color:var(--muted);border:1px solid var(--line);background:var(--panel2);padding:4px 6px}.model-grid.mapping-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mapping-item{border:2px solid var(--line);background:#ffffff;min-height:54px;display:grid;grid-template-columns:minmax(0,1fr) 32px minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 9px}.mapping-side{min-width:0}.mapping-side b{display:block;font-size:12px;font-weight:1000;line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mapping-side span{display:block;margin-top:4px;color:var(--muted);font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.mapping-arrow{display:grid;place-items:center;border-left:1px dashed var(--line);border-right:1px dashed var(--line);height:100%;font-family:var(--mono);font-weight:1000;color:var(--muted)}.mapping-item.readonly{background:var(--panel2)}\n/* v24: model mapping alignment fix. Type visibility classes have higher specificity than .mapping-item, so force visible mapping cards back to grid. */\n.type-compatible .mapping-item.t-compatible,\n.type-codex-api .mapping-item.t-codex-api,\n.type-codex-oauth .mapping-item.t-codex-oauth,\n.type-token-plan .mapping-item.t-token-plan{display:grid;grid-template-columns:minmax(0,1fr) 2.25rem minmax(0,1fr) auto;align-items:center;min-height:4.25rem;padding:.65rem .75rem;}\n.mapping-grid{align-items:stretch;}\n.mapping-item{overflow:hidden;}\n.mapping-side{display:grid;align-content:center;min-width:0;}\n.mapping-side b{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\n.mapping-arrow{min-height:2.75rem;align-self:stretch;}\n.model-toolbar{align-items:center;}\n@media (max-width: 1280px){.model-grid.mapping-grid{grid-template-columns:1fr}.model-toolbar{align-items:flex-start;flex-direction:column}.model-toolbar .actions{justify-content:flex-start}}\n.compact-main{display:grid!important;grid-template-columns:1fr!important;grid-template-rows:auto 1fr;gap:0!important;align-items:stretch!important;padding:0!important}.type-chip-row{display:flex;align-items:stretch;gap:0;border-bottom:2px solid var(--line);min-width:0;overflow:hidden}.summary-chip{display:grid;align-content:center;min-width:0;border-right:1px solid var(--line);padding:7px 9px;background:#ffffff}.summary-chip:last-child{border-right:0}.summary-chip.primary{min-width:13rem;background:var(--panel2)}.summary-chip b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);white-space:nowrap}.summary-chip span{display:block;margin-top:3px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description{display:flex;align-items:center;padding:8px 9px;font-size:12px;font-weight:900;line-height:1.35;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description:before{content:\"\";display:inline-block;width:1.75rem;border-top:2px solid var(--line);margin-right:.65rem;opacity:.7}\n/* v26: header summary tags use real pill/tag styling, matching READY chip language. */\n.compact-main{grid-template-rows:auto 1fr!important;}\n.type-chip-row{display:flex!important;align-items:center!important;gap:6px!important;border-bottom:0!important;padding:7px 9px!important;min-width:0;overflow:hidden;}\n.summary-chip{display:inline-flex!important;align-items:center!important;gap:5px!important;min-height:24px!important;min-width:0!important;max-width:13rem!important;border:1.5px solid var(--line)!important;background:#ffffff!important;padding:4px 8px!important;font-family:var(--mono)!important;font-size:10px!important;font-weight:1000!important;letter-spacing:.08em!important;text-transform:uppercase!important;white-space:nowrap!important;}\n.summary-chip.primary{background:var(--ink)!important;color:var(--panel)!important;border-color:var(--ink)!important;max-width:15rem!important;}\n.summary-chip b{display:inline!important;font-size:10px!important;letter-spacing:.08em!important;color:inherit!important;opacity:.68!important;}\n.summary-chip b:after{content:\":\";}\n.summary-chip span{display:inline!important;margin:0!important;min-width:0!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;font-size:10px!important;color:inherit!important;}\n.type-description{padding:7px 9px!important;}\n.type-chip-row{border-bottom:0!important}.type-description:before{display:none!important}.type-description{padding-top:4px!important}.compact-main{gap:0!important}/* v27: remove header middle separator */</style>\n <style>\n /* v09 footer redesign: allow state summary to wrap and grow naturally */\n .modal {\n grid-template-rows: auto minmax(0, 1fr) auto;\n }\n\n .footer {\n min-height: 72px;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: 12px;\n align-items: start;\n padding: 10px 16px;\n }\n\n .footer-msg {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: flex-start;\n min-width: 0;\n padding-top: 2px;\n }\n\n .footer-msg > div {\n min-width: 0;\n display: grid;\n gap: 2px;\n }\n\n .footer-msg > div strong,\n .footer-msg > div span {\n white-space: normal;\n overflow: visible;\n text-overflow: clip;\n line-height: 1.22;\n }\n\n .footer-msg > div strong {\n font-size: 13px;\n letter-spacing: 0.01em;\n }\n\n .footer-msg > div span {\n font-size: 12px;\n color: var(--muted);\n font-weight: 850;\n }\n\n .footer-msg > div .state-only {\n display: none;\n }\n\n .state-ready .footer-msg > div .si-ready,\n .state-dirty .footer-msg > div .si-dirty,\n .state-loading .footer-msg > div .si-loading,\n .state-error .footer-msg > div .si-error {\n display: block;\n }\n\n @media (max-width: 1120px) {\n .footer {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .footer .actions {\n justify-content: flex-start;\n flex-wrap: wrap;\n }\n }\n .compact-head{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 220px;grid-template-rows:1fr;border-bottom:3px solid var(--line);background:var(--panel2)}.compact-head>div{border-right:2px solid var(--line);padding:10px 14px;min-width:0}.compact-head>div:last-child{border-right:0}.compact-account{display:flex;align-items:center;justify-content:space-between;gap:10px}.compact-account .account-name{font-size:20px;margin:0;min-width:0}.compact-main{display:grid;grid-template-columns:280px minmax(0,1fr);gap:12px;align-items:center}.compact-main .type-line{margin:0}.compact-main .type-label{font-size:14px}.compact-main .head-metrics{border-width:1px}.compact-main .metric{padding:6px 8px}.compact-last{display:grid;align-content:center;gap:6px}.compact-last .pair{display:flex;justify-content:space-between;gap:10px;font-size:12px;font-weight:950}.compact-last .pair b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-card-mode{display:grid;grid-template-columns:1fr 1fr;gap:0;padding:0}.balance-card{min-width:0;padding:14px;background:var(--panel)}.balance-card:first-child{border-right:2px solid var(--line)}.balance-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;border-bottom:2px solid var(--line);padding-bottom:10px;margin-bottom:10px}.balance-card-head strong{display:block;margin-top:5px;font-size:18px;line-height:1;font-weight:1000;text-transform:uppercase;font-style:italic}.balance-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border:2px solid var(--line);background:#ffffff}.balance-stats>div{border-right:1px solid var(--line);padding:9px 10px;min-width:0}.balance-stats>div:last-child{border-right:0}.balance-stats b,.balance-log b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-stats span{display:block;margin-top:6px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.balance-log{margin-top:10px;border:2px solid var(--line);background:var(--panel2);padding:9px 10px;min-height:64px}.balance-log span{display:block;margin-top:6px;font-size:12px;font-weight:900;line-height:1.35}textarea[data-autosize]{height:auto;min-height:0}.account-type-line{align-items:baseline}.type-key{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:1000;color:var(--muted);border:1px solid var(--line);background:var(--panel2);padding:4px 6px}.model-grid.mapping-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mapping-item{border:2px solid var(--line);background:#ffffff;min-height:54px;display:grid;grid-template-columns:minmax(0,1fr) 32px minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 9px}.mapping-side{min-width:0}.mapping-side b{display:block;font-size:12px;font-weight:1000;line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mapping-side span{display:block;margin-top:4px;color:var(--muted);font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.mapping-arrow{display:grid;place-items:center;border-left:1px dashed var(--line);border-right:1px dashed var(--line);height:100%;font-family:var(--mono);font-weight:1000;color:var(--muted)}.mapping-item.readonly{background:var(--panel2)}\n/* v24: model mapping alignment fix. Type visibility classes have higher specificity than .mapping-item, so force visible mapping cards back to grid. */\n.type-compatible .mapping-item.t-compatible,\n.type-codex-api .mapping-item.t-codex-api,\n.type-codex-oauth .mapping-item.t-codex-oauth,\n.type-token-plan .mapping-item.t-token-plan{display:grid;grid-template-columns:minmax(0,1fr) 2.25rem minmax(0,1fr) auto;align-items:center;min-height:4.25rem;padding:.65rem .75rem;}\n.mapping-grid{align-items:stretch;}\n.mapping-item{overflow:hidden;}\n.mapping-side{display:grid;align-content:center;min-width:0;}\n.mapping-side b{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\n.mapping-arrow{min-height:2.75rem;align-self:stretch;}\n.model-toolbar{align-items:center;}\n@media (max-width: 1280px){.model-grid.mapping-grid{grid-template-columns:1fr}.model-toolbar{align-items:flex-start;flex-direction:column}.model-toolbar .actions{justify-content:flex-start}}\n.compact-main{display:grid!important;grid-template-columns:1fr!important;grid-template-rows:auto 1fr;gap:0!important;align-items:stretch!important;padding:0!important}.type-chip-row{display:flex;align-items:stretch;gap:0;border-bottom:2px solid var(--line);min-width:0;overflow:hidden}.summary-chip{display:grid;align-content:center;min-width:0;border-right:1px solid var(--line);padding:7px 9px;background:#ffffff}.summary-chip:last-child{border-right:0}.summary-chip.primary{min-width:13rem;background:var(--panel2)}.summary-chip b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);white-space:nowrap}.summary-chip span{display:block;margin-top:3px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description{display:flex;align-items:center;padding:8px 9px;font-size:12px;font-weight:900;line-height:1.35;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description:before{content:\"\";display:inline-block;width:1.75rem;border-top:2px solid var(--line);margin-right:.65rem;opacity:.7}\n/* v26: header summary tags use real pill/tag styling, matching READY chip language. */\n.compact-main{grid-template-rows:auto 1fr!important;}\n.type-chip-row{display:flex!important;align-items:center!important;gap:6px!important;border-bottom:0!important;padding:7px 9px!important;min-width:0;overflow:hidden;}\n.summary-chip{display:inline-flex!important;align-items:center!important;gap:5px!important;min-height:24px!important;min-width:0!important;max-width:13rem!important;border:1.5px solid var(--line)!important;background:#ffffff!important;padding:4px 8px!important;font-family:var(--mono)!important;font-size:10px!important;font-weight:1000!important;letter-spacing:.08em!important;text-transform:uppercase!important;white-space:nowrap!important;}\n.summary-chip.primary{background:var(--ink)!important;color:var(--panel)!important;border-color:var(--ink)!important;max-width:15rem!important;}\n.summary-chip b{display:inline!important;font-size:10px!important;letter-spacing:.08em!important;color:inherit!important;opacity:.68!important;}\n.summary-chip b:after{content:\":\";}\n.summary-chip span{display:inline!important;margin:0!important;min-width:0!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;font-size:10px!important;color:inherit!important;}\n.type-description{padding:7px 9px!important;}\n.type-chip-row{border-bottom:0!important}.type-description:before{display:none!important}.type-description{padding-top:4px!important}.compact-main{gap:0!important}/* v27: remove header middle separator */</style>\n</head>\n<body class=\"type-compatible state-ready\">\n<main class=\"page\">\n <section class=\"cover\"><div><div class=\"k\">GETTOKENS / ACCOUNT DETAIL / V26 REAL HEADER TAGS</div><h1>Real Header Tags</h1><p>修正配色:从偏米黄工业纸底改为 GetTokens 浅色工作台配色,使用白色主面、浅灰 surface、黑色边框与克制状态色。</p></div><div class=\"brief\">这版专门解决配色不对的问题;保留 v08 的左右请求配置结构。</div></section>\n <div class=\"toolbar\"><div class=\"tabs\"><button class=\"tab\" data-type=\"compatible\" aria-pressed=\"true\">OpenAI-Compatible</button><button class=\"tab\" data-type=\"codex-api\" aria-pressed=\"false\">Codex API Key</button><button class=\"tab\" data-type=\"codex-oauth\" aria-pressed=\"false\">Codex Auth-file</button><button class=\"tab\" data-type=\"token-plan\" aria-pressed=\"false\">Split Credential</button></div><div class=\"states\"><button class=\"state\" data-state=\"ready\" aria-pressed=\"true\">Ready</button><button class=\"state\" data-state=\"dirty\" aria-pressed=\"false\">Dirty</button><button class=\"state\" data-state=\"loading\" aria-pressed=\"false\">Loading</button><button class=\"state\" data-state=\"error\" aria-pressed=\"false\">Error</button></div></div>\n <section class=\"shell\"><div class=\"app\"><article class=\"modal\">\n <header class=\"fixed-head compact-head\">\n <div class=\"compact-account\"><div class=\"account-name type-only t-compatible\">DeepSeek</div><div class=\"account-name type-only t-codex-api\">Codex Stable</div><div class=\"account-name type-only t-codex-oauth\">codex-pro.json</div><div class=\"account-name type-only t-token-plan\">Split Provider</div><span class=\"pill green si-ready state-only\">READY</span><span class=\"pill amber si-dirty state-only\">DIRTY</span><span class=\"pill blue si-loading state-only\">BUSY</span><span class=\"pill red si-error state-only\">ERROR</span></div>\n <div class=\"compact-main\"><div class=\"type-chip-row\"><span class=\"summary-chip primary\"><b>类型</b><span id=\"head-title\">OpenAI-compatible</span></span><span class=\"summary-chip\"><b>凭据</b><span id=\"metric-credential\">API Key</span></span><span class=\"summary-chip\"><b>验证</b><span id=\"metric-verify\">Success</span></span><span class=\"summary-chip\"><b>路由</b><span id=\"metric-route\">Pool Tokyo-01</span></span><span class=\"summary-chip\"><b>余额/额度</b><span id=\"metric-balance\">Configured</span></span></div><div id=\"head-description\" class=\"type-description\">API Key provider · custom headers · model mapping · optional quota/billing scripts</div></div>\n <div class=\"compact-last\"><div class=\"pair\"><b>Last</b><span class=\"hide-loading hide-error\">2026-06-04 16:20</span><span class=\"si-loading state-only\">pending</span><span class=\"si-error state-only red\">—</span></div><div class=\"pair\"><b>Latency</b><span class=\"hide-error\">42ms</span><span class=\"si-error state-only red\">bad</span></div></div>\n </header>\n <div class=\"body\">\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">01</div><h2>身份与凭据</h2></div><div class=\"band-content\">\n <div class=\"fields type-only t-compatible\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"DeepSeek\"></label><label class=\"field span5\"><span class=\"label\">Base URL</span><input class=\"input\" value=\"https://api.deepseek.com/v1\"></label><label class=\"field span4\"><span class=\"label\">API Key</span><input class=\"input\" value=\"sk-••••••••••\"></label></div>\n <div class=\"fields type-only t-codex-api\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"Codex Stable\"></label><label class=\"field span4\"><span class=\"label\">API Key</span><input class=\"input\" value=\"sk-codex-••••••\"></label><label class=\"field span4\"><span class=\"label\">Base URL</span><input class=\"input\" value=\"https://api.openai.com/v1\"></label><label class=\"field span1\"><span class=\"label\">Prefix</span><input class=\"input\" value=\"/v1\"></label></div>\n <div class=\"fields type-only t-codex-oauth\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"codex-pro.json\"></label><label class=\"field span5\"><span class=\"label\">Auth File</span><input class=\"input\" value=\"~/.codex/auth.json\" readonly></label><div class=\"actions span4\"><button class=\"button primary\">Reauth</button><button class=\"button\">Normalize</button><button class=\"button\">Download</button></div></div>\n <div class=\"fields type-only t-token-plan\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"MiMo Split Credential\"></label><label class=\"field span5\"><span class=\"label\">Chat Base URL</span><input class=\"input\" value=\"https://chat-provider.example.com/v1\"></label><label class=\"field span4\"><span class=\"label\">Chat Key</span><input class=\"input\" value=\"chat-••••••••\"></label><label class=\"field span5\"><span class=\"label\">Model Fetch URL</span><input class=\"input\" value=\"https://models-provider.example.com/v1\"></label><label class=\"field span4\"><span class=\"label\">Model Fetch Key</span><input class=\"input\" value=\"sk-••••••••\"></label><div class=\"span3 hint\">模型拉取凭据,仅用于 /models。</div></div>\n </div></section>\n <section class=\"band hide-codex-oauth\"><div class=\"band-index\"><div class=\"num\">02</div><h2>请求配置</h2></div><div class=\"band-content request-config request-split\">\n <div class=\"request-left\">\n <div class=\"fields\">\n <label class=\"field span12\"><span class=\"label\">Custom Headers</span><textarea class=\"textarea\" data-autosize=\"true\" rows=\"2\">HTTP-Referer: https://gettokens.local\nX-Title: GetTokens Workbench</textarea></label>\n <div class=\"span12 hint\">这里只保留当前支持的 headersText 配置;计数与路由不作为可编辑字段展示。</div>\n </div>\n </div>\n <div class=\"request-right\">\n <div class=\"fields\">\n <label class=\"field span12\"><span class=\"label\">Proxy Pool Node</span><select class=\"input hide-error\"><option>Tokyo-01 · socks5 · 127.0.0.1:7890 · 42ms</option><option>Singapore-02 · http · 10.0.0.2:8080 · 58ms</option></select><select class=\"input si-error state-only\"><option>无可用代理池节点</option></select></label>\n <label class=\"field span12\"><span class=\"label\">Resolved URL</span><input class=\"input hide-error\" value=\"socks5://127.0.0.1:7890\" readonly><input class=\"input si-error state-only\" value=\"—\" readonly></label>\n <div class=\"span12 hint\">账号详情只选择已保存的代理池节点;刷新、测速、增删节点在代理池页面完成。</div>\n </div>\n </div>\n </div></section>\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">03</div><h2>模型</h2></div><div class=\"band-content model-section\">\n <div class=\"model-toolbar\"><span class=\"hint\">模型是映射关系:Source Model → Alias / Route。</span><div class=\"actions\"><button class=\"button hide-codex-oauth\">拉取远端模型</button><button class=\"button primary hide-codex-oauth\">使用远端模型</button><button class=\"button hide-codex-oauth\">新增映射</button><button class=\"button type-only t-codex-oauth\">刷新模型目录</button></div></div>\n <div class=\"model-grid mapping-grid\">\n <div class=\"mapping-item type-only t-compatible\"><div class=\"mapping-side\"><span>Source Model</span><b>deepseek-v4-pro</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>DeepSeek V4 Pro</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-compatible\"><div class=\"mapping-side\"><span>Source Model</span><b>deepseek-v4-flash</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>DeepSeek Flash</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-codex-api\"><div class=\"mapping-side\"><span>Source Model</span><b>gpt-5.4-mini</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Verify Alias</span><b>fast verify</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-codex-api\"><div class=\"mapping-side\"><span>Source Model</span><b>gpt-5.4</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Verify Alias</span><b>stable</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-codex-oauth readonly\"><div class=\"mapping-side\"><span>Auth-file Model</span><b>gpt-5.5</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Route</span><b>oauth available</b></div><span class=\"pill violet\">只读</span></div>\n <div class=\"mapping-item type-only t-codex-oauth readonly\"><div class=\"mapping-side\"><span>Auth-file Model</span><b>codex-mini-latest</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Route</span><b>codex</b></div><span class=\"pill violet\">只读</span></div>\n <div class=\"mapping-item type-only t-token-plan\"><div class=\"mapping-side\"><span>Source Model</span><b>provider-main-model</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>Primary Model</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-token-plan\"><div class=\"mapping-side\"><span>Source Model</span><b>provider-fast-model</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>Fast Model</b></div><button class=\"button danger\">删除</button></div>\n </div>\n </div></section>\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">04</div><h2>验证</h2></div><div class=\"band-content\"><div class=\"verify-row verify-row-simple\"><div class=\"fields\"><label class=\"field span12 hide-codex-oauth\"><span class=\"label\">测试模型</span><input class=\"input type-only t-compatible\" value=\"deepseek-v4-pro\"><input class=\"input type-only t-codex-api\" value=\"gpt-5.4-mini\"><input class=\"input type-only t-token-plan\" value=\"provider-main-model\"></label><div class=\"actions span12 type-only t-codex-oauth\"><button class=\"button primary\">Reauth</button><button class=\"button\">刷新模型</button></div></div><div class=\"actions verify-actions\"><button class=\"button primary\"><span class=\"hide-loading\">执行验证</span><span class=\"si-loading state-only\">验证中</span></button></div></div></div></section>\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">05</div><h2>余额与额度</h2></div><div class=\"band-content quota-billing balance-card-mode\">\n <article class=\"balance-card quota-card\">\n <header class=\"balance-card-head\">\n <div><span class=\"label\">Quota / 额度</span><strong class=\"hide-loading hide-error\">72% remaining</strong><strong class=\"si-loading state-only blue\">testing...</strong><strong class=\"si-error state-only red\">test failed</strong></div>\n <div class=\"actions\"><button class=\"button hide-codex-oauth\">配置</button><button class=\"button primary hide-codex-oauth\"><span class=\"hide-loading\">测试</span><span class=\"si-loading state-only\">测试中</span></button><button class=\"button type-only t-codex-oauth\">查看 Provider</button></div>\n </header>\n <div class=\"balance-stats\">\n <div><b>Remaining</b><span>720K tokens</span></div>\n <div><b>Percent</b><span>72%</span></div>\n <div><b>Reset</b><span>18:00</span></div>\n </div>\n <div class=\"balance-log\"><b>Last Quota Test</b><span class=\"hide-loading hide-error\">success · script returned remaining quota</span><span class=\"si-loading state-only\">request pending, keep last result visible</span><span class=\"si-error state-only red\">HTTP 403 · check quota script / credential</span></div>\n </article>\n <article class=\"balance-card billing-card\">\n <header class=\"balance-card-head\">\n <div><span class=\"label\">Billing / 余额</span><strong class=\"hide-codex-oauth hide-loading hide-error\">120.00 USD</strong><strong class=\"type-only t-codex-oauth\">provider managed</strong><strong class=\"hide-codex-oauth si-loading state-only blue\">testing...</strong><strong class=\"hide-codex-oauth si-error state-only red\">test failed</strong></div>\n <div class=\"actions\"><button class=\"button hide-codex-oauth\">配置</button><button class=\"button primary hide-codex-oauth\">测试</button><button class=\"button type-only t-codex-oauth\">查看 Provider</button></div>\n </header>\n <div class=\"balance-stats\">\n <div><b>Total</b><span class=\"hide-codex-oauth\">120.00 USD</span><span class=\"type-only t-codex-oauth\">—</span></div>\n <div><b>Granted</b><span class=\"hide-codex-oauth\">80.00 USD</span><span class=\"type-only t-codex-oauth\">provider</span></div>\n <div><b>Topped</b><span class=\"hide-codex-oauth\">40.00 USD</span><span class=\"type-only t-codex-oauth\">managed</span></div>\n </div>\n <div class=\"balance-log\"><b>Last Billing Test</b><span class=\"hide-codex-oauth hide-loading hide-error\">success · granted 80 USD · topped up 40 USD</span><span class=\"hide-codex-oauth si-loading state-only\">request pending</span><span class=\"hide-codex-oauth si-error state-only red\">empty response or parse failed</span><span class=\"type-only t-codex-oauth\">managed by provider / OAuth account</span></div>\n </article>\n </div></section>\n </div>\n <footer class=\"footer\"><div class=\"footer-msg\"><span class=\"pill green si-ready state-only\">READY</span><span class=\"pill amber si-dirty state-only\">DIRTY</span><span class=\"pill blue si-loading state-only\">BUSY</span><span class=\"pill red si-error state-only\">BLOCKED</span><div><strong class=\"si-ready state-only\">可保存或关闭</strong><strong class=\"si-dirty state-only\">存在未保存更改</strong><strong class=\"si-loading state-only\">请求处理中</strong><strong class=\"si-error state-only\">保存被阻断</strong><span class=\"si-ready state-only\">当前账号详情状态正常。</span><span class=\"si-dirty state-only\">保存后写入 Wails/sidecar。</span><span class=\"si-loading state-only\">不清空已有信息。</span><span class=\"si-error state-only\">修复 credential/proxy/reauth 错误后才能保存。</span></div></div><div class=\"actions\"><button class=\"button primary hide-error\">保存更改</button><button class=\"button si-error state-only\" disabled>保存不可用</button><button class=\"button\">放弃操作</button></div></footer>\n </article></div></section>\n <div class=\"note\"><b>v16 请求配置字段收窄:</b>移除不支持的派生/运行态字段,只保留 Custom Headers 与代理池选择。<br><b>v15 顶部关闭修正:</b>已移除头部重复关闭入口,关闭/放弃操作只保留在底部命令栏。<br><b>v14 模型列表修正:</b>模型列表改为两列,避免模型多时单列表格过高。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v20 余额额度按钮文案修正:</b>Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v19 余额额度卡片修正:</b>余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。<br><b>v18 头部精简修正:</b>头部改为 74px 紧凑业务摘要,移除 detail/frame/actions 等实现文本。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v20 余额额度按钮文案修正:</b>Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v19 余额额度卡片修正:</b>余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。<br><b>v17 余额额度分栏修正:</b>余额与额度改为左右两栏,中间明确分割线;Quota 与 Billing 各自包含配置、测试和结果。<br><b>v13 余额额度修正:</b>余额/额度支持测试,测试结果在本模块内显示。<br><b>v12 验证区修正:</b>已移除不存在的 限流/路由状态 摘要,仅保留测试模型与执行验证动作。<br><b>v09 配色修正:</b>当前文件已改为 GetTokens 浅色工作台配色:白色主面、浅灰 surface、黑色边框、灰色弱文本、状态色只用于状态。保留 v08 的左右请求配置结构。</div>\n</main>\n<script>\nfunction autosizeTextareas(){document.querySelectorAll('textarea[data-autosize]').forEach((el)=>{el.style.height='auto';el.style.height=`${el.scrollHeight}px`;});}\ndocument.addEventListener('input',(event)=>{if(event.target?.matches?.('textarea[data-autosize]')) autosizeTextareas();});\nwindow.addEventListener('load', autosizeTextareas);\nconst body=document.body,tabs=[...document.querySelectorAll('.tab')],states=[...document.querySelectorAll('.state')],headTitle=document.querySelector('#head-title'),headDescription=document.querySelector('#head-description'),metricCredential=document.querySelector('#metric-credential'),metricVerify=document.querySelector('#metric-verify'),metricRoute=document.querySelector('#metric-route'),metricBalance=document.querySelector('#metric-balance');let type='compatible',state='ready';const typeMeta={compatible:{title:'OpenAI-compatible',credential:'API Key',balance:'Configured',desc:'API Key provider · custom headers · model mapping · optional quota/billing scripts'},'codex-api':{title:'Codex API Key',credential:'Codex Key',balance:'Configured',desc:'Codex API key account · prefix · model verification · quota/billing curl scripts'},'codex-oauth':{title:'Codex Auth-file / OAuth',credential:'Auth File',balance:'Provider',desc:'OAuth auth-file account · reauth · read-only provider managed quota'},'token-plan':{title:'Split Credential',credential:'chat + fetch key',balance:'Configured',desc:'Separate chat credential and model-fetch credential for one provider'}};const stateMeta={ready:{verify:'Success',route:'Pool Tokyo-01',routeTone:''},dirty:{verify:'Draft',route:'Pool Tokyo-01',routeTone:''},loading:{verify:'Running',route:'Checking…',routeTone:''},error:{verify:'Failed',route:'Proxy Bad',routeTone:'red'}};function renderHeadSummary(){const t=typeMeta[type],s=stateMeta[state];if(!t||!s)return;headTitle.textContent=t.title;if(headDescription)headDescription.textContent=t.desc;metricCredential.textContent=t.credential;metricBalance.textContent=t.balance;metricVerify.textContent=s.verify;metricRoute.textContent=s.route;metricRoute.className=s.routeTone?`red`:''}function render(){body.className=`type-${type} state-${state}`;tabs.forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.type===type)));states.forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.state===state)));renderHeadSummary()}tabs.forEach(b=>b.onclick=()=>{type=b.dataset.type;render()});states.forEach(b=>b.onclick=()=>{state=b.dataset.state;render()});render();</script>\n</body>\n</html>\n"}, "73c17b20ebe17d46f871845562a3bf04c04e27f7": {"t": 1785759462.9398632, "n": 47135, "h": "860104a39f6fb032", "c": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>GetTokens · Account Detail v09 Palette Fix</title>\n <style>\n :root{--paper:#f9f9f9;--panel:#ffffff;--panel2:#f0f0f0;--ink:#000000;--muted:#666666;--line:#000000;--hair:rgba(0,0,0,.14);--red:#ef4444;--green:#16a34a;--blue:#1f6feb;--amber:#eab308;--violet:#6366f1;--index:214px;--mono:JetBrains Mono,IBM Plex Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--sans:JetBrains Mono,IBM Plex Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--shadow:4px 4px 0 #e5e5e5}\n *{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);min-width:1420px}.page{padding:28px}.k,.mono{font-family:var(--mono)}.k{font-size:11px;letter-spacing:.22em;text-transform:uppercase;font-weight:1000;color:var(--muted)}.cover{border:2px solid var(--line);background:var(--panel);box-shadow:var(--shadow);padding:18px;margin-bottom:16px;display:grid;grid-template-columns:1fr 430px;gap:18px;align-items:end}.cover h1{margin:6px 0 0;font-size:48px;line-height:.9;letter-spacing:-.055em;text-transform:uppercase;font-style:italic;font-weight:1000}.cover p{margin:10px 0 0;color:var(--muted);font-size:13px;line-height:1.45;font-weight:850;max-width:880px}.brief{border:2px solid var(--line);background:var(--panel2);padding:12px;font-size:12px;line-height:1.45;color:var(--muted);font-weight:850}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;margin-bottom:14px}.tabs,.states{display:flex;gap:8px;flex-wrap:wrap}.tab,.state{border:2px solid var(--line);background:var(--panel);padding:8px 10px;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;font-weight:1000;cursor:pointer}.tab[aria-pressed=true],.state[aria-pressed=true]{background:var(--ink);color:var(--panel)}.shell{border:2px solid var(--line);background:#000000;padding:10px;box-shadow:var(--shadow)}.app{height:852px;border:2px solid var(--line);background:rgba(0,0,0,.72);padding:22px;display:grid;place-items:center}.modal{width:min(1370px,calc(100vw - 88px));height:792px;border:2px solid var(--line);background:var(--panel);display:grid;grid-template-rows:74px minmax(0,1fr)72px;box-shadow:0 0 0 1px rgba(0,0,0,.08), 0 18px 48px rgba(0,0,0,.28)}\n\n /* v06 header: two-level context, no single-line long title clipping */\n .fixed-head{border-bottom:3px solid var(--line);background:var(--panel2);display:grid;grid-template-rows:44px auto}.head-bar{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 250px;border-bottom:2px solid var(--line)}.head-bar>div{border-right:2px solid var(--line);padding:9px 14px;display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0}.head-bar>div:last-child{border-right:0}.sys{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:1000;color:var(--muted);white-space:nowrap}.head-context{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 250px;align-items:stretch}.head-context>div{border-right:2px solid var(--line);padding:10px 14px;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:8px}.head-context>div:last-child{border-right:0}.account-name{font-size:22px;line-height:.95;letter-spacing:-.035em;text-transform:uppercase;font-style:italic;font-weight:1000;white-space:normal;overflow:visible}.type-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:0}.type-label{font-size:15px;line-height:1.15;font-weight:1000;text-transform:uppercase;font-style:italic;letter-spacing:-.015em;white-space:normal;overflow:visible}.head-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:2px solid var(--line);background:#ffffff}.metric{border-right:1px solid var(--line);padding:7px 9px;min-width:0}.metric:last-child{border-right:0}.metric b{display:block;font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);white-space:nowrap}.metric span{display:block;margin-top:4px;font-size:12px;font-weight:950;white-space:normal;overflow:visible;text-overflow:clip;line-height:1.18}.last-box{display:grid;gap:7px;align-content:start}.last-box .pair{display:flex;justify-content:space-between;gap:12px;font-size:12px;font-weight:950}.last-box .pair b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}\n\n .pill{display:inline-flex;align-items:center;min-height:23px;border:1.5px solid var(--line);background:#ffffff;padding:4px 8px;font-family:var(--mono);font-size:10px;font-weight:1000;letter-spacing:.09em;text-transform:uppercase;white-space:nowrap}.green{color:var(--green);border-color:var(--green)}.red{color:var(--red);border-color:var(--red)}.blue{color:var(--blue);border-color:var(--blue)}.amber{color:var(--amber);border-color:var(--amber)}.violet{color:var(--violet);border-color:var(--violet)}.button{border:2px solid var(--line);background:#ffffff;padding:8px 10px;font-family:var(--mono);font-size:10px;font-weight:1000;letter-spacing:.1em;text-transform:uppercase;cursor:pointer}.button.primary{background:var(--ink);color:var(--panel)}.button.danger{color:var(--red);border-color:var(--red)}.button:disabled{opacity:.45;cursor:not-allowed}.body{min-height:0;overflow:auto;background:var(--panel)}.band{border-bottom:2px solid var(--line);display:grid;grid-template-columns:var(--index) minmax(0,1fr);min-height:96px}.band.hide-codex-oauth{display:grid}.band-index{border-right:2px solid var(--line);background:#f9f9f9;padding:14px}.band-index .num{font-family:var(--mono);font-size:13px;letter-spacing:.16em;font-weight:1000;color:var(--muted)}.band-index h2{margin:9px 0 0;font-size:17px;line-height:1;text-transform:uppercase;font-style:italic}.band-content{padding:14px;min-width:0}.fields{display:grid;grid-template-columns:repeat(12,1fr);gap:10px}.field{display:grid;gap:6px;min-width:0}.span1{grid-column:span 1}.span3{grid-column:span 3}.span4{grid-column:span 4}.span5{grid-column:span 5}.span7{grid-column:span 7}.span12{grid-column:span 12}.label{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:1000;color:var(--muted)}.input,.textarea{width:100%;border:2px solid var(--line);background:#ffffff;padding:8px 9px;min-height:36px;font-family:var(--mono);font-size:12px;font-weight:850;outline:0}.textarea{min-height:72px;line-height:1.42;resize:vertical}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.hint{color:var(--muted);font-size:12px;line-height:1.35;font-weight:800}.module-actions{margin-top:10px;border-top:1px dashed var(--hair);padding-top:10px;display:flex;align-items:center;justify-content:space-between;gap:12px}.request-config .textarea{min-height:0;height:auto;overflow:hidden;resize:none;line-height:1.42}.request-split{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(360px,.65fr);gap:14px;align-items:start}.request-left{min-width:0}.request-right{min-width:0;border-left:2px solid var(--line);padding-left:14px}.request-actions{margin-top:10px;border-top:1px dashed var(--hair);padding-top:10px;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.quota-result{border:2px solid var(--line);background:var(--panel2);display:grid;grid-template-columns:1fr 1fr}.quota-result>div{border-right:1px solid var(--line);padding:9px 10px}.quota-result>div:last-child{border-right:0}.quota-result b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.quota-result span{display:block;margin-top:5px;font-size:12px;font-weight:900;line-height:1.3}.quota-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0;padding:0}.quota-pane,.billing-pane{padding:14px;min-width:0}.quota-pane{border-right:2px solid var(--line)}.quota-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.result-log{margin-top:10px;border:2px solid var(--line);background:var(--panel2);padding:9px 10px;min-height:58px}.result-log b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.result-log span{display:block;margin-top:5px;font-size:12px;font-weight:900;line-height:1.3}.table{border:2px solid var(--line);width:100%;border-collapse:collapse;background:#ffffff}.table th,.table td{border:1px solid var(--line);padding:8px 9px;text-align:left;font-size:12px;vertical-align:middle}.table th{background:var(--panel2);font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.table input{width:100%;border:0;outline:0;background:transparent;font-family:var(--mono);font-size:12px;font-weight:850}.model-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.model-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.model-item{border:2px solid var(--line);background:#ffffff;min-height:46px;padding:8px 9px;display:flex;align-items:center;justify-content:space-between;gap:10px}.model-item b{display:block;font-size:12px;font-weight:1000;line-height:1.1}.model-item span{display:block;margin-top:4px;color:var(--muted);font-size:11px;font-weight:850}.model-item.readonly{background:var(--panel2)}.verify-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:end}.verify-actions{align-self:end}.flat-status{border:2px solid var(--line);background:#ffffff;display:grid;grid-template-columns:repeat(4,1fr)}.flat-status>div{border-right:1px solid var(--line);padding:8px 9px;min-width:0}.flat-status>div:last-child{border-right:0}.flat-status b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.flat-status span{display:block;margin-top:5px;font-size:13px;font-weight:950;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.footer{border-top:2px solid var(--line);background:var(--panel2);display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;padding:12px 16px}.footer-msg{display:flex;gap:10px;align-items:center;min-width:0}.footer-msg strong{font-size:13px;text-transform:uppercase;letter-spacing:.07em}.footer-msg span{display:block;color:var(--muted);font-size:12px;font-weight:850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-only,.state-only{display:none}.type-compatible .t-compatible,.type-codex-api .t-codex-api,.type-codex-oauth .t-codex-oauth,.type-token-plan .t-token-plan{display:block}.type-compatible .ti-compatible,.type-codex-api .ti-codex-api,.type-codex-oauth .ti-codex-oauth,.type-token-plan .ti-token-plan{display:inline-flex}.type-compatible .tr-compatible,.type-codex-api .tr-codex-api,.type-codex-oauth .tr-codex-oauth,.type-token-plan .tr-token-plan{display:table-row}.state-ready .s-ready,.state-dirty .s-dirty,.state-loading .s-loading,.state-error .s-error{display:block}.state-ready .si-ready,.state-dirty .si-dirty,.state-loading .si-loading,.state-error .si-error{display:inline-flex}.hide-compatible,.hide-codex-api,.hide-codex-oauth,.hide-token-plan,.hide-ready,.hide-dirty,.hide-loading,.hide-error{display:block}.type-compatible .hide-compatible,.type-codex-api .hide-codex-api,.type-codex-oauth .hide-codex-oauth,.type-token-plan .hide-token-plan,.state-ready .hide-ready,.state-dirty .hide-dirty,.state-loading .hide-loading,.state-error .hide-error{display:none!important}.note{margin-top:16px;border:2px dashed var(--line);background:#ffffff;padding:12px;color:var(--muted);font-size:12px;line-height:1.5;font-weight:850}\n .compact-head{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 220px;grid-template-rows:1fr;border-bottom:3px solid var(--line);background:var(--panel2)}.compact-head>div{border-right:2px solid var(--line);padding:10px 14px;min-width:0}.compact-head>div:last-child{border-right:0}.compact-account{display:flex;align-items:center;justify-content:space-between;gap:10px}.compact-account .account-name{font-size:20px;margin:0;min-width:0}.compact-main{display:grid;grid-template-columns:280px minmax(0,1fr);gap:12px;align-items:center}.compact-main .type-line{margin:0}.compact-main .type-label{font-size:14px}.compact-main .head-metrics{border-width:1px}.compact-main .metric{padding:6px 8px}.compact-last{display:grid;align-content:center;gap:6px}.compact-last .pair{display:flex;justify-content:space-between;gap:10px;font-size:12px;font-weight:950}.compact-last .pair b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-card-mode{display:grid;grid-template-columns:1fr 1fr;gap:0;padding:0}.balance-card{min-width:0;padding:14px;background:var(--panel)}.balance-card:first-child{border-right:2px solid var(--line)}.balance-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;border-bottom:2px solid var(--line);padding-bottom:10px;margin-bottom:10px}.balance-card-head strong{display:block;margin-top:5px;font-size:18px;line-height:1;font-weight:1000;text-transform:uppercase;font-style:italic}.balance-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border:2px solid var(--line);background:#ffffff}.balance-stats>div{border-right:1px solid var(--line);padding:9px 10px;min-width:0}.balance-stats>div:last-child{border-right:0}.balance-stats b,.balance-log b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-stats span{display:block;margin-top:6px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.balance-log{margin-top:10px;border:2px solid var(--line);background:var(--panel2);padding:9px 10px;min-height:64px}.balance-log span{display:block;margin-top:6px;font-size:12px;font-weight:900;line-height:1.35}textarea[data-autosize]{height:auto;min-height:0}.account-type-line{align-items:baseline}.type-key{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:1000;color:var(--muted);border:1px solid var(--line);background:var(--panel2);padding:4px 6px}.model-grid.mapping-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mapping-item{border:2px solid var(--line);background:#ffffff;min-height:54px;display:grid;grid-template-columns:minmax(0,1fr) 32px minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 9px}.mapping-side{min-width:0}.mapping-side b{display:block;font-size:12px;font-weight:1000;line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mapping-side span{display:block;margin-top:4px;color:var(--muted);font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.mapping-arrow{display:grid;place-items:center;border-left:1px dashed var(--line);border-right:1px dashed var(--line);height:100%;font-family:var(--mono);font-weight:1000;color:var(--muted)}.mapping-item.readonly{background:var(--panel2)}\n/* v24: model mapping alignment fix. Type visibility classes have higher specificity than .mapping-item, so force visible mapping cards back to grid. */\n.type-compatible .mapping-item.t-compatible,\n.type-codex-api .mapping-item.t-codex-api,\n.type-codex-oauth .mapping-item.t-codex-oauth,\n.type-token-plan .mapping-item.t-token-plan{display:grid;grid-template-columns:minmax(0,1fr) 2.25rem minmax(0,1fr) auto;align-items:center;min-height:4.25rem;padding:.65rem .75rem;}\n.mapping-grid{align-items:stretch;}\n.mapping-item{overflow:hidden;}\n.mapping-side{display:grid;align-content:center;min-width:0;}\n.mapping-side b{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\n.mapping-arrow{min-height:2.75rem;align-self:stretch;}\n.model-toolbar{align-items:center;}\n@media (max-width: 1280px){.model-grid.mapping-grid{grid-template-columns:1fr}.model-toolbar{align-items:flex-start;flex-direction:column}.model-toolbar .actions{justify-content:flex-start}}\n.compact-main{display:grid!important;grid-template-columns:1fr!important;grid-template-rows:auto 1fr;gap:0!important;align-items:stretch!important;padding:0!important}.type-chip-row{display:flex;align-items:stretch;gap:0;border-bottom:2px solid var(--line);min-width:0;overflow:hidden}.summary-chip{display:grid;align-content:center;min-width:0;border-right:1px solid var(--line);padding:7px 9px;background:#ffffff}.summary-chip:last-child{border-right:0}.summary-chip.primary{min-width:13rem;background:var(--panel2)}.summary-chip b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);white-space:nowrap}.summary-chip span{display:block;margin-top:3px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description{display:flex;align-items:center;padding:8px 9px;font-size:12px;font-weight:900;line-height:1.35;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description:before{content:\"\";display:inline-block;width:1.75rem;border-top:2px solid var(--line);margin-right:.65rem;opacity:.7}\n/* v26: header summary tags use real pill/tag styling, matching READY chip language. */\n.compact-main{grid-template-rows:auto 1fr!important;}\n.type-chip-row{display:flex!important;align-items:center!important;gap:6px!important;border-bottom:0!important;padding:7px 9px!important;min-width:0;overflow:hidden;}\n.summary-chip{display:inline-flex!important;align-items:center!important;gap:5px!important;min-height:24px!important;min-width:0!important;max-width:13rem!important;border:1.5px solid var(--line)!important;background:#ffffff!important;padding:4px 8px!important;font-family:var(--mono)!important;font-size:10px!important;font-weight:1000!important;letter-spacing:.08em!important;text-transform:uppercase!important;white-space:nowrap!important;}\n.summary-chip.primary{background:var(--ink)!important;color:var(--panel)!important;border-color:var(--ink)!important;max-width:15rem!important;}\n.summary-chip b{display:inline!important;font-size:10px!important;letter-spacing:.08em!important;color:inherit!important;opacity:.68!important;}\n.summary-chip b:after{content:\":\";}\n.summary-chip span{display:inline!important;margin:0!important;min-width:0!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;font-size:10px!important;color:inherit!important;}\n.type-description{padding:7px 9px!important;}\n.type-chip-row{border-bottom:0!important}.type-description:before{display:none!important}.type-description{padding-top:4px!important}.compact-main{gap:0!important}/* v27: remove header middle separator */</style>\n <style>\n /* v09 footer redesign: allow state summary to wrap and grow naturally */\n .modal {\n grid-template-rows: auto minmax(0, 1fr) auto;\n }\n\n .footer {\n min-height: 72px;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: 12px;\n align-items: start;\n padding: 10px 16px;\n }\n\n .footer-msg {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: flex-start;\n min-width: 0;\n padding-top: 2px;\n }\n\n .footer-msg > div {\n min-width: 0;\n display: grid;\n gap: 2px;\n }\n\n .footer-msg > div strong,\n .footer-msg > div span {\n white-space: normal;\n overflow: visible;\n text-overflow: clip;\n line-height: 1.22;\n }\n\n .footer-msg > div strong {\n font-size: 13px;\n letter-spacing: 0.01em;\n }\n\n .footer-msg > div span {\n font-size: 12px;\n color: var(--muted);\n font-weight: 850;\n }\n\n .footer-msg > div .state-only {\n display: none;\n }\n\n .state-ready .footer-msg > div .si-ready,\n .state-dirty .footer-msg > div .si-dirty,\n .state-loading .footer-msg > div .si-loading,\n .state-error .footer-msg > div .si-error {\n display: block;\n }\n\n @media (max-width: 1120px) {\n .footer {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .footer .actions {\n justify-content: flex-start;\n flex-wrap: wrap;\n }\n }\n .compact-head{display:grid;grid-template-columns:var(--index) minmax(0,1fr) 220px;grid-template-rows:1fr;border-bottom:3px solid var(--line);background:var(--panel2)}.compact-head>div{border-right:2px solid var(--line);padding:10px 14px;min-width:0}.compact-head>div:last-child{border-right:0}.compact-account{display:flex;align-items:center;justify-content:space-between;gap:10px}.compact-account .account-name{font-size:20px;margin:0;min-width:0}.compact-main{display:grid;grid-template-columns:280px minmax(0,1fr);gap:12px;align-items:center}.compact-main .type-line{margin:0}.compact-main .type-label{font-size:14px}.compact-main .head-metrics{border-width:1px}.compact-main .metric{padding:6px 8px}.compact-last{display:grid;align-content:center;gap:6px}.compact-last .pair{display:flex;justify-content:space-between;gap:10px;font-size:12px;font-weight:950}.compact-last .pair b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-card-mode{display:grid;grid-template-columns:1fr 1fr;gap:0;padding:0}.balance-card{min-width:0;padding:14px;background:var(--panel)}.balance-card:first-child{border-right:2px solid var(--line)}.balance-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;border-bottom:2px solid var(--line);padding-bottom:10px;margin-bottom:10px}.balance-card-head strong{display:block;margin-top:5px;font-size:18px;line-height:1;font-weight:1000;text-transform:uppercase;font-style:italic}.balance-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border:2px solid var(--line);background:#ffffff}.balance-stats>div{border-right:1px solid var(--line);padding:9px 10px;min-width:0}.balance-stats>div:last-child{border-right:0}.balance-stats b,.balance-log b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.balance-stats span{display:block;margin-top:6px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.balance-log{margin-top:10px;border:2px solid var(--line);background:var(--panel2);padding:9px 10px;min-height:64px}.balance-log span{display:block;margin-top:6px;font-size:12px;font-weight:900;line-height:1.35}textarea[data-autosize]{height:auto;min-height:0}.account-type-line{align-items:baseline}.type-key{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:1000;color:var(--muted);border:1px solid var(--line);background:var(--panel2);padding:4px 6px}.model-grid.mapping-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mapping-item{border:2px solid var(--line);background:#ffffff;min-height:54px;display:grid;grid-template-columns:minmax(0,1fr) 32px minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 9px}.mapping-side{min-width:0}.mapping-side b{display:block;font-size:12px;font-weight:1000;line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mapping-side span{display:block;margin-top:4px;color:var(--muted);font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.mapping-arrow{display:grid;place-items:center;border-left:1px dashed var(--line);border-right:1px dashed var(--line);height:100%;font-family:var(--mono);font-weight:1000;color:var(--muted)}.mapping-item.readonly{background:var(--panel2)}\n/* v24: model mapping alignment fix. Type visibility classes have higher specificity than .mapping-item, so force visible mapping cards back to grid. */\n.type-compatible .mapping-item.t-compatible,\n.type-codex-api .mapping-item.t-codex-api,\n.type-codex-oauth .mapping-item.t-codex-oauth,\n.type-token-plan .mapping-item.t-token-plan{display:grid;grid-template-columns:minmax(0,1fr) 2.25rem minmax(0,1fr) auto;align-items:center;min-height:4.25rem;padding:.65rem .75rem;}\n.mapping-grid{align-items:stretch;}\n.mapping-item{overflow:hidden;}\n.mapping-side{display:grid;align-content:center;min-width:0;}\n.mapping-side b{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\n.mapping-arrow{min-height:2.75rem;align-self:stretch;}\n.model-toolbar{align-items:center;}\n@media (max-width: 1280px){.model-grid.mapping-grid{grid-template-columns:1fr}.model-toolbar{align-items:flex-start;flex-direction:column}.model-toolbar .actions{justify-content:flex-start}}\n.compact-main{display:grid!important;grid-template-columns:1fr!important;grid-template-rows:auto 1fr;gap:0!important;align-items:stretch!important;padding:0!important}.type-chip-row{display:flex;align-items:stretch;gap:0;border-bottom:2px solid var(--line);min-width:0;overflow:hidden}.summary-chip{display:grid;align-content:center;min-width:0;border-right:1px solid var(--line);padding:7px 9px;background:#ffffff}.summary-chip:last-child{border-right:0}.summary-chip.primary{min-width:13rem;background:var(--panel2)}.summary-chip b{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);white-space:nowrap}.summary-chip span{display:block;margin-top:3px;font-size:12px;font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description{display:flex;align-items:center;padding:8px 9px;font-size:12px;font-weight:900;line-height:1.35;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.type-description:before{content:\"\";display:inline-block;width:1.75rem;border-top:2px solid var(--line);margin-right:.65rem;opacity:.7}\n/* v26: header summary tags use real pill/tag styling, matching READY chip language. */\n.compact-main{grid-template-rows:auto 1fr!important;}\n.type-chip-row{display:flex!important;align-items:center!important;gap:6px!important;border-bottom:0!important;padding:7px 9px!important;min-width:0;overflow:hidden;}\n.summary-chip{display:inline-flex!important;align-items:center!important;gap:5px!important;min-height:24px!important;min-width:0!important;max-width:13rem!important;border:1.5px solid var(--line)!important;background:#ffffff!important;padding:4px 8px!important;font-family:var(--mono)!important;font-size:10px!important;font-weight:1000!important;letter-spacing:.08em!important;text-transform:uppercase!important;white-space:nowrap!important;}\n.summary-chip.primary{background:var(--ink)!important;color:var(--panel)!important;border-color:var(--ink)!important;max-width:15rem!important;}\n.summary-chip b{display:inline!important;font-size:10px!important;letter-spacing:.08em!important;color:inherit!important;opacity:.68!important;}\n.summary-chip b:after{content:\":\";}\n.summary-chip span{display:inline!important;margin:0!important;min-width:0!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;font-size:10px!important;color:inherit!important;}\n.type-description{padding:7px 9px!important;}\n.type-chip-row{border-bottom:0!important}.type-description:before{display:none!important}.type-description{padding-top:4px!important}.compact-main{gap:0!important}/* v27: remove header middle separator */</style>\n</head>\n<body class=\"type-compatible state-ready\">\n<main class=\"page\">\n <section class=\"cover\"><div><div class=\"k\">GETTOKENS / ACCOUNT DETAIL / V26 REAL HEADER TAGS</div><h1>Real Header Tags</h1><p>修正配色:从偏米黄工业纸底改为 GetTokens 浅色工作台配色,使用白色主面、浅灰 surface、黑色边框与克制状态色。</p></div><div class=\"brief\">这版专门解决配色不对的问题;保留 v08 的左右请求配置结构。</div></section>\n <div class=\"toolbar\"><div class=\"tabs\"><button class=\"tab\" data-type=\"compatible\" aria-pressed=\"true\">OpenAI-Compatible</button><button class=\"tab\" data-type=\"codex-api\" aria-pressed=\"false\">Codex API Key</button><button class=\"tab\" data-type=\"codex-oauth\" aria-pressed=\"false\">Codex Auth-file</button><button class=\"tab\" data-type=\"token-plan\" aria-pressed=\"false\">Split Credential</button></div><div class=\"states\"><button class=\"state\" data-state=\"ready\" aria-pressed=\"true\">Ready</button><button class=\"state\" data-state=\"dirty\" aria-pressed=\"false\">Dirty</button><button class=\"state\" data-state=\"loading\" aria-pressed=\"false\">Loading</button><button class=\"state\" data-state=\"error\" aria-pressed=\"false\">Error</button></div></div>\n <section class=\"shell\"><div class=\"app\"><article class=\"modal\">\n <header class=\"fixed-head compact-head\">\n <div class=\"compact-account\"><div class=\"account-name type-only t-compatible\">DeepSeek</div><div class=\"account-name type-only t-codex-api\">Codex Stable</div><div class=\"account-name type-only t-codex-oauth\">codex-pro.json</div><div class=\"account-name type-only t-token-plan\">Split Provider</div><span class=\"pill green si-ready state-only\">READY</span><span class=\"pill amber si-dirty state-only\">DIRTY</span><span class=\"pill blue si-loading state-only\">BUSY</span><span class=\"pill red si-error state-only\">ERROR</span></div>\n <div class=\"compact-main\"><div class=\"type-chip-row\"><span class=\"summary-chip primary\"><b>类型</b><span id=\"head-title\">OpenAI-compatible</span></span><span class=\"summary-chip\"><b>凭据</b><span id=\"metric-credential\">API Key</span></span><span class=\"summary-chip\"><b>验证</b><span id=\"metric-verify\">Success</span></span><span class=\"summary-chip\"><b>路由</b><span id=\"metric-route\">Pool Tokyo-01</span></span><span class=\"summary-chip\"><b>余额/额度</b><span id=\"metric-balance\">Configured</span></span></div><div id=\"head-description\" class=\"type-description\">API Key provider · custom headers · model mapping · optional quota/billing scripts</div></div>\n <div class=\"compact-last\"><div class=\"pair\"><b>Last</b><span class=\"hide-loading hide-error\">2026-06-04 16:20</span><span class=\"si-loading state-only\">pending</span><span class=\"si-error state-only red\">—</span></div><div class=\"pair\"><b>Latency</b><span class=\"hide-error\">42ms</span><span class=\"si-error state-only red\">bad</span></div></div>\n </header>\n <div class=\"body\">\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">01</div><h2>身份与凭据</h2></div><div class=\"band-content\">\n <div class=\"fields type-only t-compatible\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"DeepSeek\"></label><label class=\"field span5\"><span class=\"label\">Base URL</span><input class=\"input\" value=\"https://api.deepseek.com/v1\"></label><label class=\"field span4\"><span class=\"label\">API Key</span><input class=\"input\" value=\"sk-••••••••••\"></label></div>\n <div class=\"fields type-only t-codex-api\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"Codex Stable\"></label><label class=\"field span4\"><span class=\"label\">API Key</span><input class=\"input\" value=\"sk-codex-••••••\"></label><label class=\"field span4\"><span class=\"label\">Base URL</span><input class=\"input\" value=\"https://api.openai.com/v1\"></label><label class=\"field span1\"><span class=\"label\">Prefix</span><input class=\"input\" value=\"/v1\"></label></div>\n <div class=\"fields type-only t-codex-oauth\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"codex-pro.json\"></label><label class=\"field span5\"><span class=\"label\">Auth File</span><input class=\"input\" value=\"~/.codex/auth.json\" readonly></label><div class=\"actions span4\"><button class=\"button primary\">Reauth</button><button class=\"button\">Normalize</button><button class=\"button\">Download</button></div></div>\n <div class=\"fields type-only t-token-plan\"><label class=\"field span3\"><span class=\"label\">Display</span><input class=\"input\" value=\"MiMo Split Credential\"></label><label class=\"field span5\"><span class=\"label\">Chat Base URL</span><input class=\"input\" value=\"https://chat-provider.example.com/v1\"></label><label class=\"field span4\"><span class=\"label\">Chat Key</span><input class=\"input\" value=\"chat-••••••••\"></label><label class=\"field span5\"><span class=\"label\">Model Fetch URL</span><input class=\"input\" value=\"https://models-provider.example.com/v1\"></label><label class=\"field span4\"><span class=\"label\">Model Fetch Key</span><input class=\"input\" value=\"sk-••••••••\"></label><div class=\"span3 hint\">模型拉取凭据,仅用于 /models。</div></div>\n </div></section>\n <section class=\"band hide-codex-oauth\"><div class=\"band-index\"><div class=\"num\">02</div><h2>请求配置</h2></div><div class=\"band-content request-config request-split\">\n <div class=\"request-left\">\n <div class=\"fields\">\n <label class=\"field span12\"><span class=\"label\">Custom Headers</span><textarea class=\"textarea\" data-autosize=\"true\" rows=\"2\">HTTP-Referer: https://gettokens.local\nX-Title: GetTokens Workbench</textarea></label>\n <div class=\"span12 hint\">这里只保留当前支持的 headersText 配置;计数与路由不作为可编辑字段展示。</div>\n </div>\n </div>\n <div class=\"request-right\">\n <div class=\"fields\">\n <label class=\"field span12\"><span class=\"label\">Proxy Pool Node</span><select class=\"input hide-error\"><option>Tokyo-01 · socks5 · 127.0.0.1:7890 · 42ms</option><option>Singapore-02 · http · 10.0.0.2:8080 · 58ms</option></select><select class=\"input si-error state-only\"><option>无可用代理池节点</option></select></label>\n <label class=\"field span12\"><span class=\"label\">Resolved URL</span><input class=\"input hide-error\" value=\"socks5://127.0.0.1:7890\" readonly><input class=\"input si-error state-only\" value=\"—\" readonly></label>\n <div class=\"span12 hint\">账号详情只选择已保存的代理池节点;刷新、测速、增删节点在代理池页面完成。</div>\n </div>\n </div>\n </div></section>\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">03</div><h2>模型</h2></div><div class=\"band-content model-section\">\n <div class=\"model-toolbar\"><span class=\"hint\">模型是映射关系:Source Model → Alias / Route。</span><div class=\"actions\"><button class=\"button hide-codex-oauth\">拉取远端模型</button><button class=\"button primary hide-codex-oauth\">使用远端模型</button><button class=\"button hide-codex-oauth\">新增映射</button><button class=\"button type-only t-codex-oauth\">刷新模型目录</button></div></div>\n <div class=\"model-grid mapping-grid\">\n <div class=\"mapping-item type-only t-compatible\"><div class=\"mapping-side\"><span>Source Model</span><b>deepseek-v4-pro</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>DeepSeek V4 Pro</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-compatible\"><div class=\"mapping-side\"><span>Source Model</span><b>deepseek-v4-flash</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>DeepSeek Flash</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-codex-api\"><div class=\"mapping-side\"><span>Source Model</span><b>gpt-5.4-mini</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Verify Alias</span><b>fast verify</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-codex-api\"><div class=\"mapping-side\"><span>Source Model</span><b>gpt-5.4</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Verify Alias</span><b>stable</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-codex-oauth readonly\"><div class=\"mapping-side\"><span>Auth-file Model</span><b>gpt-5.5</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Route</span><b>oauth available</b></div><span class=\"pill violet\">只读</span></div>\n <div class=\"mapping-item type-only t-codex-oauth readonly\"><div class=\"mapping-side\"><span>Auth-file Model</span><b>codex-mini-latest</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Route</span><b>codex</b></div><span class=\"pill violet\">只读</span></div>\n <div class=\"mapping-item type-only t-token-plan\"><div class=\"mapping-side\"><span>Source Model</span><b>provider-main-model</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>Primary Model</b></div><button class=\"button danger\">删除</button></div>\n <div class=\"mapping-item type-only t-token-plan\"><div class=\"mapping-side\"><span>Source Model</span><b>provider-fast-model</b></div><div class=\"mapping-arrow\">→</div><div class=\"mapping-side\"><span>Alias / Route</span><b>Fast Model</b></div><button class=\"button danger\">删除</button></div>\n </div>\n </div></section>\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">04</div><h2>验证</h2></div><div class=\"band-content\"><div class=\"verify-row verify-row-simple\"><div class=\"fields\"><label class=\"field span12 hide-codex-oauth\"><span class=\"label\">测试模型</span><input class=\"input type-only t-compatible\" value=\"deepseek-v4-pro\"><input class=\"input type-only t-codex-api\" value=\"gpt-5.4-mini\"><input class=\"input type-only t-token-plan\" value=\"provider-main-model\"></label><div class=\"actions span12 type-only t-codex-oauth\"><button class=\"button primary\">Reauth</button><button class=\"button\">刷新模型</button></div></div><div class=\"actions verify-actions\"><button class=\"button primary\"><span class=\"hide-loading\">执行验证</span><span class=\"si-loading state-only\">验证中</span></button></div></div></div></section>\n <section class=\"band\"><div class=\"band-index\"><div class=\"num\">05</div><h2>余额与额度</h2></div><div class=\"band-content quota-billing balance-card-mode\">\n <article class=\"balance-card quota-card\">\n <header class=\"balance-card-head\">\n <div><span class=\"label\">Quota / 额度</span><strong class=\"hide-loading hide-error\">72% remaining</strong><strong class=\"si-loading state-only blue\">testing...</strong><strong class=\"si-error state-only red\">test failed</strong></div>\n <div class=\"actions\"><button class=\"button hide-codex-oauth\">配置</button><button class=\"button primary hide-codex-oauth\"><span class=\"hide-loading\">测试</span><span class=\"si-loading state-only\">测试中</span></button><button class=\"button type-only t-codex-oauth\">查看 Provider</button></div>\n </header>\n <div class=\"balance-stats\">\n <div><b>Remaining</b><span>720K tokens</span></div>\n <div><b>Percent</b><span>72%</span></div>\n <div><b>Reset</b><span>18:00</span></div>\n </div>\n <div class=\"balance-log\"><b>Last Quota Test</b><span class=\"hide-loading hide-error\">success · script returned remaining quota</span><span class=\"si-loading state-only\">request pending, keep last result visible</span><span class=\"si-error state-only red\">HTTP 403 · check quota script / credential</span></div>\n </article>\n <article class=\"balance-card billing-card\">\n <header class=\"balance-card-head\">\n <div><span class=\"label\">Billing / 余额</span><strong class=\"hide-codex-oauth hide-loading hide-error\">120.00 USD</strong><strong class=\"type-only t-codex-oauth\">provider managed</strong><strong class=\"hide-codex-oauth si-loading state-only blue\">testing...</strong><strong class=\"hide-codex-oauth si-error state-only red\">test failed</strong></div>\n <div class=\"actions\"><button class=\"button hide-codex-oauth\">配置</button><button class=\"button primary hide-codex-oauth\">测试</button><button class=\"button type-only t-codex-oauth\">查看 Provider</button></div>\n </header>\n <div class=\"balance-stats\">\n <div><b>Total</b><span class=\"hide-codex-oauth\">120.00 USD</span><span class=\"type-only t-codex-oauth\">—</span></div>\n <div><b>Granted</b><span class=\"hide-codex-oauth\">80.00 USD</span><span class=\"type-only t-codex-oauth\">provider</span></div>\n <div><b>Topped</b><span class=\"hide-codex-oauth\">40.00 USD</span><span class=\"type-only t-codex-oauth\">managed</span></div>\n </div>\n <div class=\"balance-log\"><b>Last Billing Test</b><span class=\"hide-codex-oauth hide-loading hide-error\">success · granted 80 USD · topped up 40 USD</span><span class=\"hide-codex-oauth si-loading state-only\">request pending</span><span class=\"hide-codex-oauth si-error state-only red\">empty response or parse failed</span><span class=\"type-only t-codex-oauth\">managed by provider / OAuth account</span></div>\n </article>\n </div></section>\n </div>\n <footer class=\"footer\"><div class=\"footer-msg\"><span class=\"pill green si-ready state-only\">READY</span><span class=\"pill amber si-dirty state-only\">DIRTY</span><span class=\"pill blue si-loading state-only\">BUSY</span><span class=\"pill red si-error state-only\">BLOCKED</span><div><strong class=\"si-ready state-only\">可保存或关闭</strong><strong class=\"si-dirty state-only\">存在未保存更改</strong><strong class=\"si-loading state-only\">请求处理中</strong><strong class=\"si-error state-only\">保存被阻断</strong><span class=\"si-ready state-only\">当前账号详情状态正常。</span><span class=\"si-dirty state-only\">保存后写入 Wails/sidecar。</span><span class=\"si-loading state-only\">不清空已有信息。</span><span class=\"si-error state-only\">修复 credential/proxy/reauth 错误后才能保存。</span></div></div><div class=\"actions\"><button class=\"button primary hide-error\">保存更改</button><button class=\"button si-error state-only\" disabled>保存不可用</button><button class=\"button\">放弃操作</button></div></footer>\n </article></div></section>\n <div class=\"note\"><b>v16 请求配置字段收窄:</b>移除不支持的派生/运行态字段,只保留 Custom Headers 与代理池选择。<br><b>v15 顶部关闭修正:</b>已移除头部重复关闭入口,关闭/放弃操作只保留在底部命令栏。<br><b>v14 模型列表修正:</b>模型列表改为两列,避免模型多时单列表格过高。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v20 余额额度按钮文案修正:</b>Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v19 余额额度卡片修正:</b>余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。<br><b>v18 头部精简修正:</b>头部改为 74px 紧凑业务摘要,移除 detail/frame/actions 等实现文本。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v20 余额额度按钮文案修正:</b>Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。<br><b>v27 头部分割线移除:</b>移除标签组与描述之间的中间分割线。<br><b>v26 头部标签样式修正:</b>头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。<br><b>v25 头部类型摘要整理:</b>头部中区改为上方标签组、下方文字描述并用分割线区分。<br><b>v24 模型映射错位修正:</b>修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。<br><b>v23 账号类型与模型映射整理:</b>头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。<br><b>v22 Headers 高度自适应:</b>Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。<br><b>v21 代理功能收窄:</b>账号详情只支持选择已保存代理池节点,不提供刷新/测速。<br><b>v19 余额额度卡片修正:</b>余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。<br><b>v17 余额额度分栏修正:</b>余额与额度改为左右两栏,中间明确分割线;Quota 与 Billing 各自包含配置、测试和结果。<br><b>v13 余额额度修正:</b>余额/额度支持测试,测试结果在本模块内显示。<br><b>v12 验证区修正:</b>已移除不存在的 限流/路由状态 摘要,仅保留测试模型与执行验证动作。<br><b>v09 配色修正:</b>当前文件已改为 GetTokens 浅色工作台配色:白色主面、浅灰 surface、黑色边框、灰色弱文本、状态色只用于状态。保留 v08 的左右请求配置结构。</div>\n</main>\n<script>\nfunction autosizeTextareas(){document.querySelectorAll('textarea[data-autosize]').forEach((el)=>{el.style.height='auto';el.style.height=`${el.scrollHeight}px`;});}\ndocument.addEventListener('input',(event)=>{if(event.target?.matches?.('textarea[data-autosize]')) autosizeTextareas();});\nwindow.addEventListener('load', autosizeTextareas);\nconst body=document.body,tabs=[...document.querySelectorAll('.tab')],states=[...document.querySelectorAll('.state')],headTitle=document.querySelector('#head-title'),headDescription=document.querySelector('#head-description'),metricCredential=document.querySelector('#metric-credential'),metricVerify=document.querySelector('#metric-verify'),metricRoute=document.querySelector('#metric-route'),metricBalance=document.querySelector('#metric-balance');let type='compatible',state='ready';const typeMeta={compatible:{title:'OpenAI-compatible',credential:'API Key',balance:'Configured',desc:'API Key provider · custom headers · model mapping · optional quota/billing scripts'},'codex-api':{title:'Codex API Key',credential:'Codex Key',balance:'Configured',desc:'Codex API key account · prefix · model verification · quota/billing curl scripts'},'codex-oauth':{title:'Codex Auth-file / OAuth',credential:'Auth File',balance:'Provider',desc:'OAuth auth-file account · reauth · read-only provider managed quota'},'token-plan':{title:'Split Credential',credential:'chat + fetch key',balance:'Configured',desc:'Separate chat credential and model-fetch credential for one provider'}};const stateMeta={ready:{verify:'Success',route:'Pool Tokyo-01',routeTone:''},dirty:{verify:'Draft',route:'Pool Tokyo-01',routeTone:''},loading:{verify:'Running',route:'Checking…',routeTone:''},error:{verify:'Failed',route:'Proxy Bad',routeTone:'red'}};function renderHeadSummary(){const t=typeMeta[type],s=stateMeta[state];if(!t||!s)return;headTitle.textContent=t.title;if(headDescription)headDescription.textContent=t.desc;metricCredential.textContent=t.credential;metricBalance.textContent=t.balance;metricVerify.textContent=s.verify;metricRoute.textContent=s.route;metricRoute.className=s.routeTone?`red`:''}function render(){body.className=`type-${type} state-${state}`;tabs.forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.type===type)));states.forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.state===state)));renderHeadSummary()}tabs.forEach(b=>b.onclick=()=>{type=b.dataset.type;render()});states.forEach(b=>b.onclick=()=>{state=b.dataset.state;render()});render();</script>\n</body>\n</html>\n"}}, "frontend/src/features/accounts/tests/openAICompatible.test.mjs": {"6a6556121411b7445b1e0e28d0b7af6920236b59": {"t": 1785746583.2410576, "n": 19361, "h": "a7e277abe9187163", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /import \\{ Button \\} from 'antd'/);\n assert.match(source, /<Button/);\n assert.match(source, /danger/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-(main|surface)\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('vendor logo mark uses quiet provider badge tokens', async () => {\n const source = await readFile(new URL('../components/VendorLogoMark.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /data-provider-logo=\\{logo\\.kind\\}/);\n assert.match(source, /--vendor-logo-color/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-surface-muted/);\n assert.doesNotMatch(source, /--gt-border-strong/);\n assert.doesNotMatch(source, /--text-on-accent|--bg-(main|surface|subtle|warning)/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /import \\{ Button, Input, Select \\} from 'antd'/);\n assert.match(source, /<Button/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-(main|surface)\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[/);\n});\n\ntest('openai compatible workspace uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleWorkspace.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleWorkspaceShellClass =/);\n assert.match(source, /const openAICompatibleWorkspaceActionButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspacePrimaryButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspaceStateClass =/);\n assert.match(source, /data-openai-compatible-workspace=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-actions=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-state/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.doesNotMatch(source, /btn-swiss/);\n assert.doesNotMatch(source, /border-2/);\n assert.doesNotMatch(source, /border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n assert.doesNotMatch(source, /tracking-\\[0\\.2em\\]|tracking-tight/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "c6947be708bbefbd7e0b3d1b8ca9dcfb91a27dc7": {"t": 1785759459.188196, "n": 19361, "h": "a7e277abe9187163", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /import \\{ Button \\} from 'antd'/);\n assert.match(source, /<Button/);\n assert.match(source, /danger/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-(main|surface)\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('vendor logo mark uses quiet provider badge tokens', async () => {\n const source = await readFile(new URL('../components/VendorLogoMark.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /data-provider-logo=\\{logo\\.kind\\}/);\n assert.match(source, /--vendor-logo-color/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-surface-muted/);\n assert.doesNotMatch(source, /--gt-border-strong/);\n assert.doesNotMatch(source, /--text-on-accent|--bg-(main|surface|subtle|warning)/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /import \\{ Button, Input, Select \\} from 'antd'/);\n assert.match(source, /<Button/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-(main|surface)\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[/);\n});\n\ntest('openai compatible workspace uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleWorkspace.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleWorkspaceShellClass =/);\n assert.match(source, /const openAICompatibleWorkspaceActionButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspacePrimaryButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspaceStateClass =/);\n assert.match(source, /data-openai-compatible-workspace=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-actions=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-state/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.doesNotMatch(source, /btn-swiss/);\n assert.doesNotMatch(source, /border-2/);\n assert.doesNotMatch(source, /border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n assert.doesNotMatch(source, /tracking-\\[0\\.2em\\]|tracking-tight/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "6a21f70e078cb17c1429efae024e236c5601e993": {"t": 1785759460.6702409, "n": 19350, "h": "78cb946af5e0d5ac", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /const openAICompatibleProviderCardButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardDangerButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-(main|surface)\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('vendor logo mark uses quiet provider badge tokens', async () => {\n const source = await readFile(new URL('../components/VendorLogoMark.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /data-provider-logo=\\{logo\\.kind\\}/);\n assert.match(source, /--vendor-logo-color/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-surface-muted/);\n assert.doesNotMatch(source, /--gt-border-strong/);\n assert.doesNotMatch(source, /--text-on-accent|--bg-(main|surface|subtle|warning)/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /const openAICompatibleComposeButtonClass =/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-(main|surface)\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[/);\n});\n\ntest('openai compatible workspace uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleWorkspace.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleWorkspaceShellClass =/);\n assert.match(source, /const openAICompatibleWorkspaceActionButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspacePrimaryButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspaceStateClass =/);\n assert.match(source, /data-openai-compatible-workspace=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-actions=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-state/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.doesNotMatch(source, /btn-swiss/);\n assert.doesNotMatch(source, /border-2/);\n assert.doesNotMatch(source, /border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /font-(?:medium|bold|extrabold|black)/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n assert.doesNotMatch(source, /tracking-\\[0\\.2em\\]|tracking-tight/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "8181cf8889e9a74473a77e2a7b84281361bd4466": {"t": 1785759462.6558673, "n": 19246, "h": "b28ed0216d38a2be", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /const openAICompatibleProviderCardButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardDangerButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-(main|surface)\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-black/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('vendor logo mark uses quiet provider badge tokens', async () => {\n const source = await readFile(new URL('../components/VendorLogoMark.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /data-provider-logo=\\{logo\\.kind\\}/);\n assert.match(source, /--vendor-logo-color/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-surface-muted/);\n assert.doesNotMatch(source, /--gt-border-strong/);\n assert.doesNotMatch(source, /--text-on-accent|--bg-(main|surface|subtle|warning)/);\n assert.doesNotMatch(source, /font-black/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /const openAICompatibleComposeButtonClass =/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-(main|surface)\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-black|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[/);\n});\n\ntest('openai compatible workspace uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleWorkspace.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleWorkspaceShellClass =/);\n assert.match(source, /const openAICompatibleWorkspaceActionButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspacePrimaryButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspaceStateClass =/);\n assert.match(source, /data-openai-compatible-workspace=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-actions=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-state/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.doesNotMatch(source, /btn-swiss/);\n assert.doesNotMatch(source, /border-2/);\n assert.doesNotMatch(source, /border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /font-black/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n assert.doesNotMatch(source, /tracking-\\[0\\.2em\\]|tracking-tight/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "a3ba052e3b29ea3c107af69ec83e3df6240e13a7": {"t": 1785759463.9359365, "n": 19180, "h": "853487eaff245daa", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /const openAICompatibleProviderCardButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardDangerButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-main\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-black/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('vendor logo mark uses quiet provider badge tokens', async () => {\n const source = await readFile(new URL('../components/VendorLogoMark.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /data-provider-logo=\\{logo\\.kind\\}/);\n assert.match(source, /--vendor-logo-color/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-surface-muted/);\n assert.doesNotMatch(source, /--border-color/);\n assert.doesNotMatch(source, /--bg-main/);\n assert.doesNotMatch(source, /font-black/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /const openAICompatibleComposeButtonClass =/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-main\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-black|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[/);\n});\n\ntest('openai compatible workspace uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleWorkspace.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleWorkspaceShellClass =/);\n assert.match(source, /const openAICompatibleWorkspaceActionButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspacePrimaryButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspaceStateClass =/);\n assert.match(source, /data-openai-compatible-workspace=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-actions=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-state/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.doesNotMatch(source, /btn-swiss/);\n assert.doesNotMatch(source, /border-2/);\n assert.doesNotMatch(source, /border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /font-black/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n assert.doesNotMatch(source, /tracking-\\[0\\.2em\\]|tracking-tight/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "da2e383e422da7ea4a12dc62be707a51d5267d91": {"t": 1785759465.9539, "n": 19209, "h": "9c84bc0fbf17853b", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /const openAICompatibleProviderCardButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardDangerButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-main\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-black/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('vendor logo mark uses quiet provider badge tokens', async () => {\n const source = await readFile(new URL('../components/VendorLogoMark.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /data-provider-logo=\\{logo\\.kind\\}/);\n assert.match(source, /--vendor-logo-color/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-surface-muted/);\n assert.doesNotMatch(source, /--border-color/);\n assert.doesNotMatch(source, /--bg-main/);\n assert.doesNotMatch(source, /font-black/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /const openAICompatibleComposeButtonClass =/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-main\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-black|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[0\\.16em\\]|tracking-\\[0\\.2em\\]/);\n});\n\ntest('openai compatible workspace uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleWorkspace.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleWorkspaceShellClass =/);\n assert.match(source, /const openAICompatibleWorkspaceActionButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspacePrimaryButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspaceStateClass =/);\n assert.match(source, /data-openai-compatible-workspace=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-actions=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-state/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.doesNotMatch(source, /btn-swiss/);\n assert.doesNotMatch(source, /border-2/);\n assert.doesNotMatch(source, /border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /font-black/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n assert.doesNotMatch(source, /tracking-\\[0\\.2em\\]|tracking-tight/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "5235e38a66e96b50d2c5e5937921d38a63d253cb": {"t": 1785759467.2776673, "n": 18642, "h": "484abf59875cc36b", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /const openAICompatibleProviderCardButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardDangerButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-main\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-black/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /const openAICompatibleComposeButtonClass =/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-main\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-black|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[0\\.16em\\]|tracking-\\[0\\.2em\\]/);\n});\n\ntest('openai compatible workspace uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleWorkspace.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleWorkspaceShellClass =/);\n assert.match(source, /const openAICompatibleWorkspaceActionButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspacePrimaryButtonClass =/);\n assert.match(source, /const openAICompatibleWorkspaceStateClass =/);\n assert.match(source, /data-openai-compatible-workspace=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-actions=\"quiet\"/);\n assert.match(source, /data-openai-compatible-workspace-state/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.doesNotMatch(source, /btn-swiss/);\n assert.doesNotMatch(source, /border-2/);\n assert.doesNotMatch(source, /border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /font-black/);\n assert.doesNotMatch(source, /\\buppercase\\b/);\n assert.doesNotMatch(source, /tracking-\\[0\\.2em\\]|tracking-tight/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "35c925739b66737897394aad0a48088c6c8f71c5": {"t": 1785759469.0263186, "n": 17435, "h": "77ef77d648f8ca98", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /const openAICompatibleProviderCardButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardDangerButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-main\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-black/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('openai compatible compose modal uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleComposeModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /const openAICompatibleComposeOverlayClass =/);\n assert.match(source, /const openAICompatibleComposePanelClass =/);\n assert.match(source, /const openAICompatibleComposeHeaderClass =/);\n assert.match(source, /const openAICompatibleComposeInputClass =/);\n assert.match(source, /const openAICompatibleComposeButtonClass =/);\n assert.match(source, /const openAICompatibleComposeErrorClass =/);\n assert.match(source, /data-openai-compatible-compose-modal/);\n assert.match(source, /data-openai-compatible-compose-header/);\n assert.match(source, /data-openai-compatible-compose-body/);\n assert.match(source, /data-openai-compatible-compose-error/);\n assert.match(source, /data-openai-compatible-compose-footer/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(source, /btn-swiss|input-swiss|select-swiss|card-swiss/);\n assert.doesNotMatch(source, /border-2|border-t-2|border-b-2|border-dashed/);\n assert.doesNotMatch(source, /bg-\\[var\\(--bg-main\\)\\]|bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(source, /color-status-/);\n assert.doesNotMatch(source, /font-black|\\buppercase\\b|shadow-hard|shadow-\\[/);\n assert.doesNotMatch(source, /tracking-\\[0\\.16em\\]|tracking-\\[0\\.2em\\]/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "39c348814ca765168d34bd2f733cb17f0b58e105": {"t": 1785759471.203483, "n": 15855, "h": "88f5ec293f324e6f", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\nfunction sourceBlock(source, startMarker, endMarker) {\n const start = source.indexOf(startMarker);\n assert.notEqual(start, -1, `missing start marker: ${startMarker}`);\n if (!endMarker) {\n return source.slice(start);\n }\n const end = source.indexOf(endMarker, start + startMarker.length);\n assert.notEqual(end, -1, `missing end marker: ${endMarker}`);\n return source.slice(start, end);\n}\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('openai compatible provider card uses the quiet workspace shell', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleProviderCard.tsx', import.meta.url), 'utf8');\n const targetSource = [\n sourceBlock(source, 'export default function OpenAICompatibleProviderCard', 'function openAICompatibleProviderIdentity'),\n sourceBlock(source, 'function RegionHead', 'function MetricPanel'),\n sourceBlock(source, 'function MetricPanel', null),\n ].join('\\n');\n\n assert.match(source, /const openAICompatibleProviderCardBodyClass =/);\n assert.match(source, /const openAICompatibleProviderCardPanelClass =/);\n assert.match(source, /const openAICompatibleProviderCardButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardDangerButtonClass =/);\n assert.match(source, /const openAICompatibleProviderCardStatusClass =/);\n assert.match(targetSource, /data-openai-compatible-provider-card-body/);\n assert.match(targetSource, /data-openai-compatible-provider-card-models/);\n assert.match(targetSource, /data-openai-compatible-provider-card-actions/);\n assert.match(source, /--gt-surface-canvas/);\n assert.match(source, /--gt-surface-muted/);\n assert.match(source, /--gt-border-subtle/);\n assert.match(source, /--gt-status-success/);\n assert.match(source, /--gt-status-danger/);\n assert.doesNotMatch(targetSource, /btn-swiss/);\n assert.doesNotMatch(targetSource, /border-2|border-t-2|border-b-2/);\n assert.doesNotMatch(targetSource, /border-dashed/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-main\\)\\]/);\n assert.doesNotMatch(targetSource, /bg-\\[var\\(--bg-surface\\)\\]/);\n assert.doesNotMatch(targetSource, /color-status-/);\n assert.doesNotMatch(targetSource, /font-black/);\n assert.doesNotMatch(targetSource, /uppercase/);\n assert.doesNotMatch(targetSource, /tracking-\\[0\\.04em\\]|tracking-\\[0\\.06em\\]|tracking-\\[0\\.08em\\]|tracking-\\[0\\.16em\\]/);\n assert.doesNotMatch(targetSource, /shadow-\\[/);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "4449dfacd0a046b65312af8b51abdc1dfe8d5552": {"t": 1785759472.9405243, "n": 13481, "h": "65471fd4702158b0", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5.2[1m]', alias: 'GLM-5.2 1M' },\n { name: 'glm-5.2', alias: 'GLM-5.2' },\n { name: 'glm-4.5-air', alias: 'GLM-4.5-Air' },\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-pro[1m]', alias: '' },\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "132e6e95300ff6b86f74412b723ac9feeb4aa7dc": {"t": 1785759474.7669854, "n": 13268, "h": "9ec4c02dcd5873a7", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\nimport {\n getFormatBaseUrl,\n getVendorPreset,\n} from '../model/vendorPresets.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('legacy openai-compatible preset entries for relay vendors are derived from unified vendor presets', () => {\n for (const id of ['sub2api', 'new-api']) {\n const vendorPreset = getVendorPreset(id);\n const legacyPreset = getOpenAICompatibleProviderPreset(id);\n assert.ok(vendorPreset);\n assert.ok(legacyPreset);\n assert.equal(legacyPreset.label, vendorPreset.name);\n assert.equal(legacyPreset.baseUrl, getFormatBaseUrl(vendorPreset, 'openai_chat'));\n assert.deepEqual(\n legacyPreset.models.map((item) => item.name),\n vendorPreset.modelSuggestions,\n );\n }\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "73c17b20ebe17d46f871845562a3bf04c04e27f7": {"t": 1785759476.2822428, "n": 12586, "h": "dc24c11193c4d21f", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible provider detail opens through the unified account detail page', async () => {\n const source = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /function findOpenAICompatibleAccountForProvider/);\n assert.match(source, /setSelectedAccount\\(providerAccount\\)/);\n assert.match(source, /markAccountDetailInHash\\(providerAccount\\.id\\)/);\n assert.doesNotMatch(source, /OpenAICompatibleDetailModal/);\n assert.doesNotMatch(source, /openAICompatibleState\\.openDetailModal/);\n});\n\ntest('legacy openai compatible detail components are not part of the production detail route', async () => {\n const featureSource = await readFile(new URL('../AccountsFeature.tsx', import.meta.url), 'utf8');\n\n assert.equal((featureSource.match(/<UnifiedAccountDetailModal/g) || []).length, 1);\n assert.doesNotMatch(featureSource, /<OpenAICompatibleDetailModal/);\n});\n"}, "93312b7c2fdcb712fbf215c699705ff9aab63563": {"t": 1785759477.6491802, "n": 13035, "h": "ee8d1cc326deab34", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n quotaCurl: 'curl https://api.deepseek.com/user/balance',\n quotaEnabled: true,\n billingCurl: 'curl https://api.deepseek.com/user/balance',\n billingEnabled: true,\n platformCookie: 'service=abc',\n curlVariables: { region: 'cn' },\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible detail uses a single-column form stack inside the modal', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /<AccountDetailModuleStack layout=\"cards\" cardColumns=\\{1\\}>/);\n});\n\ntest('openai compatible model rows avoid narrow vertical delete buttons', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /md:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]/);\n assert.match(source, /xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)_auto\\]/);\n assert.match(source, /whitespace-nowrap/);\n assert.match(source, /md:col-span-2 md:justify-self-start xl:col-span-1 xl:justify-self-end/);\n});\n\ntest('openai compatible detail does not mount runtime evidence overview', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailModal.tsx', import.meta.url), 'utf8');\n\n assert.doesNotMatch(source, /AccountRuntimeEvidenceSection/);\n assert.doesNotMatch(source, /usageSummary=\\{usageSummary\\}/);\n assert.doesNotMatch(source, /buildOpenAICompatibleEvidenceRows/);\n assert.doesNotMatch(source, /AccountRuntimeSnapshotSection|AccountEvidenceSection/);\n assert.doesNotMatch(source, /lg:grid-cols-1 2xl:grid-cols/);\n});\n"}, "16e21d1a2d22d9d061cd606a32714cd4c3dd8c5b": {"t": 1785759480.2552505, "n": 12507, "h": "456826ca35b5db55", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n modelFetchApiKey: '',\n modelFetchBaseUrl: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible detail uses a single-column form stack inside the modal', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /<AccountDetailModuleStack layout=\"cards\" cardColumns=\\{1\\}>/);\n});\n\ntest('openai compatible model rows avoid narrow vertical delete buttons', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /md:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]/);\n assert.match(source, /xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)_auto\\]/);\n assert.match(source, /whitespace-nowrap/);\n assert.match(source, /md:col-span-2 md:justify-self-start xl:col-span-1 xl:justify-self-end/);\n});\n\ntest('openai compatible detail does not mount runtime evidence overview', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailModal.tsx', import.meta.url), 'utf8');\n\n assert.doesNotMatch(source, /AccountRuntimeEvidenceSection/);\n assert.doesNotMatch(source, /usageSummary=\\{usageSummary\\}/);\n assert.doesNotMatch(source, /buildOpenAICompatibleEvidenceRows/);\n assert.doesNotMatch(source, /AccountRuntimeSnapshotSection|AccountEvidenceSection/);\n assert.doesNotMatch(source, /lg:grid-cols-1 2xl:grid-cols/);\n});\n"}, "2b674ae0b8b420db3b8ea017523e1af68281694a": {"t": 1785759503.3512096, "n": 12233, "h": "067b1b57b0da2dc8", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-v4-flash', alias: '' },\n { name: 'deepseek-v4-pro', alias: '' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible detail uses a single-column form stack inside the modal', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /<AccountDetailModuleStack layout=\"cards\" cardColumns=\\{1\\}>/);\n});\n\ntest('openai compatible model rows avoid narrow vertical delete buttons', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /md:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]/);\n assert.match(source, /xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)_auto\\]/);\n assert.match(source, /whitespace-nowrap/);\n assert.match(source, /md:col-span-2 md:justify-self-start xl:col-span-1 xl:justify-self-end/);\n});\n\ntest('openai compatible overview waits for wider viewports before splitting runtime and evidence', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /className=\"lg:grid-cols-1 2xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]\"/);\n});\n"}, "7e718a9c4450bc011247d4e5bc9e79ee42757563": {"t": 1785759504.78875, "n": 12470, "h": "16f367d0ebf44235", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible detail uses a single-column form stack inside the modal', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /<AccountDetailModuleStack layout=\"cards\" cardColumns=\\{1\\}>/);\n});\n\ntest('openai compatible model rows avoid narrow vertical delete buttons', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /md:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]/);\n assert.match(source, /xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)_auto\\]/);\n assert.match(source, /whitespace-nowrap/);\n assert.match(source, /md:col-span-2 md:justify-self-start xl:col-span-1 xl:justify-self-end/);\n});\n\ntest('openai compatible detail does not mount runtime evidence overview', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailModal.tsx', import.meta.url), 'utf8');\n\n assert.doesNotMatch(source, /AccountRuntimeEvidenceSection/);\n assert.doesNotMatch(source, /usageSummary=\\{usageSummary\\}/);\n assert.doesNotMatch(source, /buildOpenAICompatibleEvidenceRows/);\n assert.doesNotMatch(source, /AccountRuntimeSnapshotSection|AccountEvidenceSection/);\n assert.doesNotMatch(source, /lg:grid-cols-1 2xl:grid-cols/);\n});\n"}, "fe7516a8237861c21628286ba047ae394347aefc": {"t": 1785759506.2240353, "n": 12496, "h": "c515827c8c0e4c36", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible detail uses a single-column form stack inside the modal', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /<AccountDetailModuleStack layout=\"cards\" cardColumns=\\{1\\}>/);\n});\n\ntest('openai compatible model rows avoid narrow vertical delete buttons', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /md:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]/);\n assert.match(source, /xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)_auto\\]/);\n assert.match(source, /whitespace-nowrap/);\n assert.match(source, /md:col-span-2 md:justify-self-start xl:col-span-1 xl:justify-self-end/);\n});\n\ntest('openai compatible overview renders merged runtime and evidence module', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /<AccountRuntimeEvidenceSection/);\n assert.match(source, /usageSummary=\\{usageSummary\\}/);\n assert.match(source, /evidenceRows=\\{buildOpenAICompatibleEvidenceRows\\(t, draft, verifyState\\)\\}/);\n assert.doesNotMatch(source, /AccountRuntimeSnapshotSection|AccountEvidenceSection/);\n assert.doesNotMatch(source, /lg:grid-cols-1 2xl:grid-cols/);\n});\n"}, "d46daa7035dd9103740522d87f4bbe28db075585": {"t": 1785759507.6982207, "n": 12253, "h": "2e787854b3adc437", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n\ntest('openai compatible detail uses a single-column form stack inside the modal', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /<AccountDetailModuleStack layout=\"cards\" cardColumns=\\{1\\}>/);\n});\n\ntest('openai compatible model rows avoid narrow vertical delete buttons', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailPanel.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /md:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]/);\n assert.match(source, /xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)_auto\\]/);\n assert.match(source, /whitespace-nowrap/);\n assert.match(source, /md:col-span-2 md:justify-self-start xl:col-span-1 xl:justify-self-end/);\n});\n\ntest('openai compatible overview waits for wider viewports before splitting runtime and evidence', async () => {\n const source = await readFile(new URL('../components/OpenAICompatibleDetailModal.tsx', import.meta.url), 'utf8');\n\n assert.match(source, /className=\"lg:grid-cols-1 2xl:grid-cols-\\[minmax\\(0,1fr\\)_minmax\\(0,1fr\\)\\]\"/);\n});\n"}, "341b2061343bdc79cccd8359afd2502677634480": {"t": 1785759509.5912201, "n": 11055, "h": "836e7d8324540cd7", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n"}, "8057be7da252981140fe0861cdd8f8fab297e49d": {"t": 1785759511.3462276, "n": 11255, "h": "34b56802e1e65eb5", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n buildOpenAICompatibleCardEvidenceRows,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.deepEqual(\n buildOpenAICompatibleCardEvidenceRows(t, provider, verifyState, 2).map((row) => row.label),\n ['账号 ID', '数据源', '模型', '最近验证时间'],\n );\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n proxyUrl: 'http://proxy.local',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n proxyUrl: 'http://proxy.local',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n"}, "f9f8c1d7cf4d24c555de1e7aa6ca5dd652be3463": {"t": 1785759512.5284214, "n": 11177, "h": "ec4a2b1fcab5c4b2", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\nimport {\n buildOpenAICompatibleCardBadges,\n buildOpenAICompatibleCardEvidenceRows,\n resolveOpenAICompatibleCardEyebrow,\n resolveOpenAICompatibleCardTone,\n resolveOpenAICompatibleVerifyMessage,\n} from '../model/openAICompatibleCard.ts';\n\nconst t = (key) =>\n ({\n 'accounts.ui_openai_compatible_badge': '兼容 OpenAI',\n 'accounts.rotation_disabled_badge': '已禁用',\n 'accounts.openai_provider_test_success': '账号验证成功',\n 'accounts.openai_provider_test_failed': '账号验证失败',\n 'accounts.openai_provider_test_idle': '尚未执行验证',\n 'accounts.card_asset': '账号 ID',\n 'accounts.card_source_type': '数据源',\n 'accounts.ui_models': '模型',\n 'accounts.openai_provider_last_verified': '最近验证时间',\n })[key] || key;\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('openai-compatible card helpers resolve shared top-region metadata', () => {\n const provider = {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n disabled: false,\n };\n const verifyState = {\n model: 'deepseek-chat',\n status: 'success',\n message: '',\n lastVerifiedAt: 1747288800000,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'positive');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '账号验证成功');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [{ label: '兼容 OpenAI' }]);\n assert.deepEqual(\n buildOpenAICompatibleCardEvidenceRows(t, provider, verifyState, 2).map((row) => row.label),\n ['账号 ID', '数据源', '模型', '最近验证时间'],\n );\n assert.equal(resolveOpenAICompatibleVerifyMessage(t, verifyState), '尚未执行验证');\n});\n\ntest('openai-compatible card helpers expose disabled warning state', () => {\n const provider = {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n disabled: true,\n };\n const verifyState = {\n model: '',\n status: 'idle',\n message: '',\n lastVerifiedAt: null,\n };\n\n assert.equal(resolveOpenAICompatibleCardTone(provider, verifyState), 'warning');\n assert.equal(resolveOpenAICompatibleCardEyebrow(t, provider, verifyState), '已禁用');\n assert.deepEqual(buildOpenAICompatibleCardBadges(t, provider), [\n { label: '兼容 OpenAI' },\n { label: '已禁用', tone: 'warning' },\n ]);\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n"}, "0821d19cbbb8f8766078006f9e5ef79856472da2": {"t": 1785759515.079494, "n": 8970, "h": "5eb9ce6bee57c83d", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n"}, "bb3a6fe1285b767b9c6944289e1f9e6c1a9a8666": {"t": 1785759516.2582746, "n": 8876, "h": "a1910f959c0ec2f8", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildProviderConfigSignature,\n buildHeadersText,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n parseHeadersText,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n shouldRefreshRemoteModels,\n} from '../model/openAICompatible.ts';\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n headersText: '',\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('buildHeadersText and parseHeadersText convert between map and textarea text', () => {\n assert.equal(\n buildHeadersText({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }),\n 'Authorization: Bearer sk-test\\nX-Team: team-a',\n );\n\n assert.deepEqual(\n parseHeadersText('Authorization: Bearer sk-test\\nX-Team: team-a\\ninvalid-line'),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildProviderConfigSignature normalizes base url and headers ordering', () => {\n const left = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1/',\n apiKey: ' sk-test ',\n headersText: 'X-Title: GetTokens\\nAuthorization: Bearer sk-test',\n });\n\n const right = buildProviderConfigSignature({\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n headers: {\n Authorization: 'Bearer sk-test',\n 'X-Title': 'GetTokens',\n },\n });\n\n assert.equal(left, right);\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n\ntest('shouldRefreshRemoteModels returns true only when cache is empty or stale for one day', () => {\n assert.equal(shouldRefreshRemoteModels(null, 1000), true);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 60 * 60 * 1000), false);\n assert.equal(shouldRefreshRemoteModels(1000, 1000 + 24 * 60 * 60 * 1000), true);\n});\n"}, "4e83cf1d73286c6a318cd5551cb663519d0467b1": {"t": 1785759517.763736, "n": 8440, "h": "512286971f42e04e", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n applyOpenAICompatibleProviderPreset,\n buildHeadersMap,\n buildHeaderRows,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n getOpenAICompatibleProviderPreset,\n maskProviderAPIKey,\n normalizeProviderAPIKeys,\n normalizeProviderModels,\n openAICompatibleProviderPresets,\n renameProviderVerifyState,\n resolveProviderDetailModelOptions,\n resolveOpenAICompatibleProviderPreset,\n resolveOpenAICompatibleProviderPresetID,\n} from '../model/openAICompatible.ts';\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n apiKey: '',\n });\n});\n\ntest('openAICompatibleProviderPresets exposes cherry-studio vendor defaults adapted for this workspace', () => {\n assert.deepEqual(openAICompatibleProviderPresets.slice(0, 3), [\n {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n },\n {\n id: 'siliconflow',\n label: 'SiliconFlow',\n baseUrl: 'https://api.siliconflow.cn/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-ai/DeepSeek-V3.2', alias: 'DeepSeek V3.2' },\n { name: 'Qwen/Qwen3-8B', alias: 'Qwen3-8B' },\n ],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'glm-5', alias: 'GLM-5' },\n { name: 'glm-4.7', alias: 'GLM-4.7' },\n { name: 'glm-4.5-flash', alias: 'GLM-4.5-Flash' },\n ],\n },\n ]);\n});\n\ntest('applyOpenAICompatibleProviderPreset fills provider name and base url while keeping user secrets intact', () => {\n assert.deepEqual(\n applyOpenAICompatibleProviderPreset(\n {\n name: '',\n baseUrl: '',\n apiKey: 'sk-test',\n },\n 'openrouter',\n ),\n {\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n },\n );\n});\n\ntest('getOpenAICompatibleProviderPreset returns null for unknown providers', () => {\n assert.equal(getOpenAICompatibleProviderPreset('unknown-provider'), null);\n});\n\ntest('resolveOpenAICompatibleProviderPresetID matches provider by name or base url', () => {\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'DeepSeek' }), 'deepseek');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ baseUrl: 'https://openrouter.ai/api/v1/' }), 'openrouter');\n assert.equal(resolveOpenAICompatibleProviderPresetID({ name: 'custom', baseUrl: 'https://relay.example.com/v1' }), '');\n});\n\ntest('resolveOpenAICompatibleProviderPreset returns preset details for matching provider', () => {\n assert.deepEqual(resolveOpenAICompatibleProviderPreset({ name: 'deepseek' }), {\n id: 'deepseek',\n label: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKeyPlaceholder: 'sk-...',\n models: [\n { name: 'deepseek-chat', alias: 'Chat' },\n { name: 'deepseek-reasoner', alias: 'Reasoner' },\n ],\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n apiKeys: ['sk-test'],\n headers: [{ key: '', value: '' }],\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft falls back to preset default verify model when provider models are empty', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft({\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n models: [],\n }).verifyModel,\n 'deepseek/deepseek-chat',\n );\n});\n\ntest('resolveProviderDetailModelOptions prefers fetched remote models over local and preset models', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-test',\n apiKeys: ['sk-test'],\n headers: [{ key: '', value: '' }],\n models: [{ name: 'local-model', alias: 'Local' }],\n verifyModel: 'deepseek-chat',\n },\n remoteModelsState: {\n status: 'success',\n message: 'ok',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n lastFetchedAt: 123,\n },\n }),\n {\n source: 'remote',\n models: [\n { name: 'deepseek-chat', alias: '' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n },\n );\n});\n\ntest('resolveProviderDetailModelOptions falls back to preset models when local and remote models are empty', () => {\n assert.deepEqual(\n resolveProviderDetailModelOptions({\n draft: {\n currentName: 'openrouter',\n name: 'openrouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n apiKeys: ['sk-test'],\n headers: [{ key: '', value: '' }],\n models: [{ name: '', alias: '' }],\n verifyModel: '',\n },\n remoteModelsState: {\n status: 'idle',\n message: '',\n models: [],\n lastFetchedAt: null,\n },\n }).source,\n 'preset',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('normalizeProviderAPIKeys trims blanks and removes duplicates', () => {\n assert.deepEqual(normalizeProviderAPIKeys([' sk-a ', '', 'sk-b', 'sk-a', ' ']), ['sk-a', 'sk-b']);\n});\n\ntest('buildHeaderRows and buildHeadersMap convert between map and editable rows', () => {\n assert.deepEqual(buildHeaderRows({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }), [\n { key: 'Authorization', value: 'Bearer sk-test' },\n { key: 'X-Team', value: 'team-a' },\n ]);\n\n assert.deepEqual(\n buildHeadersMap([\n { key: ' Authorization ', value: ' Bearer sk-test ' },\n { key: '', value: 'ignored' },\n { key: 'X-Team', value: 'team-a' },\n ]),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n"}, "62fd3fe81d7050939c92711934eefc6f6c1bc459": {"t": 1785759519.0137255, "n": 3853, "h": "a4f2c0ed47bcea10", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n buildHeadersMap,\n buildHeaderRows,\n buildModelRows,\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n maskProviderAPIKey,\n normalizeProviderAPIKeys,\n normalizeProviderModels,\n renameProviderVerifyState,\n} from '../model/openAICompatible.ts';\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n prefix: '',\n apiKey: '',\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n prefix: 'team-a',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n prefix: 'team-a',\n apiKey: 'sk-test',\n apiKeys: ['sk-test'],\n headers: [{ key: '', value: '' }],\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n verifyModel: 'deepseek-chat',\n },\n );\n});\n\ntest('buildOpenAICompatibleProviderDraft prefers cached verify model when present', () => {\n assert.equal(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n prefix: 'team-a',\n apiKey: 'sk-test',\n models: [{ name: 'deepseek-chat', alias: 'chat' }],\n },\n {\n model: 'deepseek-reasoner',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ).verifyModel,\n 'deepseek-reasoner',\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n\ntest('normalizeProviderAPIKeys trims blanks and removes duplicates', () => {\n assert.deepEqual(normalizeProviderAPIKeys([' sk-a ', '', 'sk-b', 'sk-a', ' ']), ['sk-a', 'sk-b']);\n});\n\ntest('buildHeaderRows and buildHeadersMap convert between map and editable rows', () => {\n assert.deepEqual(buildHeaderRows({ Authorization: 'Bearer sk-test', 'X-Team': 'team-a' }), [\n { key: 'Authorization', value: 'Bearer sk-test' },\n { key: 'X-Team', value: 'team-a' },\n ]);\n\n assert.deepEqual(\n buildHeadersMap([\n { key: ' Authorization ', value: ' Bearer sk-test ' },\n { key: '', value: 'ignored' },\n { key: 'X-Team', value: 'team-a' },\n ]),\n {\n Authorization: 'Bearer sk-test',\n 'X-Team': 'team-a',\n },\n );\n});\n\ntest('buildModelRows and normalizeProviderModels keep editable model aliases', () => {\n assert.deepEqual(buildModelRows([{ name: 'deepseek-chat', alias: 'chat' }]), [\n { name: 'deepseek-chat', alias: 'chat' },\n ]);\n\n assert.deepEqual(\n normalizeProviderModels([\n { name: ' deepseek-chat ', alias: ' chat ' },\n { name: '', alias: 'ignored' },\n { name: 'deepseek-chat', alias: 'dup' },\n { name: 'deepseek-reasoner', alias: '' },\n ]),\n [\n { name: 'deepseek-chat', alias: 'chat' },\n { name: 'deepseek-reasoner', alias: '' },\n ],\n );\n});\n"}, "a350ebeede6dd9da7e343d7d73b2e7cd538309df": {"t": 1785759520.2200158, "n": 1812, "h": "a1e9481545a0ed7a", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n buildOpenAICompatibleProviderDraft,\n emptyOpenAICompatibleProviderForm,\n maskProviderAPIKey,\n renameProviderVerifyState,\n} from '../model/openAICompatible.ts';\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n prefix: '',\n apiKey: '',\n });\n});\n\ntest('buildOpenAICompatibleProviderDraft keeps editable provider basics and verify model', () => {\n assert.deepEqual(\n buildOpenAICompatibleProviderDraft(\n {\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n prefix: 'team-a',\n apiKey: 'sk-test',\n },\n {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n ),\n {\n currentName: 'deepseek',\n name: 'deepseek',\n baseUrl: 'https://api.deepseek.com/v1',\n prefix: 'team-a',\n apiKey: 'sk-test',\n verifyModel: 'deepseek-chat',\n },\n );\n});\n\ntest('renameProviderVerifyState moves cached state to the new provider name', () => {\n const states = {\n deepseek: {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n };\n\n assert.deepEqual(renameProviderVerifyState(states, 'deepseek', 'deepseek-prod'), {\n 'deepseek-prod': {\n model: 'deepseek-chat',\n status: 'success',\n message: 'ok',\n lastVerifiedAt: 123,\n },\n });\n});\n"}, "8c3c645ce9ce9be78c96a05f2d0bd12865bcbbd6": {"t": 1785759522.372231, "n": 632, "h": "e44572bcc214bc78", "c": "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport {\n emptyOpenAICompatibleProviderForm,\n maskProviderAPIKey,\n} from '../model/openAICompatible.ts';\n\ntest('maskProviderAPIKey keeps short keys and masks long keys', () => {\n assert.equal(maskProviderAPIKey(''), '—');\n assert.equal(maskProviderAPIKey('sk-1234'), 'sk-1234');\n assert.equal(maskProviderAPIKey('sk-1234567890'), 'sk-1...7890');\n});\n\ntest('emptyOpenAICompatibleProviderForm starts with blank fields', () => {\n assert.deepEqual(emptyOpenAICompatibleProviderForm, {\n name: '',\n baseUrl: '',\n prefix: '',\n apiKey: '',\n });\n});\n"}}}} |