feat(ai): add Anthropic-compatible provider

This commit is contained in:
Guoyu Su authored and GitHub committed 2026-07-29 20:38:09 +08:00
1 parent 4a6422365b
commit 9571519179
11 files changed
+755 -173

No files matched your search

@@ -55,7 +55,6 @@ import { useTheme } from "@/composables/useTheme";
import { copyToClipboard } from "@/lib/common/clipboard";
import { clearDebugLogs as clearStoredDebugLogs, downloadDebugLogs, getDebugLogBundleText } from "@/lib/backend/debugLog";
import {
aiListModels,
aiTestConnection,
checkMcpServerStatus,
installMcpServer,
@@ -2339,6 +2338,7 @@ const aiTestErrorCategoryKeys: Record<string, string> = {
tokenLimit: "ai.testErrorTokenLimit",
safety: "ai.testErrorSafety",
emptyResponse: "ai.testErrorEmptyResponse",
modelDiscoveryUnsupported: "ai.modelListUnsupported",
network: "ai.testErrorNetwork",
unknown: "ai.testErrorUnknown",
};
@@ -2389,7 +2389,9 @@ watch(aiIsCliProvider, (isCliProvider) => {
if (isCliProvider) void ensureCliMcpStatus();
});
const aiRequiresApiKey = computed(() => AI_PROVIDER_PRESETS[aiEditProvider.value].requiresApiKey);
const aiSupportsAuthMethod = computed(() => aiEditProvider.value === "claude" || (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value));
const aiUsesConfigurableAnthropicAuth = computed(() => aiEditProvider.value === "claude" || aiEditProvider.value === "anthropic-compatible" || (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value));
const aiUsesCompatibleAnthropicApi = computed(() => aiEditProvider.value === "anthropic-compatible" || (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value));
const aiSupportsAuthMethod = computed(() => aiUsesConfigurableAnthropicAuth.value);
const aiCredentialLabel = computed(() => (aiSupportsAuthMethod.value && aiEditAuthMethod.value === "bearer" ? "Auth Token" : "API Key"));
const aiCredentialPlaceholder = computed(() => {
if (!aiRequiresApiKey.value) return "Optional";
@@ -2397,7 +2399,7 @@ const aiCredentialPlaceholder = computed(() => {
return "";
});
const aiEndpointPlaceholder = computed(() => {
if (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value) {
if (aiUsesCompatibleAnthropicApi.value) {
return "https://api.example.com/v1/messages";
}
if (aiEditProvider.value === "openai-compatible" || aiEditProvider.value === "custom") {
@@ -2406,7 +2408,7 @@ const aiEndpointPlaceholder = computed(() => {
return "https://api.openai.com/v1";
});
const aiEndpointHint = computed(() => {
if (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value) {
if (aiUsesCompatibleAnthropicApi.value) {
return t("ai.anthropicMessagesHint");
}
if (aiEditProvider.value === "openai-compatible" || aiEditProvider.value === "custom") {
@@ -2669,14 +2671,12 @@ async function aiTestConn() {
aiTestErrorCopied.value = false;
try {
const config = currentAiEditConfig();
if (aiIsCliProvider.value) {
const result = await aiTestConnection(config);
aiTestLatency.value = result.latencyMs ?? null;
} else {
const startedAt = performance.now();
await aiListModels(config);
aiTestLatency.value = Math.round(performance.now() - startedAt);
const activeModel = settingsStore.activeModel;
if (aiEditConfigId.value && activeModel?.configId === aiEditConfigId.value) {
config.model = activeModel.modelId;
}
const result = await aiTestConnection(config);
aiTestLatency.value = result.latencyMs ?? null;
aiTestResult.value = "success";
} catch (e: any) {
aiTestResult.value = "error";
@@ -21,7 +21,7 @@ watch(
},
);
const usesWhiteDarkIcon = computed(() => props.provider === "claude" || props.provider === "ollama" || props.provider === "openai" || props.provider === "openai-compatible");
const usesWhiteDarkIcon = computed(() => props.provider === "claude" || props.provider === "anthropic-compatible" || props.provider === "ollama" || props.provider === "openai" || props.provider === "openai-compatible");
const localIconUrl = computed(() => {
if (props.provider === "openai-compatible") return webPath("/icons/ai/openai.svg");
return props.iconSlug ? webPath(`/icons/ai/${props.iconSlug}.svg`) : "";
+7 -1
View File
@@ -12,13 +12,19 @@ describe("orderAiConfigsForDisplay", () => {
const configs: TestConfig[] = [
{ id: "claude-code-1", provider: "claude-code-cli" },
{ id: "claude", provider: "claude" },
{ id: "anthropic-compatible", provider: "anthropic-compatible" },
{ id: "openai", provider: "openai" },
{ id: "gemini", provider: "gemini" },
{ id: "deepseek", provider: "deepseek" },
{ id: "qwen", provider: "qwen" },
{ id: "ollama", provider: "ollama" },
{ id: "openai-compatible", provider: "openai-compatible" },
{ id: "codex", provider: "codex-cli" },
{ id: "pi", provider: "pi-agent-cli" },
{ id: "custom", provider: "custom" },
];
expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude", "openai", "claude-code-1", "codex", "pi", "custom"]);
expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude", "openai", "gemini", "deepseek", "qwen", "ollama", "anthropic-compatible", "openai-compatible", "claude-code-1", "codex", "pi", "custom"]);
});
it("preserves creation order for configs from the same provider", () => {
+10
View File
@@ -180,6 +180,16 @@ export const AI_PROVIDER_PRESETS: Record<AiProvider, AiProviderPreset> = {
authMethod: "bearer",
requiresApiKey: false,
},
"anthropic-compatible": {
label: "Anthropic Compatible",
iconSlug: "anthropic",
provider: "anthropic-compatible",
endpoint: "",
model: "",
apiStyle: "anthropic-messages",
authMethod: "bearer",
requiresApiKey: false,
},
"openai-compatible": {
label: "OpenAI Compatible",
iconSlug: "openai",
+1 -1
View File
@@ -1,4 +1,4 @@
export type AiProvider = "claude" | "openai" | "gemini" | "deepseek" | "qwen" | "ollama" | "openai-compatible" | "claude-code-cli" | "pi-agent-cli" | "codex-cli" | "custom";
export type AiProvider = "claude" | "openai" | "gemini" | "deepseek" | "qwen" | "ollama" | "anthropic-compatible" | "openai-compatible" | "claude-code-cli" | "pi-agent-cli" | "codex-cli" | "custom";
export type AiApiStyle = "completions" | "responses" | "anthropic-messages";
export type AiAuthMethod = "api-key" | "bearer";
export type AiEffortLevel = "low" | "medium" | "high" | "xhigh" | "max";