feat(omo): replace model select with searchable combobox and improve fallback handling

This commit is contained in:
YoVinchen
2026-02-11 22:01:10 +08:00
parent 3e9b3d0031
commit b551bb92e2
10 changed files with 563 additions and 164 deletions
+145 -69
View File
@@ -113,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,
@@ -137,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> {
@@ -581,19 +604,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();
@@ -601,9 +629,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);
}
}
})();
@@ -613,23 +645,57 @@ 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[]>,
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 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 || {},
)) {
@@ -646,55 +712,18 @@ 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;
}
variantsMap[`${providerKey}/${modelId}`] = variantKeys;
}
// Preset fallback: for models without config-defined variants,
@@ -717,11 +746,58 @@ export function ProviderForm({
}
}
return variantsMap;
return {
options: Array.from(dedupedOptions.entries())
.map(([value, label]) => ({ value, label }))
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN")),
variantsMap,
parseFailedProviders,
usedFallbackSource: omoLiveIdsLoadFailed,
};
}, [
isOmoCategory,
opencodeProvidersData?.providers,
enabledOpencodeProviderIds,
omoLiveIdsLoadFailed,
]);
const omoModelOptions = omoModelBuild.options;
const omoModelVariantsMap = omoModelBuild.variantsMap;
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 =