feat(ai): support multiple configurations and models

This commit is contained in:
lexmin0412 authored and GitHub committed 2026-07-16 17:59:40 +08:00
1 parent 5d2a363786
commit dd3a2e30a4
29 files changed
+1728 -500

No files matched your search

@@ -1,33 +0,0 @@
import { describe, expect, it } from "vitest";
import { formatAiModelOption, shouldShowAiModelSecondaryLabel } from "@/lib/ai/aiModelPresentation";
describe("shouldShowAiModelSecondaryLabel", () => {
it("hides secondary text when display name only changes case", () => {
expect(shouldShowAiModelSecondaryLabel("GPT-5.5", "gpt-5.5")).toBe(false);
});
it("hides secondary text when display name only changes separators", () => {
expect(shouldShowAiModelSecondaryLabel("GPT-5.4-Mini", "gpt-5.4-mini")).toBe(false);
});
it("hides secondary text for exact matches", () => {
expect(shouldShowAiModelSecondaryLabel("gpt-5.3-codex", "gpt-5.3-codex")).toBe(false);
});
it("keeps secondary text when provider display name is meaningfully different", () => {
expect(shouldShowAiModelSecondaryLabel("Claude Sonnet 4", "claude-sonnet-4-20250514")).toBe(true);
});
});
describe("formatAiModelOption", () => {
it("returns a single-line Codex-style model option", () => {
expect(formatAiModelOption("GPT-5.5", "gpt-5.5")).toEqual({ primary: "GPT-5.5", secondary: undefined });
});
it("returns model id as secondary text only when useful", () => {
expect(formatAiModelOption("Claude Sonnet 4", "claude-sonnet-4-20250514")).toEqual({
primary: "Claude Sonnet 4",
secondary: "claude-sonnet-4-20250514",
});
});
});
+29
View File
@@ -0,0 +1,29 @@
import type { AiConfig, AiConfigItem } from "@/types/ai";
export type { AiConfigItem };
export function generateId(): string {
return crypto.randomUUID();
}
export function getConfigKey(config: AiConfig): string {
return `${config.provider}|${config.apiKey}|${config.endpoint}|${config.model}`;
}
export function aiConfigToItem(config: AiConfig, id: string, name: string): AiConfigItem {
return {
...config,
id,
name,
};
}
export type ConfigNameValidationResult = "empty" | "duplicate" | "valid";
export function validateConfigName(name: string, configs: AiConfigItem[], excludeId?: string): ConfigNameValidationResult {
if (!name.trim()) return "empty";
if (configs.some((c) => c.name.toLowerCase() === name.toLowerCase() && c.id !== excludeId)) {
return "duplicate";
}
return "valid";
}
@@ -1,25 +0,0 @@
export interface AiModelPresentation {
primary: string;
secondary?: string;
}
function compactModelName(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[\s_-]+/g, "");
}
export function shouldShowAiModelSecondaryLabel(label: string, modelId: string): boolean {
const normalizedLabel = compactModelName(label);
const normalizedId = compactModelName(modelId);
if (!normalizedLabel || !normalizedId || normalizedLabel === normalizedId) return false;
return true;
}
export function formatAiModelOption(label: string, modelId: string): AiModelPresentation {
return {
primary: label || modelId,
secondary: shouldShowAiModelSecondaryLabel(label, modelId) ? modelId : undefined,
};
}
+8
View File
@@ -2,6 +2,7 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import type * as TauriModule from "@/lib/backend/tauri";
import { appendDebugLog } from "@/lib/backend/debugLog";
import { useSettingsStore } from "@/stores/settingsStore";
import type { AiConfigItem } from "@/types/ai";
// ---------------------------------------------------------------------------
// Lazy backend resolution (avoids top-level await)
@@ -238,6 +239,11 @@ export const aiTestConnection = forward("aiTestConnection");
export const aiListModels = forward("aiListModels");
export const saveAiConfig = forward("saveAiConfig");
export const loadAiConfig = forward("loadAiConfig");
export const saveAiConfigs = forward("saveAiConfigs");
export const loadAiConfigs = forward("loadAiConfigs");
export const setDefaultAiConfig = forward("setDefaultAiConfig");
export const saveAiConfigItem = forward("saveAiConfigItem");
export const deleteAiConfig = forward("deleteAiConfig");
export const saveAiProviderConfig = forward("saveAiProviderConfig");
export const loadAiProviderConfigs = forward("loadAiProviderConfigs");
export const loadDesktopSettings = forward("loadDesktopSettings");
@@ -487,6 +493,8 @@ export const loadSidebarLayout = forward("loadSidebarLayout");
// Re-export all types from tauri.ts (shared between both backends)
// ---------------------------------------------------------------------------
export type { AiConfigItem };
export type {
AppSupportInfo,
AiMessage,
+20
View File
@@ -1123,6 +1123,26 @@ export async function loadAiConfig(): Promise<AiConfig | null> {
return get("/api/ai/config");
}
export async function saveAiConfigs(configs: import("@/types/ai").AiConfigItem[]): Promise<void> {
return post("/api/ai/configs", { configs });
}
export async function loadAiConfigs(): Promise<import("@/types/ai").AiConfigItem[]> {
return get("/api/ai/configs");
}
export async function setDefaultAiConfig(configId: string): Promise<void> {
return post("/api/ai/default-config", { configId });
}
export async function saveAiConfigItem(config: import("@/types/ai").AiConfigItem): Promise<void> {
return post("/api/ai/config-item", { config });
}
export async function deleteAiConfig(configId: string): Promise<void> {
return del(`/api/ai/config/${configId}`);
}
export async function loadDesktopSettings(): Promise<DesktopSettings> {
try {
const raw = safeLocalStorageGet(DESKTOP_SETTINGS_STORAGE_KEY);
+21
View File
@@ -42,6 +42,7 @@ import { isTauriCommandUnavailable, normalizeConnectionTestResult } from "@/lib/
import type { CollectionInfo } from "@/types/database";
import type { SidebarObjectKind } from "@/lib/database/databaseObjectCapabilities";
import type { AiConfig, AiTestConnectionResult } from "@/stores/settingsStore";
import type { AiConfigItem } from "@/types/ai";
import type { QueryEditability } from "@/lib/sql/sqlAnalysis";
import { isTerminalTransferProgress } from "@/lib/backend/transferProgress";
import type {
@@ -413,6 +414,26 @@ export async function aiCancelStream(sessionId: string): Promise<boolean> {
return invoke("ai_cancel_stream", { sessionId });
}
export async function saveAiConfigs(configs: AiConfigItem[]): Promise<void> {
return invoke("save_ai_configs", { configs });
}
export async function loadAiConfigs(): Promise<AiConfigItem[]> {
return invoke("load_ai_configs");
}
export async function setDefaultAiConfig(configId: string): Promise<void> {
return invoke("set_default_ai_config", { configId });
}
export async function saveAiConfigItem(config: AiConfigItem): Promise<void> {
return invoke("save_ai_config_item", { config });
}
export async function deleteAiConfig(configId: string): Promise<void> {
return invoke("delete_ai_config", { configId });
}
export async function loadAiConfig(): Promise<AiConfig | null> {
return invoke("load_ai_config");
}