mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-07 03:34:20 +08:00
feat(omo): improve agent model selection UX and fix lowercase keys (#1004)
* fix(omo): use lowercase keys for builtin agent definitions OMO config schema expects all agent keys to be lowercase. Updated OMO_BUILTIN_AGENTS keys (Sisyphus → sisyphus, Hephaestus → hephaestus, etc.) and aligned Rust test fixtures accordingly. * feat(omo): add i18n support and tooltips for agent/category descriptions * feat(omo): add preset model variants for thinking level support Add OPENCODE_PRESET_MODEL_VARIANTS constant with variant definitions for Google, OpenAI, and Anthropic models. The omoModelVariantsMap builder now falls back to presets when config-defined variants are absent, enabling the variant selector for supported models. * feat(omo): replace model select with searchable combobox and improve fallback handling * feat(omo): enrich preset model defaults and metadata fallback * fix(omo): preserve custom fields and align otherFields import/validation * fix: resolve omo clippy warnings and include app update
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"@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",
|
||||
|
||||
Generated
+39
@@ -59,6 +59,9 @@ 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)
|
||||
@@ -1048,6 +1051,19 @@ 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:
|
||||
@@ -3890,6 +3906,29 @@ 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)
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
|
||||
@@ -286,10 +286,22 @@ 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 {
|
||||
@@ -297,16 +309,17 @@ impl OmoService {
|
||||
};
|
||||
|
||||
let mut global = OmoGlobalConfig::default();
|
||||
Self::merge_global_from_obj(&obj, &mut global);
|
||||
Self::merge_global_from_obj(obj, &mut global);
|
||||
global.other_fields = other_fields.clone();
|
||||
|
||||
Ok(OmoLocalFileData {
|
||||
OmoLocalFileData {
|
||||
agents,
|
||||
categories,
|
||||
other_fields,
|
||||
global,
|
||||
file_path: actual_path.to_string_lossy().to_string(),
|
||||
file_path,
|
||||
last_modified,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_jsonc_comments(input: &str) -> String {
|
||||
@@ -400,7 +413,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;
|
||||
@@ -411,7 +424,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]
|
||||
@@ -422,7 +435,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;
|
||||
@@ -434,4 +447,61 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -141,7 +141,11 @@ function App() {
|
||||
|
||||
// Fallback from sessions view when switching to an app without session support
|
||||
useEffect(() => {
|
||||
if (currentView === "sessions" && activeApp !== "claude" && activeApp !== "codex") {
|
||||
if (
|
||||
currentView === "sessions" &&
|
||||
activeApp !== "claude" &&
|
||||
activeApp !== "codex"
|
||||
) {
|
||||
setCurrentView("providers");
|
||||
}
|
||||
}, [activeApp, currentView]);
|
||||
|
||||
@@ -12,6 +12,19 @@ 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,
|
||||
@@ -21,6 +34,10 @@ import {
|
||||
Settings,
|
||||
FolderInput,
|
||||
Loader2,
|
||||
HelpCircle,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
@@ -43,6 +60,13 @@ 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>>;
|
||||
@@ -53,19 +77,149 @@ interface OmoFormFieldsProps {
|
||||
onOtherFieldsStrChange: (value: string) => void;
|
||||
}
|
||||
|
||||
type CustomModelItem = { key: string; model: string };
|
||||
export type CustomModelItem = {
|
||||
key: string;
|
||||
model: string;
|
||||
sourceKey?: string;
|
||||
};
|
||||
type BuiltinModelDef = Pick<
|
||||
OmoAgentDef | OmoCategoryDef,
|
||||
"key" | "display" | "descZh" | "descEn" | "recommended"
|
||||
"key" | "display" | "descKey" | "recommended" | "tooltipKey"
|
||||
>;
|
||||
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__";
|
||||
const UNAVAILABLE_VARIANT_VALUE = "__cc_switch_omo_variant_unavailable__";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function getAdvancedStr(config: Record<string, unknown> | undefined): string {
|
||||
if (!config) return "";
|
||||
@@ -86,24 +240,58 @@ function collectCustomModels(
|
||||
customs.push({
|
||||
key: k,
|
||||
model: ((v as Record<string, unknown>).model as string) || "",
|
||||
sourceKey: k,
|
||||
});
|
||||
}
|
||||
}
|
||||
return customs;
|
||||
}
|
||||
|
||||
function mergeCustomModelsIntoStore(
|
||||
export function mergeCustomModelsIntoStore(
|
||||
store: Record<string, Record<string, unknown>>,
|
||||
builtinKeys: Set<string>,
|
||||
customs: CustomModelItem[],
|
||||
modelVariantsMap: Record<string, string[]>,
|
||||
): Record<string, Record<string, unknown>> {
|
||||
const updated = { ...store };
|
||||
for (const key of Object.keys(updated)) {
|
||||
if (!builtinKeys.has(key)) delete updated[key];
|
||||
const updated: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(store)) {
|
||||
if (builtinKeys.has(key)) {
|
||||
updated[key] = { ...value };
|
||||
}
|
||||
}
|
||||
|
||||
for (const custom of customs) {
|
||||
if (custom.key.trim()) {
|
||||
updated[custom.key] = { ...updated[custom.key], model: custom.model };
|
||||
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];
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
@@ -112,6 +300,7 @@ function mergeCustomModelsIntoStore(
|
||||
export function OmoFormFields({
|
||||
modelOptions,
|
||||
modelVariantsMap = {},
|
||||
presetMetaMap: _presetMetaMap = {},
|
||||
agents,
|
||||
onAgentsChange,
|
||||
categories,
|
||||
@@ -119,8 +308,7 @@ export function OmoFormFields({
|
||||
otherFieldsStr,
|
||||
onOtherFieldsStrChange,
|
||||
}: OmoFormFieldsProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const isZh = i18n.language?.startsWith("zh");
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [mainAgentsOpen, setMainAgentsOpen] = useState(true);
|
||||
const [subAgentsOpen, setSubAgentsOpen] = useState(true);
|
||||
@@ -159,19 +347,29 @@ export function OmoFormFields({
|
||||
const syncCustomAgents = useCallback(
|
||||
(customs: CustomModelItem[]) => {
|
||||
onAgentsChange(
|
||||
mergeCustomModelsIntoStore(agents, BUILTIN_AGENT_KEYS, customs),
|
||||
mergeCustomModelsIntoStore(
|
||||
agents,
|
||||
BUILTIN_AGENT_KEYS,
|
||||
customs,
|
||||
modelVariantsMap,
|
||||
),
|
||||
);
|
||||
},
|
||||
[agents, onAgentsChange],
|
||||
[agents, onAgentsChange, modelVariantsMap],
|
||||
);
|
||||
|
||||
const syncCustomCategories = useCallback(
|
||||
(customs: CustomModelItem[]) => {
|
||||
onCategoriesChange(
|
||||
mergeCustomModelsIntoStore(categories, BUILTIN_CATEGORY_KEYS, customs),
|
||||
mergeCustomModelsIntoStore(
|
||||
categories,
|
||||
BUILTIN_CATEGORY_KEYS,
|
||||
customs,
|
||||
modelVariantsMap,
|
||||
),
|
||||
);
|
||||
},
|
||||
[categories, onCategoriesChange],
|
||||
[categories, onCategoriesChange, modelVariantsMap],
|
||||
);
|
||||
|
||||
const buildEffectiveModelOptions = useCallback(
|
||||
@@ -212,43 +410,16 @@ export function OmoFormFields({
|
||||
const renderModelSelect = (
|
||||
currentModel: string,
|
||||
onChange: (value: string) => void,
|
||||
placeholder?: string,
|
||||
recommended?: string,
|
||||
) => {
|
||||
const options = buildEffectiveModelOptions(currentModel);
|
||||
return (
|
||||
<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>
|
||||
<ModelCombobox
|
||||
value={currentModel}
|
||||
options={options}
|
||||
recommended={recommended}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -268,11 +439,21 @@ 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);
|
||||
@@ -283,9 +464,8 @@ export function OmoFormFields({
|
||||
onValueChange={(value) =>
|
||||
onChange(value === EMPTY_VARIANT_VALUE ? "" : value)
|
||||
}
|
||||
disabled={!hasModel}
|
||||
>
|
||||
<SelectTrigger className="w-32 h-8 text-xs shrink-0">
|
||||
<SelectTrigger className="w-28 h-8 text-xs shrink-0">
|
||||
<SelectValue
|
||||
placeholder={t("omo.variantPlaceholder", {
|
||||
defaultValue: "variant",
|
||||
@@ -296,30 +476,16 @@ export function OmoFormFields({
|
||||
<SelectItem value={EMPTY_VARIANT_VALUE}>
|
||||
{t("omo.defaultWrapped", { defaultValue: "(Default)" })}
|
||||
</SelectItem>
|
||||
{!hasModel ? (
|
||||
<SelectItem value={UNAVAILABLE_VARIANT_VALUE} disabled>
|
||||
{t("omo.selectModelFirst", {
|
||||
defaultValue: "Select model first",
|
||||
})}
|
||||
{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>
|
||||
) : 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>
|
||||
);
|
||||
@@ -536,7 +702,7 @@ export function OmoFormFields({
|
||||
toast.warning(
|
||||
t("omo.noEnabledModelsWarning", {
|
||||
defaultValue:
|
||||
"No enabled models available. Configure and enable OpenCode models first.",
|
||||
"No configured models available. Configure OpenCode models first.",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -641,9 +807,17 @@ 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="text-sm font-medium">{def.display}</div>
|
||||
<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-xs text-muted-foreground truncate">
|
||||
{isZh ? def.descZh : def.descEn}
|
||||
{t(def.descKey)}
|
||||
</div>
|
||||
</div>
|
||||
{renderModelSelect(
|
||||
@@ -727,16 +901,14 @@ export function OmoFormFields({
|
||||
className="border-b border-border/30 last:border-b-0"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1.5">
|
||||
<Input
|
||||
<DeferredKeyInput
|
||||
value={item.key}
|
||||
onChange={(e) => updateCustom({ key: e.target.value })}
|
||||
onCommit={(value) => updateCustom({ key: value })}
|
||||
placeholder={keyPlaceholder}
|
||||
className="w-32 shrink-0 h-8 text-sm text-primary"
|
||||
/>
|
||||
{renderModelSelect(
|
||||
item.model,
|
||||
(value) => updateCustom({ model: value }),
|
||||
t("omo.modelNamePlaceholder", { defaultValue: "model-name" }),
|
||||
{renderModelSelect(item.model, (value) =>
|
||||
updateCustom({ model: value }),
|
||||
)}
|
||||
{renderVariantSelect(item.model, currentVariant, (value) => {
|
||||
if (!item.key) return;
|
||||
@@ -877,11 +1049,17 @@ export function OmoFormFields({
|
||||
|
||||
const addCustomModel = (scope: AdvancedScope) => {
|
||||
if (scope === "agent") {
|
||||
setCustomAgents((prev) => [...prev, { key: "", model: "" }]);
|
||||
setCustomAgents((prev) => [
|
||||
...prev,
|
||||
{ key: "", model: "", sourceKey: "" },
|
||||
]);
|
||||
setSubAgentsOpen(true);
|
||||
return;
|
||||
}
|
||||
setCustomCategories((prev) => [...prev, { key: "", model: "" }]);
|
||||
setCustomCategories((prev) => [
|
||||
...prev,
|
||||
{ key: "", model: "", sourceKey: "" },
|
||||
]);
|
||||
setCategoriesOpen(true);
|
||||
};
|
||||
|
||||
@@ -931,7 +1109,7 @@ export function OmoFormFields({
|
||||
·{" "}
|
||||
{t("omo.enabledModelsCount", {
|
||||
count: modelOptions.length,
|
||||
defaultValue: "{{count}} enabled models available",
|
||||
defaultValue: "{{count}} configured models available",
|
||||
})}
|
||||
</span>
|
||||
{localFilePath && (
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "@/config/geminiProviderPresets";
|
||||
import {
|
||||
opencodeProviderPresets,
|
||||
OPENCODE_PRESET_MODEL_VARIANTS,
|
||||
type OpenCodeProviderPreset,
|
||||
} from "@/config/opencodeProviderPresets";
|
||||
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
||||
@@ -53,7 +54,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 } from "@/types/omo";
|
||||
import { mergeOmoConfigPreview, parseOmoOtherFieldsObject } from "@/types/omo";
|
||||
import {
|
||||
ProviderAdvancedConfig,
|
||||
type PricingModelSourceOption,
|
||||
@@ -112,21 +113,25 @@ 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 {
|
||||
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>)
|
||||
: {},
|
||||
};
|
||||
return normalize(parsed);
|
||||
} catch {
|
||||
return {
|
||||
npm: OPENCODE_DEFAULT_NPM,
|
||||
@@ -136,6 +141,25 @@ 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> {
|
||||
@@ -195,8 +219,10 @@ function buildOmoProfilePreview(
|
||||
}
|
||||
if (otherFieldsStr.trim()) {
|
||||
try {
|
||||
const other = JSON.parse(otherFieldsStr);
|
||||
Object.assign(profileOnly, other);
|
||||
const other = parseOmoOtherFieldsObject(otherFieldsStr);
|
||||
if (other) {
|
||||
Object.assign(profileOnly, other);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return profileOnly;
|
||||
@@ -580,19 +606,24 @@ export function ProviderForm({
|
||||
);
|
||||
}, [opencodeProvidersData?.providers, providerId]);
|
||||
const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
string[] | null
|
||||
>(null);
|
||||
const [omoLiveIdsLoadFailed, setOmoLiveIdsLoadFailed] = useState(false);
|
||||
const lastOmoModelSourceWarningRef = useRef<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
if (!isOmoCategory) {
|
||||
setEnabledOpencodeProviderIds([]);
|
||||
setEnabledOpencodeProviderIds(null);
|
||||
setOmoLiveIdsLoadFailed(false);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}
|
||||
|
||||
setEnabledOpencodeProviderIds(null);
|
||||
setOmoLiveIdsLoadFailed(false);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const ids = await providersApi.getOpenCodeLiveProviderIds();
|
||||
@@ -600,9 +631,13 @@ export function ProviderForm({
|
||||
setEnabledOpencodeProviderIds(ids);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load OpenCode live provider ids:", error);
|
||||
console.warn(
|
||||
"[OMO_MODEL_SOURCE_LIVE_IDS_FAILED] failed to load live provider ids",
|
||||
error,
|
||||
);
|
||||
if (active) {
|
||||
setEnabledOpencodeProviderIds([]);
|
||||
setOmoLiveIdsLoadFailed(true);
|
||||
setEnabledOpencodeProviderIds(null);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -612,23 +647,71 @@ export function ProviderForm({
|
||||
};
|
||||
}, [isOmoCategory]);
|
||||
|
||||
const omoModelOptions = useMemo(() => {
|
||||
if (!isOmoCategory) return [];
|
||||
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 allProviders = opencodeProvidersData?.providers;
|
||||
if (!allProviders) return [];
|
||||
if (!allProviders) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
const enabledSet = new Set(enabledOpencodeProviderIds);
|
||||
if (enabledSet.size === 0) return [];
|
||||
const shouldFilterByLive = !omoLiveIdsLoadFailed;
|
||||
if (shouldFilterByLive && enabledOpencodeProviderIds === null) {
|
||||
return empty;
|
||||
}
|
||||
const liveSet =
|
||||
shouldFilterByLive && enabledOpencodeProviderIds
|
||||
? new Set(enabledOpencodeProviderIds)
|
||||
: null;
|
||||
|
||||
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" || !enabledSet.has(providerKey)) {
|
||||
if (provider.category === "omo") {
|
||||
continue;
|
||||
}
|
||||
if (liveSet && !liveSet.has(providerKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedConfig = parseOpencodeConfig(provider.settingsConfig);
|
||||
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;
|
||||
}
|
||||
for (const [modelId, model] of Object.entries(
|
||||
parsedConfig.models || {},
|
||||
)) {
|
||||
@@ -645,63 +728,107 @@ export function ProviderForm({
|
||||
if (!dedupedOptions.has(value)) {
|
||||
dedupedOptions.set(value, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(dedupedOptions.entries())
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN"));
|
||||
}, [
|
||||
isOmoCategory,
|
||||
opencodeProvidersData?.providers,
|
||||
enabledOpencodeProviderIds,
|
||||
]);
|
||||
const omoModelVariantsMap = useMemo(() => {
|
||||
const variantsMap: Record<string, string[]> = {};
|
||||
if (!isOmoCategory) {
|
||||
return variantsMap;
|
||||
}
|
||||
|
||||
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)
|
||||
rawVariants &&
|
||||
typeof rawVariants === "object" &&
|
||||
!Array.isArray(rawVariants)
|
||||
) {
|
||||
continue;
|
||||
const variantKeys = Object.keys(rawVariants).filter(Boolean);
|
||||
if (variantKeys.length > 0) {
|
||||
variantsMap[value] = variantKeys;
|
||||
}
|
||||
}
|
||||
const variantKeys = Object.keys(rawVariants).filter(Boolean);
|
||||
if (variantKeys.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
variantsMap[`${providerKey}/${modelId}`] = variantKeys;
|
||||
}
|
||||
}
|
||||
|
||||
return variantsMap;
|
||||
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,
|
||||
};
|
||||
}, [
|
||||
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.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (fallback) {
|
||||
toast.warning(
|
||||
t("omo.modelSourceFallbackWarning", {
|
||||
defaultValue:
|
||||
"Failed to load live provider state. Falling back to configured providers.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
isOmoCategory,
|
||||
omoModelBuild.parseFailedProviders,
|
||||
omoModelBuild.usedFallbackSource,
|
||||
t,
|
||||
]);
|
||||
|
||||
const initialOmoSettings =
|
||||
@@ -1078,7 +1205,19 @@ export function ProviderForm({
|
||||
}
|
||||
if (omoOtherFieldsStr.trim()) {
|
||||
try {
|
||||
omoConfig.otherFields = JSON.parse(omoOtherFieldsStr);
|
||||
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;
|
||||
} catch {
|
||||
toast.error(
|
||||
t("omo.invalidJson", {
|
||||
@@ -1603,6 +1742,7 @@ export function ProviderForm({
|
||||
<OmoFormFields
|
||||
modelOptions={omoModelOptions}
|
||||
modelVariantsMap={omoModelVariantsMap}
|
||||
presetMetaMap={omoPresetMetaMap}
|
||||
agents={omoAgents}
|
||||
onAgentsChange={setOmoAgents}
|
||||
categories={omoCategories}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
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 };
|
||||
@@ -24,6 +24,384 @@ 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",
|
||||
@@ -664,7 +1042,10 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
models: {
|
||||
"gpt-5.2": { name: "GPT-5.2" },
|
||||
"gpt-5.2-codex": { name: "GPT-5.2 Codex" },
|
||||
"gpt-5.2-codex": {
|
||||
name: "GPT-5.2 Codex",
|
||||
options: { include: ["reasoning.encrypted_content"], store: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
category: "third_party",
|
||||
|
||||
@@ -1572,16 +1572,21 @@
|
||||
"clearWrapped": "(Clear)",
|
||||
"defaultWrapped": "(Default)",
|
||||
"variantPlaceholder": "variant",
|
||||
"selectEnabledModel": "Select enabled model",
|
||||
"selectEnabledModel": "Select configured model",
|
||||
"selectModel": "Select configured model",
|
||||
"recommendedHint": "Recommended: {{model}}",
|
||||
"searchModel": "Search model...",
|
||||
"selectModelFirst": "Select model first",
|
||||
"noEnabledModels": "No enabled models",
|
||||
"noEnabledModels": "No configured models",
|
||||
"noVariantsForModel": "No variants for model",
|
||||
"currentValueNotEnabled": "{{value}} (current value, not enabled)",
|
||||
"currentValueNotEnabled": "{{value}} (current value, not configured)",
|
||||
"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 enabled models available. Configure and enable OpenCode models first.",
|
||||
"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.",
|
||||
"importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields",
|
||||
"importLocalFailed": "Failed to read local file: {{error}}",
|
||||
"agentKeyPlaceholder": "agent key",
|
||||
@@ -1592,7 +1597,7 @@
|
||||
"modelConfiguration": "Model Configuration",
|
||||
"fillRecommended": "Fill Recommended",
|
||||
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
|
||||
"enabledModelsCount": "{{count}} enabled models available",
|
||||
"enabledModelsCount": "{{count}} configured models available",
|
||||
"source": "from:",
|
||||
"otherFieldsJson": "Other Fields (JSON)",
|
||||
"searchOrType": "Search or type custom value...",
|
||||
@@ -1618,6 +1623,52 @@
|
||||
"advancedExperimental": "Experimental Features",
|
||||
"advancedBackgroundTask": "Background Tasks",
|
||||
"advancedBrowserAutomation": "Browser Automation",
|
||||
"advancedClaudeCode": "Claude Code"
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,16 +1553,21 @@
|
||||
"clearWrapped": "(クリア)",
|
||||
"defaultWrapped": "(デフォルト)",
|
||||
"variantPlaceholder": "variant",
|
||||
"selectEnabledModel": "有効なモデルを選択",
|
||||
"selectEnabledModel": "設定済みモデルを選択",
|
||||
"selectModel": "設定済みモデルを選択",
|
||||
"recommendedHint": "推奨: {{model}}",
|
||||
"searchModel": "モデルを検索...",
|
||||
"selectModelFirst": "先にモデルを選択",
|
||||
"noEnabledModels": "有効なモデルがありません",
|
||||
"noEnabledModels": "設定済みモデルがありません",
|
||||
"noVariantsForModel": "このモデルには思考レベルがありません",
|
||||
"currentValueNotEnabled": "{{value}} (現在値・未有効)",
|
||||
"currentValueNotEnabled": "{{value}} (現在値・未設定)",
|
||||
"currentValueUnavailable": "{{value}} (現在値・利用不可)",
|
||||
"advancedLabel": "詳細",
|
||||
"advancedJsonInvalid": "詳細 JSON が不正です",
|
||||
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します",
|
||||
"noEnabledModelsWarning": "利用可能な有効モデルがありません。先に OpenCode モデルを有効化してください。",
|
||||
"noEnabledModelsWarning": "利用可能な設定済みモデルがありません。先に OpenCode モデルを設定してください。",
|
||||
"modelSourcePartialWarning": "一部プロバイダーのモデル設定が不正なため、候補から除外しました。",
|
||||
"modelSourceFallbackWarning": "live プロバイダー状態の取得に失敗したため、設定済みプロバイダーへフォールバックしました。",
|
||||
"importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました",
|
||||
"importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
|
||||
"agentKeyPlaceholder": "agent キー",
|
||||
@@ -1573,7 +1578,7 @@
|
||||
"modelConfiguration": "モデル設定",
|
||||
"fillRecommended": "推奨を入力",
|
||||
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
|
||||
"enabledModelsCount": "有効モデル {{count}} 件",
|
||||
"enabledModelsCount": "設定済みモデル {{count}} 件",
|
||||
"source": "出典:",
|
||||
"otherFieldsJson": "その他のフィールド (JSON)",
|
||||
"searchOrType": "検索またはカスタム値を入力...",
|
||||
@@ -1599,6 +1604,52 @@
|
||||
"advancedExperimental": "実験的機能",
|
||||
"advancedBackgroundTask": "バックグラウンドタスク",
|
||||
"advancedBrowserAutomation": "ブラウザ自動化",
|
||||
"advancedClaudeCode": "Claude Code"
|
||||
"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 高速生成モデルを使用。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1572,16 +1572,21 @@
|
||||
"clearWrapped": "(清空)",
|
||||
"defaultWrapped": "(默认)",
|
||||
"variantPlaceholder": "思考等级",
|
||||
"selectEnabledModel": "选择已启用模型",
|
||||
"selectEnabledModel": "选择已配置模型",
|
||||
"selectModel": "选择已配置模型",
|
||||
"recommendedHint": "推荐: {{model}}",
|
||||
"searchModel": "搜索模型...",
|
||||
"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 模型",
|
||||
"noEnabledModelsWarning": "当前没有可用的已配置模型,请先配置 OpenCode 模型",
|
||||
"modelSourcePartialWarning": "部分供应商模型配置无效,已自动跳过。",
|
||||
"modelSourceFallbackWarning": "读取 live 供应商状态失败,已回退到已配置供应商列表。",
|
||||
"importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields",
|
||||
"importLocalFailed": "读取本地文件失败: {{error}}",
|
||||
"agentKeyPlaceholder": "agent 键名",
|
||||
@@ -1592,7 +1597,7 @@
|
||||
"modelConfiguration": "模型配置",
|
||||
"fillRecommended": "填充推荐",
|
||||
"configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
|
||||
"enabledModelsCount": "可选已启用模型 {{count}} 个",
|
||||
"enabledModelsCount": "可选已配置模型 {{count}} 个",
|
||||
"source": "来源:",
|
||||
"otherFieldsJson": "其他字段 (JSON)",
|
||||
"searchOrType": "搜索或输入自定义值...",
|
||||
@@ -1618,6 +1623,52 @@
|
||||
"advancedExperimental": "实验性功能",
|
||||
"advancedBackgroundTask": "后台任务",
|
||||
"advancedBrowserAutomation": "浏览器自动化",
|
||||
"advancedClaudeCode": "Claude Code"
|
||||
"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 快速生成模型。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
-50
@@ -27,8 +27,8 @@ export interface OmoLocalFileData {
|
||||
export interface OmoAgentDef {
|
||||
key: string;
|
||||
display: string;
|
||||
descZh: string;
|
||||
descEn: string;
|
||||
descKey: string;
|
||||
tooltipKey: string;
|
||||
recommended?: string;
|
||||
group: "main" | "sub";
|
||||
}
|
||||
@@ -36,97 +36,97 @@ export interface OmoAgentDef {
|
||||
export interface OmoCategoryDef {
|
||||
key: string;
|
||||
display: string;
|
||||
descZh: string;
|
||||
descEn: string;
|
||||
descKey: string;
|
||||
tooltipKey: string;
|
||||
recommended?: string;
|
||||
}
|
||||
|
||||
export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
||||
{
|
||||
key: "Sisyphus",
|
||||
key: "sisyphus",
|
||||
display: "Sisyphus",
|
||||
descZh: "主编排者",
|
||||
descEn: "Main orchestrator",
|
||||
descKey: "omo.agentDesc.sisyphus",
|
||||
tooltipKey: "omo.agentTooltip.sisyphus",
|
||||
recommended: "claude-opus-4-6",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "Hephaestus",
|
||||
key: "hephaestus",
|
||||
display: "Hephaestus",
|
||||
descZh: "自主深度工作者",
|
||||
descEn: "Autonomous deep worker",
|
||||
descKey: "omo.agentDesc.hephaestus",
|
||||
tooltipKey: "omo.agentTooltip.hephaestus",
|
||||
recommended: "gpt-5.3-codex",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "Prometheus",
|
||||
key: "prometheus",
|
||||
display: "Prometheus",
|
||||
descZh: "战略规划者",
|
||||
descEn: "Strategic planner",
|
||||
descKey: "omo.agentDesc.prometheus",
|
||||
tooltipKey: "omo.agentTooltip.prometheus",
|
||||
recommended: "claude-opus-4-6",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "Atlas",
|
||||
key: "atlas",
|
||||
display: "Atlas",
|
||||
descZh: "任务管理者",
|
||||
descEn: "Task manager",
|
||||
descKey: "omo.agentDesc.atlas",
|
||||
tooltipKey: "omo.agentTooltip.atlas",
|
||||
recommended: "kimi-k2.5",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "oracle",
|
||||
display: "Oracle",
|
||||
descZh: "战略顾问",
|
||||
descEn: "Strategic advisor",
|
||||
descKey: "omo.agentDesc.oracle",
|
||||
tooltipKey: "omo.agentTooltip.oracle",
|
||||
recommended: "gpt-5.3",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "librarian",
|
||||
display: "Librarian",
|
||||
descZh: "多仓库研究员",
|
||||
descEn: "Multi-repo researcher",
|
||||
descKey: "omo.agentDesc.librarian",
|
||||
tooltipKey: "omo.agentTooltip.librarian",
|
||||
recommended: "glm-4.7",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "explore",
|
||||
display: "Explore",
|
||||
descZh: "快速代码搜索",
|
||||
descEn: "Fast code search",
|
||||
descKey: "omo.agentDesc.explore",
|
||||
tooltipKey: "omo.agentTooltip.explore",
|
||||
recommended: "grok-code-fast-1",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "multimodal-looker",
|
||||
display: "Multimodal-Looker",
|
||||
descZh: "媒体分析器",
|
||||
descEn: "Media analyzer",
|
||||
descKey: "omo.agentDesc.multimodalLooker",
|
||||
tooltipKey: "omo.agentTooltip.multimodalLooker",
|
||||
recommended: "gemini-3-flash",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "Metis",
|
||||
key: "metis",
|
||||
display: "Metis",
|
||||
descZh: "规划前分析顾问",
|
||||
descEn: "Pre-plan analysis advisor",
|
||||
descKey: "omo.agentDesc.metis",
|
||||
tooltipKey: "omo.agentTooltip.metis",
|
||||
recommended: "claude-opus-4-6",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "Momus",
|
||||
key: "momus",
|
||||
display: "Momus",
|
||||
descZh: "计划审查者",
|
||||
descEn: "Plan reviewer",
|
||||
descKey: "omo.agentDesc.momus",
|
||||
tooltipKey: "omo.agentTooltip.momus",
|
||||
recommended: "gpt-5.3",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "Sisyphus-Junior",
|
||||
key: "sisyphus-junior",
|
||||
display: "Sisyphus-Junior",
|
||||
descZh: "委托任务执行器",
|
||||
descEn: "Delegated task executor",
|
||||
descKey: "omo.agentDesc.sisyphusJunior",
|
||||
tooltipKey: "omo.agentTooltip.sisyphusJunior",
|
||||
group: "sub",
|
||||
},
|
||||
];
|
||||
@@ -135,57 +135,57 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
|
||||
{
|
||||
key: "visual-engineering",
|
||||
display: "Visual Engineering",
|
||||
descZh: "视觉/前端工程",
|
||||
descEn: "Visual/frontend engineering",
|
||||
descKey: "omo.categoryDesc.visualEngineering",
|
||||
tooltipKey: "omo.categoryTooltip.visualEngineering",
|
||||
recommended: "gemini-3-pro",
|
||||
},
|
||||
{
|
||||
key: "ultrabrain",
|
||||
display: "Ultrabrain",
|
||||
descZh: "超级思考",
|
||||
descEn: "Ultra thinking",
|
||||
descKey: "omo.categoryDesc.ultrabrain",
|
||||
tooltipKey: "omo.categoryTooltip.ultrabrain",
|
||||
recommended: "claude-opus-4-6",
|
||||
},
|
||||
{
|
||||
key: "deep",
|
||||
display: "Deep",
|
||||
descZh: "深度工作",
|
||||
descEn: "Deep work",
|
||||
descKey: "omo.categoryDesc.deep",
|
||||
tooltipKey: "omo.categoryTooltip.deep",
|
||||
recommended: "gpt-5.3-codex",
|
||||
},
|
||||
{
|
||||
key: "artistry",
|
||||
display: "Artistry",
|
||||
descZh: "创意/文艺",
|
||||
descEn: "Creative/artistic",
|
||||
descKey: "omo.categoryDesc.artistry",
|
||||
tooltipKey: "omo.categoryTooltip.artistry",
|
||||
recommended: "claude-opus-4-6",
|
||||
},
|
||||
{
|
||||
key: "quick",
|
||||
display: "Quick",
|
||||
descZh: "快速响应",
|
||||
descEn: "Quick response",
|
||||
descKey: "omo.categoryDesc.quick",
|
||||
tooltipKey: "omo.categoryTooltip.quick",
|
||||
recommended: "gemini-3-flash",
|
||||
},
|
||||
{
|
||||
key: "unspecified-low",
|
||||
display: "Unspecified Low",
|
||||
descZh: "通用低配",
|
||||
descEn: "General low tier",
|
||||
descKey: "omo.categoryDesc.unspecifiedLow",
|
||||
tooltipKey: "omo.categoryTooltip.unspecifiedLow",
|
||||
recommended: "gemini-3-flash",
|
||||
},
|
||||
{
|
||||
key: "unspecified-high",
|
||||
display: "Unspecified High",
|
||||
descZh: "通用高配",
|
||||
descEn: "General high tier",
|
||||
descKey: "omo.categoryDesc.unspecifiedHigh",
|
||||
tooltipKey: "omo.categoryTooltip.unspecifiedHigh",
|
||||
recommended: "gpt-5.3-codex",
|
||||
},
|
||||
{
|
||||
key: "writing",
|
||||
display: "Writing",
|
||||
descZh: "写作",
|
||||
descEn: "Writing",
|
||||
descKey: "omo.categoryDesc.writing",
|
||||
tooltipKey: "omo.categoryTooltip.writing",
|
||||
recommended: "claude-opus-4-6",
|
||||
},
|
||||
];
|
||||
@@ -316,6 +316,17 @@ 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>>,
|
||||
@@ -351,7 +362,8 @@ export function mergeOmoConfigPreview(
|
||||
if (Object.keys(agents).length > 0) result["agents"] = agents;
|
||||
if (Object.keys(categories).length > 0) result["categories"] = categories;
|
||||
try {
|
||||
const other = JSON.parse(otherFieldsStr || "{}");
|
||||
const other = parseOmoOtherFieldsObject(otherFieldsStr);
|
||||
if (!other) return result;
|
||||
for (const [k, v] of Object.entries(other)) {
|
||||
result[k] = v;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
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" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user