feat(ai): unify model discovery and effort handling
This commit is contained in:
1 parent
c54b95e274
commit
c7e7b7c9d9
41 files changed
+3291
-634
No files matched your search
@@ -2,16 +2,8 @@ import type { AiConfig } from "@/types/ai";
|
||||
|
||||
const CLI_PROVIDERS = new Set<AiConfig["provider"]>(["codex-cli", "claude-code-cli"]);
|
||||
|
||||
export function getAiConfigModelIds(config: Pick<AiConfig, "model" | "models">): string[] {
|
||||
const configuredModels = [...new Set(config.models?.map((model) => model.name) ?? [])];
|
||||
return config.model && !configuredModels.includes(config.model) ? [config.model, ...configuredModels] : configuredModels;
|
||||
}
|
||||
|
||||
export function isAiConfigModelCandidate(config: AiConfig, requiresApiKey: boolean): boolean {
|
||||
// CLI providers resolve their model and credentials externally, so keep the existing eligibility bypass.
|
||||
if (CLI_PROVIDERS.has(config.provider)) return true;
|
||||
if (!config.endpoint?.trim() || (requiresApiKey && !config.apiKey?.trim())) return false;
|
||||
|
||||
// A discovered model list is sufficient even before the user chooses a default model.
|
||||
return getAiConfigModelIds(config).some((model) => !!model.trim());
|
||||
return !!config.endpoint?.trim() && (!requiresApiKey || !!config.apiKey?.trim());
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { AiEffortCapability, AiEffortSelection } from "@/types/ai";
|
||||
|
||||
export function runtimeEffortFromPreference(selection: AiEffortSelection | null): AiEffortSelection | undefined {
|
||||
return selection ?? undefined;
|
||||
}
|
||||
|
||||
export function effortSelectionEquals(left: AiEffortSelection | null, right: AiEffortSelection): boolean {
|
||||
return !!left && left.kind === right.kind && ("value" in left ? left.value === ("value" in right ? right.value : undefined) : !("value" in right));
|
||||
}
|
||||
|
||||
function effortSelectionSupported(capability: AiEffortCapability, selection: AiEffortSelection): boolean {
|
||||
if (selection.kind === "providerDefault") return true;
|
||||
if (capability.kind === "enum") {
|
||||
return capability.options.some((option) => effortSelectionEquals(option.selection, selection));
|
||||
}
|
||||
if (capability.kind === "integer") {
|
||||
if (selection.kind === "integer") {
|
||||
const isSteppedValue = selection.value >= capability.min && selection.value <= capability.max && (selection.value - capability.min) % capability.step === 0;
|
||||
return isSteppedValue || !!capability.specialValues?.some((option) => effortSelectionEquals(option.selection, selection));
|
||||
}
|
||||
return !!capability.specialValues?.some((option) => effortSelectionEquals(option.selection, selection));
|
||||
}
|
||||
if (capability.kind === "boolean") return selection.kind === "boolean" || selection.kind === "disabled";
|
||||
if (capability.kind === "freeText") return selection.kind === "text";
|
||||
return false;
|
||||
}
|
||||
|
||||
export function effortPreferenceUpdateForCapability(capability: AiEffortCapability, selection: AiEffortSelection | null): AiEffortSelection | null | undefined {
|
||||
if (!selection || effortSelectionSupported(capability, selection)) return undefined;
|
||||
if (capability.kind === "unsupported" || capability.kind === "freeText") return null;
|
||||
return capability.default;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { AiEffortLevel, AiReasoningLevel } from "@/types/ai";
|
||||
|
||||
interface AiModelEffortMetadata {
|
||||
supportedEffortLevels?: unknown;
|
||||
}
|
||||
|
||||
const AI_EFFORT_LEVELS = new Set<AiEffortLevel>(["low", "medium", "high", "xhigh", "max"]);
|
||||
|
||||
export function normalizeAiModelEffortLevels(value: unknown): AiEffortLevel[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
|
||||
const seen = new Set<AiEffortLevel>();
|
||||
const levels: AiEffortLevel[] = [];
|
||||
for (const level of value) {
|
||||
if (typeof level !== "string" || !AI_EFFORT_LEVELS.has(level as AiEffortLevel)) continue;
|
||||
const normalized = level as AiEffortLevel;
|
||||
if (seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
levels.push(normalized);
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
export function normalizeClaudeCodeReasoningLevel(reasoningLevel: AiReasoningLevel | undefined, model: AiModelEffortMetadata | undefined): AiReasoningLevel {
|
||||
if (!reasoningLevel || reasoningLevel === "default") return "default";
|
||||
const supported = normalizeAiModelEffortLevels(model?.supportedEffortLevels);
|
||||
return supported.includes(reasoningLevel as AiEffortLevel) ? reasoningLevel : "default";
|
||||
}
|
||||
Reference in new issue
Block a user