Compare commits

..

2 Commits

Author SHA1 Message Date
Jason e612410deb fix(docs): align user manual with v3.10.3 codebase
- Add OpenCode as 4th supported app throughout all docs
- Fix proxy default port 15762 → 15721
- Update Claude presets (9 → 26), Codex (3 → 10), Gemini (3 → 7)
- Add OpenCode presets (25 entries)
- Fix timeout defaults and ranges (stream first byte 60s/90s, etc.)
- Fix circuit breaker defaults with per-app values (Claude vs general)
- Fix Skills support: all 4 apps, not just Claude/Codex
- Remove non-existent Gemini authMode field
- Fix prompt deletion behavior: enabled prompts cannot be deleted
- Remove non-existent Legacy deeplink protocol, use V1 only
- Fix DB table names (usage_logs → proxy_request_logs) and add missing tables
- Fix migration version v3.8.0 → v3.7.0
- Add missing V1 deeplink parameters (config, configFormat, etc.)
- Update doc version v3.9.1 → v3.10.3
- Add claude-opus-4-1 to pricing table
- Fix recovery wait time range 10-300 → 0-300
2026-02-09 14:57:57 +08:00
YoVinchen 11cc4e815b docs: add user manual documentation
Add comprehensive user manual covering getting started, provider management,
extensions (MCP/prompts/skills), proxy configuration, and FAQ sections.
Includes screenshots and a README index.
2026-02-09 12:28:45 +08:00
23 changed files with 280 additions and 1660 deletions
+3 -10
View File
@@ -15,18 +15,16 @@ English | [中文](README_ZH.md) | [日本語](README_JA.md) | [Changelog](CHANG
## ❤️Sponsor
[![MiniMax](assets/partners/banners/minimax-en.jpg)](https://bit.ly/3Nue8mA)
[![Zhipu GLM](assets/partners/banners/glm-en.jpg)](https://z.ai/subscribe?ic=8JVLJQFSKB)
MiniMax M2.1 is an open-source, SOTA model built for real-world development and agentic workflows. It delivers top-tier performance on major coding benchmarks such as SWE, VIBE, and Multi-SWE. Powered by a 10B active / 230B total MoE architecture, M2.1 enables faster inference, easier deployment, and even local execution. It excels at coding, navigating digital environments, and handling long, multi-step tasks at scale.
[Click](https://bit.ly/3Nue8mA) to get an exclusive 12% off the MiniMax Coding Plan!
This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN.GLM CODING PLAN is a subscription service designed for AI coding, starting at just $3/month. It provides access to their flagship GLM-4.6 model across 10+ popular AI coding tools (Claude Code, Cline, Roo Code, etc.), offering developers top-tier, fast, and stable coding experiences.Get 10% OFF the GLM CODING PLAN with [this link](https://z.ai/subscribe?ic=8JVLJQFSKB)!
---
<table>
<tr>
<td width="180"><a href="https://www.packyapi.com/register?aff=cc-switch"><img src="assets/partners/logos/packycode.png" alt="PackyCode" width="150"></a></td>
<td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using <a href="https://www.packyapi.com/register?aff=cc-switch">this link</a> and enter the "cc-switch" promo code during first recharge to get 10% off.</td>
<td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using <a href="https://www.packyapi.com/register?aff=cc-switch">this link</a> and enter the "cc-switch" promo code during recharge to get 10% off.</td>
</tr>
<tr>
@@ -50,11 +48,6 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
<td>Thanks to DMXAPI for sponsoring this project! DMXAPI provides global large model API services to 200+ enterprise users. One API key for all global models. Features include: instant invoicing, unlimited concurrency, starting from $0.15, 24/7 technical support. GPT/Claude/Gemini all at 32% off, domestic models 20-50% off, Claude Code exclusive models at 66% off! <a href="https://www.dmxapi.cn/register?aff=bUHu">Register here</a></td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>Thank you to Right Code for sponsoring this project! Right Code reliably provides routing services for models such as Claude Code, Codex, and Gemini. It features a highly cost-effective Codex monthly subscription plan and <strong>supports quota rollovers—unused quota from one day can be carried over and used the next day.</strong> Invoices are available upon top-up. Enterprise and team users can receive dedicated one-on-one support. Right Code also offers an exclusive discount for CC Switch users: register via <a href="https://www.right.codes/register?aff=CCSWITCH">this link</a>, and with every top-up you will receive pay-as-you-go credit equivalent to 25% of the amount paid.</td>
</tr>
<tr>
<td width="180"><a href="https://aicoding.sh/i/CCSWITCH"><img src="assets/partners/logos/aicoding.jpg" alt="AICoding" width="150"></a></td>
<td>Thanks to AICoding.sh for sponsoring this project! AICoding.sh — Global AI Model API Relay Service at Unbeatable Prices! Claude Code at 19% of original price, GPT at just 1%! Trusted by hundreds of enterprises for cost-effective AI services. Supports Claude Code, GPT, Gemini and major domestic models, with enterprise-grade high concurrency, fast invoicing, and 24/7 dedicated technical support. CC Switch users who register via <a href="https://aicoding.sh/i/CCSWITCH">this link</a> get 10% off their first top-up!</td>
+2 -9
View File
@@ -15,11 +15,9 @@
## ❤️スポンサー
[![MiniMax](assets/partners/banners/minimax-en.jpg)](https://bit.ly/3Nue8mA)
[![Zhipu GLM](assets/partners/banners/glm-en.jpg)](https://z.ai/subscribe?ic=8JVLJQFSKB)
MiniMax M2.1 は、実務開発とエージェントワークフロー向けに構築されたオープンソースの最先端モデルです。100 億のアクティブパラメータ / 2,300 億の総パラメータを持つ MoE アーキテクチャにより、高速な推論、簡単なデプロイ、ローカル実行にも対応します。SWE、VIBE、Multi-SWE などの主要コーディングベンチマークでトップクラスの性能を発揮し、コーディング、デジタル環境のナビゲーション、大規模な多段階タスクの処理に優れています
[こちら](https://bit.ly/3Nue8mA)から MiniMax Coding Plan の限定 12% オフを入手!
本プロジェクトは Z.ai の GLM CODING PLAN による支援を受けています。GLM CODING PLAN は AI コーディング向けのサブスクリプションで、月額わずか 3 ドルから。Claude Code、Cline、Roo Code など 10 以上の人気 AI コーディングツールでフラッグシップモデル GLM-4.6 を利用でき、速く安定した開発体験を提供します。[このリンク](https://z.ai/subscribe?ic=8JVLJQFSKB) から申し込むと 10% オフになります
---
@@ -50,11 +48,6 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
<td>DMXAPI のご支援に感謝します!DMXAPI は 200 社以上の企業ユーザーにグローバル大規模モデル API サービスを提供しています。1 つの API キーで全世界のモデルにアクセス可能。即時請求書発行、同時接続数無制限、最低 $0.15 から、24 時間年中無休のテクニカルサポート。GPT/Claude/Gemini が全て 32% オフ、国内モデルは 20〜50% オフ、Claude Code 専用モデルは 66% オフ実施中!<a href="https://www.dmxapi.cn/register?aff=bUHu">登録はこちら</a></td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>本プロジェクトへのご支援として、Right Code にご協賛いただき誠にありがとうございます。Right Code は、Claude Code、Codex、Gemini などのモデルに対応した中継(プロキシ)サービスを安定して提供しています。特に高いコストパフォーマンスを誇る Codex の月額プランを主力としており、<strong>未使用分の利用枠を翌日に繰り越して利用できる(繰越対応)</strong>点が特長です。チャージ(入金)後に請求書の発行が可能で、企業・チーム向けには専任担当による個別対応も行っています。さらに CC Switch ユーザー向けの特別優待として、<a href="https://www.right.codes/register?aff=CCSWITCH">こちらのリンク</a>からご登録いただくと、チャージのたびに実支払額の 25% 相当の従量課金クレジットが付与されます。</td>
</tr>
<tr>
<td width="180"><a href="https://aicoding.sh/i/CCSWITCH"><img src="assets/partners/logos/aicoding.jpg" alt="AICoding" width="150"></a></td>
<td>AICoding.sh のご支援に感謝します!AICoding.sh —— グローバル AI モデル API 超お得な中継サービス!Claude Code 81% オフ、GPT 99% オフ!数百社の企業に高コストパフォーマンスの AI サービスを提供。Claude Code、GPT、Gemini および国内主要モデルに対応、エンタープライズ級の高同時接続、迅速な請求書発行、24 時間年中無休の専属テクニカルサポート。<a href="https://aicoding.sh/i/CCSWITCH">こちらのリンク</a>から登録した CC Switch ユーザーは、初回チャージ 10% オフ!</td>
+3 -10
View File
@@ -15,18 +15,16 @@
## ❤️赞助商
[![MiniMax](assets/partners/banners/minimax-zh.jpeg)](https://platform.minimaxi.com/subscribe/coding-plan?code=7kYF2VoaCn&source=link)
[![智谱 GLM](assets/partners/banners/glm-zh.jpg)](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII)
MiniMax M2.x 系列模型是面向实际开发与智能体工作流打造的编码模型,M2.1 基于 100 亿激活 / 2300 亿总参的混合专家架构打造,推理更快、部署更便捷且支持本地运行,在 SWE、VIBE、Multi-SWE 等主流代码评测基准中均表现顶尖,擅长代码开发、数字环境适配及规模化处理长链路多步骤任务
[点击](https://platform.minimaxi.com/subscribe/coding-plan?code=7kYF2VoaCn&source=link)即可领取 MiniMax Coding Plan 专属 88 折优惠!
感谢智谱AI的 GLM CODING PLAN 赞助了本项目!GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元,即可在十余款主流AI编码工具如 Claude Code、Cline 中畅享智谱旗舰模型 GLM-4.6,为开发者提供顶尖、高速、稳定的编码体验。CC Switch 已经预设了智谱GLM,只需要填写 key 即可一键导入编程工具。智谱AI为本软件的用户提供了特别优惠,使用[此链接](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII)购买可以享受九折优惠
---
<table>
<tr>
<td width="180"><a href="https://www.packyapi.com/register?aff=cc-switch"><img src="assets/partners/logos/packycode.png" alt="PackyCode" width="150"></a></td>
<td>感谢 PackyCode 赞助了本项目!PackyCode 是一家稳定、高效的API中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。PackyCode 为本软件的用户提供了特别优惠,使用<a href="https://www.packyapi.com/register?aff=cc-switch">此链接</a>注册并在充值时填写"cc-switch"优惠码,首次充值可以享受9折优惠!</td>
<td>感谢 PackyCode 赞助了本项目!PackyCode 是一家稳定、高效的API中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。PackyCode 为本软件的用户提供了特别优惠,使用<a href="https://www.packyapi.com/register?aff=cc-switch">此链接</a>注册并在充值时填写"cc-switch"优惠码,可以享受9折优惠!</td>
</tr>
<tr>
@@ -51,11 +49,6 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
为200多家企业用户提供全球大模型API服务。· 充值即开票 ·当天开票 ·并发不限制 ·1元起充 · 7x24 在线技术辅导,GPT/Claude/Gemini全部6.8折,国内模型5~8折,Claude Code 专属模型3.4折进行中!<a href="https://www.dmxapi.cn/register?aff=bUHu">点击这里注册</a></td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>感谢 Right Code 赞助了本项目!Right Code 稳定提供 Claude Code、Codex、Gemini 等模型的中转服务。主打<strong>极高性价比</strong>的Codex包月套餐,<strong>提供额度转结,套餐当天用不完的额度,第二天还能接着用!</strong>充值即可开票,企业、团队用户一对一对接。同时为 CC Switch 的用户提供了特别优惠:通过<a href="https://www.right.codes/register?aff=CCSWITCH">此链接</a>注册,每次充值均可获得实付金额25%的按量额度!</td>
</tr>
<tr>
<td width="180"><a href="https://aicoding.sh/i/CCSWITCH"><img src="assets/partners/logos/aicoding.jpg" alt="AICoding" width="150"></a></td>
<td>感谢 AICoding.sh 赞助了本项目!AICoding.sh —— 全球大模型 API 超值中转服务!Claude Code 1.9 折,GPT 0.1 折,已为数百家企业提供高性价比 AI 服务。支持 Claude Code、GPT、Gemini 及国内主流模型,企业级高并发、极速开票、7×24 专属技术支持,通过<a href="https://aicoding.sh/i/CCSWITCH">此链接</a> 注册的 CC Switch 用户,首充可享受九折优惠!</td>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

-1
View File
@@ -57,7 +57,6 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
-39
View File
@@ -59,9 +59,6 @@ importers:
'@radix-ui/react-label':
specifier: ^2.1.7
version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-popover':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-scroll-area':
specifier: ^1.2.10
version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1051,19 +1048,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-popover@1.1.15':
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-popper@1.2.8':
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
peerDependencies:
@@ -3906,29 +3890,6 @@ snapshots:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
aria-hidden: 1.2.6
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+1 -1
View File
@@ -323,7 +323,7 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
let mut search_paths: Vec<std::path::PathBuf> = vec![
home.join(".local/bin"), // Native install (official recommended)
home.join(".npm-global/bin"),
home.join("n/bin"), // n version manager
home.join("n/bin"), // n version manager
home.join(".volta/bin"), // Volta package manager
];
+8 -78
View File
@@ -286,22 +286,10 @@ impl OmoService {
let obj = Self::read_jsonc_object(&actual_path)?;
Ok(Self::build_local_file_data_from_obj(
&obj,
actual_path.to_string_lossy().to_string(),
last_modified,
))
}
fn build_local_file_data_from_obj(
obj: &Map<String, Value>,
file_path: String,
last_modified: Option<String>,
) -> OmoLocalFileData {
let agents = obj.get("agents").cloned();
let categories = obj.get("categories").cloned();
let other = Self::extract_other_fields(obj);
let other = Self::extract_other_fields(&obj);
let other_fields = if other.is_empty() {
None
} else {
@@ -309,17 +297,16 @@ impl OmoService {
};
let mut global = OmoGlobalConfig::default();
Self::merge_global_from_obj(obj, &mut global);
global.other_fields = other_fields.clone();
Self::merge_global_from_obj(&obj, &mut global);
OmoLocalFileData {
Ok(OmoLocalFileData {
agents,
categories,
other_fields,
global,
file_path,
file_path: actual_path.to_string_lossy().to_string(),
last_modified,
}
})
}
fn strip_jsonc_comments(input: &str) -> String {
@@ -413,7 +400,7 @@ mod tests {
..Default::default()
};
let agents = Some(serde_json::json!({
"sisyphus": { "model": "claude-opus-4-5" }
"Sisyphus": { "model": "claude-opus-4-5" }
}));
let categories = None;
let other_fields = None;
@@ -424,7 +411,7 @@ mod tests {
assert_eq!(obj["$schema"], "https://example.com/schema.json");
assert_eq!(obj["disabled_agents"], serde_json::json!(["explore"]));
assert!(obj.contains_key("agents"));
assert_eq!(obj["agents"]["sisyphus"]["model"], "claude-opus-4-5");
assert_eq!(obj["agents"]["Sisyphus"]["model"], "claude-opus-4-5");
}
#[test]
@@ -435,7 +422,7 @@ mod tests {
..Default::default()
};
let agents = Some(serde_json::json!({
"sisyphus": { "model": "claude-opus-4-5" }
"Sisyphus": { "model": "claude-opus-4-5" }
}));
let categories = None;
let other_fields = None;
@@ -447,61 +434,4 @@ mod tests {
assert!(!obj.contains_key("disabled_agents"));
assert!(obj.contains_key("agents"));
}
#[test]
fn test_build_local_file_data_keeps_unknown_top_level_fields_in_global() {
let obj = serde_json::json!({
"$schema": "https://example.com/schema.json",
"disabled_agents": ["oracle"],
"agents": {
"sisyphus": { "model": "claude-opus-4-6" }
},
"categories": {
"code": { "model": "gpt-5.3" }
},
"custom_top_level": {
"enabled": true
}
});
let obj_map = obj.as_object().unwrap().clone();
let data = OmoService::build_local_file_data_from_obj(
&obj_map,
"/tmp/oh-my-opencode.jsonc".to_string(),
None,
);
assert_eq!(
data.global.schema_url.as_deref(),
Some("https://example.com/schema.json")
);
assert_eq!(data.global.disabled_agents, vec!["oracle".to_string()]);
assert_eq!(
data.other_fields,
Some(serde_json::json!({
"custom_top_level": { "enabled": true }
}))
);
assert_eq!(data.global.other_fields, data.other_fields);
}
#[test]
fn test_merge_config_ignores_non_object_other_fields() {
let global = OmoGlobalConfig {
other_fields: Some(serde_json::json!(["global_non_object"])),
..Default::default()
};
let agents = None;
let categories = None;
let other_fields = Some(serde_json::json!("profile_non_object"));
let profile_data = (agents, categories, other_fields, true);
let merged = OmoService::merge_config(&global, Some(&profile_data));
let obj = merged.as_object().unwrap();
assert!(!obj.contains_key("0"));
assert!(!obj.contains_key("global_non_object"));
assert!(!obj.contains_key("profile_non_object"));
}
}
+29 -104
View File
@@ -174,29 +174,6 @@ impl SkillService {
Self
}
/// 构建 Skill 文档 URL(指向仓库中的 SKILL.md 文件)
fn build_skill_doc_url(owner: &str, repo: &str, branch: &str, doc_path: &str) -> String {
format!("https://github.com/{owner}/{repo}/blob/{branch}/{doc_path}")
}
/// 从旧 readme_url 中提取仓库内文档路径,兼容 `blob`/`tree` 两种格式
fn extract_doc_path_from_url(url: &str) -> Option<String> {
let marker = if url.contains("/blob/") {
"/blob/"
} else if url.contains("/tree/") {
"/tree/"
} else {
return None;
};
let (_, tail) = url.split_once(marker)?;
let (_, path) = tail.split_once('/')?;
if path.is_empty() {
return None;
}
Some(path.to_string())
}
// ========== 路径管理 ==========
/// 获取 SSOT 目录(~/.cc-switch/skills/
@@ -321,8 +298,6 @@ impl SkillService {
let dest = ssot_dir.join(&install_name);
let mut repo_branch = skill.repo_branch.clone();
// 如果已存在则跳过下载
if !dest.exists() {
let repo = SkillRepo {
@@ -333,7 +308,7 @@ impl SkillService {
};
// 下载仓库
let (temp_dir, used_branch) = timeout(
let temp_dir = timeout(
std::time::Duration::from_secs(60),
self.download_repo(&repo),
)
@@ -349,7 +324,6 @@ impl SkillService {
Some("checkNetwork"),
))
})??;
repo_branch = used_branch;
// 复制到 SSOT
let source = temp_dir.join(&skill.directory);
@@ -364,39 +338,8 @@ impl SkillService {
Self::copy_dir_recursive(&source, &dest)?;
let _ = fs::remove_dir_all(&temp_dir);
// 使用实际下载成功的分支,避免 readme_url / repo_branch 与真实分支不一致。
if repo_branch != skill.repo_branch {
log::info!(
"Skill {}/{} 分支自动回退: {} -> {}",
skill.repo_owner,
skill.repo_name,
skill.repo_branch,
repo_branch
);
}
}
let doc_path = skill
.readme_url
.as_deref()
.and_then(Self::extract_doc_path_from_url)
.map(|path| {
if path.ends_with("/SKILL.md") || path == "SKILL.md" {
path
} else {
format!("{}/SKILL.md", path.trim_end_matches('/'))
}
})
.unwrap_or_else(|| format!("{}/SKILL.md", skill.directory.trim_end_matches('/')));
let readme_url = Some(Self::build_skill_doc_url(
&skill.repo_owner,
&skill.repo_name,
&repo_branch,
&doc_path,
));
// 创建 InstalledSkill 记录
let installed_skill = InstalledSkill {
id: skill.key.clone(),
@@ -409,8 +352,8 @@ impl SkillService {
directory: install_name.clone(),
repo_owner: Some(skill.repo_owner.clone()),
repo_name: Some(skill.repo_name.clone()),
repo_branch: Some(repo_branch),
readme_url,
repo_branch: Some(skill.repo_branch.clone()),
readme_url: skill.readme_url.clone(),
apps: SkillApps::only(current_app),
installed_at: chrono::Utc::now().timestamp(),
};
@@ -919,26 +862,24 @@ impl SkillService {
/// 从仓库获取技能列表
async fn fetch_repo_skills(&self, repo: &SkillRepo) -> Result<Vec<DiscoverableSkill>> {
let (temp_dir, resolved_branch) =
timeout(std::time::Duration::from_secs(60), self.download_repo(repo))
.await
.map_err(|_| {
anyhow!(format_skill_error(
"DOWNLOAD_TIMEOUT",
&[
("owner", &repo.owner),
("name", &repo.name),
("timeout", "60")
],
Some("checkNetwork"),
))
})??;
let temp_dir = timeout(std::time::Duration::from_secs(60), self.download_repo(repo))
.await
.map_err(|_| {
anyhow!(format_skill_error(
"DOWNLOAD_TIMEOUT",
&[
("owner", &repo.owner),
("name", &repo.name),
("timeout", "60")
],
Some("checkNetwork"),
))
})??;
let mut skills = Vec::new();
let scan_dir = temp_dir.clone();
let mut resolved_repo = repo.clone();
resolved_repo.branch = resolved_branch;
self.scan_dir_recursive(&scan_dir, &scan_dir, &resolved_repo, &mut skills)?;
self.scan_dir_recursive(&scan_dir, &scan_dir, repo, &mut skills)?;
let _ = fs::remove_dir_all(&temp_dir);
@@ -966,15 +907,7 @@ impl SkillService {
.to_string()
};
let doc_path = skill_md
.strip_prefix(base_dir)
.unwrap_or(skill_md.as_path())
.to_string_lossy()
.replace('\\', "/");
if let Ok(skill) =
self.build_skill_from_metadata(&skill_md, &directory, &doc_path, repo)
{
if let Ok(skill) = self.build_skill_from_metadata(&skill_md, &directory, repo) {
skills.push(skill);
}
@@ -998,7 +931,6 @@ impl SkillService {
&self,
skill_md: &Path,
directory: &str,
doc_path: &str,
repo: &SkillRepo,
) -> Result<DiscoverableSkill> {
let meta = self.parse_skill_metadata(skill_md)?;
@@ -1008,11 +940,9 @@ impl SkillService {
name: meta.name.unwrap_or_else(|| directory.to_string()),
description: meta.description.unwrap_or_default(),
directory: directory.to_string(),
readme_url: Some(Self::build_skill_doc_url(
&repo.owner,
&repo.name,
&repo.branch,
doc_path,
readme_url: Some(format!(
"https://github.com/{}/{}/tree/{}/{}",
repo.owner, repo.name, repo.branch, directory
)),
repo_owner: repo.owner.clone(),
repo_name: repo.name.clone(),
@@ -1064,21 +994,16 @@ impl SkillService {
}
/// 下载仓库
async fn download_repo(&self, repo: &SkillRepo) -> Result<(PathBuf, String)> {
async fn download_repo(&self, repo: &SkillRepo) -> Result<PathBuf> {
let temp_dir = tempfile::tempdir()?;
let temp_path = temp_dir.path().to_path_buf();
let _ = temp_dir.keep();
let mut branches = Vec::new();
if !repo.branch.is_empty() {
branches.push(repo.branch.as_str());
}
if !branches.contains(&"main") {
branches.push("main");
}
if !branches.contains(&"master") {
branches.push("master");
}
let branches = if repo.branch.is_empty() {
vec!["main", "master"]
} else {
vec![repo.branch.as_str(), "main", "master"]
};
let mut last_error = None;
for branch in branches {
@@ -1089,7 +1014,7 @@ impl SkillService {
match self.download_and_extract(&url, &temp_path).await {
Ok(_) => {
return Ok((temp_path, branch.to_string()));
return Ok(temp_path);
}
Err(e) => {
last_error = Some(e);
+2 -20
View File
@@ -139,17 +139,6 @@ function App() {
}
}, [visibleApps, activeApp]);
// Fallback from sessions view when switching to an app without session support
useEffect(() => {
if (
currentView === "sessions" &&
activeApp !== "claude" &&
activeApp !== "codex"
) {
setCurrentView("providers");
}
}, [activeApp, currentView]);
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
const [confirmAction, setConfirmAction] = useState<{
@@ -188,7 +177,6 @@ function App() {
const providers = useMemo(() => data?.providers ?? {}, [data]);
const currentProviderId = data?.currentProviderId ?? "";
const hasSkillsSupport = true;
const hasSessionSupport = activeApp === "claude" || activeApp === "codex";
const {
addProvider,
@@ -970,16 +958,10 @@ function App() {
variant="ghost"
size="sm"
onClick={() => setCurrentView("sessions")}
className={cn(
"text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5",
"transition-all duration-200 ease-in-out overflow-hidden",
hasSessionSupport
? "opacity-100 w-8 scale-100 px-2"
: "opacity-0 w-0 scale-75 pointer-events-none px-0 -ml-1",
)}
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
title={t("sessionManager.title")}
>
<History className="flex-shrink-0 w-4 h-4" />
<History className="w-4 h-4" />
</Button>
<Button
variant="ghost"
+89 -267
View File
@@ -12,19 +12,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Plus,
Trash2,
@@ -34,10 +21,6 @@ import {
Settings,
FolderInput,
Loader2,
HelpCircle,
Check,
ChevronsUpDown,
X,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
@@ -60,13 +43,6 @@ const ADVANCED_PLACEHOLDER = `{
interface OmoFormFieldsProps {
modelOptions: Array<{ value: string; label: string }>;
modelVariantsMap?: Record<string, string[]>;
presetMetaMap?: Record<
string,
{
options?: Record<string, unknown>;
limit?: { context?: number; output?: number };
}
>;
agents: Record<string, Record<string, unknown>>;
onAgentsChange: (agents: Record<string, Record<string, unknown>>) => void;
categories: Record<string, Record<string, unknown>>;
@@ -77,149 +53,19 @@ interface OmoFormFieldsProps {
onOtherFieldsStrChange: (value: string) => void;
}
export type CustomModelItem = {
key: string;
model: string;
sourceKey?: string;
};
type CustomModelItem = { key: string; model: string };
type BuiltinModelDef = Pick<
OmoAgentDef | OmoCategoryDef,
"key" | "display" | "descKey" | "recommended" | "tooltipKey"
"key" | "display" | "descZh" | "descEn" | "recommended"
>;
type ModelOption = { value: string; label: string };
function DeferredKeyInput({
value,
onCommit,
placeholder,
className,
}: {
value: string;
onCommit: (value: string) => void;
placeholder?: string;
className?: string;
}) {
const [draft, setDraft] = useState(value);
useEffect(() => {
setDraft(value);
}, [value]);
return (
<Input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onBlur={() => {
if (draft !== value) {
onCommit(draft);
}
}}
placeholder={placeholder}
className={className}
/>
);
}
const BUILTIN_AGENT_KEYS = new Set(OMO_BUILTIN_AGENTS.map((a) => a.key));
const BUILTIN_CATEGORY_KEYS = new Set(OMO_BUILTIN_CATEGORIES.map((c) => c.key));
const EMPTY_MODEL_VALUE = "__cc_switch_omo_model_empty__";
const UNAVAILABLE_MODEL_VALUE = "__cc_switch_omo_model_unavailable__";
const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__";
function ModelCombobox({
value,
options,
recommended,
onChange,
}: {
value: string;
options: ModelOption[];
recommended?: string;
onChange: (value: string) => void;
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const selectedLabel = options.find((o) => o.value === value)?.label;
const selectModelText = t("omo.selectModel", {
defaultValue: "Select configured model",
});
const placeholderText = recommended
? `${selectModelText} (${t("omo.recommendedHint", { model: recommended, defaultValue: "Recommended: {{model}}" })})`
: selectModelText;
return (
<Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
role="combobox"
aria-expanded={open}
className="flex flex-1 h-8 items-center justify-between whitespace-nowrap rounded-md border border-border-default bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus:outline-none focus-visible:outline-none focus:border-border-default focus-visible:border-border-default focus:ring-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
>
<span className={cn("truncate", !value && "text-muted-foreground")}>
{selectedLabel || placeholderText}
</span>
<span className="flex items-center shrink-0 ml-1 gap-0.5">
{value && (
<X
className="h-3.5 w-3.5 opacity-50 hover:opacity-100 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
onChange("");
}}
/>
)}
<ChevronsUpDown className="h-3.5 w-3.5 opacity-50" />
</span>
</button>
</PopoverTrigger>
<PopoverContent
side="bottom"
align="start"
sideOffset={6}
avoidCollisions={true}
collisionPadding={8}
className="z-[1000] w-[var(--radix-popover-trigger-width)] p-0 border-border-default"
>
<Command>
<CommandInput
placeholder={t("omo.searchModel", {
defaultValue: "Search model...",
})}
/>
<CommandList>
<CommandEmpty>
{t("omo.noEnabledModels", {
defaultValue: "No configured models",
})}
</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
keywords={[option.label]}
onSelect={() => {
onChange(option.value);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
const UNAVAILABLE_VARIANT_VALUE = "__cc_switch_omo_variant_unavailable__";
function getAdvancedStr(config: Record<string, unknown> | undefined): string {
if (!config) return "";
@@ -240,58 +86,24 @@ function collectCustomModels(
customs.push({
key: k,
model: ((v as Record<string, unknown>).model as string) || "",
sourceKey: k,
});
}
}
return customs;
}
export function mergeCustomModelsIntoStore(
function mergeCustomModelsIntoStore(
store: Record<string, Record<string, unknown>>,
builtinKeys: Set<string>,
customs: CustomModelItem[],
modelVariantsMap: Record<string, string[]>,
): Record<string, Record<string, unknown>> {
const updated: Record<string, Record<string, unknown>> = {};
for (const [key, value] of Object.entries(store)) {
if (builtinKeys.has(key)) {
updated[key] = { ...value };
}
const updated = { ...store };
for (const key of Object.keys(updated)) {
if (!builtinKeys.has(key)) delete updated[key];
}
for (const custom of customs) {
const targetKey = custom.key.trim();
if (!targetKey) continue;
const sourceKey = (custom.sourceKey || targetKey).trim();
const sourceEntry = store[sourceKey] ?? store[targetKey];
const nextEntry = {
...(updated[targetKey] || {}),
...(sourceEntry || {}),
};
if (custom.model.trim()) {
nextEntry.model = custom.model;
const currentVariant =
typeof nextEntry.variant === "string" ? nextEntry.variant : "";
if (currentVariant) {
const validVariants = modelVariantsMap[custom.model] || [];
if (!validVariants.includes(currentVariant)) {
delete nextEntry.variant;
}
}
updated[targetKey] = nextEntry;
continue;
}
delete nextEntry.model;
delete nextEntry.variant;
if (Object.keys(nextEntry).length > 0) {
updated[targetKey] = nextEntry;
} else {
delete updated[targetKey];
if (custom.key.trim()) {
updated[custom.key] = { ...updated[custom.key], model: custom.model };
}
}
return updated;
@@ -300,7 +112,6 @@ export function mergeCustomModelsIntoStore(
export function OmoFormFields({
modelOptions,
modelVariantsMap = {},
presetMetaMap: _presetMetaMap = {},
agents,
onAgentsChange,
categories,
@@ -308,7 +119,8 @@ export function OmoFormFields({
otherFieldsStr,
onOtherFieldsStrChange,
}: OmoFormFieldsProps) {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const isZh = i18n.language?.startsWith("zh");
const [mainAgentsOpen, setMainAgentsOpen] = useState(true);
const [subAgentsOpen, setSubAgentsOpen] = useState(true);
@@ -347,29 +159,19 @@ export function OmoFormFields({
const syncCustomAgents = useCallback(
(customs: CustomModelItem[]) => {
onAgentsChange(
mergeCustomModelsIntoStore(
agents,
BUILTIN_AGENT_KEYS,
customs,
modelVariantsMap,
),
mergeCustomModelsIntoStore(agents, BUILTIN_AGENT_KEYS, customs),
);
},
[agents, onAgentsChange, modelVariantsMap],
[agents, onAgentsChange],
);
const syncCustomCategories = useCallback(
(customs: CustomModelItem[]) => {
onCategoriesChange(
mergeCustomModelsIntoStore(
categories,
BUILTIN_CATEGORY_KEYS,
customs,
modelVariantsMap,
),
mergeCustomModelsIntoStore(categories, BUILTIN_CATEGORY_KEYS, customs),
);
},
[categories, onCategoriesChange, modelVariantsMap],
[categories, onCategoriesChange],
);
const buildEffectiveModelOptions = useCallback(
@@ -410,16 +212,43 @@ export function OmoFormFields({
const renderModelSelect = (
currentModel: string,
onChange: (value: string) => void,
recommended?: string,
placeholder?: string,
) => {
const options = buildEffectiveModelOptions(currentModel);
return (
<ModelCombobox
value={currentModel}
options={options}
recommended={recommended}
onChange={onChange}
/>
<Select
value={currentModel || EMPTY_MODEL_VALUE}
onValueChange={(value) =>
onChange(value === EMPTY_MODEL_VALUE ? "" : value)
}
>
<SelectTrigger className="flex-1 h-8 text-sm">
<SelectValue
placeholder={
placeholder ||
t("omo.selectEnabledModel", {
defaultValue: "Select enabled model",
})
}
/>
</SelectTrigger>
<SelectContent className="max-h-72">
<SelectItem value={EMPTY_MODEL_VALUE}>
{t("omo.clearWrapped", { defaultValue: "(Clear)" })}
</SelectItem>
{options.length === 0 ? (
<SelectItem value={UNAVAILABLE_MODEL_VALUE} disabled>
{t("omo.noEnabledModels", { defaultValue: "No enabled models" })}
</SelectItem>
) : (
options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))
)}
</SelectContent>
</Select>
);
};
@@ -439,21 +268,11 @@ export function OmoFormFields({
currentVariant: string,
onChange: (value: string) => void,
) => {
const hasModel = Boolean(currentModel);
const modelVariantKeys = hasModel
? modelVariantsMap[currentModel] || []
: [];
const hasVariants = modelVariantKeys.length > 0;
const shouldShow = hasModel && (hasVariants || Boolean(currentVariant));
if (!shouldShow) {
return null;
}
const variantOptions = buildEffectiveVariantOptions(
currentModel,
currentVariant,
);
const hasModel = Boolean(currentModel);
const firstIsUnavailable =
Boolean(currentVariant) &&
!(modelVariantsMap[currentModel] || []).includes(currentVariant);
@@ -464,8 +283,9 @@ export function OmoFormFields({
onValueChange={(value) =>
onChange(value === EMPTY_VARIANT_VALUE ? "" : value)
}
disabled={!hasModel}
>
<SelectTrigger className="w-28 h-8 text-xs shrink-0">
<SelectTrigger className="w-32 h-8 text-xs shrink-0">
<SelectValue
placeholder={t("omo.variantPlaceholder", {
defaultValue: "variant",
@@ -476,16 +296,30 @@ export function OmoFormFields({
<SelectItem value={EMPTY_VARIANT_VALUE}>
{t("omo.defaultWrapped", { defaultValue: "(Default)" })}
</SelectItem>
{variantOptions.map((variant, index) => (
<SelectItem key={`${variant}-${index}`} value={variant}>
{firstIsUnavailable && index === 0
? t("omo.currentValueUnavailable", {
value: variant,
defaultValue: "{{value}} (current value, unavailable)",
})
: variant}
{!hasModel ? (
<SelectItem value={UNAVAILABLE_VARIANT_VALUE} disabled>
{t("omo.selectModelFirst", {
defaultValue: "Select model first",
})}
</SelectItem>
))}
) : variantOptions.length === 0 ? (
<SelectItem value={UNAVAILABLE_VARIANT_VALUE} disabled>
{t("omo.noVariantsForModel", {
defaultValue: "No variants for model",
})}
</SelectItem>
) : (
variantOptions.map((variant, index) => (
<SelectItem key={`${variant}-${index}`} value={variant}>
{firstIsUnavailable && index === 0
? t("omo.currentValueUnavailable", {
value: variant,
defaultValue: "{{value}} (current value, unavailable)",
})
: variant}
</SelectItem>
))
)}
</SelectContent>
</Select>
);
@@ -702,7 +536,7 @@ export function OmoFormFields({
toast.warning(
t("omo.noEnabledModelsWarning", {
defaultValue:
"No configured models available. Configure OpenCode models first.",
"No enabled models available. Configure and enable OpenCode models first.",
}),
);
return;
@@ -807,17 +641,9 @@ export function OmoFormFields({
<div key={key} className="border-b border-border/30 last:border-b-0">
<div className="flex items-center gap-2 py-1.5">
<div className="w-32 shrink-0">
<div className="flex items-center gap-1 text-sm font-medium">
{def.display}
<span className="relative inline-flex group/tip">
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground/60 hover:text-muted-foreground cursor-help shrink-0" />
<span className="invisible opacity-0 group-hover/tip:visible group-hover/tip:opacity-100 transition-opacity duration-150 absolute left-0 top-full mt-1 z-50 w-[260px] rounded-md bg-popover text-popover-foreground border border-border shadow-md px-3 py-2 text-xs leading-relaxed font-normal pointer-events-none">
{t(def.tooltipKey)}
</span>
</span>
</div>
<div className="text-sm font-medium">{def.display}</div>
<div className="text-xs text-muted-foreground truncate">
{t(def.descKey)}
{isZh ? def.descZh : def.descEn}
</div>
</div>
{renderModelSelect(
@@ -901,14 +727,16 @@ export function OmoFormFields({
className="border-b border-border/30 last:border-b-0"
>
<div className="flex items-center gap-2 py-1.5">
<DeferredKeyInput
<Input
value={item.key}
onCommit={(value) => updateCustom({ key: value })}
onChange={(e) => updateCustom({ key: e.target.value })}
placeholder={keyPlaceholder}
className="w-32 shrink-0 h-8 text-sm text-primary"
/>
{renderModelSelect(item.model, (value) =>
updateCustom({ model: value }),
{renderModelSelect(
item.model,
(value) => updateCustom({ model: value }),
t("omo.modelNamePlaceholder", { defaultValue: "model-name" }),
)}
{renderVariantSelect(item.model, currentVariant, (value) => {
if (!item.key) return;
@@ -1049,17 +877,11 @@ export function OmoFormFields({
const addCustomModel = (scope: AdvancedScope) => {
if (scope === "agent") {
setCustomAgents((prev) => [
...prev,
{ key: "", model: "", sourceKey: "" },
]);
setCustomAgents((prev) => [...prev, { key: "", model: "" }]);
setSubAgentsOpen(true);
return;
}
setCustomCategories((prev) => [
...prev,
{ key: "", model: "", sourceKey: "" },
]);
setCustomCategories((prev) => [...prev, { key: "", model: "" }]);
setCategoriesOpen(true);
};
@@ -1109,7 +931,7 @@ export function OmoFormFields({
·{" "}
{t("omo.enabledModelsCount", {
count: modelOptions.length,
defaultValue: "{{count}} configured models available",
defaultValue: "{{count}} enabled models available",
})}
</span>
{localFilePath && (
+73 -213
View File
@@ -31,7 +31,6 @@ import {
} from "@/config/geminiProviderPresets";
import {
opencodeProviderPresets,
OPENCODE_PRESET_MODEL_VARIANTS,
type OpenCodeProviderPreset,
} from "@/config/opencodeProviderPresets";
import { OpenCodeFormFields } from "./OpenCodeFormFields";
@@ -54,7 +53,7 @@ import { type OmoGlobalConfigFieldsRef } from "./OmoGlobalConfigFields";
import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor";
import * as configApi from "@/lib/api/config";
import type { OmoGlobalConfig } from "@/types/omo";
import { mergeOmoConfigPreview, parseOmoOtherFieldsObject } from "@/types/omo";
import { mergeOmoConfigPreview } from "@/types/omo";
import {
ProviderAdvancedConfig,
type PricingModelSourceOption,
@@ -113,25 +112,21 @@ const isKnownOpencodeOptionKey = (key: string) =>
function parseOpencodeConfig(
settingsConfig?: Record<string, unknown>,
): OpenCodeProviderConfig {
const normalize = (
parsed: Partial<OpenCodeProviderConfig>,
): OpenCodeProviderConfig => ({
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
options:
parsed.options && typeof parsed.options === "object"
? (parsed.options as OpenCodeProviderConfig["options"])
: {},
models:
parsed.models && typeof parsed.models === "object"
? (parsed.models as Record<string, OpenCodeModel>)
: {},
});
try {
const parsed = JSON.parse(
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
) as Partial<OpenCodeProviderConfig>;
return normalize(parsed);
return {
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
options:
parsed.options && typeof parsed.options === "object"
? (parsed.options as OpenCodeProviderConfig["options"])
: {},
models:
parsed.models && typeof parsed.models === "object"
? (parsed.models as Record<string, OpenCodeModel>)
: {},
};
} catch {
return {
npm: OPENCODE_DEFAULT_NPM,
@@ -141,25 +136,6 @@ function parseOpencodeConfig(
}
}
function parseOpencodeConfigStrict(
settingsConfig?: Record<string, unknown>,
): OpenCodeProviderConfig {
const parsed = JSON.parse(
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
) as Partial<OpenCodeProviderConfig>;
return {
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
options:
parsed.options && typeof parsed.options === "object"
? (parsed.options as OpenCodeProviderConfig["options"])
: {},
models:
parsed.models && typeof parsed.models === "object"
? (parsed.models as Record<string, OpenCodeModel>)
: {},
};
}
function toOpencodeExtraOptions(
options: OpenCodeProviderConfig["options"],
): Record<string, string> {
@@ -219,10 +195,8 @@ function buildOmoProfilePreview(
}
if (otherFieldsStr.trim()) {
try {
const other = parseOmoOtherFieldsObject(otherFieldsStr);
if (other) {
Object.assign(profileOnly, other);
}
const other = JSON.parse(otherFieldsStr);
Object.assign(profileOnly, other);
} catch {}
}
return profileOnly;
@@ -606,24 +580,19 @@ export function ProviderForm({
);
}, [opencodeProvidersData?.providers, providerId]);
const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState<
string[] | null
>(null);
const [omoLiveIdsLoadFailed, setOmoLiveIdsLoadFailed] = useState(false);
const lastOmoModelSourceWarningRef = useRef<string>("");
string[]
>([]);
useEffect(() => {
let active = true;
if (!isOmoCategory) {
setEnabledOpencodeProviderIds(null);
setOmoLiveIdsLoadFailed(false);
setEnabledOpencodeProviderIds([]);
return () => {
active = false;
};
}
setEnabledOpencodeProviderIds(null);
setOmoLiveIdsLoadFailed(false);
(async () => {
try {
const ids = await providersApi.getOpenCodeLiveProviderIds();
@@ -631,13 +600,9 @@ export function ProviderForm({
setEnabledOpencodeProviderIds(ids);
}
} catch (error) {
console.warn(
"[OMO_MODEL_SOURCE_LIVE_IDS_FAILED] failed to load live provider ids",
error,
);
console.error("Failed to load OpenCode live provider ids:", error);
if (active) {
setOmoLiveIdsLoadFailed(true);
setEnabledOpencodeProviderIds(null);
setEnabledOpencodeProviderIds([]);
}
}
})();
@@ -647,71 +612,23 @@ export function ProviderForm({
};
}, [isOmoCategory]);
const omoModelBuild = useMemo(() => {
const empty = {
options: [] as Array<{ value: string; label: string }>,
variantsMap: {} as Record<string, string[]>,
presetMetaMap: {} as Record<
string,
{
options?: Record<string, unknown>;
limit?: { context?: number; output?: number };
}
>,
parseFailedProviders: [] as string[],
usedFallbackSource: false,
};
if (!isOmoCategory) {
return empty;
}
const omoModelOptions = useMemo(() => {
if (!isOmoCategory) return [];
const allProviders = opencodeProvidersData?.providers;
if (!allProviders) {
return empty;
}
if (!allProviders) return [];
const shouldFilterByLive = !omoLiveIdsLoadFailed;
if (shouldFilterByLive && enabledOpencodeProviderIds === null) {
return empty;
}
const liveSet =
shouldFilterByLive && enabledOpencodeProviderIds
? new Set(enabledOpencodeProviderIds)
: null;
const enabledSet = new Set(enabledOpencodeProviderIds);
if (enabledSet.size === 0) return [];
const dedupedOptions = new Map<string, string>();
const variantsMap: Record<string, string[]> = {};
const presetMetaMap: Record<
string,
{
options?: Record<string, unknown>;
limit?: { context?: number; output?: number };
}
> = {};
const parseFailedProviders: string[] = [];
for (const [providerKey, provider] of Object.entries(allProviders)) {
if (provider.category === "omo") {
continue;
}
if (liveSet && !liveSet.has(providerKey)) {
if (provider.category === "omo" || !enabledSet.has(providerKey)) {
continue;
}
let parsedConfig: OpenCodeProviderConfig;
try {
parsedConfig = parseOpencodeConfigStrict(provider.settingsConfig);
} catch (error) {
parseFailedProviders.push(providerKey);
console.warn(
"[OMO_MODEL_SOURCE_PARSE_FAILED] failed to parse provider settings",
{
providerKey,
error,
},
);
continue;
}
const parsedConfig = parseOpencodeConfig(provider.settingsConfig);
for (const [modelId, model] of Object.entries(
parsedConfig.models || {},
)) {
@@ -728,107 +645,63 @@ export function ProviderForm({
if (!dedupedOptions.has(value)) {
dedupedOptions.set(value, label);
}
const rawVariants = model.variants;
if (
rawVariants &&
typeof rawVariants === "object" &&
!Array.isArray(rawVariants)
) {
const variantKeys = Object.keys(rawVariants).filter(Boolean);
if (variantKeys.length > 0) {
variantsMap[value] = variantKeys;
}
}
}
// Preset fallback: for models without config-defined variants,
// check if the npm package has preset variant definitions.
// Also collect preset metadata (options, limit) for enrichment.
const presetModels = OPENCODE_PRESET_MODEL_VARIANTS[parsedConfig.npm];
if (presetModels) {
for (const modelId of Object.keys(parsedConfig.models || {})) {
const fullKey = `${providerKey}/${modelId}`;
const preset = presetModels.find((p) => p.id === modelId);
if (!preset) continue;
// Variant fallback
if (!variantsMap[fullKey] && preset.variants) {
const presetKeys = Object.keys(preset.variants).filter(Boolean);
if (presetKeys.length > 0) {
variantsMap[fullKey] = presetKeys;
}
}
// Collect preset metadata for model enrichment
const meta: (typeof presetMetaMap)[string] = {};
if (preset.options) meta.options = preset.options;
if (preset.contextLimit || preset.outputLimit) {
meta.limit = {};
if (preset.contextLimit) meta.limit.context = preset.contextLimit;
if (preset.outputLimit) meta.limit.output = preset.outputLimit;
}
if (Object.keys(meta).length > 0) {
presetMetaMap[fullKey] = meta;
}
}
}
}
return {
options: Array.from(dedupedOptions.entries())
.map(([value, label]) => ({ value, label }))
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN")),
variantsMap,
presetMetaMap,
parseFailedProviders,
usedFallbackSource: omoLiveIdsLoadFailed,
};
return Array.from(dedupedOptions.entries())
.map(([value, label]) => ({ value, label }))
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN"));
}, [
isOmoCategory,
opencodeProvidersData?.providers,
enabledOpencodeProviderIds,
omoLiveIdsLoadFailed,
]);
const omoModelOptions = omoModelBuild.options;
const omoModelVariantsMap = omoModelBuild.variantsMap;
const omoPresetMetaMap = omoModelBuild.presetMetaMap;
useEffect(() => {
if (!isOmoCategory) return;
const failed = omoModelBuild.parseFailedProviders;
const fallback = omoModelBuild.usedFallbackSource;
if (failed.length === 0 && !fallback) return;
const signature = `${fallback ? "fallback:" : ""}${failed
.slice()
.sort()
.join(",")}`;
if (lastOmoModelSourceWarningRef.current === signature) return;
lastOmoModelSourceWarningRef.current = signature;
if (failed.length > 0) {
toast.warning(
t("omo.modelSourcePartialWarning", {
count: failed.length,
defaultValue:
"Some provider model configs are invalid and were skipped.",
}),
);
const omoModelVariantsMap = useMemo(() => {
const variantsMap: Record<string, string[]> = {};
if (!isOmoCategory) {
return variantsMap;
}
if (fallback) {
toast.warning(
t("omo.modelSourceFallbackWarning", {
defaultValue:
"Failed to load live provider state. Falling back to configured providers.",
}),
);
const allProviders = opencodeProvidersData?.providers;
if (!allProviders) {
return variantsMap;
}
const enabledSet = new Set(enabledOpencodeProviderIds);
if (enabledSet.size === 0) {
return variantsMap;
}
for (const [providerKey, provider] of Object.entries(allProviders)) {
if (provider.category === "omo" || !enabledSet.has(providerKey)) {
continue;
}
const parsedConfig = parseOpencodeConfig(provider.settingsConfig);
for (const [modelId, model] of Object.entries(
parsedConfig.models || {},
)) {
const rawVariants = model.variants;
if (
!rawVariants ||
typeof rawVariants !== "object" ||
Array.isArray(rawVariants)
) {
continue;
}
const variantKeys = Object.keys(rawVariants).filter(Boolean);
if (variantKeys.length === 0) {
continue;
}
variantsMap[`${providerKey}/${modelId}`] = variantKeys;
}
}
return variantsMap;
}, [
isOmoCategory,
omoModelBuild.parseFailedProviders,
omoModelBuild.usedFallbackSource,
t,
opencodeProvidersData?.providers,
enabledOpencodeProviderIds,
]);
const initialOmoSettings =
@@ -1205,19 +1078,7 @@ export function ProviderForm({
}
if (omoOtherFieldsStr.trim()) {
try {
const otherFields = parseOmoOtherFieldsObject(omoOtherFieldsStr);
if (!otherFields) {
toast.error(
t("omo.jsonMustBeObject", {
field: t("omo.otherFields", {
defaultValue: "Other Config",
}),
defaultValue: "{{field}} must be a JSON object",
}),
);
return;
}
omoConfig.otherFields = otherFields;
omoConfig.otherFields = JSON.parse(omoOtherFieldsStr);
} catch {
toast.error(
t("omo.invalidJson", {
@@ -1742,7 +1603,6 @@ export function ProviderForm({
<OmoFormFields
modelOptions={omoModelOptions}
modelVariantsMap={omoModelVariantsMap}
presetMetaMap={omoPresetMetaMap}
agents={omoAgents}
onAgentsChange={setOmoAgents}
categories={omoCategories}
-103
View File
@@ -1,103 +0,0 @@
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
const Command = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
const CommandInput = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center border-b px-3 focus-within:outline-none focus-within:ring-0"
cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandItem = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
export {
Command,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
};
-28
View File
@@ -1,28 +0,0 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "start", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 rounded-md border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
+1 -382
View File
@@ -24,384 +24,6 @@ export const opencodeNpmPackages = [
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
] as const;
export interface PresetModelVariant {
id: string;
name?: string;
contextLimit?: number;
outputLimit?: number;
modalities?: { input: string[]; output: string[] };
options?: Record<string, unknown>;
variants?: Record<string, Record<string, unknown>>;
}
export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
string,
PresetModelVariant[]
> = {
"@ai-sdk/openai-compatible": [
{
id: "MiniMax-M2.1",
name: "MiniMax M2.1",
contextLimit: 204800,
outputLimit: 131072,
modalities: { input: ["text"], output: ["text"] },
},
{
id: "glm-4.7",
name: "GLM 4.7",
contextLimit: 204800,
outputLimit: 131072,
modalities: { input: ["text"], output: ["text"] },
},
{
id: "kimi-k2.5",
name: "Kimi K2.5",
contextLimit: 262144,
outputLimit: 262144,
modalities: { input: ["text", "image", "video"], output: ["text"] },
},
],
"@ai-sdk/google": [
{
id: "gemini-2.5-flash-lite",
name: "Gemini 2.5 Flash Lite",
contextLimit: 1048576,
outputLimit: 65536,
modalities: {
input: ["text", "image", "pdf", "video", "audio"],
output: ["text"],
},
variants: {
auto: {
thinkingConfig: { includeThoughts: true, thinkingBudget: -1 },
},
"no-thinking": { thinkingConfig: { thinkingBudget: 0 } },
},
},
{
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash Preview",
contextLimit: 1048576,
outputLimit: 65536,
modalities: {
input: ["text", "image", "pdf", "video", "audio"],
output: ["text"],
},
variants: {
minimal: {
thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" },
},
low: {
thinkingConfig: { includeThoughts: true, thinkingLevel: "low" },
},
medium: {
thinkingConfig: { includeThoughts: true, thinkingLevel: "medium" },
},
high: {
thinkingConfig: { includeThoughts: true, thinkingLevel: "high" },
},
},
},
{
id: "gemini-3-pro-preview",
name: "Gemini 3 Pro Preview",
contextLimit: 1048576,
outputLimit: 65536,
modalities: {
input: ["text", "image", "pdf", "video", "audio"],
output: ["text"],
},
variants: {
low: {
thinkingConfig: { includeThoughts: true, thinkingLevel: "low" },
},
high: {
thinkingConfig: { includeThoughts: true, thinkingLevel: "high" },
},
},
},
],
"@ai-sdk/openai": [
{
id: "gpt-5",
name: "GPT-5",
contextLimit: 400000,
outputLimit: 128000,
modalities: { input: ["text", "image"], output: ["text"] },
variants: {
low: {
reasoningEffort: "low",
reasoningSummary: "auto",
textVerbosity: "low",
},
medium: {
reasoningEffort: "medium",
reasoningSummary: "auto",
textVerbosity: "medium",
},
high: {
reasoningEffort: "high",
reasoningSummary: "auto",
textVerbosity: "high",
},
},
},
{
id: "gpt-5.1",
name: "GPT-5.1",
contextLimit: 400000,
outputLimit: 272000,
modalities: { input: ["text", "image"], output: ["text"] },
variants: {
low: {
reasoningEffort: "low",
reasoningSummary: "auto",
textVerbosity: "low",
},
medium: {
reasoningEffort: "medium",
reasoningSummary: "auto",
textVerbosity: "medium",
},
high: {
reasoningEffort: "high",
reasoningSummary: "auto",
textVerbosity: "high",
},
},
},
{
id: "gpt-5.1-codex",
name: "GPT-5.1 Codex",
contextLimit: 400000,
outputLimit: 128000,
modalities: { input: ["text", "image"], output: ["text"] },
options: { include: ["reasoning.encrypted_content"], store: false },
variants: {
low: {
reasoningEffort: "low",
reasoningSummary: "auto",
textVerbosity: "medium",
},
medium: {
reasoningEffort: "medium",
reasoningSummary: "auto",
textVerbosity: "medium",
},
high: {
reasoningEffort: "high",
reasoningSummary: "auto",
textVerbosity: "medium",
},
},
},
{
id: "gpt-5.1-codex-max",
name: "GPT-5.1 Codex Max",
contextLimit: 400000,
outputLimit: 128000,
modalities: { input: ["text", "image"], output: ["text"] },
options: { include: ["reasoning.encrypted_content"], store: false },
variants: {
low: {
reasoningEffort: "low",
reasoningSummary: "auto",
textVerbosity: "medium",
},
medium: {
reasoningEffort: "medium",
reasoningSummary: "auto",
textVerbosity: "medium",
},
high: {
reasoningEffort: "high",
reasoningSummary: "auto",
textVerbosity: "medium",
},
xhigh: {
reasoningEffort: "xhigh",
reasoningSummary: "auto",
textVerbosity: "medium",
},
},
},
{
id: "gpt-5.2",
name: "GPT-5.2",
contextLimit: 400000,
outputLimit: 128000,
modalities: { input: ["text", "image"], output: ["text"] },
variants: {
low: {
reasoningEffort: "low",
reasoningSummary: "auto",
textVerbosity: "medium",
},
medium: {
reasoningEffort: "medium",
reasoningSummary: "auto",
textVerbosity: "medium",
},
high: {
reasoningEffort: "high",
reasoningSummary: "auto",
textVerbosity: "medium",
},
xhigh: {
reasoningEffort: "xhigh",
reasoningSummary: "auto",
textVerbosity: "medium",
},
},
},
{
id: "gpt-5.2-codex",
name: "GPT-5.2 Codex",
contextLimit: 400000,
outputLimit: 128000,
modalities: { input: ["text", "image"], output: ["text"] },
options: { include: ["reasoning.encrypted_content"], store: false },
variants: {
low: {
reasoningEffort: "low",
reasoningSummary: "auto",
textVerbosity: "medium",
},
medium: {
reasoningEffort: "medium",
reasoningSummary: "auto",
textVerbosity: "medium",
},
high: {
reasoningEffort: "high",
reasoningSummary: "auto",
textVerbosity: "medium",
},
xhigh: {
reasoningEffort: "xhigh",
reasoningSummary: "auto",
textVerbosity: "medium",
},
},
},
{
id: "gpt-5.3-codex",
name: "GPT-5.3 Codex",
contextLimit: 400000,
outputLimit: 128000,
modalities: { input: ["text", "image"], output: ["text"] },
options: { include: ["reasoning.encrypted_content"], store: false },
variants: {
low: {
reasoningEffort: "low",
reasoningSummary: "auto",
textVerbosity: "medium",
},
medium: {
reasoningEffort: "medium",
reasoningSummary: "auto",
textVerbosity: "medium",
},
high: {
reasoningEffort: "high",
reasoningSummary: "auto",
textVerbosity: "medium",
},
xhigh: {
reasoningEffort: "xhigh",
reasoningSummary: "auto",
textVerbosity: "medium",
},
},
},
],
"@ai-sdk/anthropic": [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
contextLimit: 200000,
outputLimit: 64000,
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { effort: "low" },
medium: { effort: "medium" },
high: { effort: "high" },
},
},
{
id: "claude-opus-4-5-20251101",
name: "Claude Opus 4.5",
contextLimit: 200000,
outputLimit: 64000,
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinking: { budgetTokens: 5000, type: "enabled" } },
medium: { thinking: { budgetTokens: 13000, type: "enabled" } },
high: { thinking: { budgetTokens: 18000, type: "enabled" } },
},
},
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
contextLimit: 1000000,
outputLimit: 128000,
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { effort: "low" },
medium: { effort: "medium" },
high: { effort: "high" },
max: { effort: "max" },
},
},
{
id: "claude-haiku-4-5-20251001",
name: "Claude Haiku 4.5",
contextLimit: 200000,
outputLimit: 64000,
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
{
id: "gemini-claude-opus-4-5-thinking",
name: "Antigravity - Claude Opus 4.5",
contextLimit: 200000,
outputLimit: 64000,
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { effort: "low" },
medium: { effort: "medium" },
high: { effort: "high" },
},
},
{
id: "gemini-claude-sonnet-4-5-thinking",
name: "Antigravity - Claude Sonnet 4.5",
contextLimit: 200000,
outputLimit: 64000,
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinking: { budgetTokens: 5000, type: "enabled" } },
medium: { thinking: { budgetTokens: 13000, type: "enabled" } },
high: { thinking: { budgetTokens: 18000, type: "enabled" } },
},
},
],
};
/**
* Look up preset metadata for a model by npm package and model ID.
* Returns enrichment fields (options, limit, modalities) that can be
* merged into a model definition when the user's config doesn't already
* provide them.
*/
export function getPresetModelDefaults(
npm: string,
modelId: string,
): PresetModelVariant | undefined {
const models = OPENCODE_PRESET_MODEL_VARIANTS[npm];
if (!models) return undefined;
return models.find((m) => m.id === modelId);
}
export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
{
name: "DeepSeek",
@@ -1042,10 +664,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"gpt-5.2": { name: "GPT-5.2" },
"gpt-5.2-codex": {
name: "GPT-5.2 Codex",
options: { include: ["reasoning.encrypted_content"], store: false },
},
"gpt-5.2-codex": { name: "GPT-5.2 Codex" },
},
},
category: "third_party",
+6 -57
View File
@@ -1572,21 +1572,16 @@
"clearWrapped": "(Clear)",
"defaultWrapped": "(Default)",
"variantPlaceholder": "variant",
"selectEnabledModel": "Select configured model",
"selectModel": "Select configured model",
"recommendedHint": "Recommended: {{model}}",
"searchModel": "Search model...",
"selectEnabledModel": "Select enabled model",
"selectModelFirst": "Select model first",
"noEnabledModels": "No configured models",
"noEnabledModels": "No enabled models",
"noVariantsForModel": "No variants for model",
"currentValueNotEnabled": "{{value}} (current value, not configured)",
"currentValueNotEnabled": "{{value}} (current value, not enabled)",
"currentValueUnavailable": "{{value}} (current value, unavailable)",
"advancedLabel": "Advanced",
"advancedJsonInvalid": "Advanced JSON is invalid",
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission, etc. Leave empty for defaults",
"noEnabledModelsWarning": "No configured models available. Configure OpenCode models first.",
"modelSourcePartialWarning": "Some provider model configs are invalid and were skipped.",
"modelSourceFallbackWarning": "Failed to load live provider state. Falling back to configured providers.",
"noEnabledModelsWarning": "No enabled models available. Configure and enable OpenCode models first.",
"importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields",
"importLocalFailed": "Failed to read local file: {{error}}",
"agentKeyPlaceholder": "agent key",
@@ -1597,7 +1592,7 @@
"modelConfiguration": "Model Configuration",
"fillRecommended": "Fill Recommended",
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
"enabledModelsCount": "{{count}} configured models available",
"enabledModelsCount": "{{count}} enabled models available",
"source": "from:",
"otherFieldsJson": "Other Fields (JSON)",
"searchOrType": "Search or type custom value...",
@@ -1623,52 +1618,6 @@
"advancedExperimental": "Experimental Features",
"advancedBackgroundTask": "Background Tasks",
"advancedBrowserAutomation": "Browser Automation",
"advancedClaudeCode": "Claude Code",
"agentDesc": {
"sisyphus": "Main orchestrator",
"hephaestus": "Autonomous deep worker",
"prometheus": "Strategic planner",
"atlas": "Task manager",
"oracle": "Strategic advisor",
"librarian": "Multi-repo researcher",
"explore": "Fast code search",
"multimodalLooker": "Media analyzer",
"metis": "Pre-plan analysis advisor",
"momus": "Plan reviewer",
"sisyphusJunior": "Delegated task executor"
},
"agentTooltip": {
"sisyphus": "Main orchestrator responsible for task planning, delegation and parallel execution. Uses extended thinking (32k budget) and drives workflows through TODO lists to ensure task completion.",
"atlas": "Main orchestrator (holds TODO list) responsible for task distribution and coordination during execution phase. Delegates to specialized agents rather than completing all work directly.",
"prometheus": "Strategic planner that collects requirements through interview mode and creates detailed work plans. Can only read/write Markdown files in .sisyphus/ directory, never writes code directly.",
"hephaestus": "Autonomous deep worker (\"legitimate craftsman\") inspired by AmpCode deep mode. Goal-oriented execution that launches 2-5 explore/librarian agents in parallel for research before taking action.",
"oracle": "Architecture decision and debugging advisor. Read-only consulting agent providing excellent logical reasoning and deep analysis. Cannot write files or delegate tasks.",
"librarian": "Multi-repository analysis and documentation retrieval expert. Deeply understands codebases and provides evidence-based answers. Excels at finding official documentation and open-source implementation examples.",
"explore": "Fast codebase exploration and context grep expert. Uses lightweight models for high-speed search. The scout for understanding code structure.",
"multimodalLooker": "Visual content expert that analyzes PDFs, images, charts and other non-text media, extracting information and insights from them.",
"metis": "Plan consultant that performs pre-analysis before planning. Identifies hidden intents, ambiguities and AI failure points to prevent over-engineering.",
"momus": "Plan reviewer that validates plan clarity, verifiability and completeness with high precision. Rejects and requests revisions until plans are perfect.",
"sisyphusJunior": "Category-generated executor, automatically created via category parameters. Focuses on executing assigned tasks and cannot re-delegate, preventing infinite delegation loops."
},
"categoryDesc": {
"visualEngineering": "Visual/frontend engineering",
"ultrabrain": "Ultra thinking",
"deep": "Deep work",
"artistry": "Creative/artistic",
"quick": "Quick response",
"unspecifiedLow": "General low tier",
"unspecifiedHigh": "General high tier",
"writing": "Writing"
},
"categoryTooltip": {
"visualEngineering": "Frontend and visual engineering category for UI/UX design, styling, animation and interface implementation. Defaults to Gemini 3 Pro model.",
"ultrabrain": "Deep logical reasoning category for complex architectural decisions requiring extensive analysis. Defaults to GPT-5.3 Codex ultra-high reasoning variant.",
"deep": "Deep autonomous problem-solving category with goal-oriented execution. Conducts thorough research before action, suitable for difficult problems requiring deep understanding.",
"artistry": "Highly creative and artistic task category that inspires novel ideas and creative solutions. Defaults to Gemini 3 Pro max variant.",
"quick": "Lightweight task category for single-file modifications, typo fixes, and simple adjustments. Defaults to Claude Haiku 4.5 fast model.",
"unspecifiedLow": "Uncategorized low-effort task category for tasks that don't fit other categories with small workload. Defaults to Claude Sonnet 4.5.",
"unspecifiedHigh": "Uncategorized high-effort task category for tasks that don't fit other categories with large workload. Defaults to Claude Opus 4.6 max variant.",
"writing": "Writing category for documentation, prose and technical writing. Defaults to Gemini 3 Flash fast generation model."
}
"advancedClaudeCode": "Claude Code"
}
}
+6 -57
View File
@@ -1553,21 +1553,16 @@
"clearWrapped": "(クリア)",
"defaultWrapped": "(デフォルト)",
"variantPlaceholder": "variant",
"selectEnabledModel": "設定済みモデルを選択",
"selectModel": "設定済みモデルを選択",
"recommendedHint": "推奨: {{model}}",
"searchModel": "モデルを検索...",
"selectEnabledModel": "有効なモデルを選択",
"selectModelFirst": "先にモデルを選択",
"noEnabledModels": "設定済みモデルがありません",
"noEnabledModels": "有効なモデルがありません",
"noVariantsForModel": "このモデルには思考レベルがありません",
"currentValueNotEnabled": "{{value}} (現在値・未設定)",
"currentValueNotEnabled": "{{value}} (現在値・未有効)",
"currentValueUnavailable": "{{value}} (現在値・利用不可)",
"advancedLabel": "詳細",
"advancedJsonInvalid": "詳細 JSON が不正です",
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します",
"noEnabledModelsWarning": "利用可能な設定済みモデルがありません。先に OpenCode モデルを設定してください。",
"modelSourcePartialWarning": "一部プロバイダーのモデル設定が不正なため、候補から除外しました。",
"modelSourceFallbackWarning": "live プロバイダー状態の取得に失敗したため、設定済みプロバイダーへフォールバックしました。",
"noEnabledModelsWarning": "利用可能な有効モデルがありません。先に OpenCode モデルを有効化してください。",
"importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました",
"importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
"agentKeyPlaceholder": "agent キー",
@@ -1578,7 +1573,7 @@
"modelConfiguration": "モデル設定",
"fillRecommended": "推奨を入力",
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
"enabledModelsCount": "設定済みモデル {{count}} 件",
"enabledModelsCount": "有効モデル {{count}} 件",
"source": "出典:",
"otherFieldsJson": "その他のフィールド (JSON)",
"searchOrType": "検索またはカスタム値を入力...",
@@ -1604,52 +1599,6 @@
"advancedExperimental": "実験的機能",
"advancedBackgroundTask": "バックグラウンドタスク",
"advancedBrowserAutomation": "ブラウザ自動化",
"advancedClaudeCode": "Claude Code",
"agentDesc": {
"sisyphus": "メインオーケストレーター",
"hephaestus": "自律型ディープワーカー",
"prometheus": "戦略プランナー",
"atlas": "タスクマネージャー",
"oracle": "戦略アドバイザー",
"librarian": "マルチリポジトリ研究員",
"explore": "高速コード検索",
"multimodalLooker": "メディアアナライザー",
"metis": "計画前分析アドバイザー",
"momus": "プランレビュアー",
"sisyphusJunior": "委任タスクエグゼキューター"
},
"agentTooltip": {
"sisyphus": "メインオーケストレーター。タスクの計画、委任、並列実行を担当。拡張思考(32k バジェット)を使用し、TODO リストでワークフローを駆動してタスク完了を確保します。",
"atlas": "メインオーケストレーター(TODO リスト保持)。実行フェーズでのタスク配分と調整を担当。すべての作業を直接行うのではなく、専門エージェントに委任します。",
"prometheus": "戦略プランナー。インタビューモードで要件を収集し、詳細な作業計画を策定。.sisyphus/ ディレクトリ内の Markdown ファイルの読み書きのみ可能で、直接コードを書くことはありません。",
"hephaestus": "自律型ディープワーカー(「正当な職人」)。AmpCode ディープモードに着想を得た目標指向の実行を行い、行動前に 2-5 個の探索/ライブラリアンエージェントを並列起動して調査します。",
"oracle": "アーキテクチャ決定とデバッグのアドバイザー。読み取り専用のコンサルティングエージェントで、優れた論理的推論と深い分析を提供。ファイルの書き込みやタスクの委任はできません。",
"librarian": "マルチリポジトリ分析とドキュメント検索の専門家。コードベースを深く理解し、エビデンスに基づく回答を提供。公式ドキュメントやオープンソース実装例の検索に長けています。",
"explore": "高速コードベース探索とコンテキスト grep の専門家。軽量モデルを使用した高速検索で、コード構造を理解するための先鋒です。",
"multimodalLooker": "ビジュアルコンテンツの専門家。PDF、画像、グラフなどの非テキストメディアを分析し、情報とインサイトを抽出します。",
"metis": "プランコンサルタント。計画前に事前分析を行い、隠れた意図、曖昧な点、AI の失敗ポイントを特定して、過剰エンジニアリングを防止します。",
"momus": "プランレビュアー。計画の明確さ、検証可能性、完全性を高精度で検証。計画が完璧になるまで却下と修正を要求します。",
"sisyphusJunior": "カテゴリ生成のエグゼキューター。category パラメータにより自動生成され、割り当てられたタスクの実行に専念し、再委任はできません。無限委任ループを防止します。"
},
"categoryDesc": {
"visualEngineering": "ビジュアル/フロントエンド工学",
"ultrabrain": "超深度思考",
"deep": "ディープワーク",
"artistry": "クリエイティブ/芸術",
"quick": "クイックレスポンス",
"unspecifiedLow": "汎用ロースペック",
"unspecifiedHigh": "汎用ハイスペック",
"writing": "ライティング"
},
"categoryTooltip": {
"visualEngineering": "フロントエンドとビジュアルエンジニアリングカテゴリ。UI/UX デザイン、スタイリング、アニメーション、インターフェース実装に特化。デフォルトで Gemini 3 Pro モデルを使用。",
"ultrabrain": "ディープロジック推論カテゴリ。広範な分析が必要な複雑なアーキテクチャ決定に使用。デフォルトで GPT-5.3 Codex の超高推論バリアントを使用。",
"deep": "ディープ自律問題解決カテゴリ。目標指向の実行で、行動前に徹底的な調査を実施。深い理解が必要な難問に適しています。",
"artistry": "高度にクリエイティブで芸術的なタスクカテゴリ。斬新なアイデアとクリエイティブなソリューションを促進。デフォルトで Gemini 3 Pro の max バリアントを使用。",
"quick": "軽量タスクカテゴリ。単一ファイルの修正、タイポ修正、簡単な調整などの些細な作業に使用。デフォルトで Claude Haiku 4.5 高速モデルを使用。",
"unspecifiedLow": "未分類の低作業量タスクカテゴリ。他のカテゴリに該当せず作業量が小さいタスクに適用。デフォルトで Claude Sonnet 4.5 を使用。",
"unspecifiedHigh": "未分類の高作業量タスクカテゴリ。他のカテゴリに該当せず作業量が大きいタスクに適用。デフォルトで Claude Opus 4.6 の max バリアントを使用。",
"writing": "ライティングカテゴリ。ドキュメント、散文、技術文書に特化。デフォルトで Gemini 3 Flash 高速生成モデルを使用。"
}
"advancedClaudeCode": "Claude Code"
}
}
+7 -58
View File
@@ -1572,21 +1572,16 @@
"clearWrapped": "(清空)",
"defaultWrapped": "(默认)",
"variantPlaceholder": "思考等级",
"selectEnabledModel": "选择已配置模型",
"selectModel": "选择已配置模型",
"recommendedHint": "推荐: {{model}}",
"searchModel": "搜索模型...",
"selectEnabledModel": "选择已启用模型",
"selectModelFirst": "先选择模型",
"noEnabledModels": "暂无已配置模型",
"noEnabledModels": "暂无已启用模型",
"noVariantsForModel": "该模型无思考等级",
"currentValueNotEnabled": "{{value}}(当前值,未配置",
"currentValueUnavailable": "{{value}}(当前值,不可用)",
"currentValueNotEnabled": "{{value}}(当前值,未启用",
"currentValueUnavailable": "{{value}}(当前值,未启用)",
"advancedLabel": "高级参数",
"advancedJsonInvalid": "高级参数 JSON 无效",
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission 等,留空使用默认值",
"noEnabledModelsWarning": "当前没有可用的已配置模型,请先配置 OpenCode 模型",
"modelSourcePartialWarning": "部分供应商模型配置无效,已自动跳过。",
"modelSourceFallbackWarning": "读取 live 供应商状态失败,已回退到已配置供应商列表。",
"noEnabledModelsWarning": "当前没有可用的已启用模型,请先启用并配置 OpenCode 模型",
"importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields",
"importLocalFailed": "读取本地文件失败: {{error}}",
"agentKeyPlaceholder": "agent 键名",
@@ -1597,7 +1592,7 @@
"modelConfiguration": "模型配置",
"fillRecommended": "填充推荐",
"configSummary": "已配置 {{agents}} 个 Agent{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
"enabledModelsCount": "可选已配置模型 {{count}} 个",
"enabledModelsCount": "可选已启用模型 {{count}} 个",
"source": "来源:",
"otherFieldsJson": "其他字段 (JSON)",
"searchOrType": "搜索或输入自定义值...",
@@ -1623,52 +1618,6 @@
"advancedExperimental": "实验性功能",
"advancedBackgroundTask": "后台任务",
"advancedBrowserAutomation": "浏览器自动化",
"advancedClaudeCode": "Claude Code",
"agentDesc": {
"sisyphus": "主编排者",
"hephaestus": "自主深度工作者",
"prometheus": "战略规划者",
"atlas": "任务管理者",
"oracle": "战略顾问",
"librarian": "多仓库研究员",
"explore": "快速代码搜索",
"multimodalLooker": "媒体分析器",
"metis": "规划前分析顾问",
"momus": "计划审查者",
"sisyphusJunior": "委托任务执行器"
},
"agentTooltip": {
"sisyphus": "主编排器,负责任务规划、委派与并行执行,使用扩展思考(32k 预算),通过 TODO 驱动工作流确保任务完成。",
"atlas": "主编排器(持有 TODO 列表),负责执行阶段的任务分发与协调,不直接完成所有工作而是委派给专业代理。",
"prometheus": "战略规划师,通过访谈模式收集需求并制定详细工作计划,仅能在 .sisyphus/ 目录读写 Markdown 文件,从不直接写代码。",
"hephaestus": "自主深度工作者(「合法工匠」),受 AmpCode 深度模式启发,目标导向执行,行动前会并行启动 2-5 个探索/图书管理员代理进行研究。",
"oracle": "架构决策与调试顾问,只读咨询代理,提供出色的逻辑推理和深度分析,不能写文件或委派任务。",
"librarian": "多仓库分析与文档检索专家,深度理解代码库并提供基于证据的答案,擅长查找官方文档和开源实现示例。",
"explore": "快速代码库探索与上下文 grep 专家,使用轻量级模型进行高速搜索,是理解代码结构的先锋。",
"multimodalLooker": "视觉内容专家,分析 PDF、图片、图表等非文本媒体,提取其中的信息与洞察。",
"metis": "计划顾问,在规划前进行预分析,识别隐藏意图、模糊点和 AI 失败点,防止过度工程化。",
"momus": "计划评审员,高精度验证计划的清晰度、可验证性和完整性,拒绝并要求修订直到计划完美。",
"sisyphusJunior": "类别生成的执行器,由 category 参数自动生成,专注于执行分配的任务且不能再委派,防止无限委派循环。"
},
"categoryDesc": {
"visualEngineering": "视觉/前端工程",
"ultrabrain": "超级思考",
"deep": "深度工作",
"artistry": "创意/文艺",
"quick": "快速响应",
"unspecifiedLow": "通用低配",
"unspecifiedHigh": "通用高配",
"writing": "写作"
},
"categoryTooltip": {
"visualEngineering": "前端与视觉工程类别,专注于 UI/UX 设计、样式、动画和界面实现,默认使用 Gemini 3 Pro 模型。",
"ultrabrain": "深度逻辑推理类别,用于需要广泛分析的复杂架构决策,默认使用 GPT-5.3 Codex 的超高推理变体。",
"deep": "深度自主问题解决类别,目标导向执行,行动前进行彻底研究,适用于需要深度理解的棘手问题。",
"artistry": "高度创意与艺术性任务类别,激发新颖想法和创造性解决方案,默认使用 Gemini 3 Pro 的最大变体。",
"quick": "轻量任务类别,用于单文件修改、错别字修复、简单调整等琐碎工作,默认使用 Claude Haiku 4.5 快速模型。",
"unspecifiedLow": "未归类低工作量任务类别,适用于不适合其他类别且工作量较小的任务,默认使用 Claude Sonnet 4.5。",
"unspecifiedHigh": "未归类高工作量任务类别,适用于不适合其他类别且工作量较大的任务,默认使用 Claude Opus 4.6 的最大变体。",
"writing": "写作类别,专注于文档、散文和技术写作,默认使用 Gemini 3 Flash 快速生成模型。"
}
"advancedClaudeCode": "Claude Code"
}
}
+50 -62
View File
@@ -27,8 +27,8 @@ export interface OmoLocalFileData {
export interface OmoAgentDef {
key: string;
display: string;
descKey: string;
tooltipKey: string;
descZh: string;
descEn: string;
recommended?: string;
group: "main" | "sub";
}
@@ -36,97 +36,97 @@ export interface OmoAgentDef {
export interface OmoCategoryDef {
key: string;
display: string;
descKey: string;
tooltipKey: string;
descZh: string;
descEn: string;
recommended?: string;
}
export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
{
key: "sisyphus",
key: "Sisyphus",
display: "Sisyphus",
descKey: "omo.agentDesc.sisyphus",
tooltipKey: "omo.agentTooltip.sisyphus",
descZh: "主编排者",
descEn: "Main orchestrator",
recommended: "claude-opus-4-6",
group: "main",
},
{
key: "hephaestus",
key: "Hephaestus",
display: "Hephaestus",
descKey: "omo.agentDesc.hephaestus",
tooltipKey: "omo.agentTooltip.hephaestus",
descZh: "自主深度工作者",
descEn: "Autonomous deep worker",
recommended: "gpt-5.3-codex",
group: "main",
},
{
key: "prometheus",
key: "Prometheus",
display: "Prometheus",
descKey: "omo.agentDesc.prometheus",
tooltipKey: "omo.agentTooltip.prometheus",
descZh: "战略规划者",
descEn: "Strategic planner",
recommended: "claude-opus-4-6",
group: "main",
},
{
key: "atlas",
key: "Atlas",
display: "Atlas",
descKey: "omo.agentDesc.atlas",
tooltipKey: "omo.agentTooltip.atlas",
descZh: "任务管理者",
descEn: "Task manager",
recommended: "kimi-k2.5",
group: "main",
},
{
key: "oracle",
display: "Oracle",
descKey: "omo.agentDesc.oracle",
tooltipKey: "omo.agentTooltip.oracle",
descZh: "战略顾问",
descEn: "Strategic advisor",
recommended: "gpt-5.3",
group: "sub",
},
{
key: "librarian",
display: "Librarian",
descKey: "omo.agentDesc.librarian",
tooltipKey: "omo.agentTooltip.librarian",
descZh: "多仓库研究员",
descEn: "Multi-repo researcher",
recommended: "glm-4.7",
group: "sub",
},
{
key: "explore",
display: "Explore",
descKey: "omo.agentDesc.explore",
tooltipKey: "omo.agentTooltip.explore",
descZh: "快速代码搜索",
descEn: "Fast code search",
recommended: "grok-code-fast-1",
group: "sub",
},
{
key: "multimodal-looker",
display: "Multimodal-Looker",
descKey: "omo.agentDesc.multimodalLooker",
tooltipKey: "omo.agentTooltip.multimodalLooker",
descZh: "媒体分析器",
descEn: "Media analyzer",
recommended: "gemini-3-flash",
group: "sub",
},
{
key: "metis",
key: "Metis",
display: "Metis",
descKey: "omo.agentDesc.metis",
tooltipKey: "omo.agentTooltip.metis",
descZh: "规划前分析顾问",
descEn: "Pre-plan analysis advisor",
recommended: "claude-opus-4-6",
group: "sub",
},
{
key: "momus",
key: "Momus",
display: "Momus",
descKey: "omo.agentDesc.momus",
tooltipKey: "omo.agentTooltip.momus",
descZh: "计划审查者",
descEn: "Plan reviewer",
recommended: "gpt-5.3",
group: "sub",
},
{
key: "sisyphus-junior",
key: "Sisyphus-Junior",
display: "Sisyphus-Junior",
descKey: "omo.agentDesc.sisyphusJunior",
tooltipKey: "omo.agentTooltip.sisyphusJunior",
descZh: "委托任务执行器",
descEn: "Delegated task executor",
group: "sub",
},
];
@@ -135,57 +135,57 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
{
key: "visual-engineering",
display: "Visual Engineering",
descKey: "omo.categoryDesc.visualEngineering",
tooltipKey: "omo.categoryTooltip.visualEngineering",
descZh: "视觉/前端工程",
descEn: "Visual/frontend engineering",
recommended: "gemini-3-pro",
},
{
key: "ultrabrain",
display: "Ultrabrain",
descKey: "omo.categoryDesc.ultrabrain",
tooltipKey: "omo.categoryTooltip.ultrabrain",
descZh: "超级思考",
descEn: "Ultra thinking",
recommended: "claude-opus-4-6",
},
{
key: "deep",
display: "Deep",
descKey: "omo.categoryDesc.deep",
tooltipKey: "omo.categoryTooltip.deep",
descZh: "深度工作",
descEn: "Deep work",
recommended: "gpt-5.3-codex",
},
{
key: "artistry",
display: "Artistry",
descKey: "omo.categoryDesc.artistry",
tooltipKey: "omo.categoryTooltip.artistry",
descZh: "创意/文艺",
descEn: "Creative/artistic",
recommended: "claude-opus-4-6",
},
{
key: "quick",
display: "Quick",
descKey: "omo.categoryDesc.quick",
tooltipKey: "omo.categoryTooltip.quick",
descZh: "快速响应",
descEn: "Quick response",
recommended: "gemini-3-flash",
},
{
key: "unspecified-low",
display: "Unspecified Low",
descKey: "omo.categoryDesc.unspecifiedLow",
tooltipKey: "omo.categoryTooltip.unspecifiedLow",
descZh: "通用低配",
descEn: "General low tier",
recommended: "gemini-3-flash",
},
{
key: "unspecified-high",
display: "Unspecified High",
descKey: "omo.categoryDesc.unspecifiedHigh",
tooltipKey: "omo.categoryTooltip.unspecifiedHigh",
descZh: "通用高配",
descEn: "General high tier",
recommended: "gpt-5.3-codex",
},
{
key: "writing",
display: "Writing",
descKey: "omo.categoryDesc.writing",
tooltipKey: "omo.categoryTooltip.writing",
descZh: "写作",
descEn: "Writing",
recommended: "claude-opus-4-6",
},
];
@@ -316,17 +316,6 @@ export const OMO_CLAUDE_CODE_PLACEHOLDER = `{
"plugins": true
}`;
export function parseOmoOtherFieldsObject(
raw: string,
): Record<string, unknown> | undefined {
if (!raw.trim()) return undefined;
const parsed: unknown = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
return undefined;
}
return parsed as Record<string, unknown>;
}
export function mergeOmoConfigPreview(
global: OmoGlobalConfig,
agents: Record<string, Record<string, unknown>>,
@@ -362,8 +351,7 @@ export function mergeOmoConfigPreview(
if (Object.keys(agents).length > 0) result["agents"] = agents;
if (Object.keys(categories).length > 0) result["categories"] = categories;
try {
const other = parseOmoOtherFieldsObject(otherFieldsStr);
if (!other) return result;
const other = JSON.parse(otherFieldsStr || "{}");
for (const [k, v] of Object.entries(other)) {
result[k] = v;
}
@@ -1,111 +0,0 @@
import { describe, expect, it } from "vitest";
import {
mergeCustomModelsIntoStore,
type CustomModelItem,
} from "@/components/providers/forms/OmoFormFields";
describe("mergeCustomModelsIntoStore", () => {
it("保留自定义项高级字段,并在模型变更时仅按需清理非法 variant", () => {
const store = {
sisyphus: { model: "builtin-model" },
"custom-agent": {
model: "model-a",
variant: "fast",
temperature: 0.2,
permission: { edit: "allow" },
},
};
const customs: CustomModelItem[] = [
{ key: "custom-agent", model: "model-b", sourceKey: "custom-agent" },
];
const merged = mergeCustomModelsIntoStore(
store,
new Set(["sisyphus"]),
customs,
{ "model-b": ["precise"] },
);
expect(merged.sisyphus).toEqual({ model: "builtin-model" });
expect(merged["custom-agent"]).toEqual({
model: "model-b",
temperature: 0.2,
permission: { edit: "allow" },
});
});
it("重命名自定义 key 时迁移原有 variant 和高级字段", () => {
const store = {
sisyphus: { model: "builtin-model" },
"custom-agent-old": {
model: "model-a",
variant: "fast",
maxTokens: 8192,
},
};
const customs: CustomModelItem[] = [
{
key: "custom-agent-new",
sourceKey: "custom-agent-old",
model: "model-a",
},
];
const merged = mergeCustomModelsIntoStore(
store,
new Set(["sisyphus"]),
customs,
{ "model-a": ["fast", "balanced"] },
);
expect(merged["custom-agent-old"]).toBeUndefined();
expect(merged["custom-agent-new"]).toEqual({
model: "model-a",
variant: "fast",
maxTokens: 8192,
});
});
it("custom 列表为空时移除旧自定义项但保留内置项", () => {
const store = {
sisyphus: { model: "builtin-model" },
hephaestus: { model: "builtin-model-2" },
"custom-agent": { model: "model-a", temperature: 0.3 },
};
const merged = mergeCustomModelsIntoStore(
store,
new Set(["sisyphus", "hephaestus"]),
[],
{},
);
expect(merged).toEqual({
sisyphus: { model: "builtin-model" },
hephaestus: { model: "builtin-model-2" },
});
});
it("清空 model 时保留高级字段并移除 model/variant", () => {
const store = {
sisyphus: { model: "builtin-model" },
"custom-agent": {
model: "model-a",
variant: "fast",
temperature: 0.7,
},
};
const customs: CustomModelItem[] = [
{ key: "custom-agent", model: "", sourceKey: "custom-agent" },
];
const merged = mergeCustomModelsIntoStore(
store,
new Set(["sisyphus"]),
customs,
{ "model-a": ["fast"] },
);
expect(merged["custom-agent"]).toEqual({ temperature: 0.7 });
});
});
-50
View File
@@ -1,50 +0,0 @@
import { describe, expect, it } from "vitest";
import {
mergeOmoConfigPreview,
parseOmoOtherFieldsObject,
type OmoGlobalConfig,
} from "@/types/omo";
const EMPTY_GLOBAL: OmoGlobalConfig = {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: "2026-01-01T00:00:00.000Z",
};
describe("parseOmoOtherFieldsObject", () => {
it("解析对象 JSON", () => {
expect(parseOmoOtherFieldsObject('{ "foo": 1 }')).toEqual({ foo: 1 });
});
it("数组/字符串返回 undefined", () => {
expect(parseOmoOtherFieldsObject('["a"]')).toBeUndefined();
expect(parseOmoOtherFieldsObject('"hello"')).toBeUndefined();
});
it("非法 JSON 抛出异常", () => {
expect(() => parseOmoOtherFieldsObject("{")).toThrow();
});
});
describe("mergeOmoConfigPreview", () => {
it("只合并 otherFields 的对象值,忽略数组", () => {
const mergedFromArray = mergeOmoConfigPreview(
EMPTY_GLOBAL,
{},
{},
'["a", "b"]',
);
expect(mergedFromArray).toEqual({});
const mergedFromObject = mergeOmoConfigPreview(
EMPTY_GLOBAL,
{},
{},
'{ "foo": "bar" }',
);
expect(mergedFromObject).toEqual({ foo: "bar" });
});
});