fix(ai): refresh model options after adding config

This commit is contained in:
t8y2
2026-07-22 23:09:54 +08:00
parent 22ccdef594
commit e6224ec0b0
3 changed files with 74 additions and 14 deletions
@@ -55,6 +55,7 @@ import { useQueryStore } from "@/stores/queryStore";
import { useToast } from "@/composables/useToast";
import { useNavigationTargets } from "@/composables/useNavigationTargets";
import { buildAiContext, runAgentStream, isVectorDbType, isValidActionForMode, defaultActionForMode, type AiAction, type AiAssistantMode, type AiSqlFileContext, type CustomPromptContext } from "@/lib/ai/ai";
import { getAiConfigModelIds, isAiConfigModelCandidate } from "@/lib/ai/aiConfigCandidates";
import { orderAiConfigsForDisplay } from "@/lib/ai/aiConfigOrdering";
import { normalizeClaudeCodeReasoningLevel } from "@/lib/ai/aiModelEffort";
import { ACTIVE_TEMPLATES_TOTAL_MAX, promptTemplateCharacterCount } from "@/types/promptTemplate";
@@ -297,14 +298,7 @@ const modelSearchQuery = ref("");
// Configured providers for quick switching - get from aiConfigs
const configuredProviders = computed(() => {
const providers = orderAiConfigsForDisplay(
settings.aiConfigs.filter((c) => {
// Check directly if config has required fields
const preset = AI_PROVIDER_PRESETS[c.provider];
if (c.provider === "codex-cli" || c.provider === "claude-code-cli") return true;
return !!c.endpoint?.trim() && !!c.model?.trim() && (!preset.requiresApiKey || !!c.apiKey?.trim());
}),
);
const providers = orderAiConfigsForDisplay(settings.aiConfigs.filter((config) => isAiConfigModelCandidate(config, AI_PROVIDER_PRESETS[config.provider].requiresApiKey)));
// Apply search filter - hide providers with no matching models
if (modelSearchQuery.value.trim()) {
const query = modelSearchQuery.value.trim().toLowerCase();
@@ -334,12 +328,7 @@ const activeFullConfig = computed(() => {
function getModelsForConfig(configId: string): string[] {
const config = settings.aiConfigs.find((c) => c.id === configId);
if (!config) return [];
const models = config.models?.map((m) => m.name) || [];
// Always include the current model
if (config.model && !models.includes(config.model)) {
return [config.model, ...models];
}
return models;
return getAiConfigModelIds(config);
}
function getConfigModelOptionIds(configId: string): string[] {
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { getAiConfigModelIds, isAiConfigModelCandidate } from "@/lib/ai/aiConfigCandidates";
import type { AiConfig } from "@/types/ai";
function config(overrides: Partial<AiConfig> = {}): AiConfig {
return {
provider: "openai",
apiKey: "secret",
authMethod: "bearer",
endpoint: "https://api.example.com/v1",
model: "gpt-default",
apiStyle: "completions",
...overrides,
};
}
describe("isAiConfigModelCandidate", () => {
it("accepts discovered models when the default model is empty", () => {
expect(isAiConfigModelCandidate(config({ model: "", models: [{ name: "gpt-new" }] }), true)).toBe(true);
});
it("keeps endpoint and required API key validation", () => {
expect(isAiConfigModelCandidate(config({ endpoint: "" }), true)).toBe(false);
expect(isAiConfigModelCandidate(config({ apiKey: "" }), true)).toBe(false);
});
it.each(["codex-cli", "claude-code-cli"] as const)("keeps %s configs eligible without endpoint, API key, or model metadata", (provider) => {
expect(
isAiConfigModelCandidate(
config({
provider,
endpoint: "",
apiKey: "",
model: "",
models: [],
}),
false,
),
).toBe(true);
});
});
describe("getAiConfigModelIds", () => {
it("includes the default model once while preserving configured model order", () => {
expect(
getAiConfigModelIds(
config({
model: "gpt-default",
models: [{ name: "gpt-fast" }, { name: "gpt-default" }, { name: "gpt-default" }],
}),
),
).toEqual(["gpt-fast", "gpt-default"]);
});
});
@@ -0,0 +1,17 @@
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());
}