feat(ai): add Anthropic-compatible provider
This commit is contained in:
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`) : "";
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
Reference in new issue
Block a user