feat(ai): add Claude Code CLI provider

This commit is contained in:
Guoyu Su authored and GitHub committed 2026-07-17 01:17:06 +08:00
1 parent 89fca90dbc
commit d3947d246c
28 files changed
+1723 -176

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { normalizeAiModelEffortLevels, normalizeClaudeCodeReasoningLevel } from "@/lib/ai/aiModelEffort";
import type { AiModelInfo } from "@/lib/backend/tauri";
describe("normalizeAiModelEffortLevels", () => {
it("preserves the CLI order while removing duplicates and unknown levels", () => {
expect(normalizeAiModelEffortLevels(["low", "high", "xhigh", "high", "future", null, "max"])).toEqual(["low", "high", "xhigh", "max"]);
});
it("returns no configurable levels when metadata is unavailable", () => {
expect(normalizeAiModelEffortLevels(undefined)).toEqual([]);
});
});
describe("normalizeClaudeCodeReasoningLevel", () => {
const model: AiModelInfo = {
id: "claude-sonnet-5",
supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"],
};
it("keeps an effort level reported by the selected model", () => {
expect(normalizeClaudeCodeReasoningLevel("xhigh", model)).toBe("xhigh");
});
it("falls back to default for unsupported or missing model metadata", () => {
expect(normalizeClaudeCodeReasoningLevel("minimal", model)).toBe("default");
expect(normalizeClaudeCodeReasoningLevel("max", { id: "default" })).toBe("default");
});
});
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { orderAiConfigsForDisplay } from "@/lib/ai/aiConfigOrdering";
import type { AiProvider } from "@/types/ai";
interface TestConfig {
id: string;
provider: AiProvider;
}
describe("orderAiConfigsForDisplay", () => {
it("matches the canonical provider order", () => {
const configs: TestConfig[] = [
{ id: "claude-code-1", provider: "claude-code-cli" },
{ id: "claude", provider: "claude" },
{ id: "openai", provider: "openai" },
{ id: "codex", provider: "codex-cli" },
{ id: "custom", provider: "custom" },
];
expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude", "openai", "claude-code-1", "codex", "custom"]);
});
it("preserves creation order for configs from the same provider", () => {
const configs: TestConfig[] = [
{ id: "codex-1", provider: "codex-cli" },
{ id: "claude-code-1", provider: "claude-code-cli" },
{ id: "codex-2", provider: "codex-cli" },
{ id: "claude-code-2", provider: "claude-code-cli" },
];
expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude-code-1", "claude-code-2", "codex-1", "codex-2"]);
});
});
@@ -0,0 +1,15 @@
import { AI_PROVIDER_PRESETS } from "@/stores/settingsStore";
import type { AiProvider } from "@/types/ai";
const AI_PROVIDER_DISPLAY_RANK = new Map<AiProvider, number>((Object.keys(AI_PROVIDER_PRESETS) as AiProvider[]).map((provider, index) => [provider, index]));
/**
* Match configured-provider lists to the canonical provider picker order while
* preserving creation order between configs that use the same provider.
*/
export function orderAiConfigsForDisplay<T extends { provider: AiProvider }>(configs: readonly T[]): T[] {
return configs
.map((config, index) => ({ config, index }))
.sort((a, b) => (AI_PROVIDER_DISPLAY_RANK.get(a.config.provider) ?? Number.MAX_SAFE_INTEGER) - (AI_PROVIDER_DISPLAY_RANK.get(b.config.provider) ?? Number.MAX_SAFE_INTEGER) || a.index - b.index)
.map(({ config }) => config);
}
+28
View File
@@ -0,0 +1,28 @@
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";
}
+2 -2
View File
@@ -41,8 +41,7 @@ import type {
import { isTauriCommandUnavailable, normalizeConnectionTestResult } from "@/lib/connection/connectionDatabaseInfo";
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 { AiConfig, AiConfigItem, AiEffortLevel, AiTestConnectionResult } from "@/types/ai";
import type { QueryEditability } from "@/lib/sql/sqlAnalysis";
import { isTerminalTransferProgress } from "@/lib/backend/transferProgress";
import type {
@@ -336,6 +335,7 @@ export interface AiCompletionRequest {
export interface AiModelInfo {
id: string;
displayName?: string;
supportedEffortLevels?: AiEffortLevel[];
}
export async function aiComplete(request: AiCompletionRequest): Promise<string> {