From 834b73d632ec2aa32e2e4b264b8e1609f4d85745 Mon Sep 17 00:00:00 2001 From: AiYuan4047 Date: Sun, 4 Oct 2026 12:06:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B5=8F=E8=A7=88=E5=99=A8=20UA=20?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E3=80=81WebDAV=20=E5=A4=87=E4=BB=BD=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E3=80=81=E5=BE=85=E5=8A=9E=E5=A2=9E=E5=BC=BA=E4=B8=8E?= =?UTF-8?q?=E5=A4=9A=E9=A1=B9=20Bug=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基于 v1.12.0-rc2,新增/改进: - 浏览器 UA 预设(系统默认/安卓/Windows/Mac)与 AI 逐 action 权限开关 - WebDAV 备份同步(连接测试、立即同步、自动同步、云端 AiCode/ 目录) - 任务待办显示位置设置与全部完成后的删除功能 - 模型选择列表惰性渲染、批量勾选失败项 - 修复:回退后 AI 未停止、超长思考展开崩溃、WebDAV 连接测试 url 为空等 - 提示词引导优先使用子代理 --- .github/workflows/docs-deploy.yml | 7 +- app/src/main/assets/api.official.json | 2 +- .../main/assets/prompts/60-tools-and-paths.md | 4 +- .../session/MessagePersistenceUseCase.kt | 13 +- .../domain/tool/browser/BrowserManager.kt | 86 ++++ .../agent/domain/tool/browser/BrowserTool.kt | 11 +- .../domain/tool/browser/BrowserUserAgent.kt | 109 +++++ .../agent/domain/workflow/ContextCompactor.kt | 52 ++- .../domain/workflow/StatefulAgentWorkflow.kt | 14 +- .../agent/presentation/AIAgentViewModel.kt | 87 +++- .../presentation/component/AIChatPanel.kt | 42 +- .../component/AskUserQuestionPanel.kt | 19 +- .../presentation/component/ChatInputBar.kt | 11 +- .../component/ProviderDashboardBar.kt | 7 + .../presentation/component/StatusBubbles.kt | 49 +- .../component/TodoDashboardBar.kt | 37 +- .../component/ToolMessageComponents.kt | 166 +++++-- .../feature/backup/data/BackupManagerImpl.kt | 104 +++-- .../backup/data/WebDavAutoSyncWorker.kt | 102 ++++ .../feature/backup/data/WebDavClient.kt | 117 +++++ .../backup/data/WebDavSettingsRepository.kt | 133 ++++++ .../feature/backup/domain/BackupManager.kt | 4 + .../feature/backup/domain/BackupSnapshot.kt | 3 +- .../backup/presentation/BackupViewModel.kt | 79 +++- .../backup/presentation/WebDavSyncScreen.kt | 438 ++++++++++++++++++ .../presentation/WebDavSyncViewModel.kt | 167 +++++++ .../repository/FileCredentialRepository.kt | 14 +- .../repository/AIProviderRepositoryImpl.kt | 11 +- .../repository/BrowserSettingsRepository.kt | 82 ++++ .../repository/GeneralSettingsRepository.kt | 22 + .../presentation/SettingsViewModel.kt | 50 ++ .../component/BrowserSettingsSection.kt | 198 ++++++++ .../component/DefaultModelsSection.kt | 99 ++-- .../component/GeneralSettingsSection.kt | 92 +++- .../component/PromptEditorScreen.kt | 4 + .../component/ProviderEditorScreen.kt | 28 +- .../presentation/component/SettingsScreen.kt | 40 +- .../component/SkillEditorScreen.kt | 4 + .../component/SubAgentEditorScreen.kt | 4 + .../component/UpdateCheckDialog.kt | 4 +- .../domain/repository/RemoteRepository.kt | 7 +- .../remote/RemoteServerViewModel.kt | 77 ++- app/src/main/res/values-en/strings.xml | 96 ++++ app/src/main/res/values/strings.xml | 96 ++++ .../session/MessagePersistenceUseCaseTest.kt | 14 + .../domain/workflow/ContextCompactorTest.kt | 16 +- .../component/ToolGroupExpansionTest.kt | 14 + .../backup/domain/BackupMetadataTest.kt | 40 ++ docs-site/docs/advanced/dashboard-cards.md | 2 + docs-site/docs/guide/backup.md | 4 + docs-site/docs/guide/chat.md | 14 +- docs-site/docs/guide/custom-prompts.md | 2 + docs-site/docs/guide/providers.md | 2 +- docs-site/docs/guide/skills.md | 2 + docs-site/docs/guide/subagent.md | 2 + docs-site/docs/guide/sync.md | 2 + 56 files changed, 2664 insertions(+), 241 deletions(-) create mode 100644 app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserUserAgent.kt create mode 100644 app/src/main/java/com/aicode/feature/backup/data/WebDavAutoSyncWorker.kt create mode 100644 app/src/main/java/com/aicode/feature/backup/data/WebDavClient.kt create mode 100644 app/src/main/java/com/aicode/feature/backup/data/WebDavSettingsRepository.kt create mode 100644 app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncScreen.kt create mode 100644 app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncViewModel.kt create mode 100644 app/src/main/java/com/aicode/feature/settings/data/repository/BrowserSettingsRepository.kt create mode 100644 app/src/main/java/com/aicode/feature/settings/presentation/component/BrowserSettingsSection.kt create mode 100644 app/src/test/java/com/aicode/feature/backup/domain/BackupMetadataTest.kt diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 1cd292b8a..8afe2c5c1 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -4,13 +4,10 @@ name: Deploy Docs # Vercel 的 Git 自动部署已在 docs-site/vercel.json 用 git.deploymentEnabled=false 关闭, # 生产部署统一由本工作流用 Vercel CLI 完成。 # 前置配置(GitHub 仓库 Secret):VERCEL_TOKEN、VERCEL_ORG_ID、VERCEL_PROJECT_ID -# Vercel 项目需设置 Root Directory = docs-site。CLI 必须从仓库根运行(官方 monorepo 指南): -# 在子目录里跑 vercel 会读不到 docs-site/vercel.json,退回 VitePress preset 默认命令而报 vitepress not found。 on: push: tags: - 'v*' - workflow_dispatch: jobs: deploy: @@ -19,6 +16,10 @@ jobs: env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + # vercel.json / package-lock.json 都在 docs-site,项目根即此处。 + defaults: + run: + working-directory: docs-site steps: - name: Checkout uses: actions/checkout@v4 diff --git a/app/src/main/assets/api.official.json b/app/src/main/assets/api.official.json index 091ae7a6b..5305e13e8 100644 --- a/app/src/main/assets/api.official.json +++ b/app/src/main/assets/api.official.json @@ -1 +1 @@ -{"anthropic":{"name":"Anthropic","type":"ANTHROPIC","baseUrl":"https://api.anthropic.com/","models":{"claude-haiku-4-5":{"id":"claude-haiku-4-5","name":"Claude Haiku 4.5 (latest)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1,"output":5,"cache_read":0.1,"cache_write":1.25}},"claude-opus-4-5":{"id":"claude-opus-4-5","name":"Claude Opus 4.5 (latest)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]},{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-sonnet-4-5":{"id":"claude-sonnet-4-5","name":"Claude Sonnet 4.5 (latest)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75}},"claude-opus-5-5":{"id":"claude-opus-5-5","name":"Claude Opus 5.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.2,"cache_write":5}},"claude-fable-5-1":{"id":"claude-fable-5-1","name":"Claude Fable 5.1","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":10,"output":50,"cache_read":0.25,"cache_write":12.5}},"claude-opus-4-5-20251101":{"id":"claude-opus-4-5-20251101","name":"Claude Opus 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]},{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-opus-5":{"id":"claude-opus-5","name":"Claude Opus 5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-fable-5":{"id":"claude-fable-5","name":"Claude Fable 5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":10,"output":50,"cache_read":1,"cache_write":12.5}},"claude-opus-4-8":{"id":"claude-opus-4-8","name":"Claude Opus 4.8","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-sonnet-4-5-20250929":{"id":"claude-sonnet-4-5-20250929","name":"Claude Sonnet 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75}},"claude-sonnet-5":{"id":"claude-sonnet-5","name":"Claude Sonnet 5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5}},"claude-opus-4-6":{"id":"claude-opus-4-6","name":"Claude Opus 4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","max"]},{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-haiku-4-5-20251001":{"id":"claude-haiku-4-5-20251001","name":"Claude Haiku 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1,"output":5,"cache_read":0.1,"cache_write":1.25}},"claude-sonnet-4-6":{"id":"claude-sonnet-4-6","name":"Claude Sonnet 4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","max"]},{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75}},"claude-sonnet-5-5":{"id":"claude-sonnet-5-5","name":"Claude Sonnet 5.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5}},"claude-opus-4-7":{"id":"claude-opus-4-7","name":"Claude Opus 4.7","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-opus-4-1":{"id":"claude-opus-4-1","name":"Claude Opus 4.1 (latest)","tool_call":true,"reasoning":true,"limit":{"context":200000,"output":32000},"modalities":{"input":["text","image","pdf"]}},"claude-opus-4-1-20250805":{"id":"claude-opus-4-1-20250805","name":"Claude Opus 4.1","tool_call":true,"reasoning":true,"limit":{"context":200000,"output":32000},"modalities":{"input":["text","image","pdf"]}}}},"deepseek":{"name":"DeepSeek","type":"OPENAI","baseUrl":"https://api.deepseek.com","models":{"deepseek-v4-flash-vision-exp":{"id":"deepseek-v4-flash-vision-exp","name":"DeepSeek V4 Flash Vision Exp","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text","image"]},"cost":{"input":0.15,"output":0.6,"reasoning":0.6,"cache_read":0.003}},"deepseek-flash":{"id":"deepseek-flash","name":"DeepSeek V4.1 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text","image"]},"cost":{"input":0.15,"output":0.6,"reasoning":0.6,"cache_read":0.003}},"deepseek-v4-pro":{"id":"deepseek-v4-pro","name":"DeepSeek V4 Pro","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text"]},"cost":{"input":0.435,"output":0.87,"reasoning":0.87,"cache_read":0.003625}},"deepseek-v4-flash":{"id":"deepseek-v4-flash","name":"DeepSeek V4 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text","image"]},"cost":{"input":0.15,"output":0.6,"reasoning":0.6,"cache_read":0.003}},"deepseek-chat":{"id":"deepseek-chat","name":"DeepSeek Chat","tool_call":true,"reasoning":false,"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028}},"deepseek-reasoner":{"id":"deepseek-reasoner","name":"DeepSeek Reasoner","tool_call":true,"reasoning":true,"reasoning_options":[],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.14,"output":0.28,"reasoning":0.28,"cache_read":0.0028}}}},"google":{"name":"Google","type":"GEMINI","baseUrl":"https://generativelanguage.googleapis.com/","models":{"gemini-flash-latest":{"id":"gemini-flash-latest","name":"Gemini Flash Latest","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"gemma-4-31b-it":{"id":"gemma-4-31b-it","name":"Gemma 4 31B IT","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]}},"gemini-omni-flash-preview":{"id":"gemini-omni-flash-preview","name":"Gemini Omni Flash Preview","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":1.5,"output":17.5}},"gemini-3.1-flash-image-preview":{"id":"gemini-3.1-flash-image-preview","name":"Nano Banana 2","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","high"]}],"limit":{"context":65536,"output":65536},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.5,"output":60}},"veo-3.1-lite-generate-preview":{"id":"veo-3.1-lite-generate-preview","name":"Veo 3.1 lite","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":480,"output":8192},"modalities":{"input":["text","image"]}},"lyria-3-pro-preview":{"id":"lyria-3-pro-preview","name":"Lyria 3 Pro Preview","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image"]},"cost":{"input":0,"output":0}},"gemini-3.1-flash-tts-preview":{"id":"gemini-3.1-flash-tts-preview","name":"Gemini 3.1 Flash TTS Preview","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":8192,"output":16384},"modalities":{"input":["text"]},"cost":{"input":1,"output":20}},"gemini-2.5-flash-image":{"id":"gemini-2.5-flash-image","name":"Nano Banana","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.3,"output":30,"cache_read":0.075}},"gemini-flash-lite-latest":{"id":"gemini-flash-lite-latest","name":"Gemini Flash-Lite Latest","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.3,"output":2.5,"cache_read":0.03}},"gemini-embedding-2":{"id":"gemini-embedding-2","name":"Gemini Embedding 2","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8192,"output":1},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":0.2,"output":0,"input_audio":6.5}},"gemini-3.6-flash":{"id":"gemini-3.6-flash","name":"Gemini 3.6 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"gemma-4-26b-a4b-it":{"id":"gemma-4-26b-a4b-it","name":"Gemma 4 26B A4B IT","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]}},"gemini-3-pro-image-preview":{"id":"gemini-3-pro-image-preview","name":"Nano Banana Pro","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":2,"output":120}},"gemini-2.5-pro-preview-tts":{"id":"gemini-2.5-pro-preview-tts","name":"Gemini 2.5 Pro Preview TTS","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":16384},"modalities":{"input":["text"]},"cost":{"input":1,"output":20}},"gemini-3.5-flash-lite":{"id":"gemini-3.5-flash-lite","name":"Gemini 3.5 Flash Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.3,"output":2.5,"cache_read":0.03}},"gemini-3.1-flash-image":{"id":"gemini-3.1-flash-image","name":"Nano Banana 2","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","high"]}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":0.5,"output":60}},"lyria-3-clip-preview":{"id":"lyria-3-clip-preview","name":"Lyria 3 Clip Preview","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image"]},"cost":{"input":0,"output":0}},"gemini-3.1-pro-preview":{"id":"gemini-3.1-pro-preview","name":"Gemini 3.1 Pro Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"gemini-2.5-computer-use-preview-10-2025":{"id":"gemini-2.5-computer-use-preview-10-2025","name":"Gemini 2.5 Computer Use Preview 10-2025","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":128000,"output":64000},"modalities":{"input":["text","image"]},"cost":{"input":1.25,"output":10,"tiers":[{"input":2.5,"output":15,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":15}}},"gemini-3.5-flash":{"id":"gemini-3.5-flash","name":"Gemini 3.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":1.5,"output":9,"cache_read":0.15,"input_audio":1.5}},"gemini-2.5-pro":{"id":"gemini-2.5-pro","name":"Gemini 2.5 Pro","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":128,"max":32768}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":1.25,"output":10,"cache_read":0.125,"tiers":[{"input":2.5,"output":15,"cache_read":0.25,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":15,"cache_read":0.25}}},"gemini-3-pro-image":{"id":"gemini-3-pro-image","name":"Nano Banana Pro","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high"]}],"limit":{"context":65536,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":2,"output":120}},"gemini-2.5-flash":{"id":"gemini-2.5-flash","name":"Gemini 2.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","min":0,"max":24576}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":0.3,"output":2.5,"cache_read":0.03,"input_audio":1}},"gemini-2.5-flash-preview-tts":{"id":"gemini-2.5-flash-preview-tts","name":"Gemini 2.5 Flash Preview TTS","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":10}},"gemini-3.1-flash-lite-image":{"id":"gemini-3.1-flash-lite-image","name":"Nano Banana 2 Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","high"]}],"limit":{"context":65536,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":0.25,"output":30}},"deep-research-preview-04-2026":{"id":"deep-research-preview-04-2026","name":"Deep Research Preview (Apr-21-2026)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"deep-research-max-preview-04-2026":{"id":"deep-research-max-preview-04-2026","name":"Deep Research Max Preview (Apr-21-2026)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"veo-3.1-fast-generate-preview":{"id":"veo-3.1-fast-generate-preview","name":"Veo 3.1 fast","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":480,"output":8192},"modalities":{"input":["text","image","video"]}},"gemini-3.7-flash":{"id":"gemini-3.7-flash","name":"Gemini 3.7 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"veo-3.1-generate-preview":{"id":"veo-3.1-generate-preview","name":"Veo 3.1","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":480,"output":8192},"modalities":{"input":["text","image"]}},"gemini-3.1-pro-preview-customtools":{"id":"gemini-3.1-pro-preview-customtools","name":"Gemini 3.1 Pro Preview Custom Tools","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"gemini-3-flash-preview":{"id":"gemini-3-flash-preview","name":"Gemini 3 Flash Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"input_audio":1}},"gemini-3.8-flash":{"id":"gemini-3.8-flash","name":"Gemini 3.8 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"gemini-3.5-live-translate-preview":{"id":"gemini-3.5-live-translate-preview","name":"Gemini 3.5 Live Translate Preview","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":32768},"modalities":{"input":["audio"]},"cost":{"input":3.5,"output":21,"input_audio":3.5,"output_audio":21}},"gemini-2.5-flash-lite":{"id":"gemini-2.5-flash-lite","name":"Gemini 2.5 Flash-Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","min":512,"max":24576}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":0.1,"output":0.4,"cache_read":0.01,"input_audio":0.3}},"gemini-embedding-001":{"id":"gemini-embedding-001","name":"Gemini Embedding 001","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":2048,"output":1},"modalities":{"input":["text"]},"cost":{"input":0.15,"output":0}},"gemini-3.1-flash-lite-preview":{"id":"gemini-3.1-flash-lite-preview","name":"Gemini 3.1 Flash Lite Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.25,"output":1.5,"cache_read":0.025,"input_audio":0.5}},"gemini-3.1-flash-live-preview":{"id":"gemini-3.1-flash-live-preview","name":"Gemini 3.1 Flash Live Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.75,"output":4.5,"input_audio":3,"output_audio":12}},"gemini-3.1-flash-lite":{"id":"gemini-3.1-flash-lite","name":"Gemini 3.1 Flash Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.25,"output":1.5,"cache_read":0.025,"input_audio":0.5}},"gemini-robotics-er-1.6-preview":{"id":"gemini-robotics-er-1.6-preview","name":"Gemini Robotics-ER 1.6 Preview","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","min":0}],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":1,"output":5,"input_audio":2}},"gemini-2.0-flash":{"id":"gemini-2.0-flash","name":"Gemini 2.0 Flash","tool_call":true,"reasoning":false,"limit":{"context":1048576,"output":8192},"modalities":{"input":["text","image","audio","video","pdf"]}},"gemini-3-pro-preview":{"id":"gemini-3-pro-preview","name":"Gemini 3 Pro Preview","tool_call":true,"reasoning":true,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]}},"gemini-2.0-flash-lite":{"id":"gemini-2.0-flash-lite","name":"Gemini 2.0 Flash-Lite","tool_call":true,"reasoning":false,"limit":{"context":1048576,"output":8192},"modalities":{"input":["text","image","audio","video","pdf"]}}}},"xai":{"name":"xAI","type":"OPENAI","baseUrl":"https://api.x.ai/","models":{"grok-4.7":{"id":"grok-4.7","name":"Grok 4.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh"]}],"limit":{"context":500000,"output":500000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.5,"tiers":[{"input":4,"output":12,"cache_read":1,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":12,"cache_read":1}}},"grok-imagine-image":{"id":"grok-imagine-image","name":"Grok Imagine Image","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":16000,"output":0},"modalities":{"input":["text","image","pdf"]}},"grok-imagine-video":{"id":"grok-imagine-video","name":"Grok Imagine Video","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":1024,"output":0},"modalities":{"input":["text","image","video","pdf"]}},"grok-4.3":{"id":"grok-4.3","name":"Grok 4.3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high"]}],"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-4.20-0309-reasoning":{"id":"grok-4.20-0309-reasoning","name":"Grok 4.20 (Reasoning)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-imagine-video-1.5":{"id":"grok-imagine-video-1.5","name":"Grok Imagine Video 1.5","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":1024,"output":0},"modalities":{"input":["text","image","audio","pdf"]}},"grok-4.5":{"id":"grok-4.5","name":"Grok 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":500000,"output":500000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.3,"tiers":[{"input":4,"output":12,"cache_read":0.6,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":12,"cache_read":0.6}}},"grok-4.20-0309-non-reasoning":{"id":"grok-4.20-0309-non-reasoning","name":"Grok 4.20 (Non-Reasoning)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-imagine-image-quality":{"id":"grok-imagine-image-quality","name":"Grok Imagine Image Quality","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":16000,"output":0},"modalities":{"input":["text","image","pdf"]}},"grok-build-0.1":{"id":"grok-build-0.1","name":"Grok Build 0.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":256000,"output":256000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1,"output":2,"cache_read":0.2,"tiers":[{"input":2,"output":4,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2,"output":4,"cache_read":0.4}}},"grok-4.20-multi-agent-0309":{"id":"grok-4.20-multi-agent-0309","name":"Grok 4.20 Multi-Agent","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh"]}],"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-4.6":{"id":"grok-4.6","name":"Grok 4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh"]}],"limit":{"context":500000,"output":500000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.5,"tiers":[{"input":4,"output":12,"cache_read":1,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":12,"cache_read":1}}},"grok-imagine-image-2.0":{"id":"grok-imagine-image-2.0","name":"Grok Imagine Image 2.0","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":64000,"output":0},"modalities":{"input":["text","image","pdf"]}}}},"minimax":{"name":"MiniMax (minimax.io)","type":"OPENAI","baseUrl":"https://api.minimax.io/","models":{"MiniMax-M3":{"id":"MiniMax-M3","name":"MiniMax-M3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":1000000,"output":512000},"modalities":{"input":["text","image","video"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"tiers":[{"input":0.6,"output":2.4,"cache_read":0.12,"tier":{"type":"context","size":512000}}],"context_over_200k":{"input":0.6,"output":2.4,"cache_read":0.12}}},"MiniMax-M2.1":{"id":"MiniMax-M2.1","name":"MiniMax-M2.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.5":{"id":"MiniMax-M2.5","name":"MiniMax-M2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.7":{"id":"MiniMax-M2.7","name":"MiniMax-M2.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2.7-highspeed":{"id":"MiniMax-M2.7-highspeed","name":"MiniMax-M2.7-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2":{"id":"MiniMax-M2","name":"MiniMax-M2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2}},"MiniMax-M2.5-highspeed":{"id":"MiniMax-M2.5-highspeed","name":"MiniMax-M2.5-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}}}},"minimax-cn":{"name":"MiniMax (minimax.cn)","type":"OPENAI","baseUrl":"https://api.minimaxi.com/","models":{"MiniMax-M3":{"id":"MiniMax-M3","name":"MiniMax-M3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":1000000,"output":512000},"modalities":{"input":["text","image","video"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"tiers":[{"input":0.6,"output":2.4,"cache_read":0.12,"tier":{"type":"context","size":512000}}],"context_over_200k":{"input":0.6,"output":2.4,"cache_read":0.12}}},"MiniMax-M2.1":{"id":"MiniMax-M2.1","name":"MiniMax-M2.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.5":{"id":"MiniMax-M2.5","name":"MiniMax-M2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.7":{"id":"MiniMax-M2.7","name":"MiniMax-M2.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2.7-highspeed":{"id":"MiniMax-M2.7-highspeed","name":"MiniMax-M2.7-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2":{"id":"MiniMax-M2","name":"MiniMax-M2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2}},"MiniMax-M2.5-highspeed":{"id":"MiniMax-M2.5-highspeed","name":"MiniMax-M2.5-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}}}},"moonshotai":{"name":"Moonshot AI","type":"OPENAI","baseUrl":"https://api.moonshot.ai","models":{"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image","video"]},"cost":{"input":3,"output":15,"cache_read":0.3}},"kimi-k2.7-code-highspeed":{"id":"kimi-k2.7-code-highspeed","name":"Kimi K2.7 Code HighSpeed","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":1.9,"output":8,"cache_read":0.38}},"kimi-k2.6":{"id":"kimi-k2.6","name":"Kimi K2.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.16}},"kimi-k2.7-code":{"id":"kimi-k2.7-code","name":"Kimi K2.7 Code","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.19}},"kimi-k2-0711-preview":{"id":"kimi-k2-0711-preview","name":"Kimi K2 0711","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2-thinking-turbo":{"id":"kimi-k2-thinking-turbo","name":"Kimi K2 Thinking Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":1.15,"output":8,"cache_read":0.15}},"kimi-k2.5":{"id":"kimi-k2.5","name":"Kimi K2.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":3,"cache_read":0.1}},"kimi-k2-0905-preview":{"id":"kimi-k2-0905-preview","name":"Kimi K2 0905","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2-turbo-preview":{"id":"kimi-k2-turbo-preview","name":"Kimi K2 Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":2.4,"output":10,"cache_read":0.6}},"kimi-k2-thinking":{"id":"kimi-k2-thinking","name":"Kimi K2 Thinking","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}}}},"moonshotai-cn":{"name":"Moonshot AI (China)","type":"OPENAI","baseUrl":"https://api.moonshot.cn","models":{"kimi-k2.7-code":{"id":"kimi-k2.7-code","name":"Kimi K2.7 Code","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.19}},"kimi-k2.6":{"id":"kimi-k2.6","name":"Kimi K2.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.16}},"kimi-k2.7-code-highspeed":{"id":"kimi-k2.7-code-highspeed","name":"Kimi K2.7 Code HighSpeed","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":1.9,"output":8,"cache_read":0.38}},"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image","video"]},"cost":{"input":3,"output":15,"cache_read":0.3}},"kimi-k2-thinking":{"id":"kimi-k2-thinking","name":"Kimi K2 Thinking","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2-turbo-preview":{"id":"kimi-k2-turbo-preview","name":"Kimi K2 Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":2.4,"output":10,"cache_read":0.6}},"kimi-k2-0905-preview":{"id":"kimi-k2-0905-preview","name":"Kimi K2 0905","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2.5":{"id":"kimi-k2.5","name":"Kimi K2.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":3,"cache_read":0.1}},"kimi-k2-thinking-turbo":{"id":"kimi-k2-thinking-turbo","name":"Kimi K2 Thinking Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":1.15,"output":8,"cache_read":0.15}},"kimi-k2-0711-preview":{"id":"kimi-k2-0711-preview","name":"Kimi K2 0711","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}}}},"openai":{"name":"OpenAI","type":"OPENAI","baseUrl":"https://api.openai.com/","models":{"chatgpt-image-latest":{"id":"chatgpt-image-latest","name":"chatgpt-image-latest","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-5.4":{"id":"gpt-5.4","name":"GPT-5.4","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2.5,"output":15,"cache_read":0.25,"tiers":[{"input":5,"output":22.5,"cache_read":0.5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":5,"output":22.5,"cache_read":0.5}}},"gpt-5.4-pro":{"id":"gpt-5.4-pro","name":"GPT-5.4 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":30,"output":180,"tiers":[{"input":60,"output":270,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":60,"output":270}}},"gpt-3.5-turbo":{"id":"gpt-3.5-turbo","name":"GPT-3.5-turbo","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16385,"output":4096},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":1.5,"cache_read":0}},"gpt-5.5-pro":{"id":"gpt-5.5-pro","name":"GPT-5.5 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":30,"output":180,"tiers":[{"input":60,"output":270,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":60,"output":270}}},"text-embedding-3-small":{"id":"text-embedding-3-small","name":"text-embedding-3-small","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8191,"output":1536},"modalities":{"input":["text"]},"cost":{"input":0.02,"output":0}},"gpt-5.4-nano":{"id":"gpt-5.4-nano","name":"GPT-5.4 nano","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.2,"output":1.25,"cache_read":0.02}},"gpt-realtime-2.1":{"id":"gpt-realtime-2.1","name":"GPT-Realtime-2.1","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high","xhigh"]}],"limit":{"context":128000,"input":96000,"output":32000},"modalities":{"input":["text","audio","image"]},"cost":{"input":4,"output":24,"cache_read":0.4,"input_audio":32,"output_audio":64}},"gpt-4o-2024-05-13":{"id":"gpt-4o-2024-05-13","name":"GPT-4o (2024-05-13)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":5,"output":15}},"gpt-4o":{"id":"gpt-4o","name":"GPT-4o","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2.5,"output":10,"cache_read":1.25}},"gpt-5-mini":{"id":"gpt-5-mini","name":"GPT-5 Mini","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.25,"output":2,"cache_read":0.025}},"gpt-image-2":{"id":"gpt-image-2","name":"gpt-image-2","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]},"cost":{"input":5,"output":30,"cache_read":1.25}},"gpt-5.2-pro":{"id":"gpt-5.2-pro","name":"GPT-5.2 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":21,"output":168}},"o4-mini":{"id":"o4-mini","name":"o4-mini","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image"]},"cost":{"input":1.1,"output":4.4,"cache_read":0.275}},"o3-mini":{"id":"o3-mini","name":"o3-mini","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text"]},"cost":{"input":1.1,"output":4.4,"cache_read":0.55}},"text-embedding-ada-002":{"id":"text-embedding-ada-002","name":"text-embedding-ada-002","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8192,"output":1536},"modalities":{"input":["text"]},"cost":{"input":0.1,"output":0}},"gpt-4":{"id":"gpt-4","name":"GPT-4","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":8192},"modalities":{"input":["text"]},"cost":{"input":30,"output":60}},"gpt-5.3-codex":{"id":"gpt-5.3-codex","name":"GPT-5.3 Codex","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-4.1-nano":{"id":"gpt-4.1-nano","name":"GPT-4.1 nano","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1047576,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.1,"output":0.4,"cache_read":0.025}},"gpt-5-nano":{"id":"gpt-5-nano","name":"GPT-5 Nano","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.05,"output":0.4,"cache_read":0.005}},"gpt-5.6":{"id":"gpt-5.6","name":"GPT-5.6","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}}},"gpt-5.2-chat-latest":{"id":"gpt-5.2-chat-latest","name":"GPT-5.2 Chat","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium"]}],"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"o1":{"id":"o1","name":"o1","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":15,"output":60,"cache_read":7.5}},"gpt-5-pro":{"id":"gpt-5-pro","name":"GPT-5 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["high"]}],"limit":{"context":400000,"input":272000,"output":272000},"modalities":{"input":["text","image"]},"cost":{"input":15,"output":120}},"gpt-5.3-codex-spark":{"id":"gpt-5.3-codex-spark","name":"GPT-5.3 Codex Spark","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":128000,"input":100000,"output":32000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-6.1-sol":{"id":"gpt-6.1-sol","name":"GPT-6.1 Sol","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.1,"cache_write":2.5,"tiers":[{"input":4,"output":15,"cache_read":0.2,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":15,"cache_read":0.2,"cache_write":5}}},"text-embedding-3-large":{"id":"text-embedding-3-large","name":"text-embedding-3-large","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8191,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.13,"output":0}},"gpt-4o-2024-08-06":{"id":"gpt-4o-2024-08-06","name":"GPT-4o (2024-08-06)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":2.5,"output":10,"cache_read":1.25}},"gpt-6-astra":{"id":"gpt-6-astra","name":"GPT-6 Astra","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":10,"output":50,"cache_read":1,"cache_write":12.5,"tiers":[{"input":20,"output":75,"cache_read":2,"cache_write":25,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":20,"output":75,"cache_read":2,"cache_write":25}}},"gpt-5.1":{"id":"gpt-5.1","name":"GPT-5.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":1.25,"output":10,"cache_read":0.125}},"gpt-4o-mini":{"id":"gpt-4o-mini","name":"GPT-4o mini","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.15,"output":0.6,"cache_read":0.075}},"o3-pro":{"id":"o3-pro","name":"o3-pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image"]},"cost":{"input":20,"output":80}},"gpt-image-1":{"id":"gpt-image-1","name":"gpt-image-1","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-daybreak-blue-latest":{"id":"gpt-daybreak-blue-latest","name":"Daybreak Blue","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}}},"gpt-5.4-mini":{"id":"gpt-5.4-mini","name":"GPT-5.4 mini","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.75,"output":4.5,"cache_read":0.075}},"gpt-5.6-luna":{"id":"gpt-5.6-luna","name":"GPT-5.6 Luna","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.2,"output":1.2,"cache_read":0.02,"cache_write":0.25,"tiers":[{"input":0.4,"output":1.8,"cache_read":0.04,"cache_write":0.5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":0.4,"output":1.8,"cache_read":0.04,"cache_write":0.5}}},"gpt-5.2":{"id":"gpt-5.2","name":"GPT-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-5.5":{"id":"gpt-5.5","name":"GPT-5.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":30,"cache_read":0.5,"tiers":[{"input":10,"output":45,"cache_read":1,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":10,"output":45,"cache_read":1}}},"gpt-4.1":{"id":"gpt-4.1","name":"GPT-4.1","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1047576,"output":32768},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":8,"cache_read":0.5}},"gpt-4o-2024-11-20":{"id":"gpt-4o-2024-11-20","name":"GPT-4o (2024-11-20)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":2.5,"output":10,"cache_read":1.25}},"gpt-4.1-mini":{"id":"gpt-4.1-mini","name":"GPT-4.1 mini","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1047576,"output":32768},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.4,"output":1.6,"cache_read":0.1}},"gpt-6-luna":{"id":"gpt-6-luna","name":"GPT-6 Luna","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.1,"output":0.5,"cache_read":0.01,"cache_write":0.125,"tiers":[{"input":0.2,"output":0.75,"cache_read":0.02,"cache_write":0.25,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":0.2,"output":0.75,"cache_read":0.02,"cache_write":0.25}}},"gpt-5.3-chat-latest":{"id":"gpt-5.3-chat-latest","name":"GPT-5.3 Chat (latest)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-image-1-mini":{"id":"gpt-image-1-mini","name":"gpt-image-1-mini","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-image-1.5":{"id":"gpt-image-1.5","name":"gpt-image-1.5","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-5.6-terra":{"id":"gpt-5.6-terra","name":"GPT-5.6 Terra","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"cache_write":2.5,"tiers":[{"input":4,"output":18,"cache_read":0.4,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4,"cache_write":5}}},"gpt-4-turbo":{"id":"gpt-4-turbo","name":"GPT-4 Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":10,"output":30}},"gpt-daybreak-red-latest":{"id":"gpt-daybreak-red-latest","name":"Daybreak Red","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":12.5,"output":75,"cache_read":1.25,"cache_write":15.625}},"o3":{"id":"o3","name":"o3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":8,"cache_read":0.5}},"gpt-5":{"id":"gpt-5","name":"GPT-5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":1.25,"output":10,"cache_read":0.125}},"gpt-5.6-sol":{"id":"gpt-5.6-sol","name":"GPT-5.6 Sol","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}}},"gpt-6-sol":{"id":"gpt-6-sol","name":"GPT-6 Sol","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5,"tiers":[{"input":4,"output":15,"cache_read":0.4,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":15,"cache_read":0.4,"cache_write":5}}},"o1-pro":{"id":"o1-pro","name":"o1-pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image"]},"cost":{"input":150,"output":600}}}},"alibaba":{"name":"Alibaba","type":"OPENAI","baseUrl":"https://dashscope-intl.aliyuncs.com/compatible-mode","models":{"qwen-flash":{"id":"qwen-flash","name":"Qwen Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.05,"output":0.4}},"qwen3.5-flash":{"id":"qwen3.5-flash","name":"Qwen3.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.1,"output":0.4,"cache_read":0.01,"cache_write":0.125}},"qwen3.7-max":{"id":"qwen3.7-max","name":"Qwen3.7 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":2.5,"output":7.5,"cache_read":0.5,"cache_write":3.125}},"qwen2-5-32b-instruct":{"id":"qwen2-5-32b-instruct","name":"Qwen2.5 32B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.7,"output":2.8}},"qwq-plus":{"id":"qwq-plus","name":"QwQ Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.8,"output":2.4}},"qwen2-5-vl-72b-instruct":{"id":"qwen2-5-vl-72b-instruct","name":"Qwen2.5-VL 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":2.8,"output":8.4}},"qwen3-vl-plus":{"id":"qwen3-vl-plus","name":"Qwen3-VL Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.2,"output":1.6,"reasoning":4.8}},"qwen3.5-27b":{"id":"qwen3.5-27b","name":"Qwen3.5 27B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.3,"output":2.4}},"deepseek-v4-flash-0731":{"id":"deepseek-v4-flash-0731","name":"DeepSeek V4 Flash 0731","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.2,"output":0.4,"cache_read":0.04}},"qwen-max":{"id":"qwen-max","name":"Qwen Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":8192},"modalities":{"input":["text"]},"cost":{"input":1.6,"output":6.4}},"qwen3-next-80b-a3b-thinking":{"id":"qwen3-next-80b-a3b-thinking","name":"Qwen3-Next 80B-A3B (Thinking)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":6}},"qwen3.8-max":{"id":"qwen3.8-max","name":"Qwen3.8 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","min":0,"max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.25,"cache_write":2.5}},"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image"]},"cost":{"input":3,"output":15,"cache_read":0.3}},"qwen-plus-character-ja":{"id":"qwen-plus-character-ja","name":"Qwen Plus Character (Japanese)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":512},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":1.4}},"qwen3.5-plus":{"id":"qwen3.5-plus","name":"Qwen3.5 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.4,"output":2.4,"reasoning":2.4}},"qwen3-livetranslate-flash-realtime":{"id":"qwen3-livetranslate-flash-realtime","name":"Qwen3-LiveTranslate Flash Realtime","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":53248,"output":4096},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":10,"output":10,"input_audio":10,"output_audio":38}},"qwen2-5-72b-instruct":{"id":"qwen2-5-72b-instruct","name":"Qwen2.5 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":5.6}},"qwen3-32b":{"id":"qwen3-32b","name":"Qwen3 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.7,"output":2.8,"reasoning":8.4}},"qwen-plus":{"id":"qwen-plus","name":"Qwen Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.4,"output":1.2,"reasoning":4}},"qwen3.7-flash":{"id":"qwen3.7-flash","name":"Qwen3.7 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"input":991808,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.03,"output":0.13,"cache_read":0.003,"cache_write":0.0375,"tiers":[{"input":0.1,"output":0.4,"cache_read":0.01,"cache_write":0.125,"tier":{"type":"context","size":32000}},{"input":0.2,"output":0.8,"cache_read":0.02,"cache_write":0.25,"tier":{"type":"context","size":256000}}]}},"qwen3-max":{"id":"qwen3-max","name":"Qwen3 Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.2,"output":6}},"qwen-omni-turbo-realtime":{"id":"qwen-omni-turbo-realtime","name":"Qwen-Omni Turbo Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio"]},"cost":{"input":0.27,"output":1.07,"input_audio":4.44,"output_audio":8.89}},"qwen3-coder-480b-a35b-instruct":{"id":"qwen3-coder-480b-a35b-instruct","name":"Qwen3-Coder 480B-A35B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.5,"output":7.5,"tiers":[{"input":2.7,"output":13.5,"tier":{"type":"context","size":32000}},{"input":4.5,"output":22.5,"tier":{"type":"context","size":128000}}]}},"qwen-turbo":{"id":"qwen-turbo","name":"Qwen Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.05,"output":0.2,"reasoning":0.5}},"qwen3-coder-30b-a3b-instruct":{"id":"qwen3-coder-30b-a3b-instruct","name":"Qwen3-Coder 30B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.45,"output":2.25,"tiers":[{"input":0.75,"output":3.75,"tier":{"type":"context","size":32000}},{"input":1.2,"output":6,"tier":{"type":"context","size":128000}}]}},"qwen3.8-omni-flash":{"id":"qwen3.8-omni-flash","name":"Qwen3.8 Omni Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","xhigh"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.15,"output":0.47,"cache_read":0.016}},"qvq-max":{"id":"qvq-max","name":"QVQ Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":1.2,"output":4.8}},"qwen2-5-7b-instruct":{"id":"qwen2-5-7b-instruct","name":"Qwen2.5 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.175,"output":0.7}},"qwen3-omni-flash":{"id":"qwen3-omni-flash","name":"Qwen3-Omni Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.43,"output":1.66,"input_audio":3.81,"output_audio":15.11}},"qwen-vl-max":{"id":"qwen-vl-max","name":"Qwen-VL Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.8,"output":3.2}},"qwen3-235b-a22b":{"id":"qwen3-235b-a22b","name":"Qwen3 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.7,"output":2.8,"reasoning":8.4}},"qwen3-vl-30b-a3b":{"id":"qwen3-vl-30b-a3b","name":"Qwen3-VL 30B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.2,"output":0.8,"reasoning":2.4}},"qwen3.5-122b-a10b":{"id":"qwen3.5-122b-a10b","name":"Qwen3.5 122B-A10B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.4,"output":3.2}},"qwen3.6-35b-a3b":{"id":"qwen3.6-35b-a3b","name":"Qwen3.6 35B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.248,"output":1.485}},"qwen3-next-80b-a3b-instruct":{"id":"qwen3-next-80b-a3b-instruct","name":"Qwen3-Next 80B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":2}},"qwen-vl-plus":{"id":"qwen-vl-plus","name":"Qwen-VL Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.21,"output":0.63}},"qwen3.6-flash":{"id":"qwen3.6-flash","name":"Qwen3.6 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.1875,"output":1.125,"cache_write":0.234375}},"qwen3-coder-flash":{"id":"qwen3-coder-flash","name":"Qwen3 Coder Flash","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.5}},"qwen2-5-omni-7b":{"id":"qwen2-5-omni-7b","name":"Qwen2.5-Omni 7B","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.1,"output":0.4,"input_audio":6.76}},"qwen-mt-plus":{"id":"qwen-mt-plus","name":"Qwen-MT Plus","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":2.46,"output":7.37}},"qwen3-vl-235b-a22b":{"id":"qwen3-vl-235b-a22b","name":"Qwen3-VL 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.7,"output":2.8,"reasoning":8.4}},"qwen3-coder-plus":{"id":"qwen3-coder-plus","name":"Qwen3 Coder Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1,"output":5}},"qwen2-5-14b-instruct":{"id":"qwen2-5-14b-instruct","name":"Qwen2.5 14B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.35,"output":1.4}},"qwen3.8-flash":{"id":"qwen3.8-flash","name":"Qwen3.8 Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.15,"output":0.47,"cache_read":0.016,"cache_write":0.2}},"qwen2-5-vl-7b-instruct":{"id":"qwen2-5-vl-7b-instruct","name":"Qwen2.5-VL 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.35,"output":1.05}},"qwen3.5-35b-a3b":{"id":"qwen3.5-35b-a3b","name":"Qwen3.5 35B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.25,"output":2}},"qwen3.6-max-preview":{"id":"qwen3.6-max-preview","name":"Qwen3.6 Max Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.3,"output":7.8,"cache_read":0.13,"cache_write":1.625}},"qwen-mt-turbo":{"id":"qwen-mt-turbo","name":"Qwen-MT Turbo","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.16,"output":0.49}},"qwen3.5-397b-a17b":{"id":"qwen3.5-397b-a17b","name":"Qwen3.5 397B-A17B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.6,"output":3.6}},"qwen3-8b":{"id":"qwen3-8b","name":"Qwen3 8B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.18,"output":0.7,"reasoning":2.1}},"glm-5.2":{"id":"glm-5.2","name":"GLM-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","minimal","low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.28,"cache_write":0}},"qwen-omni-turbo":{"id":"qwen-omni-turbo","name":"Qwen-Omni Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.07,"output":0.27,"input_audio":4.44,"output_audio":8.89}},"qwen3-omni-flash-realtime":{"id":"qwen3-omni-flash-realtime","name":"Qwen3-Omni Flash Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.52,"output":1.99,"input_audio":4.57,"output_audio":18.13}},"qwen3-asr-flash":{"id":"qwen3-asr-flash","name":"Qwen3-ASR Flash","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":53248,"output":4096},"modalities":{"input":["audio"]},"cost":{"input":0.035,"output":0.035}},"qwen3.6-27b":{"id":"qwen3.6-27b","name":"Qwen3.6 27B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.6,"output":3.6}},"qwen3.6-plus":{"id":"qwen3.6-plus","name":"Qwen3.6 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"cache_write":0.625,"tiers":[{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5,"tier":{"type":"context","size":256000}}],"context_over_200k":{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5}}},"qwen3-14b":{"id":"qwen3-14b","name":"Qwen3 14B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.35,"output":1.4,"reasoning":4.2}},"qwen3.7-plus":{"id":"qwen3.7-plus","name":"Qwen3.7 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.4,"output":1.6,"cache_read":0.04,"cache_write":0.5,"tiers":[{"input":1.2,"output":4.8,"cache_read":0.12,"cache_write":1.5,"tier":{"type":"context","size":256000}}],"context_over_200k":{"input":1.2,"output":4.8,"cache_read":0.12,"cache_write":1.5}}},"qwen-vl-ocr":{"id":"qwen-vl-ocr","name":"Qwen-VL OCR","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":34096,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":0.72,"output":0.72}}}},"alibaba-cn":{"name":"Alibaba (China)","type":"OPENAI","baseUrl":"https://dashscope.aliyuncs.com/compatible-mode","models":{"qwen-flash":{"id":"qwen-flash","name":"Qwen Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.022,"output":0.216}},"qwen3.5-flash":{"id":"qwen3.5-flash","name":"Qwen3.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.029,"output":0.287,"reasoning":0.287,"tiers":[{"input":0.115,"output":1.147,"reasoning":1.147,"tier":{"type":"context","size":128000}},{"input":0.172,"output":1.72,"reasoning":1.72,"tier":{"type":"context","size":256000}}]}},"qwen2-5-coder-32b-instruct":{"id":"qwen2-5-coder-32b-instruct","name":"Qwen2.5-Coder 32B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"deepseek-v3-2-exp":{"id":"deepseek-v3-2-exp","name":"DeepSeek V3.2 Exp","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.431}},"deepseek-r1-distill-qwen-14b":{"id":"deepseek-r1-distill-qwen-14b","name":"DeepSeek R1 Distill Qwen 14B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.431}},"qwen-math-plus":{"id":"qwen-math-plus","name":"Qwen Math Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"qwen-deep-research":{"id":"qwen-deep-research","name":"Qwen Deep Research","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":7.742,"output":23.367}},"qwen3.7-max":{"id":"qwen3.7-max","name":"Qwen3.7 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text"]},"cost":{"input":2.5,"output":7.5,"cache_read":0.5,"cache_write":3.125}},"qwen2-5-32b-instruct":{"id":"qwen2-5-32b-instruct","name":"Qwen2.5 32B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwq-plus":{"id":"qwq-plus","name":"QwQ Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.23,"output":0.574}},"qwq-32b":{"id":"qwq-32b","name":"QwQ 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwen2-5-vl-72b-instruct":{"id":"qwen2-5-vl-72b-instruct","name":"Qwen2.5-VL 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":2.294,"output":6.881}},"deepseek-v3-1":{"id":"deepseek-v3-1","name":"DeepSeek V3.1","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"qwen3-vl-plus":{"id":"qwen3-vl-plus","name":"Qwen3-VL Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.143353,"output":1.433525,"reasoning":4.300576}},"qwen-plus-character":{"id":"qwen-plus-character","name":"Qwen Plus Character","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":4096},"modalities":{"input":["text"]},"cost":{"input":0.115,"output":0.287}},"qwen-max":{"id":"qwen-max","name":"Qwen Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.345,"output":1.377}},"qwen3-next-80b-a3b-thinking":{"id":"qwen3-next-80b-a3b-thinking","name":"Qwen3-Next 80B-A3B (Thinking)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":1.434}},"qwen3.8-max":{"id":"qwen3.8-max","name":"Qwen3.8 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","min":0,"max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":1.77744,"output":5.33231,"cache_read":0.22218,"cache_write":2.22179}},"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image"]},"cost":{"input":2.827,"output":14.133,"cache_read":0.283}},"qwen2-5-math-7b-instruct":{"id":"qwen2-5-math-7b-instruct","name":"Qwen2.5-Math 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.287}},"qwen3.5-plus":{"id":"qwen3.5-plus","name":"Qwen3.5 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.115,"output":0.688,"reasoning":0.688,"tiers":[{"input":0.287,"output":1.72,"reasoning":1.72,"tier":{"type":"context","size":128000}},{"input":0.573,"output":3.44,"reasoning":3.44,"tier":{"type":"context","size":256000}}]}},"qwen2-5-72b-instruct":{"id":"qwen2-5-72b-instruct","name":"Qwen2.5 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"glm-5":{"id":"glm-5","name":"GLM-5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":32768}],"limit":{"context":202752,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.573,"output":2.58,"tiers":[{"input":0.86,"output":3.154,"tier":{"type":"context","size":32000}}]}},"deepseek-v4.1-flash":{"id":"deepseek-v4.1-flash","name":"DeepSeek V4.1 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text","image"]},"cost":{"input":0.29754,"output":1.19015,"cache_read":0.01488}},"deepseek-r1-distill-llama-8b":{"id":"deepseek-r1-distill-llama-8b","name":"DeepSeek R1 Distill Llama 8B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0,"output":0}},"qwen3-32b":{"id":"qwen3-32b","name":"Qwen3 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":1.147,"reasoning":2.868}},"qwen2-5-coder-7b-instruct":{"id":"qwen2-5-coder-7b-instruct","name":"Qwen2.5-Coder 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.287}},"qwen-plus":{"id":"qwen-plus","name":"Qwen Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.115,"output":0.287,"reasoning":1.147,"cache_read":0.012,"cache_write":0.144}},"qwen3.7-flash":{"id":"qwen3.7-flash","name":"Qwen3.7 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"input":991808,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.02962,"output":0.1185,"cache_read":0.002962,"cache_write":0.03703,"tiers":[{"input":0.08887,"output":0.35549,"cache_read":0.008887,"cache_write":0.11109,"tier":{"type":"context","size":32000}},{"input":0.17774,"output":0.71098,"cache_read":0.017774,"cache_write":0.22218,"tier":{"type":"context","size":256000}}]}},"kimi-k2.6":{"id":"kimi-k2.6","name":"Moonshot Kimi K2.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":16384},"modalities":{"input":["text","image","video"]},"cost":{"input":0.929,"output":3.858}},"qwen3-max":{"id":"qwen3-max","name":"Qwen3 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.359,"output":1.434,"reasoning":1.434,"tiers":[{"input":0.574,"output":2.294,"reasoning":2.294,"tier":{"type":"context","size":32000}},{"input":1.004,"output":4.014,"reasoning":4.014,"tier":{"type":"context","size":128000}}]}},"qwen-omni-turbo-realtime":{"id":"qwen-omni-turbo-realtime","name":"Qwen-Omni Turbo Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio"]},"cost":{"input":0.23,"output":0.918,"input_audio":3.584,"output_audio":7.168}},"deepseek-v3":{"id":"deepseek-v3","name":"DeepSeek V3","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":65536,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":1.147}},"deepseek-r1-distill-llama-70b":{"id":"deepseek-r1-distill-llama-70b","name":"DeepSeek R1 Distill Llama 70B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwen3-coder-480b-a35b-instruct":{"id":"qwen3-coder-480b-a35b-instruct","name":"Qwen3-Coder 480B-A35B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.861,"output":3.441,"tiers":[{"input":1.291,"output":5.161,"tier":{"type":"context","size":32000}},{"input":2.151,"output":8.602,"tier":{"type":"context","size":128000}}]}},"qwen-turbo":{"id":"qwen-turbo","name":"Qwen Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":1000000,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.044,"output":0.087,"reasoning":0.431}},"deepseek-r1-distill-qwen-7b":{"id":"deepseek-r1-distill-qwen-7b","name":"DeepSeek R1 Distill Qwen 7B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.144}},"qwen2-5-math-72b-instruct":{"id":"qwen2-5-math-72b-instruct","name":"Qwen2.5-Math 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"qwen3-coder-30b-a3b-instruct":{"id":"qwen3-coder-30b-a3b-instruct","name":"Qwen3-Coder 30B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.216,"output":0.861,"tiers":[{"input":0.323,"output":1.291,"tier":{"type":"context","size":32000}},{"input":0.538,"output":2.151,"tier":{"type":"context","size":128000}}]}},"qwen-doc-turbo":{"id":"qwen-doc-turbo","name":"Qwen Doc Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.087,"output":0.144}},"kimi-k2-thinking":{"id":"kimi-k2-thinking","name":"Moonshot Kimi K2 Thinking","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":262144,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"qwen3.8-omni-flash":{"id":"qwen3.8-omni-flash","name":"Qwen3.8 Omni Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","xhigh"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.113,"output":0.382,"cache_read":0.014}},"qvq-max":{"id":"qvq-max","name":"QVQ Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":1.147,"output":4.588}},"qwen2-5-7b-instruct":{"id":"qwen2-5-7b-instruct","name":"Qwen2.5 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.144}},"qwen3-omni-flash":{"id":"qwen3-omni-flash","name":"Qwen3-Omni Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.058,"output":0.23,"input_audio":3.584,"output_audio":7.168}},"qwen-vl-max":{"id":"qwen-vl-max","name":"Qwen-VL Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.23,"output":0.574}},"qwen3-235b-a22b":{"id":"qwen3-235b-a22b","name":"Qwen3 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":1.147,"reasoning":2.868}},"qwen3-vl-30b-a3b":{"id":"qwen3-vl-30b-a3b","name":"Qwen3-VL 30B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.108,"output":0.431,"reasoning":1.076}},"kimi-k2.5":{"id":"kimi-k2.5","name":"Moonshot Kimi K2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image","video"]},"cost":{"input":0.574,"output":2.411}},"qwen3-next-80b-a3b-instruct":{"id":"qwen3-next-80b-a3b-instruct","name":"Qwen3-Next 80B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.574}},"qwen-vl-plus":{"id":"qwen-vl-plus","name":"Qwen-VL Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.115,"output":0.287}},"deepseek-r1-distill-qwen-1-5b":{"id":"deepseek-r1-distill-qwen-1-5b","name":"DeepSeek R1 Distill Qwen 1.5B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0,"output":0}},"qwen3.6-flash":{"id":"qwen3.6-flash","name":"Qwen3.6 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":131072}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.1875,"output":1.125,"cache_write":0.234375}},"qwen3-coder-flash":{"id":"qwen3-coder-flash","name":"Qwen3 Coder Flash","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.574}},"qwen2-5-omni-7b":{"id":"qwen2-5-omni-7b","name":"Qwen2.5-Omni 7B","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.087,"output":0.345,"input_audio":5.448}},"qwen-mt-plus":{"id":"qwen-mt-plus","name":"Qwen-MT Plus","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.259,"output":0.775}},"tongyi-intent-detect-v3":{"id":"tongyi-intent-detect-v3","name":"Tongyi Intent Detect V3","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":1024},"modalities":{"input":["text"]},"cost":{"input":0.058,"output":0.144}},"qwen3-vl-235b-a22b":{"id":"qwen3-vl-235b-a22b","name":"Qwen3-VL 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.286705,"output":1.14682,"reasoning":2.867051}},"qwen3-coder-plus":{"id":"qwen3-coder-plus","name":"Qwen3 Coder Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.296,"tiers":[{"input":0.861,"output":3.444,"tier":{"type":"context","size":32000}},{"input":1.435,"output":5.74,"tier":{"type":"context","size":128000}},{"input":2.87,"output":28.7,"tier":{"type":"context","size":256000}}]}},"moonshot-kimi-k2-instruct":{"id":"moonshot-kimi-k2-instruct","name":"Moonshot Kimi K2 Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"MiniMax-M2.5":{"id":"MiniMax-M2.5","name":"MiniMax-M2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2}},"qwen2-5-14b-instruct":{"id":"qwen2-5-14b-instruct","name":"Qwen2.5 14B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.431}},"qwen3.8-flash":{"id":"qwen3.8-flash","name":"Qwen3.8 Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.11875,"output":0.40073,"cache_read":0.01187,"cache_write":0.14844}},"qwen2-5-vl-7b-instruct":{"id":"qwen2-5-vl-7b-instruct","name":"Qwen2.5-VL 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.287,"output":0.717}},"qwen-math-turbo":{"id":"qwen-math-turbo","name":"Qwen Math Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwen-long":{"id":"qwen-long","name":"Qwen Long","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":10000000,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.287}},"qwen3.6-max-preview":{"id":"qwen3.6-max-preview","name":"Qwen3.6 Max Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":131072}],"limit":{"context":245800,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.32,"output":7.9,"cache_read":0.132}},"qwen-mt-turbo":{"id":"qwen-mt-turbo","name":"Qwen-MT Turbo","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.101,"output":0.28}},"qwen3.5-397b-a17b":{"id":"qwen3.5-397b-a17b","name":"Qwen3.5 397B-A17B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.172,"output":1.032,"reasoning":1.032,"tiers":[{"input":0.43,"output":2.58,"reasoning":2.58,"tier":{"type":"context","size":128000}}]}},"qwen3-8b":{"id":"qwen3-8b","name":"Qwen3 8B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.287,"reasoning":0.717}},"glm-5.2":{"id":"glm-5.2","name":"GLM-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text"]},"cost":{"input":1.1,"output":3.851,"cache_read":0.275,"cache_write":0}},"qwen-omni-turbo":{"id":"qwen-omni-turbo","name":"Qwen-Omni Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.058,"output":0.23,"input_audio":3.584,"output_audio":7.168}},"glm-5.1":{"id":"glm-5.1","name":"GLM-5.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":131072}],"limit":{"context":202752,"output":128000},"modalities":{"input":["text"]},"cost":{"input":0.825,"output":3.301,"cache_read":0.17,"tiers":[{"input":1.1,"output":3.851,"tier":{"type":"context","size":32000}}]}},"qwen3-omni-flash-realtime":{"id":"qwen3-omni-flash-realtime","name":"Qwen3-Omni Flash Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio"]},"cost":{"input":0.23,"output":0.918,"input_audio":3.584,"output_audio":7.168}},"qwen3-asr-flash":{"id":"qwen3-asr-flash","name":"Qwen3-ASR Flash","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":53248,"output":4096},"modalities":{"input":["audio"]},"cost":{"input":0.032,"output":0.032}},"deepseek-r1-distill-qwen-32b":{"id":"deepseek-r1-distill-qwen-32b","name":"DeepSeek R1 Distill Qwen 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"deepseek-r1":{"id":"deepseek-r1","name":"DeepSeek R1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"deepseek-v4-pro":{"id":"deepseek-v4-pro","name":"DeepSeek V4 Pro","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.435,"output":0.87,"cache_read":0.003625}},"deepseek-r1-0528":{"id":"deepseek-r1-0528","name":"DeepSeek R1 0528","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"qwen3.6-plus":{"id":"qwen3.6-plus","name":"Qwen3.6 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"cache_write":0.625,"tiers":[{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5,"tier":{"type":"context","size":256000}}],"context_over_200k":{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5}}},"qwen3-14b":{"id":"qwen3-14b","name":"Qwen3 14B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.574,"reasoning":1.434}},"glm-5.3":{"id":"glm-5.3","name":"GLM-5.3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.1,"output":3.851,"cache_read":0.275,"cache_write":0}},"qwen3.7-plus":{"id":"qwen3.7-plus","name":"Qwen3.7 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":64000},"modalities":{"input":["text","image","video"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"cache_write":0.625,"tiers":[{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5,"tier":{"type":"context","size":128000}}]}},"deepseek-v4-flash":{"id":"deepseek-v4-flash","name":"DeepSeek V4 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028}},"qwen-vl-ocr":{"id":"qwen-vl-ocr","name":"Qwen-VL OCR","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":34096,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":0.043,"output":0.072}},"siliconflow/deepseek-v3-0324":{"id":"siliconflow/deepseek-v3-0324","name":"siliconflow/deepseek-v3-0324","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":163840,"output":163840},"modalities":{"input":["text"]},"cost":{"input":0.25,"output":1}},"siliconflow/deepseek-v3.1-terminus":{"id":"siliconflow/deepseek-v3.1-terminus","name":"siliconflow/deepseek-v3.1-terminus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":163840,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.27,"output":1}},"siliconflow/deepseek-r1-0528":{"id":"siliconflow/deepseek-r1-0528","name":"siliconflow/deepseek-r1-0528","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":163840,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":2.18}},"siliconflow/deepseek-v3.2":{"id":"siliconflow/deepseek-v3.2","name":"siliconflow/deepseek-v3.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":163840,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.27,"output":0.42}},"kimi/kimi-k2.5":{"id":"kimi/kimi-k2.5","name":"kimi/kimi-k2.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":3,"cache_read":0.1}},"MiniMax/MiniMax-M2.7":{"id":"MiniMax/MiniMax-M2.7","name":"MiniMax-M2.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"cache_write":0.375}}}},"zhipuai":{"name":"Zhipu AI","type":"OPENAI","baseUrl":"https://open.bigmodel.cn/api/paas/v4","models":{"glm-5v-turbo":{"id":"glm-5v-turbo","name":"GLM-5V-Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":5,"output":22,"cache_read":1.2,"cache_write":0}},"glm-5.3-flash":{"id":"glm-5.3-flash","name":"GLM-5.3-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":0.15,"output":0.5,"cache_read":0.03,"cache_write":0}},"glm-5":{"id":"glm-5","name":"GLM-5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1,"output":3.2,"cache_read":0.2,"cache_write":0}},"glm-5.3-flashx":{"id":"glm-5.3-flashx","name":"GLM-5.3-FlashX","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":0.37,"output":1.25,"cache_read":0.075,"cache_write":0}},"glm-5.2":{"id":"glm-5.2","name":"GLM-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.26,"cache_write":0}},"glm-5.1":{"id":"glm-5.1","name":"GLM-5.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.26,"cache_write":0}},"glm-5.3":{"id":"glm-5.3","name":"GLM-5.3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.26,"cache_write":0}},"glm-4.5-air":{"id":"glm-4.5-air","name":"GLM-4.5-Air","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":131072,"output":98304},"modalities":{"input":["text"]},"cost":{"input":0.2,"output":1.1,"cache_read":0.03,"cache_write":0}},"glm-4.7-flashx":{"id":"glm-4.7-flashx","name":"GLM-4.7-FlashX","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.07,"output":0.4,"cache_read":0.01,"cache_write":0}},"glm-4.5-flash":{"id":"glm-4.5-flash","name":"GLM-4.5-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":131072,"output":98304},"modalities":{"input":["text"]},"cost":{"input":0,"output":0,"cache_read":0,"cache_write":0}},"glm-4.7":{"id":"glm-4.7","name":"GLM-4.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.2,"cache_read":0.11,"cache_write":0}},"glm-4.7-flash":{"id":"glm-4.7-flash","name":"GLM-4.7-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0,"output":0,"cache_read":0,"cache_write":0}},"glm-4.5v":{"id":"glm-4.5v","name":"GLM-4.5V","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":64000,"output":16384},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":1.8}},"glm-4.6v-flash":{"id":"glm-4.6v-flash","name":"GLM-4.6V-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":128000,"output":32768},"modalities":{"input":["text","image","video"]},"cost":{"input":0,"output":0,"cache_read":0,"cache_write":0}},"glm-4.6":{"id":"glm-4.6","name":"GLM-4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.2,"cache_read":0.11,"cache_write":0}},"glm-4.5":{"id":"glm-4.5","name":"GLM-4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":131072,"output":98304},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.2,"cache_read":0.11,"cache_write":0}},"glm-4.6v":{"id":"glm-4.6v","name":"GLM-4.6V","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":128000,"output":32768},"modalities":{"input":["text","image","video"]},"cost":{"input":0.3,"output":0.9}}}}} \ No newline at end of file +{"anthropic":{"name":"Anthropic","type":"ANTHROPIC","baseUrl":"https://api.anthropic.com/","models":{"claude-haiku-4-5":{"id":"claude-haiku-4-5","name":"Claude Haiku 4.5 (latest)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1,"output":5,"cache_read":0.1,"cache_write":1.25}},"claude-opus-4-5":{"id":"claude-opus-4-5","name":"Claude Opus 4.5 (latest)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]},{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-sonnet-4-5":{"id":"claude-sonnet-4-5","name":"Claude Sonnet 4.5 (latest)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75}},"claude-opus-5-5":{"id":"claude-opus-5-5","name":"Claude Opus 5.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.2,"cache_write":5}},"claude-fable-5-1":{"id":"claude-fable-5-1","name":"Claude Fable 5.1","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":10,"output":50,"cache_read":0.25,"cache_write":12.5}},"claude-opus-4-5-20251101":{"id":"claude-opus-4-5-20251101","name":"Claude Opus 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]},{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-opus-5":{"id":"claude-opus-5","name":"Claude Opus 5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-fable-5":{"id":"claude-fable-5","name":"Claude Fable 5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":10,"output":50,"cache_read":1,"cache_write":12.5}},"claude-opus-4-8":{"id":"claude-opus-4-8","name":"Claude Opus 4.8","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-sonnet-4-5-20250929":{"id":"claude-sonnet-4-5-20250929","name":"Claude Sonnet 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75}},"claude-sonnet-5":{"id":"claude-sonnet-5","name":"Claude Sonnet 5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5}},"claude-opus-4-6":{"id":"claude-opus-4-6","name":"Claude Opus 4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","max"]},{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-haiku-4-5-20251001":{"id":"claude-haiku-4-5-20251001","name":"Claude Haiku 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":1024}],"limit":{"context":200000,"output":64000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1,"output":5,"cache_read":0.1,"cache_write":1.25}},"claude-sonnet-4-6":{"id":"claude-sonnet-4-6","name":"Claude Sonnet 4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","max"]},{"type":"budget_tokens","min":1024}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75}},"claude-sonnet-5-5":{"id":"claude-sonnet-5-5","name":"Claude Sonnet 5.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5}},"claude-opus-4-7":{"id":"claude-opus-4-7","name":"Claude Opus 4.7","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25}},"claude-opus-4-1":{"id":"claude-opus-4-1","name":"Claude Opus 4.1 (latest)","tool_call":true,"reasoning":true,"limit":{"context":200000,"output":32000},"modalities":{"input":["text","image","pdf"]}},"claude-opus-4-1-20250805":{"id":"claude-opus-4-1-20250805","name":"Claude Opus 4.1","tool_call":true,"reasoning":true,"limit":{"context":200000,"output":32000},"modalities":{"input":["text","image","pdf"]}}}},"deepseek":{"name":"DeepSeek","type":"OPENAI","baseUrl":"https://api.deepseek.com","models":{"deepseek-v4-flash-vision-exp":{"id":"deepseek-v4-flash-vision-exp","name":"DeepSeek V4 Flash Vision Exp","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text","image"]},"cost":{"input":0.15,"output":0.6,"reasoning":0.6,"cache_read":0.003}},"deepseek-flash":{"id":"deepseek-flash","name":"DeepSeek V4.1 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text","image"]},"cost":{"input":0.15,"output":0.6,"reasoning":0.6,"cache_read":0.003}},"deepseek-v4-pro":{"id":"deepseek-v4-pro","name":"DeepSeek V4 Pro","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text"]},"cost":{"input":0.66,"output":1.98,"reasoning":1.98,"cache_read":0.022}},"deepseek-v4-flash":{"id":"deepseek-v4-flash","name":"DeepSeek V4 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":393216},"modalities":{"input":["text","image"]},"cost":{"input":0.15,"output":0.6,"reasoning":0.6,"cache_read":0.003}},"deepseek-chat":{"id":"deepseek-chat","name":"DeepSeek Chat","tool_call":true,"reasoning":false,"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028}},"deepseek-reasoner":{"id":"deepseek-reasoner","name":"DeepSeek Reasoner","tool_call":true,"reasoning":true,"reasoning_options":[],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.14,"output":0.28,"reasoning":0.28,"cache_read":0.0028}}}},"google":{"name":"Google","type":"GEMINI","baseUrl":"https://generativelanguage.googleapis.com/","models":{"gemini-flash-latest":{"id":"gemini-flash-latest","name":"Gemini Flash Latest","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"gemma-4-31b-it":{"id":"gemma-4-31b-it","name":"Gemma 4 31B IT","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]}},"gemini-omni-flash-preview":{"id":"gemini-omni-flash-preview","name":"Gemini Omni Flash Preview","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":1.5,"output":17.5}},"gemini-3.1-flash-image-preview":{"id":"gemini-3.1-flash-image-preview","name":"Nano Banana 2","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","high"]}],"limit":{"context":65536,"output":65536},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.5,"output":60}},"veo-3.1-lite-generate-preview":{"id":"veo-3.1-lite-generate-preview","name":"Veo 3.1 lite","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":480,"output":8192},"modalities":{"input":["text","image"]}},"lyria-3-pro-preview":{"id":"lyria-3-pro-preview","name":"Lyria 3 Pro Preview","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image"]},"cost":{"input":0,"output":0}},"gemini-3.1-flash-tts-preview":{"id":"gemini-3.1-flash-tts-preview","name":"Gemini 3.1 Flash TTS Preview","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":8192,"output":16384},"modalities":{"input":["text"]},"cost":{"input":1,"output":20}},"gemini-2.5-flash-image":{"id":"gemini-2.5-flash-image","name":"Nano Banana","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.3,"output":30,"cache_read":0.075}},"gemini-flash-lite-latest":{"id":"gemini-flash-lite-latest","name":"Gemini Flash-Lite Latest","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.3,"output":2.5,"cache_read":0.03}},"gemini-embedding-2":{"id":"gemini-embedding-2","name":"Gemini Embedding 2","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8192,"output":1},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":0.2,"output":0,"input_audio":6.5}},"gemini-3.6-flash":{"id":"gemini-3.6-flash","name":"Gemini 3.6 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"gemma-4-26b-a4b-it":{"id":"gemma-4-26b-a4b-it","name":"Gemma 4 26B A4B IT","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]}},"gemini-3-pro-image-preview":{"id":"gemini-3-pro-image-preview","name":"Nano Banana Pro","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":2,"output":120}},"gemini-2.5-pro-preview-tts":{"id":"gemini-2.5-pro-preview-tts","name":"Gemini 2.5 Pro Preview TTS","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":16384},"modalities":{"input":["text"]},"cost":{"input":1,"output":20}},"gemini-3.5-flash-lite":{"id":"gemini-3.5-flash-lite","name":"Gemini 3.5 Flash Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.3,"output":2.5,"cache_read":0.03}},"gemini-3.1-flash-image":{"id":"gemini-3.1-flash-image","name":"Nano Banana 2","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","high"]}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":0.5,"output":60}},"lyria-3-clip-preview":{"id":"lyria-3-clip-preview","name":"Lyria 3 Clip Preview","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image"]},"cost":{"input":0,"output":0}},"gemini-3.1-pro-preview":{"id":"gemini-3.1-pro-preview","name":"Gemini 3.1 Pro Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"gemini-2.5-computer-use-preview-10-2025":{"id":"gemini-2.5-computer-use-preview-10-2025","name":"Gemini 2.5 Computer Use Preview 10-2025","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":128000,"output":64000},"modalities":{"input":["text","image"]},"cost":{"input":1.25,"output":10,"tiers":[{"input":2.5,"output":15,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":15}}},"gemini-3.5-flash":{"id":"gemini-3.5-flash","name":"Gemini 3.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":1.5,"output":9,"cache_read":0.15,"input_audio":1.5}},"gemini-2.5-pro":{"id":"gemini-2.5-pro","name":"Gemini 2.5 Pro","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens","min":128,"max":32768}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":1.25,"output":10,"cache_read":0.125,"tiers":[{"input":2.5,"output":15,"cache_read":0.25,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":15,"cache_read":0.25}}},"gemini-3-pro-image":{"id":"gemini-3-pro-image","name":"Nano Banana Pro","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high"]}],"limit":{"context":65536,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":2,"output":120}},"gemini-2.5-flash":{"id":"gemini-2.5-flash","name":"Gemini 2.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","min":0,"max":24576}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":0.3,"output":2.5,"cache_read":0.03,"input_audio":1}},"gemini-2.5-flash-preview-tts":{"id":"gemini-2.5-flash-preview-tts","name":"Gemini 2.5 Flash Preview TTS","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":10}},"gemini-3.1-flash-lite-image":{"id":"gemini-3.1-flash-lite-image","name":"Nano Banana 2 Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","high"]}],"limit":{"context":65536,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":0.25,"output":30}},"deep-research-preview-04-2026":{"id":"deep-research-preview-04-2026","name":"Deep Research Preview (Apr-21-2026)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"deep-research-max-preview-04-2026":{"id":"deep-research-max-preview-04-2026","name":"Deep Research Max Preview (Apr-21-2026)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"veo-3.1-fast-generate-preview":{"id":"veo-3.1-fast-generate-preview","name":"Veo 3.1 fast","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":480,"output":8192},"modalities":{"input":["text","image","video"]}},"gemini-3.7-flash":{"id":"gemini-3.7-flash","name":"Gemini 3.7 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"veo-3.1-generate-preview":{"id":"veo-3.1-generate-preview","name":"Veo 3.1","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":480,"output":8192},"modalities":{"input":["text","image"]}},"gemini-3.1-pro-preview-customtools":{"id":"gemini-3.1-pro-preview-customtools","name":"Gemini 3.1 Pro Preview Custom Tools","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}}},"gemini-3-flash-preview":{"id":"gemini-3-flash-preview","name":"Gemini 3 Flash Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"input_audio":1}},"gemini-3.8-flash":{"id":"gemini-3.8-flash","name":"Gemini 3.8 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75}},"gemini-3.5-live-translate-preview":{"id":"gemini-3.5-live-translate-preview","name":"Gemini 3.5 Live Translate Preview","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":32768},"modalities":{"input":["audio"]},"cost":{"input":3.5,"output":21,"input_audio":3.5,"output_audio":21}},"gemini-2.5-flash-lite":{"id":"gemini-2.5-flash-lite","name":"Gemini 2.5 Flash-Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","min":512,"max":24576}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","audio","video","pdf"]},"cost":{"input":0.1,"output":0.4,"cache_read":0.01,"input_audio":0.3}},"gemini-embedding-001":{"id":"gemini-embedding-001","name":"Gemini Embedding 001","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":2048,"output":1},"modalities":{"input":["text"]},"cost":{"input":0.15,"output":0}},"gemini-3.1-flash-lite-preview":{"id":"gemini-3.1-flash-lite-preview","name":"Gemini 3.1 Flash Lite Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.25,"output":1.5,"cache_read":0.025,"input_audio":0.5}},"gemini-3.1-flash-live-preview":{"id":"gemini-3.1-flash-live-preview","name":"Gemini 3.1 Flash Live Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.75,"output":4.5,"input_audio":3,"output_audio":12}},"gemini-3.1-flash-lite":{"id":"gemini-3.1-flash-lite","name":"Gemini 3.1 Flash Lite","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]},"cost":{"input":0.25,"output":1.5,"cache_read":0.025,"input_audio":0.5}},"gemini-robotics-er-1.6-preview":{"id":"gemini-robotics-er-1.6-preview","name":"Gemini Robotics-ER 1.6 Preview","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","min":0}],"limit":{"context":131072,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":1,"output":5,"input_audio":2}},"gemini-2.0-flash":{"id":"gemini-2.0-flash","name":"Gemini 2.0 Flash","tool_call":true,"reasoning":false,"limit":{"context":1048576,"output":8192},"modalities":{"input":["text","image","audio","video","pdf"]}},"gemini-3-pro-preview":{"id":"gemini-3-pro-preview","name":"Gemini 3 Pro Preview","tool_call":true,"reasoning":true,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text","image","video","audio","pdf"]}},"gemini-2.0-flash-lite":{"id":"gemini-2.0-flash-lite","name":"Gemini 2.0 Flash-Lite","tool_call":true,"reasoning":false,"limit":{"context":1048576,"output":8192},"modalities":{"input":["text","image","audio","video","pdf"]}}}},"xai":{"name":"xAI","type":"OPENAI","baseUrl":"https://api.x.ai/","models":{"grok-4.7":{"id":"grok-4.7","name":"Grok 4.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh"]}],"limit":{"context":500000,"output":500000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.5,"tiers":[{"input":4,"output":12,"cache_read":1,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":12,"cache_read":1}}},"grok-imagine-video-1.5-lite":{"id":"grok-imagine-video-1.5-lite","name":"Grok Imagine Video 1.5 Lite","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":1024,"output":0},"modalities":{"input":["text","image","pdf"]}},"grok-imagine-image":{"id":"grok-imagine-image","name":"Grok Imagine Image","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":16000,"output":0},"modalities":{"input":["text","image","pdf"]}},"grok-imagine-video":{"id":"grok-imagine-video","name":"Grok Imagine Video","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":1024,"output":0},"modalities":{"input":["text","image","video","pdf"]}},"grok-4.3":{"id":"grok-4.3","name":"Grok 4.3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high"]}],"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-4.20-0309-reasoning":{"id":"grok-4.20-0309-reasoning","name":"Grok 4.20 (Reasoning)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-imagine-video-1.5":{"id":"grok-imagine-video-1.5","name":"Grok Imagine Video 1.5","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":1024,"output":0},"modalities":{"input":["text","image","audio","pdf"]}},"grok-4.5":{"id":"grok-4.5","name":"Grok 4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":500000,"output":500000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.3,"tiers":[{"input":4,"output":12,"cache_read":0.6,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":12,"cache_read":0.6}}},"grok-4.20-0309-non-reasoning":{"id":"grok-4.20-0309-non-reasoning","name":"Grok 4.20 (Non-Reasoning)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-imagine-image-quality":{"id":"grok-imagine-image-quality","name":"Grok Imagine Image Quality","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":16000,"output":0},"modalities":{"input":["text","image","pdf"]}},"grok-build-0.1":{"id":"grok-build-0.1","name":"Grok Build 0.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":256000,"output":256000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1,"output":2,"cache_read":0.2,"tiers":[{"input":2,"output":4,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2,"output":4,"cache_read":0.4}}},"grok-4.20-multi-agent-0309":{"id":"grok-4.20-multi-agent-0309","name":"Grok 4.20 Multi-Agent","tool_call":false,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh"]}],"limit":{"context":1000000,"output":30000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.25,"output":2.5,"cache_read":0.2,"tiers":[{"input":2.5,"output":5,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":5,"cache_read":0.4}}},"grok-4.6":{"id":"grok-4.6","name":"Grok 4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh"]}],"limit":{"context":500000,"output":500000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.5,"tiers":[{"input":4,"output":12,"cache_read":1,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":12,"cache_read":1}}},"grok-imagine-image-2.0":{"id":"grok-imagine-image-2.0","name":"Grok Imagine Image 2.0","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":64000,"output":0},"modalities":{"input":["text","image","pdf"]}}}},"minimax":{"name":"MiniMax (minimax.io)","type":"OPENAI","baseUrl":"https://api.minimax.io/","models":{"MiniMax-M3":{"id":"MiniMax-M3","name":"MiniMax-M3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":1000000,"output":512000},"modalities":{"input":["text","image","video"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"tiers":[{"input":0.6,"output":2.4,"cache_read":0.12,"tier":{"type":"context","size":512000}}],"context_over_200k":{"input":0.6,"output":2.4,"cache_read":0.12}}},"MiniMax-M2.1":{"id":"MiniMax-M2.1","name":"MiniMax-M2.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.5":{"id":"MiniMax-M2.5","name":"MiniMax-M2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.7":{"id":"MiniMax-M2.7","name":"MiniMax-M2.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2.7-highspeed":{"id":"MiniMax-M2.7-highspeed","name":"MiniMax-M2.7-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2":{"id":"MiniMax-M2","name":"MiniMax-M2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2}},"MiniMax-M2.5-highspeed":{"id":"MiniMax-M2.5-highspeed","name":"MiniMax-M2.5-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}}}},"minimax-cn":{"name":"MiniMax (minimax.cn)","type":"OPENAI","baseUrl":"https://api.minimaxi.com/","models":{"MiniMax-M3":{"id":"MiniMax-M3","name":"MiniMax-M3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":1000000,"output":512000},"modalities":{"input":["text","image","video"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"tiers":[{"input":0.6,"output":2.4,"cache_read":0.12,"tier":{"type":"context","size":512000}}],"context_over_200k":{"input":0.6,"output":2.4,"cache_read":0.12}}},"MiniMax-M2.1":{"id":"MiniMax-M2.1","name":"MiniMax-M2.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.5":{"id":"MiniMax-M2.5","name":"MiniMax-M2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.03,"cache_write":0.375}},"MiniMax-M2.7":{"id":"MiniMax-M2.7","name":"MiniMax-M2.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2.7-highspeed":{"id":"MiniMax-M2.7-highspeed","name":"MiniMax-M2.7-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}},"MiniMax-M2":{"id":"MiniMax-M2","name":"MiniMax-M2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2}},"MiniMax-M2.5-highspeed":{"id":"MiniMax-M2.5-highspeed","name":"MiniMax-M2.5-highspeed","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.4,"cache_read":0.06,"cache_write":0.375}}}},"moonshotai":{"name":"Moonshot AI","type":"OPENAI","baseUrl":"https://api.moonshot.ai","models":{"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image","video"]},"cost":{"input":3,"output":15,"cache_read":0.3}},"kimi-k2.7-code-highspeed":{"id":"kimi-k2.7-code-highspeed","name":"Kimi K2.7 Code HighSpeed","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":1.9,"output":8,"cache_read":0.38}},"kimi-k2.6":{"id":"kimi-k2.6","name":"Kimi K2.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.16}},"kimi-k2.7-code":{"id":"kimi-k2.7-code","name":"Kimi K2.7 Code","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.19}},"kimi-k2-0711-preview":{"id":"kimi-k2-0711-preview","name":"Kimi K2 0711","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2-thinking-turbo":{"id":"kimi-k2-thinking-turbo","name":"Kimi K2 Thinking Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":1.15,"output":8,"cache_read":0.15}},"kimi-k2.5":{"id":"kimi-k2.5","name":"Kimi K2.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":3,"cache_read":0.1}},"kimi-k2-0905-preview":{"id":"kimi-k2-0905-preview","name":"Kimi K2 0905","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2-turbo-preview":{"id":"kimi-k2-turbo-preview","name":"Kimi K2 Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":2.4,"output":10,"cache_read":0.6}},"kimi-k2-thinking":{"id":"kimi-k2-thinking","name":"Kimi K2 Thinking","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}}}},"moonshotai-cn":{"name":"Moonshot AI (China)","type":"OPENAI","baseUrl":"https://api.moonshot.cn","models":{"kimi-k2.7-code":{"id":"kimi-k2.7-code","name":"Kimi K2.7 Code","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.19}},"kimi-k2.6":{"id":"kimi-k2.6","name":"Kimi K2.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.95,"output":4,"cache_read":0.16}},"kimi-k2.7-code-highspeed":{"id":"kimi-k2.7-code-highspeed","name":"Kimi K2.7 Code HighSpeed","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":1.9,"output":8,"cache_read":0.38}},"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image","video"]},"cost":{"input":3,"output":15,"cache_read":0.3}},"kimi-k2-thinking":{"id":"kimi-k2-thinking","name":"Kimi K2 Thinking","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2-turbo-preview":{"id":"kimi-k2-turbo-preview","name":"Kimi K2 Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":2.4,"output":10,"cache_read":0.6}},"kimi-k2-0905-preview":{"id":"kimi-k2-0905-preview","name":"Kimi K2 0905","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}},"kimi-k2.5":{"id":"kimi-k2.5","name":"Kimi K2.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":3,"cache_read":0.1}},"kimi-k2-thinking-turbo":{"id":"kimi-k2-thinking-turbo","name":"Kimi K2 Thinking Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":262144,"output":262144},"modalities":{"input":["text"]},"cost":{"input":1.15,"output":8,"cache_read":0.15}},"kimi-k2-0711-preview":{"id":"kimi-k2-0711-preview","name":"Kimi K2 0711","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.5,"cache_read":0.15}}}},"openai":{"name":"OpenAI","type":"OPENAI","baseUrl":"https://api.openai.com/","models":{"chatgpt-image-latest":{"id":"chatgpt-image-latest","name":"chatgpt-image-latest","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-5.4":{"id":"gpt-5.4","name":"GPT-5.4","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2.5,"output":15,"cache_read":0.25,"tiers":[{"input":5,"output":22.5,"cache_read":0.5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":5,"output":22.5,"cache_read":0.5}}},"gpt-5.4-pro":{"id":"gpt-5.4-pro","name":"GPT-5.4 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":30,"output":180,"tiers":[{"input":60,"output":270,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":60,"output":270}}},"gpt-3.5-turbo":{"id":"gpt-3.5-turbo","name":"GPT-3.5-turbo","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16385,"output":4096},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":1.5,"cache_read":0}},"gpt-5.5-pro":{"id":"gpt-5.5-pro","name":"GPT-5.5 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":30,"output":180,"tiers":[{"input":60,"output":270,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":60,"output":270}}},"text-embedding-3-small":{"id":"text-embedding-3-small","name":"text-embedding-3-small","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8191,"output":1536},"modalities":{"input":["text"]},"cost":{"input":0.02,"output":0}},"gpt-5.4-nano":{"id":"gpt-5.4-nano","name":"GPT-5.4 nano","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.2,"output":1.25,"cache_read":0.02}},"gpt-realtime-2.1":{"id":"gpt-realtime-2.1","name":"GPT-Realtime-2.1","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high","xhigh"]}],"limit":{"context":128000,"input":96000,"output":32000},"modalities":{"input":["text","audio","image"]},"cost":{"input":4,"output":24,"cache_read":0.4,"input_audio":32,"output_audio":64}},"gpt-4o-2024-05-13":{"id":"gpt-4o-2024-05-13","name":"GPT-4o (2024-05-13)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":5,"output":15}},"gpt-4o":{"id":"gpt-4o","name":"GPT-4o","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2.5,"output":10,"cache_read":1.25}},"gpt-5-mini":{"id":"gpt-5-mini","name":"GPT-5 Mini","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.25,"output":2,"cache_read":0.025}},"gpt-image-2":{"id":"gpt-image-2","name":"gpt-image-2","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]},"cost":{"input":5,"output":30,"cache_read":1.25}},"gpt-5.2-pro":{"id":"gpt-5.2-pro","name":"GPT-5.2 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":21,"output":168}},"o4-mini":{"id":"o4-mini","name":"o4-mini","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image"]},"cost":{"input":1.1,"output":4.4,"cache_read":0.275}},"o3-mini":{"id":"o3-mini","name":"o3-mini","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text"]},"cost":{"input":1.1,"output":4.4,"cache_read":0.55}},"text-embedding-ada-002":{"id":"text-embedding-ada-002","name":"text-embedding-ada-002","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8192,"output":1536},"modalities":{"input":["text"]},"cost":{"input":0.1,"output":0}},"gpt-4":{"id":"gpt-4","name":"GPT-4","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":8192},"modalities":{"input":["text"]},"cost":{"input":30,"output":60}},"gpt-5.3-codex":{"id":"gpt-5.3-codex","name":"GPT-5.3 Codex","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-4.1-nano":{"id":"gpt-4.1-nano","name":"GPT-4.1 nano","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1047576,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.1,"output":0.4,"cache_read":0.025}},"gpt-5-nano":{"id":"gpt-5-nano","name":"GPT-5 Nano","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.05,"output":0.4,"cache_read":0.005}},"gpt-5.6":{"id":"gpt-5.6","name":"GPT-5.6","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}}},"gpt-5.2-chat-latest":{"id":"gpt-5.2-chat-latest","name":"GPT-5.2 Chat","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["medium"]}],"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"o1":{"id":"o1","name":"o1","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":15,"output":60,"cache_read":7.5}},"gpt-5-pro":{"id":"gpt-5-pro","name":"GPT-5 Pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["high"]}],"limit":{"context":400000,"input":272000,"output":272000},"modalities":{"input":["text","image"]},"cost":{"input":15,"output":120}},"gpt-5.3-codex-spark":{"id":"gpt-5.3-codex-spark","name":"GPT-5.3 Codex Spark","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":128000,"input":100000,"output":32000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-6.1-sol":{"id":"gpt-6.1-sol","name":"GPT-6.1 Sol","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.1,"cache_write":2.5,"tiers":[{"input":4,"output":15,"cache_read":0.2,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":15,"cache_read":0.2,"cache_write":5}}},"text-embedding-3-large":{"id":"text-embedding-3-large","name":"text-embedding-3-large","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":8191,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.13,"output":0}},"gpt-4o-2024-08-06":{"id":"gpt-4o-2024-08-06","name":"GPT-4o (2024-08-06)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":2.5,"output":10,"cache_read":1.25}},"gpt-6-astra":{"id":"gpt-6-astra","name":"GPT-6 Astra","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":10,"output":50,"cache_read":1,"cache_write":12.5,"tiers":[{"input":20,"output":75,"cache_read":2,"cache_write":25,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":20,"output":75,"cache_read":2,"cache_write":25}}},"gpt-5.1":{"id":"gpt-5.1","name":"GPT-5.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":1.25,"output":10,"cache_read":0.125}},"gpt-4o-mini":{"id":"gpt-4o-mini","name":"GPT-4o mini","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.15,"output":0.6,"cache_read":0.075}},"o3-pro":{"id":"o3-pro","name":"o3-pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image"]},"cost":{"input":20,"output":80}},"gpt-image-1":{"id":"gpt-image-1","name":"gpt-image-1","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-daybreak-blue-latest":{"id":"gpt-daybreak-blue-latest","name":"Daybreak Blue","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}}},"gpt-5.4-mini":{"id":"gpt-5.4-mini","name":"GPT-5.4 mini","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":0.75,"output":4.5,"cache_read":0.075}},"gpt-5.6-luna":{"id":"gpt-5.6-luna","name":"GPT-5.6 Luna","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.2,"output":1.2,"cache_read":0.02,"cache_write":0.25,"tiers":[{"input":0.4,"output":1.8,"cache_read":0.04,"cache_write":0.5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":0.4,"output":1.8,"cache_read":0.04,"cache_write":0.5}}},"gpt-5.2":{"id":"gpt-5.2","name":"GPT-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-5.5":{"id":"gpt-5.5","name":"GPT-5.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":5,"output":30,"cache_read":0.5,"tiers":[{"input":10,"output":45,"cache_read":1,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":10,"output":45,"cache_read":1}}},"gpt-4.1":{"id":"gpt-4.1","name":"GPT-4.1","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1047576,"output":32768},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":8,"cache_read":0.5}},"gpt-4o-2024-11-20":{"id":"gpt-4o-2024-11-20","name":"GPT-4o (2024-11-20)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":2.5,"output":10,"cache_read":1.25}},"gpt-4.1-mini":{"id":"gpt-4.1-mini","name":"GPT-4.1 mini","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1047576,"output":32768},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.4,"output":1.6,"cache_read":0.1}},"gpt-6-luna":{"id":"gpt-6-luna","name":"GPT-6 Luna","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":0.1,"output":0.5,"cache_read":0.01,"cache_write":0.125,"tiers":[{"input":0.2,"output":0.75,"cache_read":0.02,"cache_write":0.25,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":0.2,"output":0.75,"cache_read":0.02,"cache_write":0.25}}},"gpt-5.3-chat-latest":{"id":"gpt-5.3-chat-latest","name":"GPT-5.3 Chat (latest)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":16384},"modalities":{"input":["text","image"]},"cost":{"input":1.75,"output":14,"cache_read":0.175}},"gpt-image-1-mini":{"id":"gpt-image-1-mini","name":"gpt-image-1-mini","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-image-1.5":{"id":"gpt-image-1.5","name":"gpt-image-1.5","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":0,"input":0,"output":0},"modalities":{"input":["text","image"]}},"gpt-5.6-terra":{"id":"gpt-5.6-terra","name":"GPT-5.6 Terra","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":12,"cache_read":0.2,"cache_write":2.5,"tiers":[{"input":4,"output":18,"cache_read":0.4,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4,"cache_write":5}}},"gpt-4-turbo":{"id":"gpt-4-turbo","name":"GPT-4 Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":128000,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":10,"output":30}},"gpt-daybreak-red-latest":{"id":"gpt-daybreak-red-latest","name":"Daybreak Red","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":12.5,"output":75,"cache_read":1.25,"cache_write":15.625}},"o3":{"id":"o3","name":"o3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":8,"cache_read":0.5}},"gpt-5":{"id":"gpt-5","name":"GPT-5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["minimal","low","medium","high"]}],"limit":{"context":400000,"input":272000,"output":128000},"modalities":{"input":["text","image"]},"cost":{"input":1.25,"output":10,"cache_read":0.125}},"gpt-5.6-sol":{"id":"gpt-5.6-sol","name":"GPT-5.6 Sol","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}}},"gpt-6-sol":{"id":"gpt-6-sol","name":"GPT-6 Sol","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","high","xhigh","max"]}],"limit":{"context":1050000,"input":922000,"output":128000},"modalities":{"input":["text","image","pdf"]},"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5,"tiers":[{"input":4,"output":15,"cache_read":0.4,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":15,"cache_read":0.4,"cache_write":5}}},"o1-pro":{"id":"o1-pro","name":"o1-pro","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high"]}],"limit":{"context":200000,"output":100000},"modalities":{"input":["text","image"]},"cost":{"input":150,"output":600}}}},"alibaba":{"name":"Alibaba","type":"OPENAI","baseUrl":"https://dashscope-intl.aliyuncs.com/compatible-mode","models":{"qwen-flash":{"id":"qwen-flash","name":"Qwen Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.05,"output":0.4}},"qwen3.5-flash":{"id":"qwen3.5-flash","name":"Qwen3.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.1,"output":0.4,"cache_read":0.01,"cache_write":0.125}},"qwen3.7-max":{"id":"qwen3.7-max","name":"Qwen3.7 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":2.5,"output":7.5,"cache_read":0.5,"cache_write":3.125}},"qwen2-5-32b-instruct":{"id":"qwen2-5-32b-instruct","name":"Qwen2.5 32B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.7,"output":2.8}},"qwq-plus":{"id":"qwq-plus","name":"QwQ Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.8,"output":2.4}},"qwen2-5-vl-72b-instruct":{"id":"qwen2-5-vl-72b-instruct","name":"Qwen2.5-VL 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":2.8,"output":8.4}},"qwen3-vl-plus":{"id":"qwen3-vl-plus","name":"Qwen3-VL Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.2,"output":1.6,"reasoning":4.8}},"qwen3.5-27b":{"id":"qwen3.5-27b","name":"Qwen3.5 27B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.3,"output":2.4}},"deepseek-v4-flash-0731":{"id":"deepseek-v4-flash-0731","name":"DeepSeek V4 Flash 0731","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.2,"output":0.4,"cache_read":0.04}},"qwen-max":{"id":"qwen-max","name":"Qwen Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":8192},"modalities":{"input":["text"]},"cost":{"input":1.6,"output":6.4}},"qwen3-next-80b-a3b-thinking":{"id":"qwen3-next-80b-a3b-thinking","name":"Qwen3-Next 80B-A3B (Thinking)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":6}},"qwen3.8-max":{"id":"qwen3.8-max","name":"Qwen3.8 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","min":0,"max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":2,"output":6,"cache_read":0.25,"cache_write":2.5}},"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image"]},"cost":{"input":3,"output":15,"cache_read":0.3}},"qwen-plus-character-ja":{"id":"qwen-plus-character-ja","name":"Qwen Plus Character (Japanese)","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":512},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":1.4}},"qwen3.5-plus":{"id":"qwen3.5-plus","name":"Qwen3.5 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.4,"output":2.4,"reasoning":2.4}},"qwen3-livetranslate-flash-realtime":{"id":"qwen3-livetranslate-flash-realtime","name":"Qwen3-LiveTranslate Flash Realtime","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":53248,"output":4096},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":10,"output":10,"input_audio":10,"output_audio":38}},"qwen2-5-72b-instruct":{"id":"qwen2-5-72b-instruct","name":"Qwen2.5 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":5.6}},"qwen3-32b":{"id":"qwen3-32b","name":"Qwen3 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.7,"output":2.8,"reasoning":8.4}},"qwen-plus":{"id":"qwen-plus","name":"Qwen Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.4,"output":1.2,"reasoning":4}},"qwen3.7-flash":{"id":"qwen3.7-flash","name":"Qwen3.7 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"input":991808,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.03,"output":0.13,"cache_read":0.003,"cache_write":0.0375,"tiers":[{"input":0.1,"output":0.4,"cache_read":0.01,"cache_write":0.125,"tier":{"type":"context","size":32000}},{"input":0.2,"output":0.8,"cache_read":0.02,"cache_write":0.25,"tier":{"type":"context","size":256000}}]}},"qwen3-max":{"id":"qwen3-max","name":"Qwen3 Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.2,"output":6}},"qwen-omni-turbo-realtime":{"id":"qwen-omni-turbo-realtime","name":"Qwen-Omni Turbo Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio"]},"cost":{"input":0.27,"output":1.07,"input_audio":4.44,"output_audio":8.89}},"qwen3-coder-480b-a35b-instruct":{"id":"qwen3-coder-480b-a35b-instruct","name":"Qwen3-Coder 480B-A35B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.5,"output":7.5,"tiers":[{"input":2.7,"output":13.5,"tier":{"type":"context","size":32000}},{"input":4.5,"output":22.5,"tier":{"type":"context","size":128000}}]}},"qwen-turbo":{"id":"qwen-turbo","name":"Qwen Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.05,"output":0.2,"reasoning":0.5}},"qwen3-coder-30b-a3b-instruct":{"id":"qwen3-coder-30b-a3b-instruct","name":"Qwen3-Coder 30B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.45,"output":2.25,"tiers":[{"input":0.75,"output":3.75,"tier":{"type":"context","size":32000}},{"input":1.2,"output":6,"tier":{"type":"context","size":128000}}]}},"qwen3.8-omni-flash":{"id":"qwen3.8-omni-flash","name":"Qwen3.8 Omni Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","xhigh"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.15,"output":0.47,"cache_read":0.016}},"qvq-max":{"id":"qvq-max","name":"QVQ Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":1.2,"output":4.8}},"qwen2-5-7b-instruct":{"id":"qwen2-5-7b-instruct","name":"Qwen2.5 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.175,"output":0.7}},"qwen3-omni-flash":{"id":"qwen3-omni-flash","name":"Qwen3-Omni Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.43,"output":1.66,"input_audio":3.81,"output_audio":15.11}},"qwen-vl-max":{"id":"qwen-vl-max","name":"Qwen-VL Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.8,"output":3.2}},"qwen3-235b-a22b":{"id":"qwen3-235b-a22b","name":"Qwen3 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.7,"output":2.8,"reasoning":8.4}},"qwen3-vl-30b-a3b":{"id":"qwen3-vl-30b-a3b","name":"Qwen3-VL 30B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.2,"output":0.8,"reasoning":2.4}},"qwen3.5-122b-a10b":{"id":"qwen3.5-122b-a10b","name":"Qwen3.5 122B-A10B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.4,"output":3.2}},"qwen3.6-35b-a3b":{"id":"qwen3.6-35b-a3b","name":"Qwen3.6 35B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.248,"output":1.485}},"qwen3-next-80b-a3b-instruct":{"id":"qwen3-next-80b-a3b-instruct","name":"Qwen3-Next 80B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":2}},"qwen-vl-plus":{"id":"qwen-vl-plus","name":"Qwen-VL Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.21,"output":0.63}},"qwen3.6-flash":{"id":"qwen3.6-flash","name":"Qwen3.6 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.1875,"output":1.125,"cache_write":0.234375}},"qwen3-coder-flash":{"id":"qwen3-coder-flash","name":"Qwen3 Coder Flash","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.5}},"qwen2-5-omni-7b":{"id":"qwen2-5-omni-7b","name":"Qwen2.5-Omni 7B","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.1,"output":0.4,"input_audio":6.76}},"qwen-mt-plus":{"id":"qwen-mt-plus","name":"Qwen-MT Plus","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":2.46,"output":7.37}},"qwen3-vl-235b-a22b":{"id":"qwen3-vl-235b-a22b","name":"Qwen3-VL 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.7,"output":2.8,"reasoning":8.4}},"qwen3-coder-plus":{"id":"qwen3-coder-plus","name":"Qwen3 Coder Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1,"output":5}},"qwen2-5-14b-instruct":{"id":"qwen2-5-14b-instruct","name":"Qwen2.5 14B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.35,"output":1.4}},"qwen3.8-flash":{"id":"qwen3.8-flash","name":"Qwen3.8 Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.15,"output":0.47,"cache_read":0.016,"cache_write":0.2}},"qwen2-5-vl-7b-instruct":{"id":"qwen2-5-vl-7b-instruct","name":"Qwen2.5-VL 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.35,"output":1.05}},"qwen3.5-35b-a3b":{"id":"qwen3.5-35b-a3b","name":"Qwen3.5 35B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.25,"output":2}},"qwen3.6-max-preview":{"id":"qwen3.6-max-preview","name":"Qwen3.6 Max Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.3,"output":7.8,"cache_read":0.13,"cache_write":1.625}},"qwen-mt-turbo":{"id":"qwen-mt-turbo","name":"Qwen-MT Turbo","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.16,"output":0.49}},"qwen3.5-397b-a17b":{"id":"qwen3.5-397b-a17b","name":"Qwen3.5 397B-A17B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.6,"output":3.6}},"qwen3-8b":{"id":"qwen3-8b","name":"Qwen3 8B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.18,"output":0.7,"reasoning":2.1}},"glm-5.2":{"id":"glm-5.2","name":"GLM-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","minimal","low","medium","high","xhigh","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.28,"cache_write":0}},"qwen-omni-turbo":{"id":"qwen-omni-turbo","name":"Qwen-Omni Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.07,"output":0.27,"input_audio":4.44,"output_audio":8.89}},"qwen3-omni-flash-realtime":{"id":"qwen3-omni-flash-realtime","name":"Qwen3-Omni Flash Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.52,"output":1.99,"input_audio":4.57,"output_audio":18.13}},"qwen3-asr-flash":{"id":"qwen3-asr-flash","name":"Qwen3-ASR Flash","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":53248,"output":4096},"modalities":{"input":["audio"]},"cost":{"input":0.035,"output":0.035}},"qwen3.6-27b":{"id":"qwen3.6-27b","name":"Qwen3.6 27B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video","audio"]},"cost":{"input":0.6,"output":3.6}},"qwen3.6-plus":{"id":"qwen3.6-plus","name":"Qwen3.6 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"cache_write":0.625,"tiers":[{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5,"tier":{"type":"context","size":256000}}],"context_over_200k":{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5}}},"qwen3-14b":{"id":"qwen3-14b","name":"Qwen3 14B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.35,"output":1.4,"reasoning":4.2}},"qwen3.7-plus":{"id":"qwen3.7-plus","name":"Qwen3.7 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens"}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.4,"output":1.6,"cache_read":0.04,"cache_write":0.5,"tiers":[{"input":1.2,"output":4.8,"cache_read":0.12,"cache_write":1.5,"tier":{"type":"context","size":256000}}],"context_over_200k":{"input":1.2,"output":4.8,"cache_read":0.12,"cache_write":1.5}}},"qwen-vl-ocr":{"id":"qwen-vl-ocr","name":"Qwen-VL OCR","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":34096,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":0.72,"output":0.72}}}},"alibaba-cn":{"name":"Alibaba (China)","type":"OPENAI","baseUrl":"https://dashscope.aliyuncs.com/compatible-mode","models":{"qwen-flash":{"id":"qwen-flash","name":"Qwen Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.022,"output":0.216}},"qwen3.5-flash":{"id":"qwen3.5-flash","name":"Qwen3.5 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.029,"output":0.287,"reasoning":0.287,"tiers":[{"input":0.115,"output":1.147,"reasoning":1.147,"tier":{"type":"context","size":128000}},{"input":0.172,"output":1.72,"reasoning":1.72,"tier":{"type":"context","size":256000}}]}},"qwen2-5-coder-32b-instruct":{"id":"qwen2-5-coder-32b-instruct","name":"Qwen2.5-Coder 32B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"deepseek-v3-2-exp":{"id":"deepseek-v3-2-exp","name":"DeepSeek V3.2 Exp","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.431}},"deepseek-r1-distill-qwen-14b":{"id":"deepseek-r1-distill-qwen-14b","name":"DeepSeek R1 Distill Qwen 14B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.431}},"qwen-math-plus":{"id":"qwen-math-plus","name":"Qwen Math Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"qwen-deep-research":{"id":"qwen-deep-research","name":"Qwen Deep Research","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":7.742,"output":23.367}},"qwen3.7-max":{"id":"qwen3.7-max","name":"Qwen3.7 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text"]},"cost":{"input":2.5,"output":7.5,"cache_read":0.5,"cache_write":3.125}},"qwen2-5-32b-instruct":{"id":"qwen2-5-32b-instruct","name":"Qwen2.5 32B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwq-plus":{"id":"qwq-plus","name":"QwQ Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.23,"output":0.574}},"qwq-32b":{"id":"qwq-32b","name":"QwQ 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwen2-5-vl-72b-instruct":{"id":"qwen2-5-vl-72b-instruct","name":"Qwen2.5-VL 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":2.294,"output":6.881}},"deepseek-v3-1":{"id":"deepseek-v3-1","name":"DeepSeek V3.1","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"qwen3-vl-plus":{"id":"qwen3-vl-plus","name":"Qwen3-VL Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.143353,"output":1.433525,"reasoning":4.300576}},"qwen-plus-character":{"id":"qwen-plus-character","name":"Qwen Plus Character","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":4096},"modalities":{"input":["text"]},"cost":{"input":0.115,"output":0.287}},"qwen-max":{"id":"qwen-max","name":"Qwen Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.345,"output":1.377}},"qwen3-next-80b-a3b-thinking":{"id":"qwen3-next-80b-a3b-thinking","name":"Qwen3-Next 80B-A3B (Thinking)","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":1.434}},"qwen3.8-max":{"id":"qwen3.8-max","name":"Qwen3.8 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","min":0,"max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":1.77744,"output":5.33231,"cache_read":0.22218,"cache_write":2.22179}},"kimi-k3":{"id":"kimi-k3","name":"Kimi K3","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1048576,"output":1048576},"modalities":{"input":["text","image"]},"cost":{"input":2.827,"output":14.133,"cache_read":0.283}},"qwen2-5-math-7b-instruct":{"id":"qwen2-5-math-7b-instruct","name":"Qwen2.5-Math 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.287}},"qwen3.5-plus":{"id":"qwen3.5-plus","name":"Qwen3.5 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.115,"output":0.688,"reasoning":0.688,"tiers":[{"input":0.287,"output":1.72,"reasoning":1.72,"tier":{"type":"context","size":128000}},{"input":0.573,"output":3.44,"reasoning":3.44,"tier":{"type":"context","size":256000}}]}},"qwen2-5-72b-instruct":{"id":"qwen2-5-72b-instruct","name":"Qwen2.5 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"glm-5":{"id":"glm-5","name":"GLM-5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":32768}],"limit":{"context":202752,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.573,"output":2.58,"tiers":[{"input":0.86,"output":3.154,"tier":{"type":"context","size":32000}}]}},"deepseek-v4.1-flash":{"id":"deepseek-v4.1-flash","name":"DeepSeek V4.1 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text","image"]},"cost":{"input":0.29754,"output":1.19015,"cache_read":0.01488}},"deepseek-r1-distill-llama-8b":{"id":"deepseek-r1-distill-llama-8b","name":"DeepSeek R1 Distill Llama 8B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0,"output":0}},"qwen3-32b":{"id":"qwen3-32b","name":"Qwen3 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":1.147,"reasoning":2.868}},"qwen2-5-coder-7b-instruct":{"id":"qwen2-5-coder-7b-instruct","name":"Qwen2.5-Coder 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.287}},"qwen-plus":{"id":"qwen-plus","name":"Qwen Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.115,"output":0.287,"reasoning":1.147,"cache_read":0.012,"cache_write":0.144}},"qwen3.7-flash":{"id":"qwen3.7-flash","name":"Qwen3.7 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"input":991808,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.02962,"output":0.1185,"cache_read":0.002962,"cache_write":0.03703,"tiers":[{"input":0.08887,"output":0.35549,"cache_read":0.008887,"cache_write":0.11109,"tier":{"type":"context","size":32000}},{"input":0.17774,"output":0.71098,"cache_read":0.017774,"cache_write":0.22218,"tier":{"type":"context","size":256000}}]}},"kimi-k2.6":{"id":"kimi-k2.6","name":"Moonshot Kimi K2.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":16384},"modalities":{"input":["text","image","video"]},"cost":{"input":0.929,"output":3.858}},"qwen3-max":{"id":"qwen3-max","name":"Qwen3 Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.359,"output":1.434,"reasoning":1.434,"tiers":[{"input":0.574,"output":2.294,"reasoning":2.294,"tier":{"type":"context","size":32000}},{"input":1.004,"output":4.014,"reasoning":4.014,"tier":{"type":"context","size":128000}}]}},"qwen-omni-turbo-realtime":{"id":"qwen-omni-turbo-realtime","name":"Qwen-Omni Turbo Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio"]},"cost":{"input":0.23,"output":0.918,"input_audio":3.584,"output_audio":7.168}},"deepseek-v3":{"id":"deepseek-v3","name":"DeepSeek V3","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":65536,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":1.147}},"deepseek-r1-distill-llama-70b":{"id":"deepseek-r1-distill-llama-70b","name":"DeepSeek R1 Distill Llama 70B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwen3-coder-480b-a35b-instruct":{"id":"qwen3-coder-480b-a35b-instruct","name":"Qwen3-Coder 480B-A35B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.861,"output":3.441,"tiers":[{"input":1.291,"output":5.161,"tier":{"type":"context","size":32000}},{"input":2.151,"output":8.602,"tier":{"type":"context","size":128000}}]}},"qwen-turbo":{"id":"qwen-turbo","name":"Qwen Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":1000000,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.044,"output":0.087,"reasoning":0.431}},"deepseek-r1-distill-qwen-7b":{"id":"deepseek-r1-distill-qwen-7b","name":"DeepSeek R1 Distill Qwen 7B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.144}},"qwen2-5-math-72b-instruct":{"id":"qwen2-5-math-72b-instruct","name":"Qwen2.5-Math 72B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":1.721}},"qwen3-coder-30b-a3b-instruct":{"id":"qwen3-coder-30b-a3b-instruct","name":"Qwen3-Coder 30B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":262144,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.216,"output":0.861,"tiers":[{"input":0.323,"output":1.291,"tier":{"type":"context","size":32000}},{"input":0.538,"output":2.151,"tier":{"type":"context","size":128000}}]}},"qwen-doc-turbo":{"id":"qwen-doc-turbo","name":"Qwen Doc Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.087,"output":0.144}},"kimi-k2-thinking":{"id":"kimi-k2-thinking","name":"Moonshot Kimi K2 Thinking","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"budget_tokens"}],"limit":{"context":262144,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"qwen3.8-omni-flash":{"id":"qwen3.8-omni-flash","name":"Qwen3.8 Omni Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","low","medium","xhigh"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.113,"output":0.382,"cache_read":0.014}},"qvq-max":{"id":"qvq-max","name":"QVQ Max","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":1.147,"output":4.588}},"qwen2-5-7b-instruct":{"id":"qwen2-5-7b-instruct","name":"Qwen2.5 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.144}},"qwen3-omni-flash":{"id":"qwen3-omni-flash","name":"Qwen3-Omni Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.058,"output":0.23,"input_audio":3.584,"output_audio":7.168}},"qwen-vl-max":{"id":"qwen-vl-max","name":"Qwen-VL Max","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.23,"output":0.574}},"qwen3-235b-a22b":{"id":"qwen3-235b-a22b","name":"Qwen3 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":1.147,"reasoning":2.868}},"qwen3-vl-30b-a3b":{"id":"qwen3-vl-30b-a3b","name":"Qwen3-VL 30B-A3B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.108,"output":0.431,"reasoning":1.076}},"kimi-k2.5":{"id":"kimi-k2.5","name":"Moonshot Kimi K2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":32768},"modalities":{"input":["text","image","video"]},"cost":{"input":0.574,"output":2.411}},"qwen3-next-80b-a3b-instruct":{"id":"qwen3-next-80b-a3b-instruct","name":"Qwen3-Next 80B-A3B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.574}},"qwen-vl-plus":{"id":"qwen-vl-plus","name":"Qwen-VL Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.115,"output":0.287}},"deepseek-r1-distill-qwen-1-5b":{"id":"deepseek-r1-distill-qwen-1-5b","name":"DeepSeek R1 Distill Qwen 1.5B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0,"output":0}},"qwen3.6-flash":{"id":"qwen3.6-flash","name":"Qwen3.6 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":131072}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.1875,"output":1.125,"cache_write":0.234375}},"qwen3-coder-flash":{"id":"qwen3-coder-flash","name":"Qwen3 Coder Flash","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1000000,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.574}},"qwen2-5-omni-7b":{"id":"qwen2-5-omni-7b","name":"Qwen2.5-Omni 7B","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.087,"output":0.345,"input_audio":5.448}},"qwen-mt-plus":{"id":"qwen-mt-plus","name":"Qwen-MT Plus","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.259,"output":0.775}},"tongyi-intent-detect-v3":{"id":"tongyi-intent-detect-v3","name":"Tongyi Intent Detect V3","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":8192,"output":1024},"modalities":{"input":["text"]},"cost":{"input":0.058,"output":0.144}},"qwen3-vl-235b-a22b":{"id":"qwen3-vl-235b-a22b","name":"Qwen3-VL 235B-A22B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":32768},"modalities":{"input":["text","image"]},"cost":{"input":0.286705,"output":1.14682,"reasoning":2.867051}},"qwen3-coder-plus":{"id":"qwen3-coder-plus","name":"Qwen3 Coder Plus","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":1048576,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.296,"tiers":[{"input":0.861,"output":3.444,"tier":{"type":"context","size":32000}},{"input":1.435,"output":5.74,"tier":{"type":"context","size":128000}},{"input":2.87,"output":28.7,"tier":{"type":"context","size":256000}}]}},"moonshot-kimi-k2-instruct":{"id":"moonshot-kimi-k2-instruct","name":"Moonshot Kimi K2 Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"MiniMax-M2.5":{"id":"MiniMax-M2.5","name":"MiniMax-M2.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2}},"qwen2-5-14b-instruct":{"id":"qwen2-5-14b-instruct","name":"Qwen2.5 14B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.431}},"qwen3.8-flash":{"id":"qwen3.8-flash","name":"Qwen3.8 Flash","tool_call":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["low","medium","xhigh"]},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video"]},"cost":{"input":0.11875,"output":0.40073,"cache_read":0.01187,"cache_write":0.14844}},"qwen2-5-vl-7b-instruct":{"id":"qwen2-5-vl-7b-instruct","name":"Qwen2.5-VL 7B Instruct","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":131072,"output":8192},"modalities":{"input":["text","image"]},"cost":{"input":0.287,"output":0.717}},"qwen-math-turbo":{"id":"qwen-math-turbo","name":"Qwen Math Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":4096,"output":3072},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"qwen-long":{"id":"qwen-long","name":"Qwen Long","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":10000000,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.287}},"qwen3.6-max-preview":{"id":"qwen3.6-max-preview","name":"Qwen3.6 Max Preview","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":131072}],"limit":{"context":245800,"output":65536},"modalities":{"input":["text"]},"cost":{"input":1.32,"output":7.9,"cache_read":0.132}},"qwen-mt-turbo":{"id":"qwen-mt-turbo","name":"Qwen-MT Turbo","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":16384,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.101,"output":0.28}},"qwen3.5-397b-a17b":{"id":"qwen3.5-397b-a17b","name":"Qwen3.5 397B-A17B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":262144,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.172,"output":1.032,"reasoning":1.032,"tiers":[{"input":0.43,"output":2.58,"reasoning":2.58,"tier":{"type":"context","size":128000}}]}},"qwen3-8b":{"id":"qwen3-8b","name":"Qwen3 8B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.072,"output":0.287,"reasoning":0.717}},"glm-5.2":{"id":"glm-5.2","name":"GLM-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":1000000,"output":128000},"modalities":{"input":["text"]},"cost":{"input":1.1,"output":3.851,"cache_read":0.275,"cache_write":0}},"qwen-omni-turbo":{"id":"qwen-omni-turbo","name":"Qwen-Omni Turbo","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":32768,"output":2048},"modalities":{"input":["text","image","audio","video"]},"cost":{"input":0.058,"output":0.23,"input_audio":3.584,"output_audio":7.168}},"glm-5.1":{"id":"glm-5.1","name":"GLM-5.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":131072}],"limit":{"context":202752,"output":128000},"modalities":{"input":["text"]},"cost":{"input":0.825,"output":3.301,"cache_read":0.17,"tiers":[{"input":1.1,"output":3.851,"tier":{"type":"context","size":32000}}]}},"qwen3-omni-flash-realtime":{"id":"qwen3-omni-flash-realtime","name":"Qwen3-Omni Flash Realtime","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":65536,"output":16384},"modalities":{"input":["text","image","audio"]},"cost":{"input":0.23,"output":0.918,"input_audio":3.584,"output_audio":7.168}},"qwen3-asr-flash":{"id":"qwen3-asr-flash","name":"Qwen3-ASR Flash","tool_call":false,"temperature":false,"reasoning":false,"limit":{"context":53248,"output":4096},"modalities":{"input":["audio"]},"cost":{"input":0.032,"output":0.032}},"deepseek-r1-distill-qwen-32b":{"id":"deepseek-r1-distill-qwen-32b","name":"DeepSeek R1 Distill Qwen 32B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":32768,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.287,"output":0.861}},"deepseek-r1":{"id":"deepseek-r1","name":"DeepSeek R1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"deepseek-v4-pro":{"id":"deepseek-v4-pro","name":"DeepSeek V4 Pro","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.435,"output":0.87,"cache_read":0.003625}},"deepseek-r1-0528":{"id":"deepseek-r1-0528","name":"DeepSeek R1 0528","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":131072,"output":16384},"modalities":{"input":["text"]},"cost":{"input":0.574,"output":2.294}},"qwen3.6-plus":{"id":"qwen3.6-plus","name":"Qwen3.6 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":81920}],"limit":{"context":1000000,"output":65536},"modalities":{"input":["text","image","video"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"cache_write":0.625,"tiers":[{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5,"tier":{"type":"context","size":256000}}],"context_over_200k":{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5}}},"qwen3-14b":{"id":"qwen3-14b","name":"Qwen3 14B","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":38912}],"limit":{"context":131072,"output":8192},"modalities":{"input":["text"]},"cost":{"input":0.144,"output":0.574,"reasoning":1.434}},"glm-5.3":{"id":"glm-5.3","name":"GLM-5.3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.1,"output":3.851,"cache_read":0.275,"cache_write":0}},"qwen3.7-plus":{"id":"qwen3.7-plus","name":"Qwen3.7 Plus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"budget_tokens","max":262144}],"limit":{"context":1000000,"output":64000},"modalities":{"input":["text","image","video"]},"cost":{"input":0.5,"output":3,"cache_read":0.05,"cache_write":0.625,"tiers":[{"input":2,"output":6,"cache_read":0.2,"cache_write":2.5,"tier":{"type":"context","size":128000}}]}},"deepseek-v4-flash":{"id":"deepseek-v4-flash","name":"DeepSeek V4 Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"},{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":384000},"modalities":{"input":["text"]},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028}},"qwen-vl-ocr":{"id":"qwen-vl-ocr","name":"Qwen-VL OCR","tool_call":false,"temperature":true,"reasoning":false,"limit":{"context":34096,"output":4096},"modalities":{"input":["text","image"]},"cost":{"input":0.043,"output":0.072}},"siliconflow/deepseek-v3-0324":{"id":"siliconflow/deepseek-v3-0324","name":"siliconflow/deepseek-v3-0324","tool_call":true,"temperature":true,"reasoning":false,"limit":{"context":163840,"output":163840},"modalities":{"input":["text"]},"cost":{"input":0.25,"output":1}},"siliconflow/deepseek-v3.1-terminus":{"id":"siliconflow/deepseek-v3.1-terminus","name":"siliconflow/deepseek-v3.1-terminus","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":163840,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.27,"output":1}},"siliconflow/deepseek-r1-0528":{"id":"siliconflow/deepseek-r1-0528","name":"siliconflow/deepseek-r1-0528","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":163840,"output":32768},"modalities":{"input":["text"]},"cost":{"input":0.5,"output":2.18}},"siliconflow/deepseek-v3.2":{"id":"siliconflow/deepseek-v3.2","name":"siliconflow/deepseek-v3.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":163840,"output":65536},"modalities":{"input":["text"]},"cost":{"input":0.27,"output":0.42}},"kimi/kimi-k2.5":{"id":"kimi/kimi-k2.5","name":"kimi/kimi-k2.5","tool_call":true,"temperature":false,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":262144,"output":262144},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":3,"cache_read":0.1}},"MiniMax/MiniMax-M2.7":{"id":"MiniMax/MiniMax-M2.7","name":"MiniMax-M2.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.3,"output":1.2,"cache_read":0.06,"cache_write":0.375}}}},"zhipuai":{"name":"Zhipu AI","type":"OPENAI","baseUrl":"https://open.bigmodel.cn/api/paas/v4","models":{"glm-5v-turbo":{"id":"glm-5v-turbo","name":"GLM-5V-Turbo","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":5,"output":22,"cache_read":1.2,"cache_write":0}},"glm-5.3-flash":{"id":"glm-5.3-flash","name":"GLM-5.3-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":0.15,"output":0.5,"cache_read":0.03,"cache_write":0}},"glm-5":{"id":"glm-5","name":"GLM-5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1,"output":3.2,"cache_read":0.2,"cache_write":0}},"glm-5.3-flashx":{"id":"glm-5.3-flashx","name":"GLM-5.3-FlashX","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text","image","video","pdf"]},"cost":{"input":0.37,"output":1.25,"cache_read":0.075,"cache_write":0}},"glm-5.2":{"id":"glm-5.2","name":"GLM-5.2","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.26,"cache_write":0}},"glm-5.1":{"id":"glm-5.1","name":"GLM-5.1","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.26,"cache_write":0}},"glm-5.3":{"id":"glm-5.3","name":"GLM-5.3","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","high","max"]}],"limit":{"context":1000000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":1.4,"output":4.4,"cache_read":0.26,"cache_write":0}},"glm-4.5-air":{"id":"glm-4.5-air","name":"GLM-4.5-Air","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":131072,"output":98304},"modalities":{"input":["text"]},"cost":{"input":0.2,"output":1.1,"cache_read":0.03,"cache_write":0}},"glm-4.7-flashx":{"id":"glm-4.7-flashx","name":"GLM-4.7-FlashX","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.07,"output":0.4,"cache_read":0.01,"cache_write":0}},"glm-4.5-flash":{"id":"glm-4.5-flash","name":"GLM-4.5-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":131072,"output":98304},"modalities":{"input":["text"]},"cost":{"input":0,"output":0,"cache_read":0,"cache_write":0}},"glm-4.7":{"id":"glm-4.7","name":"GLM-4.7","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.2,"cache_read":0.11,"cache_write":0}},"glm-4.7-flash":{"id":"glm-4.7-flash","name":"GLM-4.7-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":200000,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0,"output":0,"cache_read":0,"cache_write":0}},"glm-4.5v":{"id":"glm-4.5v","name":"GLM-4.5V","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":64000,"output":16384},"modalities":{"input":["text","image","video"]},"cost":{"input":0.6,"output":1.8}},"glm-4.6v-flash":{"id":"glm-4.6v-flash","name":"GLM-4.6V-Flash","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":128000,"output":32768},"modalities":{"input":["text","image","video"]},"cost":{"input":0,"output":0,"cache_read":0,"cache_write":0}},"glm-4.6":{"id":"glm-4.6","name":"GLM-4.6","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":204800,"output":131072},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.2,"cache_read":0.11,"cache_write":0}},"glm-4.5":{"id":"glm-4.5","name":"GLM-4.5","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":131072,"output":98304},"modalities":{"input":["text"]},"cost":{"input":0.6,"output":2.2,"cache_read":0.11,"cache_write":0}},"glm-4.6v":{"id":"glm-4.6v","name":"GLM-4.6V","tool_call":true,"temperature":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"limit":{"context":128000,"output":32768},"modalities":{"input":["text","image","video"]},"cost":{"input":0.3,"output":0.9}}}}} \ No newline at end of file diff --git a/app/src/main/assets/prompts/60-tools-and-paths.md b/app/src/main/assets/prompts/60-tools-and-paths.md index 19ba0d9a3..7ea26e461 100644 --- a/app/src/main/assets/prompts/60-tools-and-paths.md +++ b/app/src/main/assets/prompts/60-tools-and-paths.md @@ -23,7 +23,9 @@ - AI 配置目录为 `~/.aicode`;工具完整输出日志在 `~/.aicode/tool-output/...`。 ## 子代理 -- 用 `task` 创建子代理并行工作,适用于可独立完成、不依赖当前对话细节的子任务(大范围调研、批量定位、跑验证、查资料)。通常开 1–2 个,最多同时运行 5 个。 +- **优先考虑用子代理**:只要存在可独立完成、不依赖当前对话细节的子任务(大范围调研、批量定位、并行探索多个方向、跑验证、查资料、独立实现),就主动开子代理去做,而不是自己在主会话里串行完成。宁可多开几个也不要因为“自己也能做”而全部亲历亲为。 +- 子代理适合:探索/调研代码、并行对比多个方案、跑构建/测试、独立的小改动;主会话保留决策、跨子任务综合与最终收口。 +- 用 `task` 创建子代理并行工作。通常开 2–3 个,最多同时运行 5 个。 - 子代理看不到本会话的任何上下文,`prompt` 必须自带:目标与完成标准、已知上下文(文件路径、已核实结论、已排除方向、用户约束)、边界(不该动什么)、期望产出。 - 只能读到子代理的最后一条回复;子代理完成后系统会通知,不要轮询。可用 `task(action="send", ...)` 追加指令;子代理也可用 `messageParent` 主动汇报。 - 子代理继承当前模式(PLAN 下同样只读),不能嵌套创建子代理。 diff --git a/app/src/main/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCase.kt b/app/src/main/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCase.kt index 59fb3ea0e..3a4ae639b 100644 --- a/app/src/main/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCase.kt +++ b/app/src/main/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCase.kt @@ -9,6 +9,8 @@ import com.aicode.feature.agent.domain.model.CONTEXT_SUMMARY_LEGACY_PREFIX import com.aicode.feature.agent.domain.tool.ToolCall import com.aicode.feature.agent.presentation.AgentAttachment import com.aicode.feature.agent.presentation.MessageRole +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -165,6 +167,12 @@ class MessagePersistenceUseCase @Inject constructor( /** 内嵌 base64 图片 data URL(`data:image/...;base64,...`)。 */ private val INLINE_BASE64_IMAGE = Regex("""data:image/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=\r\n]+""") + internal fun orderHistoryEntities(entities: List): List { + // 聊天按压缩触发时间展示;模型必须先读摘要,再读保留历史。 + val (compaction, retained) = entities.partition { it.isCompactionMarker || it.isContextSummary } + return compaction + retained + } + /** UTF-8 单字符最多占 3 字节,故 [String.length] 的三倍不超上限时无需实际编码即可放行。 */ private fun definitelyFits(raw: String, maxBytes: Int): Boolean = raw.length.toLong() * 3 <= maxBytes @@ -228,7 +236,9 @@ class MessagePersistenceUseCase @Inject constructor( } } } - val messages = buildHistoryUncached(sessionId, pendingToolMarker) + val messages = withContext(Dispatchers.IO) { + buildHistoryUncached(sessionId, pendingToolMarker) + } synchronized(historyCache) { historyCache[sessionId] = HistoryEntry(version, pendingToolMarker, messages) } @@ -238,6 +248,7 @@ class MessagePersistenceUseCase @Inject constructor( private suspend fun buildHistoryUncached(sessionId: String, pendingToolMarker: String): List { val entities = agentMessageDao.getMessagesBySessionOnce(sessionId) .filter { !it.isCompacted && !it.isContextExcluded } + .let { orderHistoryEntities(it) } // 第一遍:求 assistant 声明的 toolCallId 与 tool 结果 toolCallId 的交集。 val declaredIds = mutableSetOf() diff --git a/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserManager.kt b/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserManager.kt index d0f6d2bd9..e7825f1f0 100644 --- a/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserManager.kt +++ b/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserManager.kt @@ -23,6 +23,7 @@ import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import android.widget.FrameLayout +import androidx.webkit.UserAgentMetadata import androidx.webkit.WebSettingsCompat import androidx.webkit.WebViewFeature import com.aicode.R @@ -115,6 +116,35 @@ class BrowserManager @Inject constructor( private const val DIALOG_TIMEOUT_MS = 30_000L const val MAX_TABS = 10 + /** + * 自动化特征遮蔽脚本:在 UA 非默认时于页面开始加载时注入, + * 让站点看到的 navigator.webdriver / chrome 等与真实浏览器一致。 + */ + private val AUTOMATION_MASK_JS = """ + (function(){ + try { + Object.defineProperty(Navigator.prototype, 'webdriver', { get: function(){ return false; }, configurable: true }); + } catch(e){} + try { + if (window.chrome === undefined) { window.chrome = { runtime: {} }; } + } catch(e){} + try { + var origQuery = window.navigator.permissions && window.navigator.permissions.query; + if (origQuery) { + window.navigator.permissions.query = function(params){ + if (params && params.name === 'notifications') { + return Promise.resolve({ state: Notification.permission }); + } + return origQuery.apply(this, arguments); + }; + } + } catch(e){} + try { + Object.defineProperty(navigator, 'languages', { get: function(){ return ['zh-CN','zh','en']; }, configurable: true }); + } catch(e){} + })(); + """.trimIndent() + /** 地址栏输入不像网址时,作为关键词交给该搜索引擎(Bing)。 */ private const val SEARCH_ENGINE_URL = "https://www.bing.com/search?q=" @@ -168,7 +198,17 @@ class BrowserManager @Inject constructor( private val nightMode: Boolean get() = nightModeOverride ?: appDarkTheme + /** 当前生效的 UA 预设(由设置页推送;默认系统默认)。 */ + @Volatile + private var userAgent: BrowserUserAgent = BrowserUserAgent.DEFAULT + private val _state = MutableStateFlow(BrowserState()) + + /** 由设置页推送 UA 预设,并立即应用到所有标签页。 */ + fun setUserAgent(userAgent: BrowserUserAgent) { + this.userAgent = userAgent + tabs.forEach { tab -> applyUserAgent(tab.webView) } + } val state: StateFlow = _state.asStateFlow() /** 选择器辅助函数,prepend 到所有需要选择器的 JS 中。支持 ref= / text= / text*= / role= / xpath= / CSS。 */ @@ -273,6 +313,10 @@ class BrowserManager @Inject constructor( tab.error = null view?.setBackgroundColor(if (nightMode) NIGHT_BG_COLOR else Color.WHITE) if (view != null && nightMode) applyNightModeTo(view) + // 非系统默认 UA 时,页面开始加载即注入自动化特征遮蔽(尽早生效)。 + if (view != null && userAgent != BrowserUserAgent.DEFAULT) { + runCatching { view.evaluateJavascript(AUTOMATION_MASK_JS, null) } + } publishState() } @@ -372,6 +416,7 @@ class BrowserManager @Inject constructor( wv.setLayerType(View.LAYER_TYPE_HARDWARE, null) wv.setBackgroundColor(if (nightMode) NIGHT_BG_COLOR else Color.WHITE) applyDarkThemeToSettings(wv) + applyUserAgent(wv) wv.addJavascriptInterface(BrowserJsBridge(tabId), "__browserBridge__") // 设置 Headless 默认尺寸,保证后台与离屏测试可用 @@ -466,6 +511,47 @@ class BrowserManager @Inject constructor( private fun webViewThemeRes(night: Boolean): Int = if (night) R.style.Theme_AICode_WebView_Dark else R.style.Theme_AICode_WebView + // ================= User-Agent ================= + + /** + * 应用当前 UA 预设。 + * + * [BrowserUserAgent.DEFAULT]:还原系统默认 UA(清空自定义 UA,并清空客户端提示覆盖)。 + * 其余预设:套用对应 UA 字符串与 sec-ch-ua 客户端提示平台信息; + * 自动化特征遮蔽由 [AUTOMATION_MASK_JS] 在页面开始时注入。 + */ + private fun applyUserAgent(wv: WebView) { + val ua = userAgent + val uaString = ua.userAgent + if (uaString == null) { + wv.settings.userAgentString = null + runCatching { + WebSettingsCompat.setUserAgentMetadata( + wv.settings, + UserAgentMetadata.Builder().build() + ) + } + return + } + wv.settings.userAgentString = uaString + runCatching { + val builder = UserAgentMetadata.Builder() + .setBrandVersionList( + listOf( + UserAgentMetadata.BrandVersion.Builder().setBrand("Google Chrome").setMajorVersion("131").build(), + UserAgentMetadata.BrandVersion.Builder().setBrand("Chromium").setMajorVersion("131").build(), + UserAgentMetadata.BrandVersion.Builder().setBrand("Not_A Brand").setMajorVersion("24").build() + ) + ) + .setMobile(ua.mobile) + .setBitness(ua.bitness) + ua.platform?.let { builder.setPlatform(it) } + ua.platformVersion?.let { builder.setPlatformVersion(it) } + ua.architecture?.let { builder.setArchitecture(it) } + WebSettingsCompat.setUserAgentMetadata(wv.settings, builder.build()) + } + } + /** * 内核级暗色设置。prefers-color-scheme 实际由 WebView 主题的 isLightTheme 决定(见 [webViewThemeRes]), * 这里设置算法调暗并借此触发 WebView 重读偏好——夜间模式切换后需要一次 settings 变更才会下发新偏好。 diff --git a/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserTool.kt b/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserTool.kt index 54eef3ac8..1030a0b0b 100644 --- a/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserTool.kt +++ b/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserTool.kt @@ -20,7 +20,8 @@ import javax.inject.Inject class BrowserTool @Inject constructor( private val browserManager: BrowserManager, - private val fileAccess: FileAccessProvider + private val fileAccess: FileAccessProvider, + private val browserSettingsRepository: com.aicode.feature.settings.data.repository.BrowserSettingsRepository ) : AgentTool() { private companion object { @@ -101,6 +102,14 @@ class BrowserTool @Inject constructor( ?: return ToolResult.Error("缺少必需参数:action", "MISSING_ACTION") val tabId = args["tabId"]?.jsonPrimitive?.contentOrNull?.trim() + // AI 控制浏览器权限:用户在设置页逐 action 关闭的项,执行前直接拒绝。 + if (!browserSettingsRepository.isActionEnabled(action)) { + return ToolResult.Error( + "浏览器操作「$action」已被用户在设置中禁用", + "ACTION_DISABLED_BY_USER" + ) + } + return try { executeAction(action, tabId, args) } catch (e: IllegalStateException) { diff --git a/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserUserAgent.kt b/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserUserAgent.kt new file mode 100644 index 000000000..7871fe396 --- /dev/null +++ b/app/src/main/java/com/aicode/feature/agent/domain/tool/browser/BrowserUserAgent.kt @@ -0,0 +1,109 @@ +package com.aicode.feature.agent.domain.tool.browser + +/** + * 浏览器 User-Agent 预设。 + * + * 用户在「设置 → 运行环境 → 浏览器 → UA 设置」中选择,浏览器新建标签时套用对应的 + * User-Agent 字符串与客户端提示(client hints)平台信息。 + * + * [DEFAULT] 表示不覆盖,使用系统 WebView 的默认 UA;其余为常见设备/系统 UA。 + * [WINDOWS_10] 与 [WINDOWS_11] 的 UA 字符串一致(Windows 11 的 UA 仍报 Windows NT 10.0), + * 区别体现在客户端提示的 platform-version。 + */ +enum class BrowserUserAgent( + val id: String, + /** Android 手机 UA。 */ + val userAgent: String?, + /** 客户端提示 sec-ch-ua-platform(含引号按原样传入),null 表示不设置。 */ + val platform: String? = null, + val platformVersion: String? = null, + val architecture: String? = null, + val bitness: Int = 64, + val mobile: Boolean = false +) { + /** 系统默认,不覆盖 UA。 */ + DEFAULT("default", null), + + /** 安卓手机。 */ + ANDROID_PHONE( + id = "android_phone", + userAgent = "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", + platform = "Android", + platformVersion = "14.0.0", + mobile = true + ), + + /** 安卓平板。 */ + ANDROID_TABLET( + id = "android_tablet", + userAgent = "Mozilla/5.0 (Linux; Android 14; Pixel Tablet) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + platform = "Android", + platformVersion = "14.0.0", + mobile = false + ), + + /** Windows 10 电脑。 */ + WINDOWS_10( + id = "windows_10", + userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + platform = "Windows", + platformVersion = "10.0.0", + architecture = "x86" + ), + + /** Windows 11 电脑(UA 同 Win10,客户端提示版本不同)。 */ + WINDOWS_11( + id = "windows_11", + userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + platform = "Windows", + platformVersion = "13.0.0", + architecture = "x86" + ), + + /** Mac 电脑(Intel 芯片)。 */ + MAC_INTEL( + id = "mac_intel", + userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + platform = "macOS", + platformVersion = "15.0.0", + architecture = "x86" + ), + + /** Mac 电脑(Apple 芯片)。 */ + MAC_APPLE_SILICON( + id = "mac_apple_silicon", + userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + platform = "macOS", + platformVersion = "15.0.0", + architecture = "arm" + ); + + companion object { + fun fromId(id: String?): BrowserUserAgent = + entries.firstOrNull { it.id == id } ?: DEFAULT + } +} + +/** + * 浏览器工具可被 AI 调用的全部 action(与 [com.aicode.feature.agent.domain.tool.browser.BrowserTool] 的 actionEnum 一致)。 + * + * 作为「AI 控制浏览器权限」开关的唯一事实源:设置页按此顺序逐项渲染开关, + * 权限默认全开;用户关闭某项后,BrowserTool 执行时直接拒绝该 action。 + */ +object BrowserActionCatalog { + /** action 名,顺序即设置页展示顺序。 */ + val ALL: List = listOf( + "navigate", "evaluate", + "click", "fill", "select", "hover", "press", + "getText", "getHtml", "getBackbone", "screenshot", "console", "wait", + "scroll", "dialog", + "back", "forward", "reload", + "newTab", "closeTab", "selectTab", "listTabs" + ) +} diff --git a/app/src/main/java/com/aicode/feature/agent/domain/workflow/ContextCompactor.kt b/app/src/main/java/com/aicode/feature/agent/domain/workflow/ContextCompactor.kt index 25c914344..d4798a28f 100644 --- a/app/src/main/java/com/aicode/feature/agent/domain/workflow/ContextCompactor.kt +++ b/app/src/main/java/com/aicode/feature/agent/domain/workflow/ContextCompactor.kt @@ -22,6 +22,9 @@ import com.aicode.feature.settings.domain.model.ModelContextPolicy import com.aicode.feature.settings.domain.model.ProviderType import kotlinx.coroutines.CancellationException import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray @@ -62,6 +65,25 @@ class ContextCompactor @Inject constructor( tools: List = emptyList(), currentInputTokens: Int = 0, onEvent: suspend (AgentEvent) -> Unit = {} + ): CompactionResult { + val eventContext = currentCoroutineContext().minusKey(Job) + return withContext(Dispatchers.Default) { + compactOnBackground(messages, aiProvider, sessionId, force, windowProvider, systemPrompt, tools, currentInputTokens) { event -> + withContext(eventContext) { onEvent(event) } + } + } + } + + private suspend fun compactOnBackground( + messages: List, + aiProvider: AIProvider, + sessionId: String?, + force: Boolean, + windowProvider: AIProvider?, + systemPrompt: String, + tools: List, + currentInputTokens: Int, + onEvent: suspend (AgentEvent) -> Unit ): CompactionResult { val unchanged = CompactionResult(messages, compacted = false) val windowModel = windowProvider ?: aiProvider @@ -74,6 +96,8 @@ class ContextCompactor @Inject constructor( onEvent(AgentEvent.CompactionStarted(currentTokens)) val originalOutputLimit = aiProvider.maxOutputTokens + val markerTimestamp = if (sessionId != null) messagePersistenceUseCase.nextTimestamp() else null + val summaryTimestamp = if (sessionId != null) messagePersistenceUseCase.nextTimestamp() else null try { var splitIndex = CompactionText.selectTailStartIndex(messages, inputBudget) if (force && splitIndex <= 0) splitIndex = messages.lastIndex @@ -85,16 +109,14 @@ class ContextCompactor @Inject constructor( check(material.isNotEmpty()) { "No new history to summarize" } val headIds = head.map { it.id }.filter { it.isNotBlank() }.distinct() - val anchorTs = if (sessionId != null) { + if (sessionId != null) { check(headIds.isNotEmpty() && head.all { it.id.isNotBlank() }) { "History has no stable persistence IDs" } val entities = agentMessageDao.getMessagesBySessionOnce(sessionId) val persistedIds = entities.mapTo(HashSet()) { it.id } check(headIds.all { it in persistedIds }) { "History persistence is not complete yet" } val tailIds = tail.map { it.id }.toSet() - val timestamp = entities.filter { it.id in tailIds }.minOfOrNull { it.timestamp } - check(timestamp != null && timestamp > Long.MIN_VALUE + 2) { "Retained history has no persisted timestamp anchor" } - timestamp - } else null + check(entities.any { it.id in tailIds }) { "Retained history has no persisted timestamp anchor" } + } val summaryMetadata = modelMetadataService.resolve(aiProvider.providerId, inferProviderType(aiProvider), aiProvider.model) val summaryContext = summaryMetadata.contextTokens.takeIf { it > 0 } ?: ModelContextPolicy.DEFAULT_CONTEXT_TOKENS @@ -106,7 +128,9 @@ class ContextCompactor @Inject constructor( ) aiProvider.maxOutputTokens = outputLimit val summaryBudget = minOf(ModelContextPolicy.effectiveInputBudget(summaryMetadata), summaryContext - outputLimit) - val prompt = systemPromptProvider.resolvePrompt("agent/compact-summary.md").replace(LEADING_COMMENT, "") + val prompt = withContext(Dispatchers.IO) { + systemPromptProvider.resolvePrompt("agent/compact-summary.md").replace(LEADING_COMMENT, "") + } var summary = extractPreviousSummary(head) val cursor = CompactionText.Cursor(CompactionText.units(material)) var block = 0 @@ -138,9 +162,9 @@ class ContextCompactor @Inject constructor( headIds = headIds, messages = listOf( AgentMessageEntity(id = markerId, sessionId = sessionId, role = MessageRole.USER.name, - content = CONTEXT_COMPACTION_MARKER, timestamp = requireNotNull(anchorTs) - 2, isCompactionMarker = true), + content = CONTEXT_COMPACTION_MARKER, timestamp = requireNotNull(markerTimestamp), isCompactionMarker = true), AgentMessageEntity(id = summaryId, sessionId = sessionId, role = MessageRole.ASSISTANT.name, - content = summary, timestamp = requireNotNull(anchorTs) - 1, isContextSummary = true) + content = summary, timestamp = requireNotNull(summaryTimestamp), isContextSummary = true) ), summaryId = summaryId ) @@ -323,13 +347,19 @@ internal object CompactionText { fun next(budget: Int): String { val result = StringBuilder() + var ascii = 0 + var other = 0 while (!finished) { val unit = units[index] val label = "[history-unit ${index + 1}, character-offset $offset]\n" val remaining = unit.substring(offset) - val candidate = result.toString() + label + remaining + "\n\n" - if (tokens(candidate) <= budget) { - result.append(label).append(remaining).append("\n\n") + val fragment = label + remaining + "\n\n" + val fragmentAscii = fragment.count { it.code < 128 } + val fragmentOther = fragment.length - fragmentAscii + if (ModelContextPolicy.estimateTokens(ascii + fragmentAscii) + other + fragmentOther <= budget) { + result.append(fragment) + ascii += fragmentAscii + other += fragmentOther index++ offset = 0 } else { diff --git a/app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt b/app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt index fbf7a2fe7..c54f2b73f 100644 --- a/app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt +++ b/app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt @@ -507,8 +507,10 @@ class StatefulAgentWorkflow @Inject constructor( aiProvider.maxOutputTokens = ModelContextPolicy.outputReserveTokens(metadata) } val lastAssistantIndex = currentContext.history.indexOfLast { it is AgentMessage.AssistantMessage && it.inputTokens > 0 } - var baselineEstimate = ContextTokenEstimator.estimate(systemPrompt, - currentContext.history.take(lastAssistantIndex.coerceAtLeast(0)), currentTools) + var baselineEstimate = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Default) { + ContextTokenEstimator.estimate(systemPrompt, + currentContext.history.take(lastAssistantIndex.coerceAtLeast(0)), currentTools) + } var baselineUsage = if (currentContext.lastInputTokens > 0 && lastAssistantIndex >= 0) { (currentContext.history[lastAssistantIndex] as AgentMessage.AssistantMessage).inputTokens } else 0 @@ -534,7 +536,9 @@ class StatefulAgentWorkflow @Inject constructor( val compactionProvider = resolveCompactionFallbackProvider(currentContext.sessionId) ?: providerInUse var compactedMessages = state.messages if (!compactionAttemptFailed) { - val estimate = ContextTokenEstimator.estimate(systemPrompt, state.messages, currentTools) + val estimate = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Default) { + ContextTokenEstimator.estimate(systemPrompt, state.messages, currentTools) + } val predictedInput = ContextTokenEstimator.calibrated(estimate, baselineEstimate, baselineUsage) val compaction = contextCompactor.compactIfNeeded(state.messages, compactionProvider, context.sessionId, windowProvider = aiProvider, systemPrompt = systemPrompt, tools = currentTools, @@ -549,7 +553,9 @@ class StatefulAgentWorkflow @Inject constructor( baselineEstimate = 0 } } - val requestEstimate = ContextTokenEstimator.estimate(systemPrompt, compactedMessages, currentTools) + val requestEstimate = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Default) { + ContextTokenEstimator.estimate(systemPrompt, compactedMessages, currentTools) + } val predictedInput = ContextTokenEstimator.calibrated(requestEstimate, baselineEstimate, baselineUsage) send(AgentEvent.ContextUsage(predictedInput, inputBudget, true)) if (predictedInput >= inputBudget) { diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/AIAgentViewModel.kt b/app/src/main/java/com/aicode/feature/agent/presentation/AIAgentViewModel.kt index fb560d3b5..34c3ad4a9 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/AIAgentViewModel.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/AIAgentViewModel.kt @@ -95,6 +95,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -157,6 +158,12 @@ class AIAgentViewModel @Inject constructor( private val sessionJobs = mutableMapOf() + /** + * 正在回退(rewind)的会话集合:回退会删除对话消息,此后到达的后台任务/子代理完成等 + * 系统事件不应再触发新一轮(否则会在已清空的历史上凭空跑起来)。 + */ + private val rewindSuppressedSessions = java.util.concurrent.ConcurrentHashMap.newKeySet() + /** * agent 执行期间持有的 CPU 唤醒锁:熄屏后系统会挂起进程,使流式响应中断、工具调用卡死。 * 不计数(setReferenceCounted(false)),多会话共用一把锁,最后一个任务结束时统一释放。 @@ -174,13 +181,37 @@ class AIAgentViewModel @Inject constructor( private val _currentSessionId = MutableStateFlow(null) val currentSessionId: StateFlow = _currentSessionId.asStateFlow() + /** 各会话「用户已手动删除待办」时记录的 id 集合;待办更新后自动失效。 */ + private val _dismissedTodoIds = MutableStateFlow>>(emptyMap()) + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) - val currentSessionTodoItems: StateFlow> = _currentSessionId - .flatMapLatest { id -> - if (id.isNullOrBlank()) flowOf(emptyList()) - else todoItemDao.getBySession(id).map { entities -> entities.map { it.toDomain() } } + val currentSessionTodoItems: StateFlow> = combine(_currentSessionId, _dismissedTodoIds) { id, dismissed -> + id to dismissed + }.flatMapLatest { (id, dismissed) -> + if (id.isNullOrBlank()) flowOf(emptyList()) + else todoItemDao.getBySession(id).map { entities -> + val items = entities.map { it.toDomain() } + val dismissedIds = dismissed[id] + // 用户主动删除后:若待办集合与删除时完全一致,则不展示;一旦有更新(新增/变更)则重新展示。 + if (dismissedIds != null && items.isNotEmpty() && items.map { it.id }.toSet() == dismissedIds) { + emptyList() + } else { + items + } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + + /** + * 删除当前会话全部待办并隐藏面板,直到下一次待办更新。 + * 记录被删 id 集合:若之后待办集合与之一致则不展示,有新增/变化则自动恢复展示。 + */ + fun dismissTodos(sessionId: String) { + viewModelScope.launch { + val ids = todoItemDao.getBySessionOnce(sessionId).map { it.id }.toSet() + _dismissedTodoIds.value = _dismissedTodoIds.value + (sessionId to ids) + todoItemDao.deleteBySession(sessionId) + } + } private val _agentStates = MutableStateFlow>(emptyMap()) val agentStates: StateFlow> = _agentStates.asStateFlow() @@ -221,6 +252,7 @@ class AIAgentViewModel @Inject constructor( private val _messageLimit = MutableStateFlow>(emptyMap()) private val defaultLimit = 30 + private val loadedMessageLimits = mutableMapOf() /** 聊天记录搜索:命中条数上限、输入防抖、片段上下文宽度、定位时预留的分页余量。 */ private val chatSearchLimit = 50 @@ -309,7 +341,10 @@ class AIAgentViewModel @Inject constructor( fun loadMoreMessages() { val sid = _currentSessionId.value ?: return + val state = messagesState.value + if (state.sessionId != sid || !state.loaded || !state.hasMore) return val currentLimit = _messageLimit.value[sid] ?: defaultLimit + if (loadedMessageLimits[sid] != currentLimit) return _messageLimit.value = _messageLimit.value + (sid to (currentLimit + 30)) } @@ -773,7 +808,8 @@ class AIAgentViewModel @Inject constructor( hasMore = list.size >= limit, isLoadingMore = false ) - } + }.flowOn(Dispatchers.Default) + .onEach { loadedMessageLimits[id] = limit } } .stateIn(viewModelScope, SharingStarted.Eagerly, ChatMessagesState(null, emptyList(), loaded = false)) @@ -1041,6 +1077,9 @@ class AIAgentViewModel @Inject constructor( /** 写文件会连珠触发多个 inotify 事件,合并后再重读目录。 */ const val BROWSE_DEBOUNCE_MS = 300L + + /** 回退抑制窗口:回退后短时间内丢弃系统事件,避免旧 job 的 finally 触发新一轮。 */ + const val REWIND_SUPPRESS_MS = 1_500L } init { @@ -1262,6 +1301,11 @@ class AIAgentViewModel @Inject constructor( * 避免各处重复判断忙碌/空闲。 */ private fun deliverSystemEvent(sessionId: String, item: PendingNotification) { + // 回退期间:丢弃系统事件,不回退刚清空的对话基础上又跑起来。 + if (sessionId in rewindSuppressedSessions) { + FileLogger.d(TAG, "deliverSystemEvent suppressed (rewinding): sid=$sessionId") + return + } if (sessionJobs[sessionId]?.isActive == true) { agentNotificationCenter.enqueue(sessionId, item) return @@ -1290,6 +1334,11 @@ class AIAgentViewModel @Inject constructor( * 已被工具结果搭车送达的通知此时已 ack 移除,取到空则什么都不做。 */ private fun flushPendingNotifications(sessionId: String) { + // 回退期间不搭车发送残留通知,避免回退后自动起新一轮。 + if (sessionId in rewindSuppressedSessions) { + agentNotificationCenter.clear(sessionId) + return + } val items = agentNotificationCenter.drain(sessionId) if (items.isEmpty()) return FileLogger.d(TAG, "flushPendingNotifications: sid=$sessionId items=${items.size} state=${_agentStates.value[sessionId]}") @@ -2321,14 +2370,38 @@ class AIAgentViewModel @Inject constructor( val sessionId = _currentSessionId.value ?: return@launch dismissRewindMenu() + // 0. 进入回退抑制窗口:取消旧 job 的 finally / 后台任务完成事件可能触发新一轮, + // 在窗口内丢弃这些系统事件,避免回退后 AI 又在已清空的历史上跑起来。 + rewindSuppressedSessions.add(sessionId) + viewModelScope.launch { + delay(REWIND_SUPPRESS_MS) + rewindSuppressedSessions.remove(sessionId) + } + // 1. 停止当前正在运行的 Agent 任务及后续排队 _queuedRequests.value = _queuedRequests.value + (sessionId to emptyList()) agentNotificationCenter.clear(sessionId) val runningJob = sessionJobs[sessionId] if (runningJob != null && runningJob.isActive) { runningJob.cancelAndJoin() + // 只删自己取消的那个 job:cancelAndJoin 期间 finally 可能已启动接替的新 job 并注册。 + if (sessionJobs[sessionId] === runningJob) sessionJobs.remove(sessionId) + } else { + sessionJobs.remove(sessionId) + } + + // 1b. 停止该会话的全部子代理 job(回退父会话时它们仍在独立 job 上跑, + // 完成后还会反向唤起父会话)。 + runCatching { + chatSessionDao.getSubSessionsByParentOnce(sessionId).forEach { child -> + sessionJobs[child.id]?.let { subJob -> + if (subJob.isActive) subJob.cancel() + if (sessionJobs[child.id] === subJob) sessionJobs.remove(child.id) + } + subAgentEventBus.release(child.id) + agentNotificationCenter.clear(child.id) + } } - sessionJobs.remove(sessionId) // 2. 重置会话运行、流式与检查点状态 setAgentState(sessionId, AgentUIState.Idle) diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt index 552f74411..ffe80010c 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/AIChatPanel.kt @@ -56,6 +56,7 @@ import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.zIndex import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.graphicsLayer @@ -339,7 +340,7 @@ private fun splitChatTurns(messages: List): Pair? = null for (message in messages) { - if (message.role == MessageRole.USER && !message.isBackgroundNotification) { + if (message.role == MessageRole.USER && !message.isBackgroundNotification && !message.isCompactionMarker) { user?.let { turns += ChatTurn(turnKeyOf(it.id), it, body?.toList().orEmpty()) } user = message body = ArrayList() @@ -582,6 +583,9 @@ fun AIChatPanel( val pendingPermissionSessionTitle by viewModel.pendingToolPermissionSessionTitle.collectAsStateWithLifecycle() val pendingQuestion by viewModel.pendingUserQuestion.collectAsStateWithLifecycle() val currentTodoItems by viewModel.currentSessionTodoItems.collectAsStateWithLifecycle() + // 待办显示位置:标题栏下方 / 消息栏上方(默认)。 + val todoDisplayPositionState = settingsViewModel?.todoDisplayPosition?.collectAsStateWithLifecycle() + val todoBelowTitle = todoDisplayPositionState?.value == com.aicode.feature.settings.data.repository.TodoDisplayPosition.BELOW_TITLE val queuedRequests by viewModel.queuedRequests.collectAsStateWithLifecycle() val targetRewindMessageId by viewModel.targetRewindMessageId.collectAsStateWithLifecycle() val providers = (settingsViewModel?.providers?.collectAsStateWithLifecycle()?.value ?: emptyList()).filter { it.isEnabled } @@ -614,11 +618,7 @@ fun AIChatPanel( val currentMode by viewModel.currentSessionMode.collectAsStateWithLifecycle() val slashCommands by viewModel.slashCommands.collectAsStateWithLifecycle() - var inputText by remember { mutableStateOf("") } - val inputDraft by viewModel.inputDraft.collectAsStateWithLifecycle() - LaunchedEffect(inputDraft) { - if (inputText != inputDraft) inputText = inputDraft - } + val inputText by viewModel.inputDraft.collectAsStateWithLifecycle() // 输入 "/" 打开命令菜单时重扫技能,反映磁盘上技能的增删改。 LaunchedEffect(inputText) { if (inputText == "/") viewModel.refreshSlashCommands() @@ -675,7 +675,7 @@ fun AIChatPanel( if (!isBusy) { null } else { - messages.lastOrNull { it.role == MessageRole.USER && !it.isBackgroundNotification } + messages.lastOrNull { it.role == MessageRole.USER && !it.isBackgroundNotification && !it.isCompactionMarker } ?.let { turnKeyOf(it.id) } } } @@ -1205,7 +1205,6 @@ fun AIChatPanel( inputImages = images, inputAttachments = attachments.toAgentAttachments() ) - inputText = "" viewModel.clearInputDraft() pendingAttachments = emptyList() followBottom = true @@ -1434,6 +1433,22 @@ fun AIChatPanel( .widthIn(max = readableContentMaxWidth()) .fillMaxSize() ) { + // 待办显示位置为「标题栏下方」时,在页面头部(顶栏之下)渲染待办面板。 + if (todoBelowTitle && currentTodoItems.isNotEmpty()) { + TodoDashboardBar( + items = currentTodoItems, + sessionId = currentSessionId.orEmpty(), + forceCollapse = pendingPermission != null || pendingQuestion != null || imeVisible, + onExpandedChange = { todoExpanded = it }, + onDismiss = { viewModel.dismissTodos(currentSessionId.orEmpty()) }, + modifier = Modifier + .align(Alignment.TopCenter) + .zIndex(1f) + .padding(horizontal = Spacing.lg) + .padding(top = Spacing.xs) + ) + } + // 内容层:消息列表延伸到屏幕底部,输入框悬浮其上,滚动时卡片可滑入输入框后面 Column(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.weight(1f)) { @@ -1576,7 +1591,9 @@ fun AIChatPanel( val questionForPanel = rememberLastNonNull(pendingQuestion) AnimatedVisibility( visible = pendingQuestion != null, - modifier = Modifier.graphicsLayer { alpha = floatingPanelAlpha }, + modifier = Modifier + .weight(1f, fill = false) + .graphicsLayer { alpha = floatingPanelAlpha }, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { @@ -1610,7 +1627,7 @@ fun AIChatPanel( ChatInputBar( value = inputText, - onValueChange = { inputText = it; viewModel.updateInputDraft(it) }, + onValueChange = viewModel::updateInputDraft, onSend = sendMessage, enterToSend = settingsViewModel?.enterToSend?.collectAsStateWithLifecycle()?.value ?: false, onStop = { viewModel.stopAgent() }, @@ -1646,9 +1663,10 @@ fun AIChatPanel( onEditQueued = { queuedEditing = it }, onInterjectQueued = { viewModel.interjectQueuedRequest(it) }, dashboardState = currentDashboardState, - todoItems = currentTodoItems, + todoItems = if (todoBelowTitle) emptyList() else currentTodoItems, sessionId = currentSessionId.orEmpty(), onTodoExpandedChange = { todoExpanded = it }, + onTodoDismiss = { viewModel.dismissTodos(currentSessionId.orEmpty()) }, forceCollapseDashboard = pendingPermission != null || pendingQuestion != null || planApproval != null || imeVisible, onDashboardExpandedChange = { dashboardExpanded = it }, onRefreshDashboard = { @@ -1741,7 +1759,7 @@ fun AIChatPanel( promptSnippet = targetMsg?.content ?: "", onOptionSelected = { option -> viewModel.executeRewindOption(targetId, option) { text, attachments -> - inputText = text + viewModel.updateInputDraft(text) pendingAttachments = attachments.map { it.toPendingAttachment() } } }, diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/AskUserQuestionPanel.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/AskUserQuestionPanel.kt index 3db95e322..6f2666bed 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/AskUserQuestionPanel.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/AskUserQuestionPanel.kt @@ -31,8 +31,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -43,7 +41,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextOverflow @@ -56,6 +53,7 @@ import com.aicode.feature.agent.domain.tool.question.SingleAnswer import com.aicode.feature.agent.domain.tool.question.UserQuestionAnswer import androidx.compose.ui.res.stringResource import com.aicode.R +import com.aicode.core.ui.AppTextField import com.aicode.core.ui.ExpandableChevronIcon import compose.icons.FeatherIcons import compose.icons.feathericons.ChevronDown @@ -309,25 +307,16 @@ private fun QuestionCard( // 「其他」被选中时展开文本输入框 if (isOther && isSelected) { Spacer(Modifier.height(Spacing.xs)) - TextField( + AppTextField( value = customText, onValueChange = onCustomTextChanged, - placeholder = { Text(stringResource(R.string.ask_input_hint), style = MaterialTheme.typography.bodySmall) }, + placeholder = stringResource(R.string.ask_input_hint), modifier = Modifier .fillMaxWidth() .padding(start = 28.dp), textStyle = MaterialTheme.typography.bodySmall, singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - cursorColor = MaterialTheme.colorScheme.primary - ), - shape = RoundedCornerShape(Radius.sm) + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) ) } } diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputBar.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputBar.kt index d404e9462..775877b73 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputBar.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/ChatInputBar.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -174,6 +175,7 @@ internal fun ChatInputBar( todoItems: List = emptyList(), sessionId: String = "", onTodoExpandedChange: (Boolean) -> Unit = {}, + onTodoDismiss: (() -> Unit)? = null, forceCollapseDashboard: Boolean = false, /** 消息列表正在滚动时内容区淡出到 40%,停止滚动恢复;用于长列表阅读时降低底部干扰(同 git 页 tab 栏)。 */ isScrolling: Boolean = false, @@ -234,9 +236,10 @@ internal fun ChatInputBar( // 渐变终点固定在蒙版可视高度内:若跟随整个 Box(含 imeInset 被键盘拉长的部分), // 键盘弹起时可见区域只占渐变前段,alpha 被摊薄到几乎透明——看起来像没有蒙版。 val maskGradientEndY = with(LocalDensity.current) { INPUT_BAR_MASK_HEIGHT.toPx() } - Box( + BoxWithConstraints( modifier = Modifier.fillMaxWidth() ) { + val dashboardBodyMaxHeight = (maxHeight - imeInset - Spacing.md).coerceAtLeast(0.dp) * 0.4f // 半透明渐变蒙版:盖住输入框区域 + 导航栏(手势小白条)区域,滚动内容滑入时被遮罩 // (能看见但看不清);高度含 IME inset 随键盘上移,渐变在 INPUT_BAR_MASK_HEIGHT 内完成, // 之下为纯色。注意 IME padding 不能加在外层 Box 上——align(BottomCenter) 的子项 @@ -279,6 +282,8 @@ internal fun ChatInputBar( Column( modifier = Modifier .fillMaxWidth() + .heightIn(max = 200.dp) + .verticalScroll(rememberScrollState()) .padding(horizontal = Spacing.sm, vertical = Spacing.xs) ) { filteredCommands.forEach { command -> @@ -340,7 +345,8 @@ internal fun ChatInputBar( items = todoItems, sessionId = sessionId, forceCollapse = forceCollapseDashboard, - onExpandedChange = onTodoExpandedChange + onExpandedChange = onTodoExpandedChange, + onDismiss = onTodoDismiss ) } @@ -351,6 +357,7 @@ internal fun ChatInputBar( onRefresh = onRefreshDashboard, onRefreshByButton = onRefreshDashboardByButton, onExpandedChange = onDashboardExpandedChange, + maxExpandedBodyHeight = dashboardBodyMaxHeight, forceCollapse = forceCollapseDashboard ) } diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/ProviderDashboardBar.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/ProviderDashboardBar.kt index 0d193eecc..b11310c73 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/ProviderDashboardBar.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/ProviderDashboardBar.kt @@ -20,9 +20,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator @@ -42,6 +45,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.aicode.core.theme.Radius @@ -65,6 +69,7 @@ fun ProviderDashboardBar( provider: AIProviderConfig, state: ProviderDashboardState, onRefresh: () -> Unit, + maxExpandedBodyHeight: Dp, modifier: Modifier = Modifier, forceCollapse: Boolean = false, onRefreshByButton: () -> Unit = {}, @@ -242,6 +247,8 @@ fun ProviderDashboardBar( Column( modifier = Modifier .fillMaxWidth() + .heightIn(max = maxExpandedBodyHeight) + .verticalScroll(rememberScrollState()) .padding(top = Spacing.sm), verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/StatusBubbles.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/StatusBubbles.kt index c393746a5..f690a106d 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/StatusBubbles.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/StatusBubbles.kt @@ -29,6 +29,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -655,6 +657,26 @@ private val ReasoningWindowMaxHeight = 300.dp /** 思考窗口底部渐隐遮罩的高度(仅当内容还可继续下滚时显示)。 */ private val ReasoningWindowFadeHeight = 28.dp +/** + * 将超长思考文本按块切分,供展开时惰性渲染。 + * 优先在换行处切;单块目标长度适中,避免单个 MarkdownContent 解析过大文本造成主线程卡顿。 + */ +private fun chunkReasoningText(text: String, chunkSize: Int = 4_000): List { + if (text.length <= chunkSize) return listOf(text) + val result = ArrayList((text.length / chunkSize) + 1) + var start = 0 + while (start < text.length) { + var end = (start + chunkSize).coerceAtMost(text.length) + if (end < text.length) { + val newline = text.lastIndexOf('\n', end) + if (newline > start) end = newline + 1 + } + result.add(text.substring(start, end)) + start = end + } + return result +} + /** * 思考过程折叠行:左对齐、浅色弱化,与正式回复区分。**默认收起**,点这一行随时展开/收起。 * @@ -760,29 +782,36 @@ internal fun ReasoningBubble( ) { Column { Spacer(Modifier.height(Spacing.sm)) - val scrollState = rememberScrollState() + // 超长思考文本分块惰性渲染:避免一次性把巨量 Markdown 交给解析器造成主线程卡死/崩溃。 + val chunks = remember(renderText) { chunkReasoningText(renderText) } + val listState = rememberLazyListState() val fadeColor = MaterialTheme.colorScheme.background Box( modifier = Modifier.fillMaxWidth() ) { - MarkdownContent( - text = renderText, - color = MaterialTheme.colorScheme.onSurfaceVariant, - cache = cache, - compact = true, + LazyColumn( + state = listState, modifier = Modifier .fillMaxWidth() .heightIn(max = ReasoningWindowMaxHeight) - .nestedScroll(rememberBoundNestedScrollConnection(scrollState)) - .verticalScroll(scrollState) .pointerInput(text) { detectTapGestures( onDoubleTap = { toggleExpanded(false) } ) } - ) + ) { + items(chunks.size) { index -> + MarkdownContent( + text = chunks[index], + color = MaterialTheme.colorScheme.onSurfaceVariant, + cache = cache, + compact = true, + modifier = Modifier.fillMaxWidth() + ) + } + } // 底部渐隐:只有还能继续往下滚时才盖一层,提示“下面还有内容” - if (scrollState.canScrollForward) { + if (listState.canScrollForward) { Box( modifier = Modifier .align(Alignment.BottomStart) diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/TodoDashboardBar.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/TodoDashboardBar.kt index 2bc5f8356..d2a24a248 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/TodoDashboardBar.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/TodoDashboardBar.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -49,6 +50,7 @@ import compose.icons.FeatherIcons import compose.icons.feathericons.CheckSquare import compose.icons.feathericons.ChevronDown import compose.icons.feathericons.ChevronUp +import compose.icons.feathericons.Trash2 /** * 位于输入框上方的待办任务常驻面板: @@ -63,7 +65,8 @@ fun TodoDashboardBar( sessionId: String, modifier: Modifier = Modifier, forceCollapse: Boolean = false, - onExpandedChange: (Boolean) -> Unit = {} + onExpandedChange: (Boolean) -> Unit = {}, + onDismiss: (() -> Unit)? = null ) { if (items.isEmpty()) return @@ -154,6 +157,38 @@ fun TodoDashboardBar( Spacer(Modifier.weight(1f)) } + // 全部完成时,在展开/收起按钮左侧提供删除按钮(带确认)。 + if (onDismiss != null && totalCount > 0 && completedCount == totalCount) { + var showDeleteConfirm by remember { mutableStateOf(false) } + Icon( + imageVector = FeatherIcons.Trash2, + contentDescription = stringResource(R.string.todo_dashboard_delete), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .size(16.dp) + .clickable { showDeleteConfirm = true } + ) + Spacer(Modifier.width(Spacing.sm)) + if (showDeleteConfirm) { + androidx.compose.material3.AlertDialog( + onDismissRequest = { showDeleteConfirm = false }, + title = { Text(stringResource(R.string.todo_dashboard_delete_title)) }, + text = { Text(stringResource(R.string.todo_dashboard_delete_confirm)) }, + confirmButton = { + androidx.compose.material3.TextButton(onClick = { + showDeleteConfirm = false + onDismiss() + }) { Text(stringResource(R.string.common_delete)) } + }, + dismissButton = { + androidx.compose.material3.TextButton(onClick = { showDeleteConfirm = false }) { + Text(stringResource(R.string.common_cancel)) + } + } + ) + } + } + ExpandableChevronIcon( expanded = effectiveExpanded, contentDescription = if (effectiveExpanded) { diff --git a/app/src/main/java/com/aicode/feature/agent/presentation/component/ToolMessageComponents.kt b/app/src/main/java/com/aicode/feature/agent/presentation/component/ToolMessageComponents.kt index 7c06918e2..26bab5ce7 100644 --- a/app/src/main/java/com/aicode/feature/agent/presentation/component/ToolMessageComponents.kt +++ b/app/src/main/java/com/aicode/feature/agent/presentation/component/ToolMessageComponents.kt @@ -43,6 +43,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.key +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -85,6 +87,8 @@ import compose.icons.feathericons.FileText import compose.icons.feathericons.Search import compose.icons.feathericons.Terminal import compose.icons.feathericons.Tool +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray @@ -146,33 +150,47 @@ internal fun ToolMessageBody( ) { val streaming = liveOutput != null val running = message.isToolRunning(liveOutput) - val edit = if (!running && !message.isError && - (message.toolName == "editFile" || message.toolName == "writeFile") - ) { - remember(message.id, message.content) { parseEditDiff(message.content) } - } else null - - val resultText = if (!running) { - remember(message.id, message.content) { formatToolResult(message.content) } - } else null - val argHint = remember(message.toolArgs) { toolArgHint(message.toolArgs) } - val argsFull = remember(message.toolArgs) { formatToolArgs(message.toolArgs) } - - val todoData = if (message.toolName == "todo" && !running && !message.isError) { - remember(message.id, message.content) { parseTodoResult(message.content) } - } else null - val webSearchData = if (message.toolName == "websearch" && !running && !message.isError) { - remember(message.id, message.content) { parseWebSearchResult(message.content) } - } else null - // 后台任务/子代理完成通知:搭车在本次工具结果里送给 AI 的,同时常显给用户看。 - val notifications = if (!running) { - remember(message.id, message.content) { parseToolNotifications(message.content) } - } else emptyList() + // produceState 的 key 只重启任务,不重置 value;组合 key 同时隔离旧输入的结果。 + val metadata by key(message.id, message.content, message.toolName, message.isError, running) { + produceState(initialValue = null) { + value = withContext(Dispatchers.Default) { + ToolMessageMetadata( + edit = if (!running && !message.isError && + (message.toolName == "editFile" || message.toolName == "writeFile") + ) parseEditDiff(message.content) else null, + todo = if (message.toolName == "todo" && !running && !message.isError) { + parseTodoResult(message.content) + } else null, + webSearch = if (message.toolName == "websearch" && !running && !message.isError) { + parseWebSearchResult(message.content) + } else null, + notifications = if (!running) parseToolNotifications(message.content) else emptyList(), + hasResult = !running && hasToolResult(message.content) + ) + } + } + } + val argumentMetadata by key(message.id, message.toolArgs) { + produceState(initialValue = null) { + value = withContext(Dispatchers.Default) { + ToolArgumentMetadata( + hint = toolArgHint(message.toolArgs), + path = extractFilePathArg(message.toolArgs), + hasArgs = hasToolArgs(message.toolArgs) + ) + } + } + } + val edit = metadata?.edit + val todoData = metadata?.todo + val argHint = argumentMetadata?.hint + val notifications = metadata?.notifications.orEmpty() // 执行中也可折叠/展开(如 bash 刷屏时可收起只看标题行),无论当前是否有输出;无输出时折叠态无内容,但保持可点击与箭头一致 val hasLiveOutput = !liveOutput.isNullOrBlank() - val expandable = streaming || (!running && (edit != null || !resultText.isNullOrBlank() || !argsFull.isNullOrBlank() - || (todoData != null && todoData.items.isNotEmpty()) || webSearchData != null)) + val expandable = streaming || (!running && (metadata == null || argumentMetadata == null || + edit != null || metadata?.hasResult == true || argumentMetadata?.hasArgs == true || + (todoData != null && todoData.items.isNotEmpty()) || metadata?.webSearch != null)) // 工具调用一律默认收起(差异卡、待办卡也不例外):要不要看细节由用户点开, // 手动开关过(expandedOverride 非 null)就以用户的选择为准。展开态由宿主保管, // 因此滚出视口、切页返回都不会再丢。 @@ -183,7 +201,7 @@ internal fun ToolMessageBody( val filePath = if (edit != null) { edit.path } else { - remember(message.toolArgs) { extractFilePathArg(message.toolArgs) } + argumentMetadata?.path } Column(modifier = Modifier.fillMaxWidth()) { @@ -298,6 +316,12 @@ internal fun ToolMessageBody( enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), ) { + val preparedArgs by key(message.id, message.toolArgs, running) { + produceState(initialValue = null) { + value = withContext(Dispatchers.Default) { formatToolArgs(message.toolArgs) } + } + } + val argsFull = preparedArgs Column { if (!argsFull.isNullOrBlank()) { Spacer(Modifier.height(Spacing.sm)) @@ -316,6 +340,29 @@ internal fun ToolMessageBody( enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), ) { + // 详情任务跟随 AnimatedVisibility 的内容生命周期,收起时保留至退出动画结束。 + val details by key( + message.id, message.content, message.toolArgs, message.toolName, + message.isError, running + ) { + produceState(initialValue = null, key1 = metadata) { + val prepared = metadata ?: return@produceState + value = withContext(Dispatchers.Default) { + val webSearch = prepared.webSearch + val plain = prepared.edit == null && + prepared.todo?.items.isNullOrEmpty() && webSearch == null + ToolMessageDetails( + args = if (plain) formatToolArgs(message.toolArgs) else null, + result = if (plain && !running) formatToolResult(message.content) else null, + webSearch = webSearch, + diffCopyText = prepared.edit?.hunks?.joinToString("\n") { it.diff } + ) + } + } + } + val argsFull = details?.args + val resultText = details?.result + val webSearchData = details?.webSearch Column( modifier = Modifier.pointerInput(message.id) { detectDoubleTapToCollapse { @@ -330,12 +377,12 @@ internal fun ToolMessageBody( } else if (webSearchData != null) { Spacer(Modifier.height(Spacing.xs)) WebSearchResultCard(result = webSearchData) - } else if (edit != null) { + } else if (edit != null && details != null) { // 差异卡:头部给路径与「复制」,页脚给增删统计(DSH 那块白底描边卡) Spacer(Modifier.height(Spacing.xs)) ChatCodeCard( title = edit.path.ifBlank { null }, - copyText = edit.hunks.joinToString("\n") { it.diff }, + copyText = details?.diffCopyText.orEmpty(), footer = stringResource( R.string.tool_changed_files_summary, edit.added, @@ -376,6 +423,23 @@ internal fun ToolMessageBody( } } +private data class ToolMessageMetadata( + val edit: EditDiff?, + val todo: ParsedTodoResult?, + val webSearch: ParsedWebSearchResult?, + val notifications: List, + val hasResult: Boolean +) + +private data class ToolArgumentMetadata(val hint: String?, val path: String?, val hasArgs: Boolean) + +private data class ToolMessageDetails( + val args: String?, + val result: String?, + val webSearch: ParsedWebSearchResult?, + val diffCopyText: String? +) + internal data class ToolNotificationInfo(val summary: String, val succeeded: Boolean, val isMessage: Boolean = false) /** @@ -939,6 +1003,54 @@ internal fun formatJsonData(jsonStr: String): String? = runCatching { } }.getOrNull() +private fun hasToolResult(raw: String): Boolean { + val s = raw.withoutToolStatusPrefix() + parseToolTransport(s)?.let { obj -> + return when (obj["status"]?.jsonPrimitive?.contentOrNull) { + "error" -> (obj["message"]?.jsonPrimitive?.contentOrNull ?: s).isNotBlank() + "success", "partial" -> hasToolData(obj["data"]) ?: s.isNotBlank() + else -> s.isNotBlank() + } + } + val inner = when { + s.startsWith("Error(") -> { + val msgIdx = s.indexOf("message=") + if (msgIdx < 0) return s.isNotBlank() + val body = s.substring(msgIdx + "message=".length) + val codeIdx = body.lastIndexOf(", code=") + return (if (codeIdx >= 0) body.substring(0, codeIdx) else body.removeSuffix(")")).isNotBlank() + } + s.startsWith("Success(data=") -> s.removePrefix("Success(data=").removeSuffix(")") + s.startsWith("Partial(data=") -> { + val body = s.removePrefix("Partial(data=") + val msgIdx = body.lastIndexOf(", message=") + if (msgIdx >= 0) body.substring(0, msgIdx) else body.removeSuffix(")") + } + else -> return s.isNotBlank() + } + return runCatching { + when (val data = Json.parseToJsonElement(inner.trim())) { + is JsonPrimitive, is JsonObject -> hasToolData(data) ?: inner.isNotBlank() + else -> inner.isNotBlank() + } + }.getOrElse { inner.isNotBlank() } +} + +private fun hasToolData(data: JsonElement?): Boolean? = when (data) { + is JsonPrimitive -> data.contentOrNull?.isNotBlank() ?: true + is JsonObject -> { + val main = data["content"] ?: data["output"] ?: data["stdout"] ?: data["text"] + (main as? JsonPrimitive)?.contentOrNull?.isNotBlank() ?: data.isNotEmpty() + } + null -> null + else -> true +} + +private fun hasToolArgs(argsJson: String?): Boolean { + if (argsJson.isNullOrBlank()) return false + return runCatching { Json.parseToJsonElement(argsJson).jsonObject.isNotEmpty() }.getOrDefault(true) +} + /** 把传入参数 JSON 列成 `key: value` 多行 */ internal fun formatToolArgs(argsJson: String?): String? { if (argsJson.isNullOrBlank()) return null diff --git a/app/src/main/java/com/aicode/feature/backup/data/BackupManagerImpl.kt b/app/src/main/java/com/aicode/feature/backup/data/BackupManagerImpl.kt index 9ebf9d00b..64b678532 100644 --- a/app/src/main/java/com/aicode/feature/backup/data/BackupManagerImpl.kt +++ b/app/src/main/java/com/aicode/feature/backup/data/BackupManagerImpl.kt @@ -62,6 +62,7 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream import java.io.BufferedInputStream import java.io.ByteArrayOutputStream import java.io.File +import java.io.FilterOutputStream import java.io.FileInputStream import java.io.FileOutputStream import java.io.InputStream @@ -106,16 +107,16 @@ class BackupManagerImpl @Inject constructor( override suspend fun export(password: CharArray?, options: BackupOptions, output: OutputStream) { withContext(Dispatchers.IO) { + val pw = password?.takeIf { it.isNotEmpty() } + if (pw == null) { + writeTarGz(output, options) + return@withContext + } val temp = createTempFile() try { - writeTarGz(temp, options) - val pw = password?.takeIf { it.isNotEmpty() } - FileInputStream(temp).use { input -> - if (pw != null) { - BackupCrypto.encryptStream(input, output, pw) - } else { - input.copyTo(output) - } + FileOutputStream(temp).buffered().use { writeTarGz(it, options) } + FileInputStream(temp).buffered().use { input -> + BackupCrypto.encryptStream(input, output, pw) } } finally { temp.delete() @@ -123,6 +124,21 @@ class BackupManagerImpl @Inject constructor( } } + override suspend fun prepareImport(input: InputStream, password: CharArray?): File = withContext(Dispatchers.IO) { + val temp = createTempFile() + try { + FileOutputStream(temp).buffered().use { output -> + val source = input.buffered() + val pw = password?.takeIf { it.isNotEmpty() } + if (pw == null) source.copyTo(output) else BackupCrypto.decryptStream(source, output, pw) + } + temp + } catch (e: Throwable) { + temp.delete() + throw e + } + } + override suspend fun exportSession(sessionId: String, output: OutputStream) { withContext(Dispatchers.IO) { val session = chatSessionDao.getById(sessionId) ?: error("Session not found: $sessionId") @@ -248,9 +264,9 @@ class BackupManagerImpl @Inject constructor( val temp = createTempFile() try { BufferedInputStream(input).use { src -> - FileOutputStream(temp).use { dst -> BackupCrypto.decryptStream(src, dst, pw) } + FileOutputStream(temp).buffered().use { dst -> BackupCrypto.decryptStream(src, dst, pw) } } - val p = FileInputStream(temp) + val p = FileInputStream(temp).buffered() val gz = GzipCompressorInputStream(p) return TarSource(TarArchiveInputStream(gz), temp) } catch (e: Throwable) { @@ -261,8 +277,17 @@ class BackupManagerImpl @Inject constructor( // ── 导出辅助 ────────────────────────────────────────────── - private suspend fun writeTarGz(file: File, options: BackupOptions) { - FileOutputStream(file).use { fos -> + private suspend fun writeTarGz(output: OutputStream, options: BackupOptions) { + val stream = object : FilterOutputStream(output) { + override fun write(bytes: ByteArray, offset: Int, length: Int) { + out.write(bytes, offset, length) + } + + override fun close() { + flush() + } + } + stream.buffered().use { fos -> GzipCompressorOutputStream(fos).use { gz -> TarArchiveOutputStream(gz).use { tar -> tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU) @@ -340,7 +365,8 @@ class BackupManagerImpl @Inject constructor( compactionProviderId = if (options.appSettings) compactionModelSettingsRepository.getCompactionProviderId() else "", compactionModel = if (options.appSettings) compactionModelSettingsRepository.getCompactionModel() else "", syncSettings = if (options.appSettings) syncSettingsRepository.snapshot() else null, - workspaces = if (options.workspaceFiles) collectWorkspaceMetas() else emptyList() + workspaces = if (options.workspaceFiles) collectWorkspaceMetas() else emptyList(), + includesAppSettings = options.appSettings ) private fun writeMetadataEntry(tar: TarArchiveOutputStream, metadata: BackupMetadata) { @@ -519,8 +545,11 @@ class BackupManagerImpl @Inject constructor( while (true) { val n = tar.read(buffer) if (n < 0) break + var start = 0 for (i in 0 until n) { if (buffer[i] == '\n'.code.toByte()) { + line.write(buffer, start, i - start) + start = i + 1 if (line.size() > 0) { // 注意:ByteArrayOutputStream.toString(Charset) 是 API 33 才有的方法, // 在 Android 13 以下会抛 NoSuchMethodError,必须用 String(byte[], Charset) 构造器。 @@ -534,10 +563,9 @@ class BackupManagerImpl @Inject constructor( } else { line.reset() } - } else { - line.write(buffer[i].toInt()) } } + line.write(buffer, start, n - start) } if (line.size() > 0) { batch.add(json.decodeFromString(serializer, String(line.toByteArray(), Charsets.UTF_8))) @@ -607,28 +635,30 @@ class BackupManagerImpl @Inject constructor( if (meta.globalPermissionRules.isNotEmpty()) { permissionRulesRepository.setGlobalRules(meta.globalPermissionRules) } - meta.themeMode?.let { themeSettingsRepository.restore(it) } - themeSettingsRepository.restoreColors(meta.themePresetId, meta.dynamicColorEnabled) - keepaliveSettingsRepository.restore(meta.keepaliveEnabled) - screenOnSettingsRepository.restore(meta.screenOnEnabled) - agentSoundSettingsRepository.restore(meta.agentSoundEnabled) - generalSettingsRepository.restoreAutoRemoveStaleModels(meta.autoRemoveStaleModels) - generalSettingsRepository.restoreStartupSessionMode(meta.startupSessionMode) - generalSettingsRepository.restoreFirstByteTimeoutSec(meta.firstByteTimeoutSec) - generalSettingsRepository.restoreStreamIdleTimeoutSec(meta.streamIdleTimeoutSec) - generalSettingsRepository.restoreMaxNetworkRetries(meta.maxNetworkRetries) - generalSettingsRepository.restoreEnterToSend(meta.enterToSend) - generalSettingsRepository.restoreCompactionThresholdPercent(meta.compactionThresholdPercent) - generalSettingsRepository.restoreSendFileMaxSizeMb(meta.sendFileMaxSizeMb) - generalSettingsRepository.restoreDeleteExternalWorkspaceSessions(meta.deleteExternalWorkspaceSessions) - logSettingsRepository.restore(meta.logLevel) - if (meta.visionProviderId.isNotBlank() || meta.visionModel.isNotBlank()) { - visionModelSettingsRepository.setVisionModel(meta.visionProviderId, meta.visionModel) - } - if (meta.compactionProviderId.isNotBlank() || meta.compactionModel.isNotBlank()) { - compactionModelSettingsRepository.setCompactionModel(meta.compactionProviderId, meta.compactionModel) - } - meta.syncSettings?.let { syncSettingsRepository.restore(it) } + if (meta.includesAppSettings) { + meta.themeMode?.let { themeSettingsRepository.restore(it) } + themeSettingsRepository.restoreColors(meta.themePresetId, meta.dynamicColorEnabled) + keepaliveSettingsRepository.restore(meta.keepaliveEnabled) + screenOnSettingsRepository.restore(meta.screenOnEnabled) + agentSoundSettingsRepository.restore(meta.agentSoundEnabled) + generalSettingsRepository.restoreAutoRemoveStaleModels(meta.autoRemoveStaleModels) + generalSettingsRepository.restoreStartupSessionMode(meta.startupSessionMode) + generalSettingsRepository.restoreFirstByteTimeoutSec(meta.firstByteTimeoutSec) + generalSettingsRepository.restoreStreamIdleTimeoutSec(meta.streamIdleTimeoutSec) + generalSettingsRepository.restoreMaxNetworkRetries(meta.maxNetworkRetries) + generalSettingsRepository.restoreEnterToSend(meta.enterToSend) + generalSettingsRepository.restoreCompactionThresholdPercent(meta.compactionThresholdPercent) + generalSettingsRepository.restoreSendFileMaxSizeMb(meta.sendFileMaxSizeMb) + generalSettingsRepository.restoreDeleteExternalWorkspaceSessions(meta.deleteExternalWorkspaceSessions) + logSettingsRepository.restore(meta.logLevel) + if (meta.visionProviderId.isNotBlank() || meta.visionModel.isNotBlank()) { + visionModelSettingsRepository.setVisionModel(meta.visionProviderId, meta.visionModel) + } + if (meta.compactionProviderId.isNotBlank() || meta.compactionModel.isNotBlank()) { + compactionModelSettingsRepository.setCompactionModel(meta.compactionProviderId, meta.compactionModel) + } + meta.syncSettings?.let { syncSettingsRepository.restore(it) } + } return RestoreStats( providers = meta.providers.size, diff --git a/app/src/main/java/com/aicode/feature/backup/data/WebDavAutoSyncWorker.kt b/app/src/main/java/com/aicode/feature/backup/data/WebDavAutoSyncWorker.kt new file mode 100644 index 000000000..38943a67a --- /dev/null +++ b/app/src/main/java/com/aicode/feature/backup/data/WebDavAutoSyncWorker.kt @@ -0,0 +1,102 @@ +package com.aicode.feature.backup.data + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.aicode.core.util.FileLogger +import com.aicode.feature.backup.domain.BackupManager +import com.aicode.feature.backup.domain.BackupOptions +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.first +import java.io.File +import java.util.concurrent.TimeUnit + +/** + * WebDAV 自动备份同步 Worker:按用户设定的间隔周期执行,本地生成备份并上传到云端 + * `AiCode/` 目录。任务在用户开启「自动备份同步」时调度,关闭后取消。 + * + * Worker 内复查一次开关与配置,避免调度后用户又关闭导致的误触发。 + */ +@HiltWorker +class WebDavAutoSyncWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted workerParameters: WorkerParameters, + private val repository: WebDavSettingsRepository, + private val client: WebDavClient, + private val backupManager: BackupManager +) : CoroutineWorker(context, workerParameters) { + + override suspend fun doWork(): Result { + val config = repository.snapshot() + if (!config.autoSync || !config.isConfigured) return Result.success() + + return try { + val options = BackupOptions( + providers = repository.syncProvidersFlow.first(), + remoteConnections = repository.syncRemoteFlow.first(), + chatHistory = repository.syncChatHistoryFlow.first(), + mcpServers = repository.syncMcpFlow.first(), + permissionRules = repository.syncPermissionsFlow.first(), + appSettings = repository.syncAppSettingsFlow.first(), + workspaceFiles = repository.syncWorkspaceFilesFlow.first() + ) + val tempFile = File(applicationContext.cacheDir, "webdav-auto-${System.currentTimeMillis()}.tar.gz") + tempFile.outputStream().use { out -> backupManager.export(null, options, out) } + val result = client.upload(config, tempFile, REMOTE_FILE_PATH) + tempFile.delete() + result.fold( + onSuccess = { + repository.setLastSyncAt(System.currentTimeMillis()) + Result.success() + }, + onFailure = { + FileLogger.e(TAG, "WebDAV 自动同步失败", it) + Result.retry() + } + ) + } catch (e: Exception) { + FileLogger.e(TAG, "WebDAV 自动同步异常", e) + Result.retry() + } + } + + companion object { + private const val TAG = "WebDavAutoSync" + private const val UNIQUE_NAME = "webdav-auto-sync" + + /** 云端根目录下自动创建的文件夹与备份文件名。 */ + private const val REMOTE_FILE_PATH = "AiCode/aicode-backup.tar.gz" + + /** 将「数值 + 单位」换算为分钟,并夹到 WorkManager 的合法下限(15 分钟)。 */ + private fun toMinutes(value: Int, unit: SyncIntervalUnit): Long { + val minutes = when (unit) { + SyncIntervalUnit.MINUTE -> value.toLong() + SyncIntervalUnit.HOUR -> value * 60L + SyncIntervalUnit.DAY -> value * 60L * 24L + } + return minutes.coerceAtLeast(15L) + } + + /** 调度周期任务(幂等)。开启自动同步时调用。 */ + fun schedule(context: Context, value: Int, unit: SyncIntervalUnit) { + val request = PeriodicWorkRequestBuilder( + toMinutes(value, unit), TimeUnit.MINUTES + ).build() + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + UNIQUE_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + request + ) + } + + /** 取消周期任务。关闭自动同步时调用。 */ + fun cancel(context: Context) { + WorkManager.getInstance(context).cancelUniqueWork(UNIQUE_NAME) + } + } +} diff --git a/app/src/main/java/com/aicode/feature/backup/data/WebDavClient.kt b/app/src/main/java/com/aicode/feature/backup/data/WebDavClient.kt new file mode 100644 index 000000000..969a92c9c --- /dev/null +++ b/app/src/main/java/com/aicode/feature/backup/data/WebDavClient.kt @@ -0,0 +1,117 @@ +package com.aicode.feature.backup.data + +import com.aicode.core.util.FileLogger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Credentials +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +/** WebDAV 操作失败,携带可直接展示的原因。 */ +class WebDavException(message: String, cause: Throwable? = null) : Exception(message, cause) + +/** + * 极简 WebDAV 客户端:仅覆盖本功能需要的能力——连接测试(PROPFIND)、上传文件(PUT)、 + * 删除文件(DELETE)。不做完整的 WebDAV 目录管理。 + * + * 鉴权用 HTTP Basic(绝大多数 WebDAV 服务如 Nextcloud、坚果云、群晖均支持)。 + */ +@Singleton +class WebDavClient @Inject constructor() { + + private val client: OkHttpClient = DEFAULT_CLIENT + + /** 连接测试:对目标 URL 发 PROPFIND(Depth: 0),2xx/207 视为成功。 */ + suspend fun test(config: WebDavConfig): Result = withContext(Dispatchers.IO) { + runCatching { + require(config.url.isNotBlank()) { "WebDAV 地址未填写" } + val request = baseRequest(config) + .url(config.url) + .method("PROPFIND", EMPTY_BODY) + .header("Depth", "0") + .build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful && response.code != 207) { + throw WebDavException("服务器返回 ${response.code} ${response.message}") + } + } + }.onFailure { FileLogger.e(TAG, "WebDAV 连接测试失败", it) } + } + + /** 上传文件到 [remoteFileName](相对配置的目录 URL,可含子目录如 `AiCode/xxx`)。 + * 若含子目录,先 MKCOL 逐级创建(已存在返回 405 也视为成功)。 */ + suspend fun upload(config: WebDavConfig, localFile: File, remoteFileName: String): Result = + withContext(Dispatchers.IO) { + runCatching { + require(config.url.isNotBlank()) { "WebDAV 地址未填写" } + ensureDirectories(config, remoteFileName) + val target = buildTargetUrl(config.url, remoteFileName) + val request = baseRequest(config) + .url(target) + .put(localFile.asRequestBody("application/octet-stream".toMediaType())) + .build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw WebDavException("上传失败:服务器返回 ${response.code} ${response.message}") + } + } + }.onFailure { FileLogger.e(TAG, "WebDAV 上传失败", it) } + } + + /** 为 [remoteFileName] 中的子目录逐级 MKCOL(如 `AiCode/backup.tar.gz` → 创建 `AiCode/`)。 */ + private fun ensureDirectories(config: WebDavConfig, remoteFileName: String) { + val parts = remoteFileName.split('/').dropLast(1).filter { it.isNotEmpty() } + var accum = "" + for (part in parts) { + accum = if (accum.isEmpty()) part else "$accum/$part" + val dirUrl = buildTargetUrl(config.url, accum) + "/" + val request = baseRequest(config).url(dirUrl).method("MKCOL", EMPTY_BODY).build() + runCatching { + client.newCall(request).execute().use { response -> + // 201 创建成功;405 已存在;两者都视为可用。 + if (response.code != 201 && response.code != 405 && !response.isSuccessful) { + throw WebDavException("创建目录失败:服务器返回 ${response.code}") + } + } + }.getOrElse { throw it } + } + } + + private fun baseRequest(config: WebDavConfig): Request.Builder { + val builder = Request.Builder() + if (config.username.isNotBlank() || config.password.isNotBlank()) { + builder.header("Authorization", Credentials.basic(config.username, config.password)) + } + return builder + } + + /** 拼接目录 URL 与文件名,容忍目录末尾有无 `/`。 */ + private fun buildTargetUrl(baseUrl: String, fileName: String): String { + val base = baseUrl.trimEnd('/') + return "$base/$fileName" + } + + companion object { + private const val TAG = "WebDavClient" + + private val EMPTY_BODY = ByteArray(0).toRequestBodyCompat() + + private val DEFAULT_CLIENT: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(120, TimeUnit.SECONDS) + .followRedirects(true) + .build() + } + + private fun ByteArray.toRequestBodyCompat() = + okhttp3.RequestBody.create("application/xml; charset=utf-8".toMediaType(), this) + } +} diff --git a/app/src/main/java/com/aicode/feature/backup/data/WebDavSettingsRepository.kt b/app/src/main/java/com/aicode/feature/backup/data/WebDavSettingsRepository.kt new file mode 100644 index 000000000..8acc617db --- /dev/null +++ b/app/src/main/java/com/aicode/feature/backup/data/WebDavSettingsRepository.kt @@ -0,0 +1,133 @@ +package com.aicode.feature.backup.data + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.webDavDataStore by preferencesDataStore(name = "webdav_prefs") + +/** 自动同步频率单位。 */ +enum class SyncIntervalUnit { + MINUTE, HOUR, DAY; + + companion object { + fun fromName(name: String?): SyncIntervalUnit = + entries.firstOrNull { it.name == name } ?: HOUR + } +} + +/** + * WebDAV 同步设置: + * + * - 服务器地址 / 用户名 / 密码(密码加密存储由调用方负责,这里存明文占位)。 + * - 是否自动备份同步;自动同步时选择要同步的内容与间隔。 + * + * 持久化用独立 DataStore。密码不做加密属已知取舍:与内置 FTP 服务端等本地凭据同级, + * 仅存于应用私有目录。 + */ +@Singleton +class WebDavSettingsRepository @Inject constructor( + @param:ApplicationContext private val context: Context +) { + private companion object { + val URL_KEY = stringPreferencesKey("url") + val USERNAME_KEY = stringPreferencesKey("username") + val PASSWORD_KEY = stringPreferencesKey("password") + + val AUTO_SYNC_KEY = booleanPreferencesKey("auto_sync") + val SYNC_PROVIDERS_KEY = booleanPreferencesKey("sync_providers") + val SYNC_REMOTE_KEY = booleanPreferencesKey("sync_remote_connections") + val SYNC_CHAT_HISTORY_KEY = booleanPreferencesKey("sync_chat_history") + val SYNC_MCP_KEY = booleanPreferencesKey("sync_mcp_servers") + val SYNC_PERMISSIONS_KEY = booleanPreferencesKey("sync_permission_rules") + val SYNC_APP_SETTINGS_KEY = booleanPreferencesKey("sync_app_settings") + val SYNC_WORKSPACE_FILES_KEY = booleanPreferencesKey("sync_workspace_files") + val INTERVAL_VALUE_KEY = intPreferencesKey("interval_value") + val INTERVAL_UNIT_KEY = stringPreferencesKey("interval_unit") + val LAST_SYNC_AT_KEY = stringPreferencesKey("last_sync_at") + } + + val urlFlow: Flow = context.webDavDataStore.data.map { it[URL_KEY] ?: "" } + val usernameFlow: Flow = context.webDavDataStore.data.map { it[USERNAME_KEY] ?: "" } + val passwordFlow: Flow = context.webDavDataStore.data.map { it[PASSWORD_KEY] ?: "" } + + val autoSyncFlow: Flow = context.webDavDataStore.data.map { it[AUTO_SYNC_KEY] ?: false } + val syncProvidersFlow: Flow = context.webDavDataStore.data.map { it[SYNC_PROVIDERS_KEY] ?: true } + val syncRemoteFlow: Flow = context.webDavDataStore.data.map { it[SYNC_REMOTE_KEY] ?: true } + val syncChatHistoryFlow: Flow = context.webDavDataStore.data.map { it[SYNC_CHAT_HISTORY_KEY] ?: true } + val syncMcpFlow: Flow = context.webDavDataStore.data.map { it[SYNC_MCP_KEY] ?: true } + val syncPermissionsFlow: Flow = context.webDavDataStore.data.map { it[SYNC_PERMISSIONS_KEY] ?: true } + val syncAppSettingsFlow: Flow = context.webDavDataStore.data.map { it[SYNC_APP_SETTINGS_KEY] ?: true } + val syncWorkspaceFilesFlow: Flow = context.webDavDataStore.data.map { it[SYNC_WORKSPACE_FILES_KEY] ?: false } + + /** 自动同步间隔数值;默认 1。 */ + val intervalValueFlow: Flow = context.webDavDataStore.data.map { + (it[INTERVAL_VALUE_KEY] ?: 1).coerceAtLeast(1) + } + + /** 自动同步间隔单位;默认小时。 */ + val intervalUnitFlow: Flow = context.webDavDataStore.data.map { + SyncIntervalUnit.fromName(it[INTERVAL_UNIT_KEY]) + } + + /** 上次同步时间(毫秒时间戳字符串);从未同步时为 null。 */ + val lastSyncAtFlow: Flow = context.webDavDataStore.data.map { + it[LAST_SYNC_AT_KEY]?.toLongOrNull() + } + + suspend fun setUrl(url: String) = edit { it[URL_KEY] = url } + suspend fun setUsername(username: String) = edit { it[USERNAME_KEY] = username } + suspend fun setPassword(password: String) = edit { it[PASSWORD_KEY] = password } + + suspend fun setAutoSync(enabled: Boolean) = edit { it[AUTO_SYNC_KEY] = enabled } + suspend fun setSyncProviders(enabled: Boolean) = edit { it[SYNC_PROVIDERS_KEY] = enabled } + suspend fun setSyncRemote(enabled: Boolean) = edit { it[SYNC_REMOTE_KEY] = enabled } + suspend fun setSyncChatHistory(enabled: Boolean) = edit { it[SYNC_CHAT_HISTORY_KEY] = enabled } + suspend fun setSyncMcp(enabled: Boolean) = edit { it[SYNC_MCP_KEY] = enabled } + suspend fun setSyncPermissions(enabled: Boolean) = edit { it[SYNC_PERMISSIONS_KEY] = enabled } + suspend fun setSyncAppSettings(enabled: Boolean) = edit { it[SYNC_APP_SETTINGS_KEY] = enabled } + suspend fun setSyncWorkspaceFiles(enabled: Boolean) = edit { it[SYNC_WORKSPACE_FILES_KEY] = enabled } + + suspend fun setIntervalValue(value: Int) = edit { it[INTERVAL_VALUE_KEY] = value.coerceAtLeast(1) } + suspend fun setIntervalUnit(unit: SyncIntervalUnit) = edit { it[INTERVAL_UNIT_KEY] = unit.name } + + suspend fun setLastSyncAt(millis: Long) = edit { it[LAST_SYNC_AT_KEY] = millis.toString() } + + /** 读取一次当前配置(供同步任务构造请求)。 */ + suspend fun snapshot(): WebDavConfig { + val prefs = context.webDavDataStore.data.first() + return WebDavConfig( + url = prefs[URL_KEY] ?: "", + username = prefs[USERNAME_KEY] ?: "", + password = prefs[PASSWORD_KEY] ?: "", + autoSync = prefs[AUTO_SYNC_KEY] ?: false, + intervalValue = (prefs[INTERVAL_VALUE_KEY] ?: 1).coerceAtLeast(1), + intervalUnit = SyncIntervalUnit.fromName(prefs[INTERVAL_UNIT_KEY]) + ) + } + + private suspend fun edit(block: (androidx.datastore.preferences.core.MutablePreferences) -> Unit) { + context.webDavDataStore.edit(block) + } +} + +/** WebDAV 连接与同步参数快照。 */ +data class WebDavConfig( + val url: String, + val username: String, + val password: String, + val autoSync: Boolean, + val intervalValue: Int, + val intervalUnit: SyncIntervalUnit +) { + val isConfigured: Boolean get() = url.isNotBlank() +} diff --git a/app/src/main/java/com/aicode/feature/backup/domain/BackupManager.kt b/app/src/main/java/com/aicode/feature/backup/domain/BackupManager.kt index d56127eb9..4eed08eb1 100644 --- a/app/src/main/java/com/aicode/feature/backup/domain/BackupManager.kt +++ b/app/src/main/java/com/aicode/feature/backup/domain/BackupManager.kt @@ -1,5 +1,6 @@ package com.aicode.feature.backup.domain +import java.io.File import java.io.InputStream import java.io.OutputStream @@ -43,6 +44,9 @@ interface BackupManager { * @param input 调用方负责打开与关闭输入流 */ suspend fun previewImport(input: InputStream, password: CharArray?): Result + + /** 准备经过完整解密校验的压缩包,调用方负责在预览和恢复结束后删除临时文件。 */ + suspend fun prepareImport(input: InputStream, password: CharArray?): File } /** 导入预览结果:备份中的工作区列表(供勾选)。 */ diff --git a/app/src/main/java/com/aicode/feature/backup/domain/BackupSnapshot.kt b/app/src/main/java/com/aicode/feature/backup/domain/BackupSnapshot.kt index ec3972542..e39bb5c90 100644 --- a/app/src/main/java/com/aicode/feature/backup/domain/BackupSnapshot.kt +++ b/app/src/main/java/com/aicode/feature/backup/domain/BackupSnapshot.kt @@ -83,7 +83,8 @@ data class BackupMetadata( val compactionProviderId: String = "", val compactionModel: String = "", val syncSettings: SyncSettingsSnapshot? = null, - val workspaces: List = emptyList() + val workspaces: List = emptyList(), + val includesAppSettings: Boolean = themeMode != null ) /** 备份元数据中的一个工作区段:名称 + 备份的文件数(用于导入摘要)。 */ diff --git a/app/src/main/java/com/aicode/feature/backup/presentation/BackupViewModel.kt b/app/src/main/java/com/aicode/feature/backup/presentation/BackupViewModel.kt index 637f9bdf4..45fdbdc0b 100644 --- a/app/src/main/java/com/aicode/feature/backup/presentation/BackupViewModel.kt +++ b/app/src/main/java/com/aicode/feature/backup/presentation/BackupViewModel.kt @@ -18,8 +18,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +import java.io.File import java.io.OutputStream +import kotlinx.coroutines.NonCancellable import javax.inject.Inject sealed class BackupState { @@ -27,11 +30,10 @@ sealed class BackupState { data object Working : BackupState() data object ExportDone : BackupState() data class ImportSuccess(val stats: RestoreStats) : BackupState() - /** 导入预览完成,备份含工作区数据:等待用户勾选要恢复的工作区(暂存 uri 与口令)。 */ + /** 导入预览完成,等待选择要恢复的工作区。 */ data class WorkspaceSelection( val workspaces: List, - val uri: Uri, - val password: String + val uri: Uri ) : BackupState() data class Error(val message: String) : BackupState() } @@ -45,6 +47,9 @@ class BackupViewModel @Inject constructor( private val _state = MutableStateFlow(BackupState.Idle) val state: StateFlow = _state.asStateFlow() + @Volatile + private var preparedImport: File? = null + private val prefs = context.getSharedPreferences("backup_options", Context.MODE_PRIVATE) private val _exportOptions = MutableStateFlow(loadExportOptions()) @@ -97,32 +102,43 @@ class BackupViewModel @Inject constructor( val pw = password.toCharArray().takeIf { it.isNotEmpty() } FileLogger.i(TAG, "导入请求:${describeUri(uri)}(${if (pw != null) "加密" else "明文"})") try { - val input = withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri) + val preview = withContext(Dispatchers.IO + NonCancellable) { + val input = context.contentResolver.openInputStream(uri) ?: throw IllegalArgumentException(context.getString(R.string.backup_read_failed)) - } - input.use { backupManager.previewImport(it, pw) } - .onSuccess { preview -> - if (preview.hasWorkspaceData) { - _state.value = BackupState.WorkspaceSelection(preview.workspaces, uri, password) + input.use { + if (pw == null) { + backupManager.previewImport(it, null) } else { - restoreFromUri(uri, pw, null) + val file = backupManager.prepareImport(it, pw) + preparedImport = file + file.inputStream().use { source -> backupManager.previewImport(source, null) } } } - .onFailure { _state.value = BackupState.Error(describeImportError(it)) } + } + preview.onSuccess { preview -> + if (preview.hasWorkspaceData) { + _state.value = BackupState.WorkspaceSelection(preview.workspaces, uri) + } else { + restoreFromUri(uri, null) + } + }.onFailure { _state.value = BackupState.Error(describeImportError(it)) } } catch (e: Exception) { _state.value = BackupState.Error(describeImportError(e)) + } finally { + pw?.fill('\u0000') + if (_state.value !is BackupState.WorkspaceSelection || !isActive) { + clearPreparedImport() + } } } } - /** 勾选确认后按所选工作区恢复(重新打开输入流执行真正还原)。 */ + /** 勾选确认后按所选工作区恢复,复用已校验的解密文件。 */ fun confirmImportSelection(selected: Set) { val current = _state.value as? BackupState.WorkspaceSelection ?: return _state.value = BackupState.Working viewModelScope.launch { - val pw = current.password.toCharArray().takeIf { it.isNotEmpty() } - restoreFromUri(current.uri, pw, selected) + restoreFromUri(current.uri, selected) } } @@ -130,18 +146,21 @@ class BackupViewModel @Inject constructor( fun cancelImportSelection() { if (_state.value is BackupState.WorkspaceSelection) { _state.value = BackupState.Idle + val file = preparedImport + preparedImport = null + viewModelScope.launch(Dispatchers.IO + NonCancellable) { file?.delete() } } } - private suspend fun restoreFromUri(uri: Uri, pw: CharArray?, selected: Set?) { + private suspend fun restoreFromUri(uri: Uri, selected: Set?) { try { FileLogger.i(TAG, "开始还原:${describeUri(uri)}${if (selected != null) ",勾选工作区=${selected.size}个" else ",全量"}") - val input = withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri) + val result = withContext(Dispatchers.IO) { + val input = preparedImport?.inputStream() ?: context.contentResolver.openInputStream(uri) ?: throw IllegalArgumentException(context.getString(R.string.backup_read_failed)) + input.use { backupManager.import(it, null, selected) } } - input.use { backupManager.import(it, pw, selected) } - .onSuccess { + result.onSuccess { FileLogger.i(TAG, "还原成功:$it") _state.value = BackupState.ImportSuccess(it) } @@ -152,9 +171,23 @@ class BackupViewModel @Inject constructor( } catch (e: Exception) { FileLogger.e(TAG, "还原异常", e) _state.value = BackupState.Error(describeImportError(e)) + } finally { + clearPreparedImport() } } + private suspend fun clearPreparedImport() { + val file = preparedImport + preparedImport = null + withContext(Dispatchers.IO + NonCancellable) { file?.delete() } + } + + override fun onCleared() { + preparedImport?.delete() + preparedImport = null + super.onCleared() + } + /** 日志用 URI 摘要:只保留 scheme://authority/最后一段,避免记录完整路径。 */ private fun describeUri(uri: Uri): String { val last = uri.lastPathSegment ?: "" @@ -180,7 +213,11 @@ class BackupViewModel @Inject constructor( } fun reset() { - _state.value = BackupState.Idle + if (_state.value is BackupState.WorkspaceSelection) { + cancelImportSelection() + } else { + _state.value = BackupState.Idle + } } companion object { diff --git a/app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncScreen.kt b/app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncScreen.kt new file mode 100644 index 000000000..fc53a5b9e --- /dev/null +++ b/app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncScreen.kt @@ -0,0 +1,438 @@ +package com.aicode.feature.backup.presentation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.aicode.R +import com.aicode.core.theme.Spacing +import com.aicode.core.ui.AppSwitch +import com.aicode.core.ui.AppTextField +import com.aicode.core.ui.FloatingTabBar +import com.aicode.core.ui.FloatingTabItem +import com.aicode.core.ui.SegmentedTabs +import com.aicode.feature.backup.data.SyncIntervalUnit +import com.aicode.feature.settings.presentation.component.SettingsDivider +import com.aicode.feature.settings.presentation.component.SettingsGroup +import com.aicode.feature.settings.presentation.component.SettingsGroupHeader +import com.aicode.feature.settings.presentation.component.SettingsRow +import com.aicode.feature.settings.presentation.component.settingsPageBackground +import compose.icons.FeatherIcons +import compose.icons.feathericons.ArrowLeft +import compose.icons.feathericons.Cloud +import compose.icons.feathericons.RefreshCw +import compose.icons.feathericons.UploadCloud +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * WebDAV 备份同步独立界面: + * + * - 「连接」tab:地址 / 用户名 / 密码 + 连接测试 + 立即同步。 + * - 「自动同步」tab:自动备份同步开关;开启后选择同步内容与间隔(数值 + 单位)。 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WebDavSyncScreen( + onNavigateBack: () -> Unit, + viewModel: WebDavSyncViewModel = hiltViewModel() +) { + val url by viewModel.url.collectAsStateWithLifecycle() + val username by viewModel.username.collectAsStateWithLifecycle() + val password by viewModel.password.collectAsStateWithLifecycle() + val autoSync by viewModel.autoSync.collectAsStateWithLifecycle() + val syncProviders by viewModel.syncProviders.collectAsStateWithLifecycle() + val syncRemote by viewModel.syncRemote.collectAsStateWithLifecycle() + val syncAppSettings by viewModel.syncAppSettings.collectAsStateWithLifecycle() + val syncChatHistory by viewModel.syncChatHistory.collectAsStateWithLifecycle() + val syncMcp by viewModel.syncMcp.collectAsStateWithLifecycle() + val syncPermissions by viewModel.syncPermissions.collectAsStateWithLifecycle() + val syncWorkspaceFiles by viewModel.syncWorkspaceFiles.collectAsStateWithLifecycle() + val intervalValue by viewModel.intervalValue.collectAsStateWithLifecycle() + val intervalUnit by viewModel.intervalUnit.collectAsStateWithLifecycle() + val lastSyncAt by viewModel.lastSyncAt.collectAsStateWithLifecycle() + val opState by viewModel.opState.collectAsStateWithLifecycle() + + val pagerState = rememberPagerState { 2 } + val connScrollState = rememberScrollState() + val autoScrollState = rememberScrollState() + val tabsScrolling by remember { + derivedStateOf { connScrollState.isScrollInProgress || autoScrollState.isScrollInProgress } + } + + var showResult by remember { mutableStateOf(false) } + LaunchedEffect(opState) { + if (opState is WebDavOpState.Success || opState is WebDavOpState.Error) showResult = true + } + + Scaffold( + containerColor = settingsPageBackground(), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = settingsPageBackground(), + titleContentColor = MaterialTheme.colorScheme.onBackground + ), + title = { Text(stringResource(R.string.webdav_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(FeatherIcons.ArrowLeft, contentDescription = stringResource(R.string.common_back)) + } + } + ) + } + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding)) { + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { tab -> + when (tab) { + 0 -> ConnectionTab( + scrollStateHolder = connScrollState, + url = url, + username = username, + password = password, + lastSyncAt = lastSyncAt, + testing = opState is WebDavOpState.Testing, + syncing = opState is WebDavOpState.Syncing, + onUrlChange = viewModel::setUrl, + onUsernameChange = viewModel::setUsername, + onPasswordChange = viewModel::setPassword, + onTest = viewModel::testConnection, + onSyncNow = viewModel::syncNow + ) + 1 -> AutoSyncTab( + scrollStateHolder = autoScrollState, + autoSync = autoSync, + syncProviders = syncProviders, + syncRemote = syncRemote, + syncChatHistory = syncChatHistory, + syncMcp = syncMcp, + syncPermissions = syncPermissions, + syncAppSettings = syncAppSettings, + syncWorkspaceFiles = syncWorkspaceFiles, + intervalValue = intervalValue, + intervalUnit = intervalUnit, + onToggleAutoSync = viewModel::setAutoSync, + onToggleProviders = viewModel::setSyncProviders, + onToggleRemote = viewModel::setSyncRemote, + onToggleChatHistory = viewModel::setSyncChatHistory, + onToggleMcp = viewModel::setSyncMcp, + onTogglePermissions = viewModel::setSyncPermissions, + onToggleAppSettings = viewModel::setSyncAppSettings, + onToggleWorkspaceFiles = viewModel::setSyncWorkspaceFiles, + onIntervalValueChange = viewModel::setIntervalValue, + onIntervalUnitChange = viewModel::setIntervalUnit + ) + } + } + + FloatingTabBar( + pagerState = pagerState, + items = listOf( + FloatingTabItem(FeatherIcons.Cloud, stringResource(R.string.webdav_tab_connection)), + FloatingTabItem(FeatherIcons.RefreshCw, stringResource(R.string.webdav_tab_auto)) + ), + maskColor = settingsPageBackground(), + isScrolling = tabsScrolling, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + } + + if (showResult) { + val message = when (val s = opState) { + is WebDavOpState.Success -> s.message + is WebDavOpState.Error -> s.message + else -> "" + } + val isError = opState is WebDavOpState.Error + androidx.compose.material3.AlertDialog( + onDismissRequest = { showResult = false; viewModel.resetOpState() }, + title = { Text(stringResource(if (isError) R.string.webdav_op_failed else R.string.webdav_op_done)) }, + text = { Text(message) }, + confirmButton = { + TextButton(onClick = { showResult = false; viewModel.resetOpState() }) { + Text(stringResource(R.string.common_got_it)) + } + } + ) + } +} + +@Composable +private fun ConnectionTab( + scrollStateHolder: androidx.compose.foundation.ScrollState, + url: String, + username: String, + password: String, + lastSyncAt: Long?, + testing: Boolean, + syncing: Boolean, + onUrlChange: (String) -> Unit, + onUsernameChange: (String) -> Unit, + onPasswordChange: (String) -> Unit, + onTest: () -> Unit, + onSyncNow: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollStateHolder) + .padding(horizontal = Spacing.lg) + .padding(bottom = 90.dp) + ) { + SettingsGroupHeader(text = stringResource(R.string.webdav_server)) + SettingsGroup { + Column(modifier = Modifier.padding(Spacing.md)) { + AppTextField( + value = url, + onValueChange = onUrlChange, + label = stringResource(R.string.webdav_address), + placeholder = "https://example.com/dav/", + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(Spacing.sm)) + AppTextField( + value = username, + onValueChange = onUsernameChange, + label = stringResource(R.string.webdav_username), + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(Spacing.sm)) + AppTextField( + value = password, + onValueChange = onPasswordChange, + label = stringResource(R.string.webdav_password), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth() + ) + } + } + + Spacer(Modifier.height(Spacing.md)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) + ) { + OutlinedButton( + onClick = onTest, + enabled = !testing && !syncing, + modifier = Modifier.weight(1f).height(44.dp) + ) { + if (testing) { + CircularProgressIndicator(modifier = Modifier.width(18.dp).height(18.dp), strokeWidth = 2.dp) + } else { + Text(stringResource(R.string.webdav_test)) + } + } + Button( + onClick = onSyncNow, + enabled = !testing && !syncing, + modifier = Modifier.weight(1f).height(44.dp) + ) { + if (syncing) { + CircularProgressIndicator(modifier = Modifier.width(18.dp).height(18.dp), strokeWidth = 2.dp) + } else { + Icon(FeatherIcons.UploadCloud, contentDescription = null, modifier = Modifier.width(18.dp).height(18.dp)) + Spacer(Modifier.width(Spacing.xs)) + Text(stringResource(R.string.webdav_sync_now)) + } + } + } + + lastSyncAt?.let { ts -> + Spacer(Modifier.height(Spacing.sm)) + Text( + text = stringResource(R.string.webdav_last_sync, formatTime(ts)), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun AutoSyncTab( + scrollStateHolder: androidx.compose.foundation.ScrollState, + autoSync: Boolean, + syncProviders: Boolean, + syncRemote: Boolean, + syncChatHistory: Boolean, + syncMcp: Boolean, + syncPermissions: Boolean, + syncAppSettings: Boolean, + syncWorkspaceFiles: Boolean, + intervalValue: Int, + intervalUnit: SyncIntervalUnit, + onToggleAutoSync: (Boolean) -> Unit, + onToggleProviders: (Boolean) -> Unit, + onToggleRemote: (Boolean) -> Unit, + onToggleChatHistory: (Boolean) -> Unit, + onToggleMcp: (Boolean) -> Unit, + onTogglePermissions: (Boolean) -> Unit, + onToggleAppSettings: (Boolean) -> Unit, + onToggleWorkspaceFiles: (Boolean) -> Unit, + onIntervalValueChange: (Int) -> Unit, + onIntervalUnitChange: (SyncIntervalUnit) -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollStateHolder) + .padding(horizontal = Spacing.lg) + .padding(bottom = 90.dp) + ) { + SettingsGroupHeader(text = stringResource(R.string.webdav_auto_sync)) + SettingsGroup { + SettingsRow( + title = stringResource(R.string.webdav_auto_sync_toggle), + subtitle = stringResource(R.string.webdav_auto_sync_toggle_desc), + trailing = { + AppSwitch(checked = autoSync, onCheckedChange = onToggleAutoSync) + } + ) + } + + if (autoSync) { + SettingsGroupHeader(text = stringResource(R.string.webdav_sync_content)) + SettingsGroup { + SettingsRow( + title = stringResource(R.string.common_ai_providers), + trailing = { AppSwitch(checked = syncProviders, onCheckedChange = onToggleProviders) } + ) + SettingsDivider() + SettingsRow( + title = stringResource(R.string.backup_data_remote), + trailing = { AppSwitch(checked = syncRemote, onCheckedChange = onToggleRemote) } + ) + SettingsDivider() + SettingsRow( + title = stringResource(R.string.backup_data_chat_history), + trailing = { AppSwitch(checked = syncChatHistory, onCheckedChange = onToggleChatHistory) } + ) + SettingsDivider() + SettingsRow( + title = stringResource(R.string.backup_data_mcp), + trailing = { AppSwitch(checked = syncMcp, onCheckedChange = onToggleMcp) } + ) + SettingsDivider() + SettingsRow( + title = stringResource(R.string.backup_data_permissions), + trailing = { AppSwitch(checked = syncPermissions, onCheckedChange = onTogglePermissions) } + ) + SettingsDivider() + SettingsRow( + title = stringResource(R.string.backup_data_app_settings), + trailing = { AppSwitch(checked = syncAppSettings, onCheckedChange = onToggleAppSettings) } + ) + SettingsDivider() + SettingsRow( + title = stringResource(R.string.backup_data_workspace), + trailing = { AppSwitch(checked = syncWorkspaceFiles, onCheckedChange = onToggleWorkspaceFiles) } + ) + } + + SettingsGroupHeader(text = stringResource(R.string.webdav_interval)) + SettingsGroup { + Column(modifier = Modifier.padding(Spacing.md)) { + IntervalRow( + value = intervalValue, + unit = intervalUnit, + onValueChange = onIntervalValueChange, + onUnitChange = onIntervalUnitChange + ) + } + } + } + } +} + +@Composable +private fun IntervalRow( + value: Int, + unit: SyncIntervalUnit, + onValueChange: (Int) -> Unit, + onUnitChange: (SyncIntervalUnit) -> Unit +) { + var text by remember(value) { mutableStateOf(value.toString()) } + val units = listOf(SyncIntervalUnit.MINUTE, SyncIntervalUnit.HOUR, SyncIntervalUnit.DAY) + val labels = listOf( + stringResource(R.string.webdav_unit_minute), + stringResource(R.string.webdav_unit_hour), + stringResource(R.string.webdav_unit_day) + ) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + AppTextField( + value = text, + onValueChange = { input -> + val digits = input.filter { it.isDigit() }.take(4) + text = digits + digits.toIntOrNull()?.let { if (it > 0) onValueChange(it) } + }, + label = stringResource(R.string.webdav_interval_number), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(110.dp) + ) + Spacer(Modifier.width(Spacing.md)) + Text( + text = stringResource(R.string.webdav_interval_every), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(Modifier.height(Spacing.sm)) + SegmentedTabs( + selected = units.indexOf(unit).coerceAtLeast(0), + labels = labels, + onSelect = { index -> onUnitChange(units[index]) } + ) + } +} + +private fun formatTime(millis: Long): String = + SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(millis)) diff --git a/app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncViewModel.kt b/app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncViewModel.kt new file mode 100644 index 000000000..961f7c03e --- /dev/null +++ b/app/src/main/java/com/aicode/feature/backup/presentation/WebDavSyncViewModel.kt @@ -0,0 +1,167 @@ +package com.aicode.feature.backup.presentation + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aicode.core.util.FileLogger +import com.aicode.feature.backup.data.SyncIntervalUnit +import com.aicode.feature.backup.data.WebDavClient +import com.aicode.feature.backup.data.WebDavSettingsRepository +import com.aicode.feature.backup.domain.BackupManager +import com.aicode.feature.backup.domain.BackupOptions +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.io.File +import javax.inject.Inject + +/** 立即同步/测试连接的操作状态。 */ +sealed interface WebDavOpState { + data object Idle : WebDavOpState + data object Testing : WebDavOpState + data object Syncing : WebDavOpState + data class Success(val message: String) : WebDavOpState + data class Error(val message: String) : WebDavOpState +} + +@HiltViewModel +class WebDavSyncViewModel @Inject constructor( + @param:ApplicationContext private val context: Context, + private val repository: WebDavSettingsRepository, + private val client: WebDavClient, + private val backupManager: BackupManager +) : ViewModel() { + + private val _opState = MutableStateFlow(WebDavOpState.Idle) + val opState: StateFlow = _opState.asStateFlow() + + val url = repository.urlFlow.stateIn(viewModelScope, SharingStarted.Eagerly, "") + val username = repository.usernameFlow.stateIn(viewModelScope, SharingStarted.Eagerly, "") + val password = repository.passwordFlow.stateIn(viewModelScope, SharingStarted.Eagerly, "") + val autoSync = repository.autoSyncFlow.stateIn(viewModelScope, SharingStarted.Eagerly, false) + val syncProviders = repository.syncProvidersFlow.stateIn(viewModelScope, SharingStarted.Eagerly, true) + val syncRemote = repository.syncRemoteFlow.stateIn(viewModelScope, SharingStarted.Eagerly, true) + val syncChatHistory = repository.syncChatHistoryFlow.stateIn(viewModelScope, SharingStarted.Eagerly, true) + val syncMcp = repository.syncMcpFlow.stateIn(viewModelScope, SharingStarted.Eagerly, true) + val syncPermissions = repository.syncPermissionsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, true) + val syncAppSettings = repository.syncAppSettingsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, true) + val syncWorkspaceFiles = repository.syncWorkspaceFilesFlow.stateIn(viewModelScope, SharingStarted.Eagerly, false) + val intervalValue = repository.intervalValueFlow.stateIn(viewModelScope, SharingStarted.Eagerly, 1) + val intervalUnit = repository.intervalUnitFlow.stateIn(viewModelScope, SharingStarted.Eagerly, SyncIntervalUnit.HOUR) + val lastSyncAt = repository.lastSyncAtFlow.stateIn(viewModelScope, SharingStarted.Eagerly, null) + + fun setUrl(value: String) = viewModelScope.launch { repository.setUrl(value) } + fun setUsername(value: String) = viewModelScope.launch { repository.setUsername(value) } + fun setPassword(value: String) = viewModelScope.launch { repository.setPassword(value) } + + fun setAutoSync(enabled: Boolean) = viewModelScope.launch { + repository.setAutoSync(enabled) + if (enabled) { + val prefs = repository.snapshot() + com.aicode.feature.backup.data.WebDavAutoSyncWorker + .schedule(context, prefs.intervalValue, prefs.intervalUnit) + } else { + com.aicode.feature.backup.data.WebDavAutoSyncWorker.cancel(context) + } + } + fun setSyncProviders(enabled: Boolean) = viewModelScope.launch { repository.setSyncProviders(enabled) } + fun setSyncRemote(enabled: Boolean) = viewModelScope.launch { repository.setSyncRemote(enabled) } + fun setSyncMcp(enabled: Boolean) = viewModelScope.launch { repository.setSyncMcp(enabled) } + fun setSyncPermissions(enabled: Boolean) = viewModelScope.launch { repository.setSyncPermissions(enabled) } + fun setSyncAppSettings(enabled: Boolean) = viewModelScope.launch { repository.setSyncAppSettings(enabled) } + fun setSyncChatHistory(enabled: Boolean) = viewModelScope.launch { repository.setSyncChatHistory(enabled) } + fun setSyncWorkspaceFiles(enabled: Boolean) = viewModelScope.launch { repository.setSyncWorkspaceFiles(enabled) } + + fun setIntervalValue(value: Int) = viewModelScope.launch { + repository.setIntervalValue(value) + rescheduleIfAuto() + } + fun setIntervalUnit(unit: SyncIntervalUnit) = viewModelScope.launch { + repository.setIntervalUnit(unit) + rescheduleIfAuto() + } + + private suspend fun rescheduleIfAuto() { + val prefs = repository.snapshot() + if (prefs.autoSync) { + com.aicode.feature.backup.data.WebDavAutoSyncWorker + .schedule(context, prefs.intervalValue, prefs.intervalUnit) + } + } + + /** 连接测试。 */ + fun testConnection() { + if (_opState.value is WebDavOpState.Testing) return + viewModelScope.launch { + _opState.value = WebDavOpState.Testing + val config = repository.snapshot() + _opState.value = client.test(config).fold( + onSuccess = { WebDavOpState.Success("连接成功") }, + onFailure = { WebDavOpState.Error(it.message ?: "连接失败") } + ) + } + } + + /** 立即同步:本地生成一份备份并上传到 WebDAV。 */ + fun syncNow() { + if (_opState.value is WebDavOpState.Syncing) return + viewModelScope.launch { + _opState.value = WebDavOpState.Syncing + val config = repository.snapshot() + if (!config.isConfigured) { + _opState.value = WebDavOpState.Error("请先填写 WebDAV 地址") + return@launch + } + try { + val options = buildOptions() + val tempFile = File(context.cacheDir, "webdav-backup-${System.currentTimeMillis()}.tar.gz") + tempFile.outputStream().use { out -> + backupManager.export(null, options, out) + } + val result = client.upload(config, tempFile, REMOTE_FILE_PATH) + tempFile.delete() + _opState.value = result.fold( + onSuccess = { + val now = System.currentTimeMillis() + repository.setLastSyncAt(now) + WebDavOpState.Success("同步完成") + }, + onFailure = { WebDavOpState.Error(it.message ?: "同步失败") } + ) + } catch (e: Exception) { + FileLogger.e(TAG, "WebDAV 立即同步失败", e) + _opState.value = WebDavOpState.Error(e.message ?: "同步失败") + } + } + } + + fun resetOpState() { + _opState.value = WebDavOpState.Idle + } + + private suspend fun buildOptions(): BackupOptions { + return BackupOptions( + providers = repository.syncProvidersFlow.first(), + remoteConnections = repository.syncRemoteFlow.first(), + chatHistory = repository.syncChatHistoryFlow.first(), + mcpServers = repository.syncMcpFlow.first(), + permissionRules = repository.syncPermissionsFlow.first(), + appSettings = repository.syncAppSettingsFlow.first(), + workspaceFiles = repository.syncWorkspaceFilesFlow.first() + ) + } + + companion object { + private const val TAG = "WebDavSync" + + /** 云端根目录下自动创建的文件夹与备份文件名。 */ + private const val REMOTE_FILE_PATH = "AiCode/aicode-backup.tar.gz" + } +} diff --git a/app/src/main/java/com/aicode/feature/credentials/data/repository/FileCredentialRepository.kt b/app/src/main/java/com/aicode/feature/credentials/data/repository/FileCredentialRepository.kt index 6c5171c33..04f0e742a 100644 --- a/app/src/main/java/com/aicode/feature/credentials/data/repository/FileCredentialRepository.kt +++ b/app/src/main/java/com/aicode/feature/credentials/data/repository/FileCredentialRepository.kt @@ -119,9 +119,19 @@ class FileCredentialRepository @Inject constructor( private fun encode(plain: String): String = Base64.encodeToString(plain.toByteArray(Charsets.UTF_8), Base64.NO_WRAP).reversed() - /** 解码编码串;不是本格式(如旧版明文)时返回 null。 */ + /** + * 解码编码串;不是本格式(如旧版明文)时返回 null。 + * + * android.util.Base64 的 DEFAULT 解码是「宽容」的:`:`、`/`、`@` 等非法字符一律按 SKIP 忽略、 + * 不抛异常,明文也能被「解码」成乱码。因此不能以「未抛异常」判定格式,改用往返校验:只有 + * `encode(decode(raw)) == raw` 才认定为编码格式,否则按明文处理(旧版明文需回退解析,否则凭据全部读空)。 + */ private fun tryDecode(text: String): String? = runCatching { - String(Base64.decode(text.reversed(), Base64.DEFAULT), Charsets.UTF_8) + val raw = text.trim() + if (raw.isEmpty()) return@runCatching null + val decoded = String(Base64.decode(raw.reversed(), Base64.DEFAULT), Charsets.UTF_8) + if (encode(decoded) != raw) return@runCatching null + decoded }.getOrNull() /** 把旧版明文凭据文件迁移为编码格式(幂等)。 */ diff --git a/app/src/main/java/com/aicode/feature/settings/data/repository/AIProviderRepositoryImpl.kt b/app/src/main/java/com/aicode/feature/settings/data/repository/AIProviderRepositoryImpl.kt index 11750debc..46bf8d7a7 100644 --- a/app/src/main/java/com/aicode/feature/settings/data/repository/AIProviderRepositoryImpl.kt +++ b/app/src/main/java/com/aicode/feature/settings/data/repository/AIProviderRepositoryImpl.kt @@ -15,8 +15,11 @@ import com.aicode.feature.settings.domain.model.ProxyType import com.aicode.feature.settings.domain.model.sanitized import com.aicode.feature.settings.domain.repository.AIProviderRepository import androidx.room.withTransaction +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import javax.inject.Inject @@ -56,18 +59,18 @@ class AIProviderRepositoryImpl @Inject constructor( override fun getAllProviders(): Flow> { return aiProviderDao.getAllProviders().map { entities -> entities.map { it.toDomainModel() } - } + }.flowOn(Dispatchers.IO) } - override suspend fun getProviderById(id: String): AIProviderConfig? { - return aiProviderDao.getProviderById(id)?.toDomainModel() + override suspend fun getProviderById(id: String): AIProviderConfig? = withContext(Dispatchers.IO) { + aiProviderDao.getProviderById(id)?.toDomainModel() } /** * 保存提供商。排序值以数据库当前值为准(重排可能异步持久化,UI 传入的 * sortOrder 可能陈旧,直接使用会撤销刚完成的排序);新提供商取最大排序 +1。 */ - override suspend fun saveProvider(provider: AIProviderConfig) { + override suspend fun saveProvider(provider: AIProviderConfig): Unit = withContext(Dispatchers.IO) { agentDatabase.withTransaction { val current = aiProviderDao.getProviderById(provider.id) val sortOrder = current?.sortOrder ?: (aiProviderDao.getMaxSortOrder() + 1) diff --git a/app/src/main/java/com/aicode/feature/settings/data/repository/BrowserSettingsRepository.kt b/app/src/main/java/com/aicode/feature/settings/data/repository/BrowserSettingsRepository.kt new file mode 100644 index 000000000..93ba0f92d --- /dev/null +++ b/app/src/main/java/com/aicode/feature/settings/data/repository/BrowserSettingsRepository.kt @@ -0,0 +1,82 @@ +package com.aicode.feature.settings.data.repository + +import android.content.Context +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.aicode.feature.agent.domain.tool.browser.BrowserActionCatalog +import com.aicode.feature.agent.domain.tool.browser.BrowserUserAgent +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.browserDataStore by preferencesDataStore(name = "browser_prefs") + +/** + * 浏览器设置: + * + * - User-Agent 预设(默认 / 安卓手机 / 安卓平板 / Windows / Mac),默认「系统默认」。 + * - AI 控制浏览器的逐 action 权限开关,默认全开。 + * + * 持久化设计与 [ToolSafetySettingsRepository] 一致(独立 DataStore)。 + */ +@Singleton +class BrowserSettingsRepository @Inject constructor( + @param:ApplicationContext private val context: Context +) { + private companion object { + val USER_AGENT_KEY = stringPreferencesKey("user_agent") + + /** 被禁用的 action 集合,逗号分隔(空 = 全开)。 */ + val DISABLED_ACTIONS_KEY = stringPreferencesKey("disabled_actions") + } + + /** 当前 UA 预设;未设置时回退「系统默认」。 */ + val userAgentFlow: Flow = context.browserDataStore.data.map { prefs -> + BrowserUserAgent.fromId(prefs[USER_AGENT_KEY]) + } + + suspend fun setUserAgent(userAgent: BrowserUserAgent) { + context.browserDataStore.edit { it[USER_AGENT_KEY] = userAgent.id } + } + + suspend fun userAgent(): BrowserUserAgent = userAgentFlow.first() + + /** 被禁用的 action 集合。 */ + val disabledActionsFlow: Flow> = context.browserDataStore.data.map { prefs -> + prefs[DISABLED_ACTIONS_KEY] + ?.split(',') + ?.map { it.trim() } + ?.filter { it.isNotEmpty() } + ?.toSet() + ?: emptySet() + } + + /** + * 逐 action 开关的当前状态(默认全开):返回 action 名 → 是否启用。 + * 仅包含 [BrowserActionCatalog.ALL] 中已知的 action,避免脏数据影响 UI。 + */ + val actionEnabledFlow: Flow> = + disabledActionsFlow.map { disabled -> + BrowserActionCatalog.ALL.associateWith { it !in disabled } + } + + suspend fun setActionEnabled(action: String, enabled: Boolean) { + context.browserDataStore.edit { prefs -> + val current = prefs[DISABLED_ACTIONS_KEY] + ?.split(',') + ?.map { it.trim() } + ?.filter { it.isNotEmpty() } + ?.toMutableSet() + ?: mutableSetOf() + if (enabled) current.remove(action) else current.add(action) + prefs[DISABLED_ACTIONS_KEY] = current.joinToString(",") + } + } + + suspend fun isActionEnabled(action: String): Boolean = + action !in disabledActionsFlow.first() +} diff --git a/app/src/main/java/com/aicode/feature/settings/data/repository/GeneralSettingsRepository.kt b/app/src/main/java/com/aicode/feature/settings/data/repository/GeneralSettingsRepository.kt index ce0d03121..87e188864 100644 --- a/app/src/main/java/com/aicode/feature/settings/data/repository/GeneralSettingsRepository.kt +++ b/app/src/main/java/com/aicode/feature/settings/data/repository/GeneralSettingsRepository.kt @@ -24,6 +24,15 @@ enum class StartupSessionMode { RECENT_SESSION } +/** 任务待办(Todo)在聊天页的显示位置。 */ +enum class TodoDisplayPosition { + /** 消息栏上方(默认):紧贴输入框上方展示。 */ + ABOVE_INPUT, + + /** 标题栏下方:在页面头部顶栏之下展示。 */ + BELOW_TITLE +} + /** * 「偏好设置」里的用户偏好。 * @@ -48,6 +57,7 @@ class GeneralSettingsRepository @Inject constructor( val COMPACTION_THRESHOLD_PERCENT_KEY = intPreferencesKey("compaction_threshold_percent") val SENDFILE_MAX_SIZE_MB_KEY = intPreferencesKey("sendfile_max_size_mb") val DELETE_EXTERNAL_WORKSPACE_SESSIONS_KEY = booleanPreferencesKey("delete_external_workspace_sessions") + val TODO_DISPLAY_POSITION_KEY = stringPreferencesKey("todo_display_position") /** 「提示词」页使用说明公告已展示内容的哈希;无值或与当前内容哈希不一致时重新弹出。 */ val PROMPTS_ANNOUNCEMENT_SHOWN_HASH_KEY = stringPreferencesKey("prompts_announcement_shown_hash") @@ -87,6 +97,18 @@ class GeneralSettingsRepository @Inject constructor( /** 切换到工作区前读一次偏好,避免在会话初始化路径上多开一条收集流。 */ suspend fun startupSessionMode(): StartupSessionMode = startupSessionModeFlow.first() + /** 任务待办显示位置;未设置或值无法识别时回退到 [TodoDisplayPosition.ABOVE_INPUT]。 */ + val todoDisplayPositionFlow: Flow = context.generalDataStore.data.map { prefs -> + when (prefs[TODO_DISPLAY_POSITION_KEY]) { + TodoDisplayPosition.BELOW_TITLE.name -> TodoDisplayPosition.BELOW_TITLE + else -> TodoDisplayPosition.ABOVE_INPUT + } + } + + suspend fun setTodoDisplayPosition(position: TodoDisplayPosition) { + context.generalDataStore.edit { it[TODO_DISPLAY_POSITION_KEY] = position.name } + } + /** 备份快照:当前自动对齐开关。 */ suspend fun autoRemoveStaleModelsSnapshot(): Boolean = autoRemoveStaleModelsFlow.first() diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/SettingsViewModel.kt b/app/src/main/java/com/aicode/feature/settings/presentation/SettingsViewModel.kt index 9e62b8292..524210daf 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/SettingsViewModel.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/SettingsViewModel.kt @@ -70,6 +70,7 @@ import com.aicode.feature.settings.data.repository.ProxyConfig import com.aicode.feature.settings.data.repository.ProxySettingsRepository import com.aicode.feature.settings.data.repository.ScreenOnSettingsRepository import com.aicode.feature.settings.data.repository.StartupSessionMode +import com.aicode.feature.settings.data.repository.TodoDisplayPosition import com.aicode.feature.settings.data.repository.ThemeSettingsRepository import com.aicode.feature.settings.data.repository.ToolSafetySettingsRepository import com.aicode.feature.settings.data.repository.BackgroundSettingsRepository @@ -313,6 +314,8 @@ class SettingsViewModel @Inject constructor( private val mcpManager: McpManager, private val permissionRulesRepository: PermissionRulesRepository, private val toolSafetySettingsRepository: ToolSafetySettingsRepository, + private val browserSettingsRepository: com.aicode.feature.settings.data.repository.BrowserSettingsRepository, + private val browserManager: com.aicode.feature.agent.domain.tool.browser.BrowserManager, private val skillRepository: SkillRepository, private val agentDefinitionRepository: AgentDefinitionRepository, private val agentDefinitionConfigRepository: AgentDefinitionConfigRepository, @@ -481,6 +484,17 @@ class SettingsViewModel @Inject constructor( private val _startupSessionMode = MutableStateFlow(StartupSessionMode.NEW_SESSION) val startupSessionMode: StateFlow = _startupSessionMode.asStateFlow() + private val _todoDisplayPosition = MutableStateFlow(TodoDisplayPosition.ABOVE_INPUT) + val todoDisplayPosition: StateFlow = _todoDisplayPosition.asStateFlow() + + private val _browserUserAgent = MutableStateFlow(com.aicode.feature.agent.domain.tool.browser.BrowserUserAgent.DEFAULT) + val browserUserAgent: StateFlow = _browserUserAgent.asStateFlow() + + private val _browserActionEnabled = MutableStateFlow( + com.aicode.feature.agent.domain.tool.browser.BrowserActionCatalog.ALL.associateWith { true } + ) + val browserActionEnabled: StateFlow> = _browserActionEnabled.asStateFlow() + private val _firstByteTimeoutSec = MutableStateFlow(300) val firstByteTimeoutSec: StateFlow = _firstByteTimeoutSec.asStateFlow() @@ -813,6 +827,25 @@ class SettingsViewModel @Inject constructor( } } + launch { + generalSettingsRepository.todoDisplayPositionFlow.collectLatest { + _todoDisplayPosition.value = it + } + } + + launch { + browserSettingsRepository.userAgentFlow.collectLatest { + _browserUserAgent.value = it + browserManager.setUserAgent(it) + } + } + + launch { + browserSettingsRepository.actionEnabledFlow.collectLatest { + _browserActionEnabled.value = it + } + } + launch { generalSettingsRepository.firstByteTimeoutSecFlow.collectLatest { _firstByteTimeoutSec.value = it @@ -1555,6 +1588,23 @@ class SettingsViewModel @Inject constructor( } } + /** 任务待办在聊天页的显示位置。 */ + fun setTodoDisplayPosition(position: TodoDisplayPosition) { + viewModelScope.launch { + generalSettingsRepository.setTodoDisplayPosition(position) + } + } + + /** 浏览器 User-Agent 预设。 */ + fun setBrowserUserAgent(userAgent: com.aicode.feature.agent.domain.tool.browser.BrowserUserAgent) { + viewModelScope.launch { browserSettingsRepository.setUserAgent(userAgent) } + } + + /** 浏览器 AI 控制权限:逐 action 开关。 */ + fun setBrowserActionEnabled(action: String, enabled: Boolean) { + viewModelScope.launch { browserSettingsRepository.setActionEnabled(action, enabled) } + } + /** 流式请求首字超时(秒);0 表示不限制。 */ fun setFirstByteTimeoutSec(sec: Int) { viewModelScope.launch { diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/BrowserSettingsSection.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/BrowserSettingsSection.kt new file mode 100644 index 000000000..c81b88201 --- /dev/null +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/BrowserSettingsSection.kt @@ -0,0 +1,198 @@ +package com.aicode.feature.settings.presentation.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.aicode.R +import com.aicode.core.theme.Spacing +import com.aicode.core.ui.AppSwitch +import com.aicode.feature.agent.domain.tool.browser.BrowserActionCatalog +import com.aicode.feature.agent.domain.tool.browser.BrowserUserAgent +import compose.icons.FeatherIcons +import compose.icons.feathericons.Check +import compose.icons.feathericons.Globe + +/** + * 浏览器设置二级页: + * + * - UA 设置组:User-Agent 预设(默认 / 安卓手机 / 安卓平板 / Windows 10 / Windows 11 / Mac)。 + * - AI 权限组:逐 action 的控制开关,默认全开;关闭后 AI 调用对应操作会被拒绝。 + */ +@Composable +internal fun BrowserSettingsSection( + userAgent: BrowserUserAgent, + actionEnabled: Map, + onSelectUserAgent: (BrowserUserAgent) -> Unit, + onToggleAction: (String, Boolean) -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = Spacing.lg) + .padding(bottom = Spacing.xl) + ) { + // ── UA 设置 ── + SettingsGroupHeader(text = stringResource(R.string.browser_settings_group_ua)) + SettingsGroup { + BrowserUserAgent.entries.forEachIndexed { index, entry -> + if (index > 0) SettingsDivider() + UserAgentRow( + title = userAgentTitle(entry), + selected = userAgent == entry, + onClick = { onSelectUserAgent(entry) } + ) + } + } + Text( + text = stringResource(R.string.browser_settings_ua_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(start = Spacing.md, end = Spacing.md, top = Spacing.sm) + ) + + // ── AI 控制权限 ── + SettingsGroupHeader(text = stringResource(R.string.browser_settings_group_ai)) + SettingsGroup { + BrowserActionCatalog.ALL.forEachIndexed { index, action -> + if (index > 0) SettingsDivider() + SettingsRow( + title = browserActionTitle(action), + subtitle = browserActionSubtitle(action), + trailing = { + AppSwitch( + checked = actionEnabled[action] ?: true, + onCheckedChange = { enabled -> onToggleAction(action, enabled) } + ) + } + ) + } + } + Text( + text = stringResource(R.string.browser_settings_ai_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(start = Spacing.md, end = Spacing.md, top = Spacing.sm) + ) + } +} + +/** UA 单选行:左侧地球图标 + 标题,右侧选中勾。 */ +@Composable +private fun UserAgentRow( + title: String, + selected: Boolean, + onClick: () -> Unit +) { + SettingsRow( + icon = FeatherIcons.Globe, + title = title, + onClick = onClick, + trailing = { + if (selected) { + Icon( + imageVector = FeatherIcons.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(end = Spacing.xs) + .size(18.dp) + ) + } + } + ) +} + +@Composable +private fun userAgentTitle(entry: BrowserUserAgent): String = stringResource( + when (entry) { + BrowserUserAgent.DEFAULT -> R.string.browser_ua_default + BrowserUserAgent.ANDROID_PHONE -> R.string.browser_ua_android_phone + BrowserUserAgent.ANDROID_TABLET -> R.string.browser_ua_android_tablet + BrowserUserAgent.WINDOWS_10 -> R.string.browser_ua_windows_10 + BrowserUserAgent.WINDOWS_11 -> R.string.browser_ua_windows_11 + BrowserUserAgent.MAC_INTEL -> R.string.browser_ua_mac_intel + BrowserUserAgent.MAC_APPLE_SILICON -> R.string.browser_ua_mac_apple + } +) + +/** action 的本地化标题;未知 action 回退为原始名。 */ +@Composable +private fun browserActionTitle(action: String): String { + val res = browserActionTitleRes(action) + return if (res != null) stringResource(res) else action +} + +@Composable +private fun browserActionSubtitle(action: String): String { + val res = browserActionSubtitleRes(action) + return if (res != null) stringResource(res) else action +} + +private fun browserActionTitleRes(action: String): Int? = when (action) { + "navigate" -> R.string.browser_action_navigate + "evaluate" -> R.string.browser_action_evaluate + "click" -> R.string.browser_action_click + "fill" -> R.string.browser_action_fill + "select" -> R.string.browser_action_select + "hover" -> R.string.browser_action_hover + "press" -> R.string.browser_action_press + "getText" -> R.string.browser_action_get_text + "getHtml" -> R.string.browser_action_get_html + "getBackbone" -> R.string.browser_action_get_backbone + "screenshot" -> R.string.browser_action_screenshot + "console" -> R.string.browser_action_console + "wait" -> R.string.browser_action_wait + "scroll" -> R.string.browser_action_scroll + "dialog" -> R.string.browser_action_dialog + "back" -> R.string.browser_action_back + "forward" -> R.string.browser_action_forward + "reload" -> R.string.browser_action_reload + "newTab" -> R.string.browser_action_new_tab + "closeTab" -> R.string.browser_action_close_tab + "selectTab" -> R.string.browser_action_select_tab + "listTabs" -> R.string.browser_action_list_tabs + else -> null +} + +private fun browserActionSubtitleRes(action: String): Int? = when (action) { + "navigate" -> R.string.browser_action_navigate_desc + "evaluate" -> R.string.browser_action_evaluate_desc + "click" -> R.string.browser_action_click_desc + "fill" -> R.string.browser_action_fill_desc + "select" -> R.string.browser_action_select_desc + "hover" -> R.string.browser_action_hover_desc + "press" -> R.string.browser_action_press_desc + "getText" -> R.string.browser_action_get_text_desc + "getHtml" -> R.string.browser_action_get_html_desc + "getBackbone" -> R.string.browser_action_get_backbone_desc + "screenshot" -> R.string.browser_action_screenshot_desc + "console" -> R.string.browser_action_console_desc + "wait" -> R.string.browser_action_wait_desc + "scroll" -> R.string.browser_action_scroll_desc + "dialog" -> R.string.browser_action_dialog_desc + "back" -> R.string.browser_action_back_desc + "forward" -> R.string.browser_action_forward_desc + "reload" -> R.string.browser_action_reload_desc + "newTab" -> R.string.browser_action_new_tab_desc + "closeTab" -> R.string.browser_action_close_tab_desc + "selectTab" -> R.string.browser_action_select_tab_desc + "listTabs" -> R.string.browser_action_list_tabs_desc + else -> null +} diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/DefaultModelsSection.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/DefaultModelsSection.kt index 383346591..0ec81e089 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/DefaultModelsSection.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/DefaultModelsSection.kt @@ -1,5 +1,6 @@ package com.aicode.feature.settings.presentation.component +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -34,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.draw.clip import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -44,6 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.aicode.R import com.aicode.core.theme.Spacing +import com.aicode.core.theme.semanticColors import com.aicode.feature.onboarding.domain.OnboardingStep import com.aicode.feature.onboarding.presentation.onboardingTarget import com.aicode.feature.settings.data.local.ModelSheetCollapseStore @@ -386,20 +390,18 @@ internal fun ModelSelectionSheet( ) } if (expanded) { - item(key = "card_${provider.id}") { - SettingsGroup { - filteredModels.forEachIndexed { index, model -> - if (index > 0) { - SettingsDivider() - } - ModelSelectionRow( - model = model, - selected = provider.id == currentProviderId && model == currentModel, - metadata = modelMetadata[modelMetadataKey(provider.id, model)], - onClick = { onSelect(provider.id, model) } - ) - } - } + itemsIndexed( + items = filteredModels, + key = { _, model -> "model_${provider.id}_$model" } + ) { index, model -> + ModelSelectionRow( + model = model, + selected = provider.id == currentProviderId && model == currentModel, + metadata = modelMetadata[modelMetadataKey(provider.id, model)], + isFirst = index == 0, + isLast = index == filteredModels.lastIndex, + onClick = { onSelect(provider.id, model) } + ) } } } @@ -415,37 +417,52 @@ private fun ModelSelectionRow( model: String, selected: Boolean, metadata: ModelMetadata?, + isFirst: Boolean, + isLast: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable { onClick() } - .padding(vertical = Spacing.sm, horizontal = Spacing.lg), - verticalAlignment = Alignment.CenterVertically - ) { - ModelLogoIcon(modelName = model, size = 20.dp) - Spacer(Modifier.width(Spacing.md)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = model, - style = MaterialTheme.typography.bodyMedium, - color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(Modifier.height(4.dp)) - ModelMetadataTags(metadata) + val shape = RoundedCornerShape( + topStart = if (isFirst) 14.dp else 0.dp, + topEnd = if (isFirst) 14.dp else 0.dp, + bottomStart = if (isLast) 14.dp else 0.dp, + bottomEnd = if (isLast) 14.dp else 0.dp + ) + Column(modifier = modifier.fillMaxWidth()) { + if (!isFirst) { + SettingsDivider() } - if (selected) { - Spacer(Modifier.width(Spacing.sm)) - Icon( - imageVector = FeatherIcons.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .background(MaterialTheme.semanticColors.cardSurface) + .clickable { onClick() } + .padding(vertical = Spacing.sm, horizontal = Spacing.lg), + verticalAlignment = Alignment.CenterVertically + ) { + ModelLogoIcon(modelName = model, size = 20.dp) + Spacer(Modifier.width(Spacing.md)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = model, + style = MaterialTheme.typography.bodyMedium, + color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.height(4.dp)) + ModelMetadataTags(metadata) + } + if (selected) { + Spacer(Modifier.width(Spacing.sm)) + Icon( + imageVector = FeatherIcons.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } } } } diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/GeneralSettingsSection.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/GeneralSettingsSection.kt index 370e54c47..a7e5f08af 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/GeneralSettingsSection.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/GeneralSettingsSection.kt @@ -36,6 +36,7 @@ import com.aicode.core.theme.semanticColors import com.aicode.core.ui.AppSwitch import com.aicode.core.ui.AppTextField import com.aicode.feature.settings.data.repository.StartupSessionMode +import com.aicode.feature.settings.data.repository.TodoDisplayPosition import compose.icons.FeatherIcons import compose.icons.feathericons.Check @@ -61,9 +62,12 @@ internal fun GeneralSettingsSection( sendFileMaxSizeMb: Int, onSetSendFileMaxSizeMb: (Int) -> Unit, deleteExternalWorkspaceSessions: Boolean, - onToggleDeleteExternalWorkspaceSessions: (Boolean) -> Unit + onToggleDeleteExternalWorkspaceSessions: (Boolean) -> Unit, + todoDisplayPosition: TodoDisplayPosition, + onSetTodoDisplayPosition: (TodoDisplayPosition) -> Unit ) { var showStartupSessionSheet by remember { mutableStateOf(false) } + var showTodoPositionSheet by remember { mutableStateOf(false) } var editingFirstByteTimeout by remember { mutableStateOf(false) } var editingStreamIdleTimeout by remember { mutableStateOf(false) } var editingMaxNetworkRetries by remember { mutableStateOf(false) } @@ -210,6 +214,20 @@ internal fun GeneralSettingsSection( ) } ) + SettingsDivider() + SettingsRow( + icon = null, + title = stringResource(R.string.settings_todo_display_position), + subtitle = stringResource(R.string.settings_todo_display_position_desc), + onClick = { showTodoPositionSheet = true }, + trailing = { + Text( + text = stringResource(todoPositionLabelRes(todoDisplayPosition)), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.semanticColors.subtleText + ) + } + ) } } @@ -224,6 +242,17 @@ internal fun GeneralSettingsSection( ) } + if (showTodoPositionSheet) { + TodoPositionSheet( + selected = todoDisplayPosition, + onSelect = { + onSetTodoDisplayPosition(it) + showTodoPositionSheet = false + }, + onDismiss = { showTodoPositionSheet = false } + ) + } + if (editingFirstByteTimeout) { NumberInputDialog( title = stringResource(R.string.settings_first_byte_timeout), @@ -416,4 +445,65 @@ private fun StartupSessionMode.labelRes(): Int = when (this) { private fun StartupSessionMode.descRes(): Int = when (this) { StartupSessionMode.NEW_SESSION -> R.string.settings_startup_session_new_desc StartupSessionMode.RECENT_SESSION -> R.string.settings_startup_session_recent_desc +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TodoPositionSheet( + selected: TodoDisplayPosition, + onSelect: (TodoDisplayPosition) -> Unit, + onDismiss: () -> Unit +) { + AdaptiveModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(), + containerColor = MaterialTheme.colorScheme.surface + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = Spacing.xl) + ) { + Text( + text = stringResource(R.string.settings_todo_display_position), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(horizontal = Spacing.lg) + .padding(bottom = Spacing.md) + ) + + TodoDisplayPosition.entries.forEach { position -> + val isSelected = position == selected + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(position) } + .padding(horizontal = Spacing.lg, vertical = Spacing.md), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(todoPositionLabelRes(position)), + style = MaterialTheme.typography.bodyLarge, + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + if (isSelected) { + Icon( + imageVector = FeatherIcons.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + } + } + } + } +} + +private fun todoPositionLabelRes(position: TodoDisplayPosition): Int = when (position) { + TodoDisplayPosition.BELOW_TITLE -> R.string.settings_todo_position_below_title + TodoDisplayPosition.ABOVE_INPUT -> R.string.settings_todo_position_above_input } \ No newline at end of file diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/PromptEditorScreen.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/PromptEditorScreen.kt index b0743de1e..92619ef16 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/PromptEditorScreen.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/PromptEditorScreen.kt @@ -4,6 +4,8 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -109,6 +111,8 @@ internal fun PromptEditorScreen( modifier = Modifier .fillMaxSize() .padding(padding) + .consumeWindowInsets(padding) + .imePadding() .verticalScroll(rememberScrollState()) .padding(horizontal = Spacing.lg) .padding(bottom = Spacing.xl), diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/ProviderEditorScreen.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/ProviderEditorScreen.kt index 131008dca..4ae6ee322 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/ProviderEditorScreen.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/ProviderEditorScreen.kt @@ -348,9 +348,16 @@ fun ProviderEditorScreen( customHeaders.any { it.first.isNotBlank() } || models.isNotEmpty() + var lastSubmittedConfig by remember { + mutableStateOf(if (initialProvider != null) currentConfig() else null) + } + fun saveCurrent() { if (!hasSubstantiveInput()) return - onSave(currentConfig()) + val config = currentConfig() + if (config == lastSubmittedConfig) return + onSave(config) + lastSubmittedConfig = config } // 拉取成功后自动对齐:远端已不存在的本地模型直接移除(可在「偏好设置」关掉)。拉取失败或返回空列表时不动列表。 @@ -780,6 +787,25 @@ fun ProviderEditorScreen( modifier = Modifier.weight(1f) ) val allSelected = models.isNotEmpty() && selectedModels.size == models.size + // 勾选失败项:一键把测试未返回 200 的模型加入选择(未测试的不动)。 + TextButton( + onClick = { + models.forEach { m -> + val r = testResults[m] + if (r != null && r.responseCode != 200) { + if (m !in selectedModels) selectedModels.add(m) + } + } + }, + enabled = models.any { testResults[it]?.responseCode?.let { code -> code != 200 } == true }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Icon(FeatherIcons.AlertCircle, contentDescription = null, modifier = Modifier.size(16.dp)) + Spacer(Modifier.width(Spacing.xs)) + Text(stringResource(R.string.provider_models_select_failed)) + } TextButton( onClick = { if (allSelected) { diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/SettingsScreen.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/SettingsScreen.kt index eab2fabe9..78e6719d2 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/SettingsScreen.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/SettingsScreen.kt @@ -78,6 +78,7 @@ import com.aicode.feature.agent.domain.mcp.McpServerStatus import com.aicode.feature.agent.presentation.component.MarkdownContent import com.aicode.feature.agent.presentation.component.MarkdownRenderCache import com.aicode.feature.backup.presentation.BackupSection +import com.aicode.feature.backup.presentation.WebDavSyncScreen import com.aicode.feature.settings.data.repository.AppThemeMode import com.aicode.feature.settings.data.repository.BackgroundSettingsRepository import com.aicode.feature.settings.domain.model.AIProviderConfig @@ -157,6 +158,7 @@ internal enum class SettingsSection(@param:StringRes val titleRes: Int) { SubAgentEditor(R.string.settings_subagents), Container(R.string.settings_container), ContainerDownloads(R.string.container_download_image), + Browser(R.string.settings_browser), Proxy(R.string.proxy_title), Log(R.string.settings_log), Permissions(R.string.settings_permissions), @@ -166,6 +168,7 @@ internal enum class SettingsSection(@param:StringRes val titleRes: Int) { Storage(R.string.settings_storage), TokenStats(R.string.settings_token_stats_title), Backup(R.string.settings_backup), + WebDav(R.string.webdav_title), About(R.string.settings_about) } @@ -187,6 +190,7 @@ private fun SettingsSection.depth(): Int = when (this) { SettingsSection.SkillDetail, SettingsSection.SubAgentDetail, SettingsSection.ContainerDownloads, + SettingsSection.WebDav, SettingsSection.Log -> 2 else -> 1 } @@ -224,6 +228,9 @@ fun SettingsScreen( val agentSoundEnabled by viewModel.agentSoundEnabled.collectAsStateWithLifecycle() val autoRemoveStaleModels by viewModel.autoRemoveStaleModels.collectAsStateWithLifecycle() val startupSessionMode by viewModel.startupSessionMode.collectAsStateWithLifecycle() + val todoDisplayPosition by viewModel.todoDisplayPosition.collectAsStateWithLifecycle() + val browserUserAgent by viewModel.browserUserAgent.collectAsStateWithLifecycle() + val browserActionEnabled by viewModel.browserActionEnabled.collectAsStateWithLifecycle() val firstByteTimeoutSec by viewModel.firstByteTimeoutSec.collectAsStateWithLifecycle() val streamIdleTimeoutSec by viewModel.streamIdleTimeoutSec.collectAsStateWithLifecycle() val maxNetworkRetries by viewModel.maxNetworkRetries.collectAsStateWithLifecycle() @@ -396,6 +403,7 @@ fun SettingsScreen( SettingsSection.SubAgentDetail -> SettingsSection.SubAgents SettingsSection.SubAgentEditor -> subAgentEditorReturn SettingsSection.ContainerDownloads -> SettingsSection.Container + SettingsSection.WebDav -> SettingsSection.Backup else -> if (expanded) null else SettingsSection.Menu } @@ -520,6 +528,11 @@ fun SettingsScreen( label = "settings-section" ) { current -> when { + // 自带顶栏的页面,不能嵌进下面的 Scaffold(会双层顶栏):窄窗占整屏,大屏占右栏。 + current == SettingsSection.WebDav -> WebDavSyncScreen( + onNavigateBack = { section = SettingsSection.Backup } + ) + // 这两页自带顶栏,不能嵌进下面的 Scaffold(会双层顶栏):窄窗占整屏,大屏占右栏。 current == SettingsSection.ProviderEditor -> ProviderEditorScreen( viewModel = viewModel, @@ -711,6 +724,16 @@ fun SettingsScreen( ) } } + SettingsSection.Backup -> { + IconButton(onClick = { section = SettingsSection.WebDav }) { + Icon( + FeatherIcons.Cloud, + contentDescription = stringResource(R.string.backup_webdav_entry), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(22.dp) + ) + } + } SettingsSection.Skills -> IconButton(onClick = { showSkillAddSheet = true }) { @@ -850,7 +873,9 @@ fun SettingsScreen( sendFileMaxSizeMb = sendFileMaxSizeMb, onSetSendFileMaxSizeMb = { viewModel.setSendFileMaxSizeMb(it) }, deleteExternalWorkspaceSessions = deleteExternalWorkspaceSessions, - onToggleDeleteExternalWorkspaceSessions = { viewModel.setDeleteExternalWorkspaceSessions(it) } + onToggleDeleteExternalWorkspaceSessions = { viewModel.setDeleteExternalWorkspaceSessions(it) }, + todoDisplayPosition = todoDisplayPosition, + onSetTodoDisplayPosition = { viewModel.setTodoDisplayPosition(it) } ) SettingsSection.Providers -> ProvidersSection( providers = providers, @@ -1012,6 +1037,12 @@ fun SettingsScreen( onImport = { entryId, fileUri -> viewModel.importDownloadedImage(entryId, fileUri) }, onDelete = { entryId -> viewModel.deleteDownloadedImage(entryId) } ) + SettingsSection.Browser -> BrowserSettingsSection( + userAgent = browserUserAgent, + actionEnabled = browserActionEnabled, + onSelectUserAgent = { viewModel.setBrowserUserAgent(it) }, + onToggleAction = { action, enabled -> viewModel.setBrowserActionEnabled(action, enabled) } + ) SettingsSection.Proxy -> ProxySection( config = proxyConfig, testState = proxyTestState, @@ -1081,6 +1112,7 @@ fun SettingsScreen( SettingsSection.SkillEditor -> {} // 已在上方 early return 处理 SettingsSection.SubAgentEditor -> {} // 已在上方 early return 处理 SettingsSection.RemoteServers -> {} // 已在上方 early return 处理 + SettingsSection.WebDav -> {} // 已在上方 early return 处理 SettingsSection.About -> AboutSection( updateCheckEnabled = updateCheckEnabled, updateCheckChannel = updateCheckChannel, @@ -1599,6 +1631,12 @@ internal fun SettingsMenu( title = stringResource(SettingsSection.RemoteServers.titleRes), onClick = { onOpen(SettingsSection.RemoteServers) } ) + SettingsDivider() + SettingsRow( + icon = FeatherIcons.Globe, + title = stringResource(SettingsSection.Browser.titleRes), + onClick = { onOpen(SettingsSection.Browser) } + ) } // ── 权限与后台 ── diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/SkillEditorScreen.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/SkillEditorScreen.kt index 00c2301b8..c568b0e2a 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/SkillEditorScreen.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/SkillEditorScreen.kt @@ -4,6 +4,8 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -126,6 +128,8 @@ internal fun SkillEditorScreen( modifier = Modifier .fillMaxSize() .padding(padding) + .consumeWindowInsets(padding) + .imePadding() .verticalScroll(rememberScrollState()) .padding(horizontal = Spacing.lg) .padding(bottom = Spacing.xl), diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/SubAgentEditorScreen.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/SubAgentEditorScreen.kt index bda5abf99..ab3e09472 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/SubAgentEditorScreen.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/SubAgentEditorScreen.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -170,6 +172,8 @@ internal fun SubAgentEditorScreen( modifier = Modifier .fillMaxSize() .padding(padding) + .consumeWindowInsets(padding) + .imePadding() .verticalScroll(rememberScrollState()) .padding(horizontal = Spacing.lg) .padding(bottom = Spacing.xl), diff --git a/app/src/main/java/com/aicode/feature/settings/presentation/component/UpdateCheckDialog.kt b/app/src/main/java/com/aicode/feature/settings/presentation/component/UpdateCheckDialog.kt index 07c3b6221..a2f10d408 100644 --- a/app/src/main/java/com/aicode/feature/settings/presentation/component/UpdateCheckDialog.kt +++ b/app/src/main/java/com/aicode/feature/settings/presentation/component/UpdateCheckDialog.kt @@ -49,8 +49,8 @@ internal fun UpdateCheckDialog( text = state.changelog, modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .heightIn(max = 320.dp), + .heightIn(max = 320.dp) + .verticalScroll(rememberScrollState()), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/aicode/feature/workspace/domain/repository/RemoteRepository.kt b/app/src/main/java/com/aicode/feature/workspace/domain/repository/RemoteRepository.kt index 16bc7cb3a..a782e1daf 100644 --- a/app/src/main/java/com/aicode/feature/workspace/domain/repository/RemoteRepository.kt +++ b/app/src/main/java/com/aicode/feature/workspace/domain/repository/RemoteRepository.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.MutableStateFlow @@ -118,7 +119,7 @@ class RemoteRepository @Inject constructor( fun getConnections(): Flow> = dao.getAllConnections().map { list -> list.map { it.toDomainModel() } - } + }.flowOn(Dispatchers.IO) fun getMounts(): Flow> = combine( dao.getAllMounts(), @@ -132,9 +133,9 @@ class RemoteRepository @Inject constructor( connectionState = states[mountEntity.id] ) } - } + }.flowOn(Dispatchers.IO) - suspend fun addConnection(conn: RemoteConnection, auth: RemoteAuth) { + suspend fun addConnection(conn: RemoteConnection, auth: RemoteAuth) = withContext(Dispatchers.IO) { val authType = if (auth is RemoteAuth.Password) "PASSWORD" else "PRIVATE_KEY" val authData = if (auth is RemoteAuth.Password) auth.password else (auth as RemoteAuth.PrivateKey).privateKeyPath val passphrase = if (auth is RemoteAuth.PrivateKey) auth.passphrase else null diff --git a/app/src/main/java/com/aicode/feature/workspace/presentation/remote/RemoteServerViewModel.kt b/app/src/main/java/com/aicode/feature/workspace/presentation/remote/RemoteServerViewModel.kt index 8eef5e996..bbbb353d5 100644 --- a/app/src/main/java/com/aicode/feature/workspace/presentation/remote/RemoteServerViewModel.kt +++ b/app/src/main/java/com/aicode/feature/workspace/presentation/remote/RemoteServerViewModel.kt @@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File import java.util.UUID @@ -46,10 +48,12 @@ class RemoteServerViewModel @Inject constructor( val ftpServerManager: FtpServerManager ) : ViewModel() { + private val loginKeyMutex = Mutex() + private val _uiState = MutableStateFlow( RemoteServerUiState( hostKeys = remoteSshConnection.savedHostKeys(), - loginKeys = loginKeyStore.entries() + loginKeys = emptyList() ) ) val uiState: StateFlow = _uiState.asStateFlow() @@ -63,6 +67,14 @@ class RemoteServerViewModel @Inject constructor( private fun loadData() { viewModelScope.launch { + launch { + loginKeyMutex.withLock { + val loginKeys = withContext(Dispatchers.IO) { + loginKeyStore.entries() + } + _uiState.value = _uiState.value.copy(loginKeys = loginKeys) + } + } launch { repository.getConnections() .catch { e -> _uiState.value = _uiState.value.copy(error = e.message) } @@ -205,38 +217,48 @@ class RemoteServerViewModel @Inject constructor( /** 解析私钥指纹并写入登录密钥库,随后刷新列表。 */ private suspend fun registerLoginKey(file: File, passphrase: String? = null) { - val fingerprint = withContext(Dispatchers.IO) { - privateKeyStore.fingerprint(privateKeyStore.readPem(file.absolutePath)) + loginKeyMutex.withLock { + val loginKeys = withContext(Dispatchers.IO) { + val fingerprint = privateKeyStore.fingerprint(privateKeyStore.readPem(file.absolutePath)) + loginKeyStore.add( + SshLoginKey( + id = UUID.randomUUID().toString(), + name = file.name, + path = file.absolutePath, + fingerprint = fingerprint, + passphrase = passphrase + ) + ) + loginKeyStore.entries() + } + _uiState.value = _uiState.value.copy(loginKeys = loginKeys) } - loginKeyStore.add( - SshLoginKey( - id = UUID.randomUUID().toString(), - name = file.name, - path = file.absolutePath, - fingerprint = fingerprint, - passphrase = passphrase - ) - ) - _uiState.value = _uiState.value.copy(loginKeys = loginKeyStore.entries()) } /** 编辑登录密钥:更新显示名与口令(私钥内容不可改)。 */ fun updateLoginKey(id: String, name: String, passphrase: String) { - val existing = loginKeyStore.entries().firstOrNull { it.id == id } ?: return - loginKeyStore.add( - existing.copy( - name = name.trim().ifBlank { existing.name }, - passphrase = passphrase.ifBlank { null } - ) - ) - _uiState.value = _uiState.value.copy(loginKeys = loginKeyStore.entries()) + viewModelScope.launch { + loginKeyMutex.withLock { + val loginKeys = withContext(Dispatchers.IO) { + val existing = loginKeyStore.entries().firstOrNull { it.id == id } ?: return@withContext null + loginKeyStore.add( + existing.copy( + name = name.trim().ifBlank { existing.name }, + passphrase = passphrase.ifBlank { null } + ) + ) + loginKeyStore.entries() + } ?: return@withLock + _uiState.value = _uiState.value.copy(loginKeys = loginKeys) + } + } } /** 读取登录密钥的私钥明文 PEM(供编辑页只读查看);失败返回 null。 */ fun readLoginKeyPem(id: String, onResult: (String?) -> Unit) { - val key = loginKeyStore.entries().firstOrNull { it.id == id } ?: return onResult(null) viewModelScope.launch { val pem = withContext(Dispatchers.IO) { + val key = loginKeyStore.entries().firstOrNull { it.id == id } ?: return@withContext null runCatching { privateKeyStore.readPem(key.path) }.getOrNull() } onResult(pem) @@ -244,8 +266,15 @@ class RemoteServerViewModel @Inject constructor( } fun removeLoginKey(id: String) { - loginKeyStore.remove(id) - _uiState.value = _uiState.value.copy(loginKeys = loginKeyStore.entries()) + viewModelScope.launch { + loginKeyMutex.withLock { + val loginKeys = withContext(Dispatchers.IO) { + loginKeyStore.remove(id) + loginKeyStore.entries() + } + _uiState.value = _uiState.value.copy(loginKeys = loginKeys) + } + } } fun addConnection( diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index c720d1773..0e8d53ad4 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1604,4 +1604,100 @@ Turn off night mode Turn on night mode Developer tools + + + Browser + User-Agent + Choose the device and OS the browser reports; non-default selections also align the matching client hints. + System default + Android phone + Android tablet + Windows 10 PC + Windows 11 PC + Mac (Intel chip) + Mac (Apple chip) + AI Control Permissions + Turning an item off makes the AI call to that browser action be rejected. All are on by default. + Navigate + Run JavaScript + Click element + Fill form + Select option + Hover element + Press key + Read text + Read HTML + Read accessibility tree + Screenshot + Read console logs + Wait for condition + Scroll page + Handle dialog + Back + Forward + Reload + New tab + Close tab + Switch tab + List tabs + Navigate to a URL and wait for load + Execute arbitrary JavaScript in the page + Click a page element by selector + Type a value into an input field + Choose an option in a native dropdown + Hover the pointer over an element + Send a key (Enter, Esc, arrows, ...) + Extract text from the page or an element + Extract HTML from the page or an element + Extract the accessibility tree to locate elements + Capture the current page for analysis + Read the page console logs + Wait until a condition is met + Scroll to an element or the bottom + Accept or dismiss a page dialog + Go back one page + Go forward one page + Reload the current page + Open a new tab (optionally with a URL) + Close a specific tab + Switch to a specific tab + List all tabs and their state + + + WebDAV Sync + Sync backups to a WebDAV server + WebDAV Sync + Connection + Auto sync + Server + Address + Username + Password + Test connection + Sync now + Last sync: %1$s + Sync result + Operation failed + Auto sync + Automatic backup sync + Upload backups automatically at the configured interval + Sync content + Sync interval + Interval + every + Minutes + Hours + Days + + + Todo display position + Where the todo panel shows on the chat screen + Below the title bar + Above the input bar + Delete todos + Delete todos + Delete all todos for this session? They will stay hidden until the next todo update. + + + Select failed \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f3fb336a..fcef40699 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1603,4 +1603,100 @@ 关闭夜间模式 开启夜间模式 开发者工具 + + + 浏览器 + UA 设置 + 选择浏览器向网站报告的设备与系统;非系统默认时会同时对齐对应的客户端提示头。 + 系统默认 + 安卓手机 + 安卓平板 + Windows 10 电脑 + Windows 11 电脑 + Mac 电脑(Intel 芯片) + Mac 电脑(Apple 芯片) + AI 控制权限 + 关闭某项后,AI 调用该浏览器操作会被拒绝;默认全部开启。 + 导航网页 + 执行 JavaScript + 点击元素 + 填写表单 + 选择下拉项 + 悬停元素 + 按键 + 读取文本 + 读取 HTML + 读取无障碍树 + 页面截图 + 读取控制台日志 + 等待页面条件 + 滚动页面 + 处理弹窗 + 后退 + 前进 + 刷新 + 新建标签页 + 关闭标签页 + 切换标签页 + 列出标签页 + 导航到指定网址并等待加载完成 + 在页面中执行任意 JavaScript + 按选择器点击网页元素 + 向输入框中填入内容 + 选择原生下拉框的选项 + 将鼠标悬停在元素上 + 发送按键(回车、Esc、方向键等) + 提取页面或元素的文本 + 提取页面或元素的 HTML + 提取无障碍树以定位元素 + 截取当前页面供识别 + 读取页面控制台日志 + 等待某个条件满足 + 滚动到元素或页面底部 + 接受或取消页面弹窗 + 返回上一页 + 前进到下一页 + 重新加载当前页面 + 新建标签页(可指定网址) + 关闭指定标签页 + 切换到指定标签页 + 列出所有标签页及状态 + + + WebDAV 备份同步 + 将备份同步到 WebDAV 云端 + WebDAV 备份同步 + 连接 + 自动同步 + 服务器 + 地址 + 用户名 + 密码 + 连接测试 + 立即同步 + 上次同步:%1$s + 同步结果 + 操作失败 + 自动同步 + 自动备份同步 + 按设定频率自动将备份上传到云端 + 同步内容 + 同步频率 + 间隔 + 每隔 + 分钟 + 小时 + 天 + + + 任务待办显示位置 + 任务待办面板在聊天页的展示位置 + 标题栏下方 + 消息栏上方 + 删除待办 + 删除待办 + 确定删除当前会话的全部待办?删除后将隐藏,直到下次待办更新。 + + + 勾选失败 diff --git a/app/src/test/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCaseTest.kt b/app/src/test/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCaseTest.kt index 54b08c145..35ddfcf24 100644 --- a/app/src/test/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCaseTest.kt +++ b/app/src/test/java/com/aicode/feature/agent/domain/session/MessagePersistenceUseCaseTest.kt @@ -1,5 +1,6 @@ package com.aicode.feature.agent.domain.session +import com.aicode.feature.agent.data.local.entity.AgentMessageEntity import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -11,6 +12,19 @@ import org.junit.Test */ class MessagePersistenceUseCaseTest { + @Test + fun historyPlacesSummaryBeforeRetainedMessagesWithoutChangingDisplayTimestamps() { + val retained = AgentMessageEntity(id = "retained", sessionId = "session", role = "USER", content = "goal", timestamp = 100) + val marker = AgentMessageEntity(id = "marker", sessionId = "session", role = "USER", content = "marker", timestamp = 200, isCompactionMarker = true) + val summary = AgentMessageEntity(id = "summary", sessionId = "session", role = "ASSISTANT", content = "summary", timestamp = 201, isContextSummary = true) + val latest = retained.copy(id = "latest", timestamp = 300) + val display = listOf(retained, marker, summary, latest) + val history = MessagePersistenceUseCase.orderHistoryEntities(display) + assertEquals(listOf("marker", "summary", "retained", "latest"), history.map { it.id }) + assertEquals(listOf(200L, 201L, 100L, 300L), history.map { it.timestamp }) + assertEquals(listOf("retained", "marker", "summary", "latest"), display.map { it.id }) + } + @Test fun sanitizeContent_stripsInlineBase64Image() { val input = "这是回复。看图:![截图](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==) 结束。" diff --git a/app/src/test/java/com/aicode/feature/agent/domain/workflow/ContextCompactorTest.kt b/app/src/test/java/com/aicode/feature/agent/domain/workflow/ContextCompactorTest.kt index 55ec8142c..c7ba046e5 100644 --- a/app/src/test/java/com/aicode/feature/agent/domain/workflow/ContextCompactorTest.kt +++ b/app/src/test/java/com/aicode/feature/agent/domain/workflow/ContextCompactorTest.kt @@ -112,6 +112,17 @@ class ContextCompactorTest { assertTrue(CompactionText.units(listOf(AgentMessage.AssistantMessage(content = "leading"))).single().contains("leading")) } + @Test + fun cursorIncrementalBudgetMatchesWholeTextCounting() { + val units = List(120) { index -> if (index % 2 == 0) "ascii-$index" else "混合-$index" } + val cursor = CompactionText.Cursor(units) + val chunks = mutableListOf() + while (!cursor.finished) chunks.add(cursor.next(160)) + assertTrue(chunks.all { CompactionText.tokens(it) <= 160 }) + assertEquals(units.size, chunks.sumOf { chunk -> Regex("\\[history-unit ").findAll(chunk).count() }) + units.forEach { unit -> assertTrue(chunks.any { it.contains("\n$unit\n") }) } + } + @Test fun multipleBlocksFusePreviousSummaryAndRetainLatestGoal() = runTest { prepare() @@ -174,8 +185,9 @@ class ContextCompactorTest { } @Test - fun commitUsesHeadIdsAndEarliestTailTimestamp() = runTest { + fun commitUsesHeadIdsAndCompactionTriggerTimestamp() = runTest { prepare() + every { persistence.nextTimestamp() } returnsMany listOf(300L, 301L) coEvery { dao.getMessagesBySessionOnce("session") } returns listOf( AgentMessageEntity(id = "old", sessionId = "session", role = "USER", content = "history", timestamp = 50), AgentMessageEntity(id = "answer", sessionId = "session", role = "ASSISTANT", content = "done", timestamp = 200), @@ -184,7 +196,7 @@ class ContextCompactorTest { val result = compactor.compactIfNeeded(history(), provider, sessionId = "session", force = true) assertTrue(result.compacted) coVerify(exactly = 1) { - dao.commitCompaction("session", listOf("old"), match { it.map { row -> row.timestamp } == listOf(98L, 99L) }, any()) + dao.commitCompaction("session", listOf("old"), match { it.map { row -> row.timestamp } == listOf(300L, 301L) }, any()) } coVerify(exactly = 1) { persistence.invalidateHistory("session") } } diff --git a/app/src/test/java/com/aicode/feature/agent/presentation/component/ToolGroupExpansionTest.kt b/app/src/test/java/com/aicode/feature/agent/presentation/component/ToolGroupExpansionTest.kt index bd45507f5..edf215966 100644 --- a/app/src/test/java/com/aicode/feature/agent/presentation/component/ToolGroupExpansionTest.kt +++ b/app/src/test/java/com/aicode/feature/agent/presentation/component/ToolGroupExpansionTest.kt @@ -46,6 +46,20 @@ class ToolGroupExpansionTest { private fun List.groupItems(): List = flatMap { it.turnProcess }.filter { it.toolGroup != null } + @Test + fun compactionMarkerDoesNotStartNewTurn() { + val marker = user("marker").copy(isCompactionMarker = true) + val summary = assistant("summary").copy(isContextSummary = true) + val rendered = items( + listOf(user("u0"), tool("t1"), marker, summary, assistant("answer")), + activeTurnKey = "turn:u0" + ) + assertEquals(1, rendered.count { it.turnHeader != null }) + assertEquals("turn:u0", rendered.header().turnHeader?.key) + assertTrue(rendered.any { it.message.id == "marker" }) + assertTrue(rendered.any { it.message.id == "summary" }) + } + // ---- 连续工具调用分组 ---- @Test diff --git a/app/src/test/java/com/aicode/feature/backup/domain/BackupMetadataTest.kt b/app/src/test/java/com/aicode/feature/backup/domain/BackupMetadataTest.kt new file mode 100644 index 000000000..a87040e3c --- /dev/null +++ b/app/src/test/java/com/aicode/feature/backup/domain/BackupMetadataTest.kt @@ -0,0 +1,40 @@ +package com.aicode.feature.backup.domain + +import kotlinx.serialization.json.Json +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class BackupMetadataTest { + private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true } + + @Test + fun chatBackupDoesNotIncludeAppSettings() { + val metadata = BackupMetadata(schemaVersion = 55, createdAt = 0) + val restored = json.decodeFromString(json.encodeToString(BackupMetadata.serializer(), metadata)) + assertFalse(restored.includesAppSettings) + } + + @Test + fun legacyMetadataInfersSettingsFromThemeMode() { + assertTrue(json.decodeFromString( + """{"schemaVersion":55,"createdAt":0,"themeMode":"DARK"}""" + ).includesAppSettings) + assertFalse(json.decodeFromString( + """{"schemaVersion":55,"createdAt":0,"themeMode":null}""" + ).includesAppSettings) + } + + @Test + fun explicitSettingsScopeOverridesLegacyInference() { + assertFalse(json.decodeFromString( + """{"schemaVersion":55,"createdAt":0,"themeMode":"DARK","includesAppSettings":false}""" + ).includesAppSettings) + } + + @Test + fun legacySnapshotPreservesSettingsScope() { + assertFalse(BackupSnapshot(schemaVersion = 55, createdAt = 0).toMetadata().includesAppSettings) + assertTrue(BackupSnapshot(schemaVersion = 55, createdAt = 0, themeMode = "AUTO").toMetadata().includesAppSettings) + } +} diff --git a/docs-site/docs/advanced/dashboard-cards.md b/docs-site/docs/advanced/dashboard-cards.md index a119b0b03..7eeae6f7f 100644 --- a/docs-site/docs/advanced/dashboard-cards.md +++ b/docs-site/docs/advanced/dashboard-cards.md @@ -4,6 +4,8 @@ AI 助手与开发者可以依据此规范,编写面板脚本(Python / Node / Shell)输出结构化卡片 JSON,从而完全自定义面板在收起态(Compact)与展开态(Expanded)下的排版、配色、进度条、指标卡片及明细信息。 +点击聊天输入框上方的面板标题可展开或收起。展开正文最多占聊天区域可用高度的约 40%;内容较长时,在正文内上下滑动即可查看全部内容,顶部标题与收起按钮保持固定,输入框仍保留在面板下方。短面板按实际内容高度显示,无需在脚本中截断明细列表。 + --- ## 1. 脚本存放路径与管理方式 diff --git a/docs-site/docs/guide/backup.md b/docs-site/docs/guide/backup.md index cea3d8291..f625041ff 100644 --- a/docs-site/docs/guide/backup.md +++ b/docs-site/docs/guide/backup.md @@ -27,6 +27,10 @@ Git 仓库本身就完整保存了代码和历史,这里的导出更适合作 从备份文件还原,采用**合并方式**,不会清除现有数据。需要输入对应的口令,未加密的备份留空即可。 +只有导出时勾选了「应用设置」的备份才会恢复主题、配色和偏好设置。单独导出的聊天记录,以及未勾选「应用设置」的备份,导入后会保留当前应用设置。 + +加密备份在预览前会完整解密并校验一次,确认恢复时复用校验结果,不再重复解密。较大的备份需要足够的应用内部存储空间保存临时压缩包;完成、取消或失败后会清理临时文件。 + 如果备份中含工作区文件,会先弹出「恢复工作区」选择弹窗,列出备份中的每个工作区(名称和文件数),你可以勾选要恢复哪些。 ::: warning 恢复工作区会直接覆盖 diff --git a/docs-site/docs/guide/chat.md b/docs-site/docs/guide/chat.md index 337662264..6282a41d7 100644 --- a/docs-site/docs/guide/chat.md +++ b/docs-site/docs/guide/chat.md @@ -55,10 +55,16 @@ 上传的附件会以预览卡显示在输入框上方,发送前可以移除。上传较大的文件需要一定时间,期间输入框上方会显示上传中的提示,传完自动变成预览卡。点击图片缩略图可以[全屏看大图](#全屏看图)。 -未发送的草稿按会话分别保存:在当前会话输入内容后切换至其它会话,返回时草稿依然保留。草稿持久化到本地,强制关闭 App 后重新打开(进程重启)依然存在。 +未发送的文字草稿按会话持久化保存到本地。 滚动消息列表时,输入框区域会淡出到 40% 透明度以减少干扰,停止滚动后恢复。 +## AI 询问面板 + +AI 需要你选择方案时,会在聊天输入框上方显示询问面板。选择预设选项,或选择「其他」填写自己的回答,再点「确认」提交;也可以点「补充」,回到聊天输入框补充说明。 + +填写「其他」时,面板会随键盘弹出缩小可见区域。问题较多时,可以在面板内部上下滚动,找到输入框和底部操作按钮。面板标题可点击收起或展开。 + ## 工作区管理 点击工作区按钮弹出的面板里可以: @@ -72,7 +78,7 @@ ## 斜杠命令 -在输入框输入 `/` 会弹出命令菜单,**内置命令排在前面(按字母序),技能排在后面(也按字母序)**。 +在输入框输入 `/` 会弹出命令菜单,**内置命令排在前面(按字母序),技能排在后面(也按字母序)**。命令较多时,可以在菜单内上下滑动查看;继续输入命令名可以筛选列表,点击条目即可补全到输入框。 内置命令: @@ -91,7 +97,7 @@ 发送按钮外圈反映模型可用输入预算的占用,而非累计消耗。发送前会估算新输入、回复和工具结果增加的用量,模型返回后再用实际报告校准;读屏说明会区分预计用量。切换模型、压缩和回退后,旧用量基线不再沿用,后续请求会重新计算。 -上下文接近阈值时,较早的对话会转成接手摘要,最近消息继续保留。压缩模型窗口较小时会分块处理,可能增加等待时间和调用费用;摘要为空、截断或无法释放足够空间时,不替换原历史。明确的上下文超限最多自动压缩重试一次,仍失败时请缩短输入、减少工具或检查模型窗口设置。 +上下文接近阈值时,较早的对话会转成接手摘要,最近消息继续保留。历史整理和摘要材料处理在后台进行,压缩期间仍可浏览消息。压缩成功后,分隔线和摘要卡片显示在本次压缩触发的位置,而不是被压缩历史的截断处。压缩模型窗口较小时会分块处理,可能增加等待时间和调用费用;摘要为空、截断或无法释放足够空间时,不替换原历史。明确的上下文超限最多自动压缩重试一次,仍失败时请缩短输入、减少工具或检查模型窗口设置。 ## 消息队列 @@ -136,7 +142,7 @@ AI 的思考过程另外占一行,**默认收起**,收起时只有一行: - **`N 次工具调用`**:一轮任务里连续多次工具调用合并成这一行,**默认收起**——运行中也不自动展开,如需查看细节可自行点开;只要这批里还有工具在运行,**这行文案自身**就带着一条来回划过的高光。点这一行可以随时手动展开或收起,**手动选择之后会一直按你的选择显示**——不会被自动规则改回去,也不会因为滚出屏幕、切去终端或设置页再回来而变回默认态。 - 展开后每次调用占一行,并且**整体向内缩进一级**,一眼能看出这些行属于上方分组,而不是和分组头平级的独立内容。 - 正在运行的那条工具,行尾显示它**正在做的事**(折叠态也可见),而不是笼统的「执行中」:`正在执行命令`、`正在编辑文件`、`正在读取文件`、`正在联网搜索`、`正在生成图片`、`正在整理待办`、`正在启动子代理`、`正在读写记忆`;认不出具体动作的工具(MCP 工具等)显示 `正在调用工具`。 -- 点单行可以查看「指令」与「执行结果」;内容过长时在一个固定高度的窗口里上下滚动查看,底部渐隐提示下方还有内容。单行的展开/收起同样会被记住,划走再划回来不会自动合上。工具仍在输出时,在窗口内滚动会暂时固定正在浏览的内容,避免新输出替换文字导致跳动;滚回窗口底部并停止滚动后恢复实时更新。浏览期间工具仍会继续执行,外层聊天不会自动滚动抢走位置。工具和思考窗口内,按住拖动到顶部或底部后继续拖动,可以带动外层聊天列表;快速滑动并松手后,窗口的惯性滚动到边界即停止,不会带动外层列表。 +- 点单行可以查看「指令」与「执行结果」;较大的工具结果会在后台整理,展开后可能稍等片刻才显示完整详情。内容过长时在一个固定高度的窗口里上下滚动查看,底部渐隐提示下方还有内容。单行的展开/收起同样会被记住,划走再划回来不会自动合上。工具仍在输出时,在窗口内滚动会暂时固定正在浏览的内容,避免新输出替换文字导致跳动;滚回窗口底部并停止滚动后恢复实时更新。浏览期间工具仍会继续执行,外层聊天不会自动滚动抢走位置。工具和思考窗口内,按住拖动到顶部或底部后继续拖动,可以带动外层聊天列表;快速滑动并松手后,窗口的惯性滚动到边界即停止,不会带动外层列表。 - 改动文件的工具(写文件、改文件)**和其他工具一样默认收起**,点击展开后是一张**差异卡**:左上角是文件路径,右上角「复制」可复制这段差异,正文是带行号的 `+ / −` 彩色差异,底部汇总本次改动(如 `+3 −0 · 1 个文件`)。 若工具卡片底部显示「后台任务/子代理已结束」提示(绿点代表成功,红点代表失败或被手动终止),表明后台任务或子代理已在该轮中完成并被当前会话捕获,AI 会自动接续处理。 diff --git a/docs-site/docs/guide/custom-prompts.md b/docs-site/docs/guide/custom-prompts.md index e361bfe09..fae7f62fb 100644 --- a/docs-site/docs/guide/custom-prompts.md +++ b/docs-site/docs/guide/custom-prompts.md @@ -21,6 +21,8 @@ AiCode 的系统提示词可以自行修改。默认提示词随 App 内置、 - 右上角「+」可用未占用的编号新增片段。编辑时摘要与正文分开填写;摘要显示在列表和预览页,不会注入 AI。手工编写文件时,开头的 `` 同样会被识别为摘要,正文预览会隐藏这段注释。 - 首次进入会自动弹出使用说明公告,右上角 Info 可随时重看。 +编辑页会避让软键盘;摘要或正文较长时可以上下滚动,继续编辑末尾内容。 + App 里的改动与手工放文件完全等价,两种方式可以混用。 ## 目录结构 diff --git a/docs-site/docs/guide/providers.md b/docs-site/docs/guide/providers.md index 8bd39d5aa..cf7c2d4d9 100644 --- a/docs-site/docs/guide/providers.md +++ b/docs-site/docs/guide/providers.md @@ -24,7 +24,7 @@ AiCode 自身不提供模型,需先接入一个模型服务。入口在「设 列表每行显示名称、上游格式、模型数量和启用状态。 -- 点一行进入编辑页。 +- 点一行进入编辑页。修改后点左上角返回箭头或按系统返回键,配置会自动保存;未修改时直接返回,不重复保存。 - **左滑一行**露出红色删除按钮。 - **长按名称或上游格式区域拖动**调整顺序,重启后保持。 diff --git a/docs-site/docs/guide/skills.md b/docs-site/docs/guide/skills.md index 8003ac1d6..408f7f618 100644 --- a/docs-site/docs/guide/skills.md +++ b/docs-site/docs/guide/skills.md @@ -53,6 +53,8 @@ - **基本信息**:名称、描述、作用域。名称就是 AI 加载技能时使用的标识,建议使用英文小写加连字符(如 `pdf-report`);描述写明「何时该使用它」,AI 依据这句话判断是否需要加载;作用域选「当前项目」或「全局」,决定技能目录落在哪一级。 - **指令正文**:技能的完整指令,支持 Markdown。写清这类任务的标准流程、约束与注意事项。 +软键盘弹出时,编辑区域会避让键盘;正文较长时可以上下滚动,继续编辑末尾内容。 + 名称和正文都填写后才能点击右上角「保存」。保存后会写成 `<技能目录>/SKILL.md`,与手写的技能完全等价,之后同样可以用文本编辑器或容器命令修改。 几个需要留意的地方: diff --git a/docs-site/docs/guide/subagent.md b/docs-site/docs/guide/subagent.md index 0e45cf8b0..35930be34 100644 --- a/docs-site/docs/guide/subagent.md +++ b/docs-site/docs/guide/subagent.md @@ -133,6 +133,8 @@ inject: [base, projectRules] - **启用 / 禁用**:详情页顶部的开关。禁用后它不再出现在主会话 AI 的可派发清单里,AI 也无法再指名派发它,但定义文件保留,已在运行的子会话也不受影响; - **删除**:列表里左滑条目,会删掉对应的 `.md` 定义文件,不可恢复。 +新建和编辑时,表单会避让软键盘;提示词正文较长时可以上下滚动,继续编辑末尾内容。 + 启停状态存在 `agents.json`,与技能的停用名单同一套约定:全局名单位于当前执行环境的 `~/.aicode/agents.json`,本地模式保存在手机 App 私有目录,远程模式保存在服务器上;两者独立,不会自动迁移或同步。项目级名单在本地模式位于 `<工作区>/.aicode/agents.json`,远程模式仍保存在手机 App 私有目录,按服务器和工作区隔离。两级取并集,任一作用域禁用即生效。 在 App 中切换启停状态后列表会刷新。本地模式下手工修改定义或名单,列表几秒内自动刷新;远程模式下不会监听服务器上的文件变化,手工修改后需退出并重新进入设置页刷新列表。 diff --git a/docs-site/docs/guide/sync.md b/docs-site/docs/guide/sync.md index 150273020..ffd27389a 100644 --- a/docs-site/docs/guide/sync.md +++ b/docs-site/docs/guide/sync.md @@ -22,6 +22,8 @@ **密钥**:管理登录用的私钥。点右上角 + 有两种添加方式:**从文件导入**(从文件选择器选私钥文件)或**粘贴密钥**(进入与编辑页相同的表单,粘贴私钥内容并填名称、口令,名称留空自动命名)。私钥会加密复制到私有目录并解析出公钥指纹,加密的私钥会标注「需口令」。点某条密钥进入编辑页:可改名称和口令(加密私钥的 passphrase),私钥内容只读(默认隐藏,点右侧眼睛可查看)。口令在密钥上设置,连接选用该密钥时会自动带上。左滑删除。 +密钥列表会在进入页面后异步加载;导入、修改或删除后自动刷新。保存带口令的密钥时无需停留等待,返回列表后即可查看更新结果。 + **指纹**:管理已信任的服务器主机指纹。首次连接某台服务器时会弹窗显示密钥类型和 `SHA256:` 指纹,确认「信任并连接」后保存下来,之后直接放行。如果服务器指纹变了会再次要求确认——这是防中间人攻击的机制,指纹突然变化要当心。左滑删除后下次连接重新确认。 ## 挂载目录