{"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>;\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:` | `UploadAuthFiles(files)` 经 sidecar `/v0/management/auth-files` 上传 |\n| Codex API key | `codex-api-key:` 或指纹 ID | `CreateCodexAPIKey(input)` 写入本地 `~/.config/gettokens-data/codex-api-keys/*.json` 并同步 sidecar |\n| OpenAI-compatible provider | `openai-compatible:` | `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.` 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:` / `codex-api-key:` / `openai-compatible:` |\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=\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.].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:` |\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.]`。\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:`\n\n### 场景 3:导入 auth-file\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=auth-file&config=`\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.]`\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..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..wire_api\", \"value\": \"responses\" },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..experimental_bearer_token\", \"value\": \"\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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\": \"\" },\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..name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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:` | `UploadAuthFiles(files)` 经 sidecar `/v0/management/auth-files` 上传 |\n| Codex API key | `codex-api-key:` 或指纹 ID | `CreateCodexAPIKey(input)` 写入本地 `~/.config/gettokens-data/codex-api-keys/*.json` 并同步 sidecar |\n| OpenAI-compatible provider | `openai-compatible:` | `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.` 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:` / `codex-api-key:` / `openai-compatible:` |\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=\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.].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:` |\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.]`。\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:`\n\n### 场景 3:导入 auth-file\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=auth-file&config=`\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.]`\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..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..wire_api\", \"value\": \"responses\" },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..experimental_bearer_token\", \"value\": \"\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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\": \"\" },\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..name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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:` | `UploadAuthFiles(files)` 经 sidecar `/v0/management/auth-files` 上传 |\n| Codex API key | `codex-api-key:` 或指纹 ID | `CreateCodexAPIKey(input)` 写入本地 `~/.config/gettokens-data/codex-api-keys/*.json` 并同步 sidecar |\n| OpenAI-compatible provider | `openai-compatible:` | `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.` 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:` / `codex-api-key:` / `openai-compatible:` |\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=\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.].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:` |\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.]`。\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:`\n\n### 场景 3:导入 auth-file\n\nGiven deep link 包含 `channel=codex&resource=account&accountType=auth-file&config=`\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.]`\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..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"deepseek\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.deepseek.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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..name\", \"value\": \"Relay Preserve\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..experimental_bearer_token\", \"value\": \"\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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\": \"\" },\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..name\", \"value\": \"Team Relay\" },\n { \"op\": \"set\", \"path\": \"model_providers..base_url\", \"value\": \"https://api.example.com/v1\" },\n { \"op\": \"set\", \"path\": \"model_providers..requires_openai_auth\", \"value\": true },\n { \"op\": \"set\", \"path\": \"model_providers..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": "\n\n\n \n \n GetTokens · Account Detail v09 Palette Fix\n \n \n\n\n
\n
GETTOKENS / ACCOUNT DETAIL / V26 REAL HEADER TAGS

Real Header Tags

修正配色:从偏米黄工业纸底改为 GetTokens 浅色工作台配色,使用白色主面、浅灰 surface、黑色边框与克制状态色。

这版专门解决配色不对的问题;保留 v08 的左右请求配置结构。
\n
\n
\n
\n
DeepSeek
Codex Stable
codex-pro.json
Split Provider
READYDIRTYBUSYERROR
\n
类型OpenAI-compatible凭据API Key验证Success路由Pool Tokyo-01余额/额度Configured
API Key provider · custom headers · model mapping · optional quota/billing scripts
\n
Last2026-06-04 16:20pending—
Latency42msbad
\n
\n
\n
01

身份与凭据

\n
\n
\n
\n
模型拉取凭据,仅用于 /models。
\n
\n
02

请求配置

\n
\n
\n \n
这里只保留当前支持的 headersText 配置;计数与路由不作为可编辑字段展示。
\n
\n
\n
\n
\n \n \n
账号详情只选择已保存的代理池节点;刷新、测速、增删节点在代理池页面完成。
\n
\n
\n
\n
03

模型

\n
模型是映射关系:Source Model → Alias / Route。
\n
\n
Source Modeldeepseek-v4-pro
→
Alias / RouteDeepSeek V4 Pro
\n
Source Modeldeepseek-v4-flash
→
Alias / RouteDeepSeek Flash
\n
Source Modelgpt-5.4-mini
→
Verify Aliasfast verify
\n
Source Modelgpt-5.4
→
Verify Aliasstable
\n
Auth-file Modelgpt-5.5
→
Routeoauth available
只读
\n
Auth-file Modelcodex-mini-latest
→
Routecodex
只读
\n
Source Modelprovider-main-model
→
Alias / RoutePrimary Model
\n
Source Modelprovider-fast-model
→
Alias / RouteFast Model
\n
\n
\n
04

验证

\n
05

余额与额度

\n
\n
\n
Quota / 额度72% remainingtesting...test failed
\n
\n
\n
\n
Remaining720K tokens
\n
Percent72%
\n
Reset18:00
\n
\n
Last Quota Testsuccess · script returned remaining quotarequest pending, keep last result visibleHTTP 403 · check quota script / credential
\n
\n
\n
\n
Billing / 余额120.00 USDprovider managedtesting...test failed
\n
\n
\n
\n
Total120.00 USD—
\n
Granted80.00 USDprovider
\n
Topped40.00 USDmanaged
\n
\n
Last Billing Testsuccess · granted 80 USD · topped up 40 USDrequest pendingempty response or parse failedmanaged by provider / OAuth account
\n
\n
\n
\n
READYDIRTYBUSYBLOCKED
可保存或关闭存在未保存更改请求处理中保存被阻断当前账号详情状态正常。保存后写入 Wails/sidecar。不清空已有信息。修复 credential/proxy/reauth 错误后才能保存。
\n
\n
v16 请求配置字段收窄:移除不支持的派生/运行态字段,只保留 Custom Headers 与代理池选择。
v15 顶部关闭修正:已移除头部重复关闭入口,关闭/放弃操作只保留在底部命令栏。
v14 模型列表修正:模型列表改为两列,避免模型多时单列表格过高。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v20 余额额度按钮文案修正:Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v19 余额额度卡片修正:余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。
v18 头部精简修正:头部改为 74px 紧凑业务摘要,移除 detail/frame/actions 等实现文本。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v20 余额额度按钮文案修正:Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v19 余额额度卡片修正:余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。
v17 余额额度分栏修正:余额与额度改为左右两栏,中间明确分割线;Quota 与 Billing 各自包含配置、测试和结果。
v13 余额额度修正:余额/额度支持测试,测试结果在本模块内显示。
v12 验证区修正:已移除不存在的 限流/路由状态 摘要,仅保留测试模型与执行验证动作。
v09 配色修正:当前文件已改为 GetTokens 浅色工作台配色:白色主面、浅灰 surface、黑色边框、灰色弱文本、状态色只用于状态。保留 v08 的左右请求配置结构。
\n
\n\n\n\n"}, "73c17b20ebe17d46f871845562a3bf04c04e27f7": {"t": 1785759462.9398632, "n": 47135, "h": "860104a39f6fb032", "c": "\n\n\n \n \n GetTokens · Account Detail v09 Palette Fix\n \n \n\n\n
\n
GETTOKENS / ACCOUNT DETAIL / V26 REAL HEADER TAGS

Real Header Tags

修正配色:从偏米黄工业纸底改为 GetTokens 浅色工作台配色,使用白色主面、浅灰 surface、黑色边框与克制状态色。

这版专门解决配色不对的问题;保留 v08 的左右请求配置结构。
\n
\n
\n
\n
DeepSeek
Codex Stable
codex-pro.json
Split Provider
READYDIRTYBUSYERROR
\n
类型OpenAI-compatible凭据API Key验证Success路由Pool Tokyo-01余额/额度Configured
API Key provider · custom headers · model mapping · optional quota/billing scripts
\n
Last2026-06-04 16:20pending—
Latency42msbad
\n
\n
\n
01

身份与凭据

\n
\n
\n
\n
模型拉取凭据,仅用于 /models。
\n
\n
02

请求配置

\n
\n
\n \n
这里只保留当前支持的 headersText 配置;计数与路由不作为可编辑字段展示。
\n
\n
\n
\n
\n \n \n
账号详情只选择已保存的代理池节点;刷新、测速、增删节点在代理池页面完成。
\n
\n
\n
\n
03

模型

\n
模型是映射关系:Source Model → Alias / Route。
\n
\n
Source Modeldeepseek-v4-pro
→
Alias / RouteDeepSeek V4 Pro
\n
Source Modeldeepseek-v4-flash
→
Alias / RouteDeepSeek Flash
\n
Source Modelgpt-5.4-mini
→
Verify Aliasfast verify
\n
Source Modelgpt-5.4
→
Verify Aliasstable
\n
Auth-file Modelgpt-5.5
→
Routeoauth available
只读
\n
Auth-file Modelcodex-mini-latest
→
Routecodex
只读
\n
Source Modelprovider-main-model
→
Alias / RoutePrimary Model
\n
Source Modelprovider-fast-model
→
Alias / RouteFast Model
\n
\n
\n
04

验证

\n
05

余额与额度

\n
\n
\n
Quota / 额度72% remainingtesting...test failed
\n
\n
\n
\n
Remaining720K tokens
\n
Percent72%
\n
Reset18:00
\n
\n
Last Quota Testsuccess · script returned remaining quotarequest pending, keep last result visibleHTTP 403 · check quota script / credential
\n
\n
\n
\n
Billing / 余额120.00 USDprovider managedtesting...test failed
\n
\n
\n
\n
Total120.00 USD—
\n
Granted80.00 USDprovider
\n
Topped40.00 USDmanaged
\n
\n
Last Billing Testsuccess · granted 80 USD · topped up 40 USDrequest pendingempty response or parse failedmanaged by provider / OAuth account
\n
\n
\n
\n
READYDIRTYBUSYBLOCKED
可保存或关闭存在未保存更改请求处理中保存被阻断当前账号详情状态正常。保存后写入 Wails/sidecar。不清空已有信息。修复 credential/proxy/reauth 错误后才能保存。
\n
\n
v16 请求配置字段收窄:移除不支持的派生/运行态字段,只保留 Custom Headers 与代理池选择。
v15 顶部关闭修正:已移除头部重复关闭入口,关闭/放弃操作只保留在底部命令栏。
v14 模型列表修正:模型列表改为两列,避免模型多时单列表格过高。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v20 余额额度按钮文案修正:Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v19 余额额度卡片修正:余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。
v18 头部精简修正:头部改为 74px 紧凑业务摘要,移除 detail/frame/actions 等实现文本。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v20 余额额度按钮文案修正:Quota/Billing 卡片内按钮去掉重复对象名,只保留“配置/测试”。
v27 头部分割线移除:移除标签组与描述之间的中间分割线。
v26 头部标签样式修正:头部摘要改成和 READY 一致的 pill/tag,而不是表格格子。
v25 头部类型摘要整理:头部中区改为上方标签组、下方文字描述并用分割线区分。
v24 模型映射错位修正:修复类型切换 display 规则覆盖 mapping grid 的问题,映射卡稳定为 Source → Alias 三段式。
v23 账号类型与模型映射整理:头部改成通用账号类型表达;模型区改成 Source Model → Alias/Route 映射卡。
v22 Headers 高度自适应:Custom Headers 使用真实换行和 autosize textarea,避免固定高度造成空白。
v21 代理功能收窄:账号详情只支持选择已保存代理池节点,不提供刷新/测速。
v19 余额额度卡片修正:余额/额度回到卡片模式,移除 Source/Test Result 字段化展示。
v17 余额额度分栏修正:余额与额度改为左右两栏,中间明确分割线;Quota 与 Billing 各自包含配置、测试和结果。
v13 余额额度修正:余额/额度支持测试,测试结果在本模块内显示。
v12 验证区修正:已移除不存在的 限流/路由状态 摘要,仅保留测试模型与执行验证动作。
v09 配色修正:当前文件已改为 GetTokens 浅色工作台配色:白色主面、浅灰 surface、黑色边框、灰色弱文本、状态色只用于状态。保留 v08 的左右请求配置结构。
\n
\n\n\n\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, /