feat(ai): support multiple configurations and models
This commit is contained in:
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user