mirror of
https://github.com/delibae/claude-prism.git
synced 2026-08-28 05:14:59 +00:00
feat(provider): add Ollama local model support
This commit is contained in:
parent
39c2b4d000
commit
6dd567c7d9
8 changed files with 196 additions and 26 deletions
|
|
@ -202,11 +202,11 @@ async fn handle_messages(
|
|||
.timeout(std::time::Duration::from_secs(300))
|
||||
.build()
|
||||
.map_err(|err| format!("Failed to create provider client: {}", err))?;
|
||||
let response = client
|
||||
let request = client
|
||||
.post(openai_chat_completions_url(&credential.base_url))
|
||||
.bearer_auth(&credential.api_key)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(openai_request.to_string())
|
||||
.body(openai_request.to_string());
|
||||
let response = with_optional_bearer_auth(request, &credential.api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("Provider request failed: {}", err))?;
|
||||
|
|
@ -873,6 +873,17 @@ fn openai_chat_completions_url(base_url: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn with_optional_bearer_auth(
|
||||
request: reqwest::RequestBuilder,
|
||||
api_key: &str,
|
||||
) -> reqwest::RequestBuilder {
|
||||
if api_key.trim().is_empty() {
|
||||
request
|
||||
} else {
|
||||
request.bearer_auth(api_key)
|
||||
}
|
||||
}
|
||||
|
||||
fn openai_compatible_base_url_has_chat_root(base_url: &str) -> bool {
|
||||
let lower = base_url.to_ascii_lowercase();
|
||||
if lower == "https://api.deepseek.com" || lower == "http://api.deepseek.com" {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,15 @@ fn normalize_api_key(value: &str) -> Result<String, String> {
|
|||
Ok(clean)
|
||||
}
|
||||
|
||||
fn normalize_optional_api_key(value: &str) -> Result<String, String> {
|
||||
let clean = strip_nul(value).trim().to_string();
|
||||
if clean.chars().any(char::is_whitespace) {
|
||||
return Err("API key cannot contain spaces or line breaks".to_string());
|
||||
}
|
||||
|
||||
Ok(clean)
|
||||
}
|
||||
|
||||
fn normalize_base_url(value: Option<&str>) -> Result<Option<String>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
|
|
@ -318,7 +327,7 @@ fn normalized_openai_compatible_credentials(
|
|||
) -> Vec<StoredOpenAiCompatibleCredential> {
|
||||
let mut credentials = Vec::new();
|
||||
for credential in &config.openai_credentials {
|
||||
let Ok(api_key) = normalize_api_key(&credential.api_key) else {
|
||||
let Ok(api_key) = normalize_optional_api_key(&credential.api_key) else {
|
||||
continue;
|
||||
};
|
||||
let Some(base_url) = normalize_base_url(Some(credential.base_url.as_str()))
|
||||
|
|
@ -356,7 +365,7 @@ fn normalized_openai_compatible_credentials(
|
|||
config
|
||||
.openai_api_key
|
||||
.as_deref()
|
||||
.and_then(|value| normalize_api_key(value).ok()),
|
||||
.and_then(|value| normalize_optional_api_key(value).ok()),
|
||||
normalize_base_url(config.openai_base_url.as_deref())
|
||||
.ok()
|
||||
.flatten(),
|
||||
|
|
@ -481,9 +490,13 @@ pub async fn save_anthropic_api_key(
|
|||
credential_label: Option<String>,
|
||||
credential_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let api_key = normalize_api_key(&api_key)?;
|
||||
let base_url = normalize_base_url(base_url.as_deref())?;
|
||||
let provider = normalize_provider(provider.as_deref())?;
|
||||
let api_key = if provider == PROVIDER_OPENAI_COMPATIBLE {
|
||||
normalize_optional_api_key(&api_key)?
|
||||
} else {
|
||||
normalize_api_key(&api_key)?
|
||||
};
|
||||
let base_url = normalize_base_url(base_url.as_deref())?;
|
||||
let model = normalize_model(model.as_deref())?;
|
||||
if let Some(message) = known_proxy_mismatch_error(&provider, base_url.as_deref()) {
|
||||
return Err(message);
|
||||
|
|
@ -564,7 +577,7 @@ pub async fn verify_openai_compatible_api_key(
|
|||
base_url: String,
|
||||
model: String,
|
||||
) -> Result<(), String> {
|
||||
let api_key = normalize_api_key(&api_key)?;
|
||||
let api_key = normalize_optional_api_key(&api_key)?;
|
||||
let base_url = normalize_base_url(Some(base_url.as_str()))?
|
||||
.ok_or("OpenAI-compatible provider requires a Base URL")?;
|
||||
if let Some(message) =
|
||||
|
|
@ -590,7 +603,7 @@ pub async fn list_openai_compatible_models(
|
|||
api_key: String,
|
||||
base_url: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let api_key = normalize_api_key(&api_key)?;
|
||||
let api_key = normalize_optional_api_key(&api_key)?;
|
||||
let base_url = normalize_base_url(Some(base_url.as_str()))?
|
||||
.ok_or("OpenAI-compatible provider requires a Base URL")?;
|
||||
if let Some(message) =
|
||||
|
|
@ -619,9 +632,8 @@ async fn fetch_openai_compatible_models(
|
|||
api_key: &str,
|
||||
base_url: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let response = reqwest::Client::new()
|
||||
.get(openai_models_url(base_url))
|
||||
.bearer_auth(api_key)
|
||||
let request = reqwest::Client::new().get(openai_models_url(base_url));
|
||||
let response = with_optional_bearer_auth(request, api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("Failed to fetch provider models: {}", err))?;
|
||||
|
|
@ -2351,6 +2363,17 @@ fn openai_chat_completions_url(base_url: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn with_optional_bearer_auth(
|
||||
request: reqwest::RequestBuilder,
|
||||
api_key: &str,
|
||||
) -> reqwest::RequestBuilder {
|
||||
if api_key.trim().is_empty() {
|
||||
request
|
||||
} else {
|
||||
request.bearer_auth(api_key)
|
||||
}
|
||||
}
|
||||
|
||||
fn openai_models_url(base_url: &str) -> String {
|
||||
let clean = base_url.trim_end_matches('/');
|
||||
if let Some(root) = clean.strip_suffix("/chat/completions") {
|
||||
|
|
@ -2439,11 +2462,11 @@ async fn verify_openai_compatible_credential(
|
|||
.map_err(|err| format!("Failed to create provider client: {}", err))?;
|
||||
let request_body = openai_compatible_verification_body(&credential.model);
|
||||
|
||||
let response = client
|
||||
let request = client
|
||||
.post(openai_chat_completions_url(&credential.base_url))
|
||||
.bearer_auth(&credential.api_key)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(request_body.to_string())
|
||||
.body(request_body.to_string());
|
||||
let response = with_optional_bearer_auth(request, &credential.api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("Provider verification request failed: {}", err))?;
|
||||
|
|
@ -2487,11 +2510,11 @@ async fn send_openai_compatible_no_tools_text_request(
|
|||
"stream": false,
|
||||
});
|
||||
|
||||
let response = client
|
||||
let request = client
|
||||
.post(openai_chat_completions_url(&credential.base_url))
|
||||
.bearer_auth(&credential.api_key)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(request_body.to_string())
|
||||
.body(request_body.to_string());
|
||||
let response = with_optional_bearer_auth(request, &credential.api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("Provider request failed: {}", err))?;
|
||||
|
|
@ -3930,6 +3953,18 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openai_chat_completions_url_supports_ollama_roots() {
|
||||
assert_eq!(
|
||||
openai_chat_completions_url("http://localhost:11434"),
|
||||
"http://localhost:11434/v1/chat/completions"
|
||||
);
|
||||
assert_eq!(
|
||||
openai_chat_completions_url("http://localhost:11434/v1"),
|
||||
"http://localhost:11434/v1/chat/completions"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openai_models_url_matches_provider_roots() {
|
||||
assert_eq!(
|
||||
|
|
@ -3948,6 +3983,22 @@ mod tests {
|
|||
openai_models_url("https://api.openai.com"),
|
||||
"https://api.openai.com/v1/models"
|
||||
);
|
||||
assert_eq!(
|
||||
openai_models_url("http://localhost:11434"),
|
||||
"http://localhost:11434/v1/models"
|
||||
);
|
||||
assert_eq!(
|
||||
openai_models_url("http://localhost:11434/v1"),
|
||||
"http://localhost:11434/v1/models"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openai_compatible_api_key_can_be_empty_for_local_providers() {
|
||||
assert!(normalize_api_key("").is_err());
|
||||
assert_eq!(normalize_optional_api_key("").unwrap(), "");
|
||||
assert_eq!(normalize_optional_api_key(" ollama ").unwrap(), "ollama");
|
||||
assert!(normalize_optional_api_key("bad key").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { getProviderDisplayName } from "@/lib/provider-icons";
|
||||
import {
|
||||
getProviderDisplayName,
|
||||
getProviderIconSrc,
|
||||
} from "@/lib/provider-icons";
|
||||
|
||||
describe("getProviderDisplayName", () => {
|
||||
it("derives provider names from old custom labels", () => {
|
||||
|
|
@ -29,4 +32,15 @@ describe("getProviderDisplayName", () => {
|
|||
}),
|
||||
).toBe("Acme AI");
|
||||
});
|
||||
|
||||
it("recognizes local Ollama endpoints", () => {
|
||||
const provider = {
|
||||
label: "Custom OpenAI API",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
model: "llama3.2",
|
||||
};
|
||||
|
||||
expect(getProviderDisplayName(provider)).toBe("Ollama");
|
||||
expect(getProviderIconSrc(provider)).toContain("ollama");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -178,6 +178,63 @@ describe("useClaudeSetupStore.saveApiKey", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("allows local OpenAI-compatible providers without an API key", async () => {
|
||||
vi.mocked(invoke).mockImplementation(async (command) => {
|
||||
if (command === "check_claude_status") {
|
||||
return {
|
||||
installed: true,
|
||||
authenticated: true,
|
||||
binary_path: null,
|
||||
version: "OpenAI-compatible provider",
|
||||
provider_kind: "openai-compatible",
|
||||
account_email: null,
|
||||
provider_model: "llama3.2",
|
||||
provider_base_url: "http://localhost:11434/v1",
|
||||
missing_git: false,
|
||||
};
|
||||
}
|
||||
if (command === "list_openai_compatible_credentials") {
|
||||
return [
|
||||
{
|
||||
id: "ollama-cred",
|
||||
label: "Ollama",
|
||||
model: "llama3.2",
|
||||
base_url: "http://localhost:11434/v1",
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const success = await useClaudeSetupStore
|
||||
.getState()
|
||||
.saveApiKey(
|
||||
"",
|
||||
"http://localhost:11434/v1",
|
||||
"openai-compatible",
|
||||
"llama3.2",
|
||||
"Ollama",
|
||||
);
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(invoke).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"verify_openai_compatible_api_key",
|
||||
{
|
||||
apiKey: "",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
model: "llama3.2",
|
||||
},
|
||||
);
|
||||
expect(invoke).toHaveBeenNthCalledWith(2, "save_anthropic_api_key", {
|
||||
apiKey: "",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
provider: "openai-compatible",
|
||||
model: "llama3.2",
|
||||
credentialLabel: "Ollama",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not save OpenAI-compatible credentials when verification fails", async () => {
|
||||
vi.mocked(invoke).mockRejectedValueOnce(
|
||||
new Error("Invalid provider API key"),
|
||||
|
|
|
|||
1
apps/desktop/src/assets/providers/ollama.svg
Normal file
1
apps/desktop/src/assets/providers/ollama.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" style="fill:#fff"/><defs><circle id="ollama_svg__a" cx="256" cy="256" r="256"/></defs><clipPath id="ollama_svg__b"><use xlink:href="#ollama_svg__a" style="overflow:visible"/></clipPath><g style="clip-path:url(#ollama_svg__b)"><path d="M157.3 35.9c-4.3.7-9.5 3-13.1 5.7-11 8.4-19.6 26.2-23.2 48.4-1.4 8.4-2.3 20-2.3 28.9 0 10.5 1.2 23.9 3 33.1.4 2.1.6 3.9.4 4-.1.1-1.8 1.5-3.6 2.9-6.2 5-13.4 12.6-18.3 19.6-9.4 13.4-15.5 28.6-18.1 45-1 6.5-1.3 19.6-.5 26.1 1.8 15 6.3 27.7 14 39.3l2.5 3.7-.7 1.2c-5.2 8.7-9.6 21.3-11.6 33.3-1.6 9.6-1.8 12.1-1.8 24.9 0 12.9.2 15.5 1.7 24.4 1.8 10.7 5.5 22 9.7 29.5 1.4 2.5 4.7 7.6 5.1 7.8.1.1-.3 1.3-.9 2.7-4.8 10.5-8.9 24.4-10.6 36.1-1.2 8-1.4 10.6-1.4 19.1 0 10.8.6 16 2.9 24.6l.3 1.3h28.4l-.9-1.8c-5.7-10.6-6.3-30.3-1.3-50 2.3-9.1 4.8-15.8 9.6-24.9l2.9-5.6v-3.4c0-3.2-.1-3.5-1.1-5.6-.8-1.6-1.9-3-3.7-4.8-3.2-3.1-5.5-6.4-7.4-10.5-8.2-17.7-9.8-44-4-66.5 2.4-9.4 6.3-17.7 10.5-22.2 2.8-3.1 4.3-6.6 4.3-10.2 0-3.7-1.3-6.8-4.3-10.1-8.6-9.2-13.8-20.3-15.7-33.3-2.7-18.5 2.2-38.6 13.3-54.6 10.8-15.7 26.1-25.7 43.1-28.4 3.8-.6 10.9-.5 14.9.2 4.3.8 7.1.5 9.9-.8 3.5-1.6 5.2-3.6 7.2-8.3 1.8-4.1 3.2-6.4 6.9-11.1 4.5-5.6 8.9-9.4 15.8-14 8-5.2 17-9 26-10.8 3.3-.7 4.8-.8 10.9-.8s7.7.1 10.9.8c13.2 2.7 26.4 9.5 36.9 19.2 2.3 2.1 7.7 8.8 9.4 11.6.7 1.1 1.8 3.4 2.6 5.1 2 4.6 3.7 6.7 7.2 8.3 2.7 1.3 5.5 1.6 9.7.9 6.6-1.1 11.7-1 18.1.3 22 4.4 41.2 22.6 49.7 46.9 7.4 21.3 5.3 43.7-5.7 60.7-1.9 2.9-3.7 5.2-6.4 8.1-5.8 6.2-5.8 13.9 0 20.3 9.5 10.4 15.4 35.9 13.6 58.5-1.2 14.9-5 28.2-10.3 35.7-.9 1.3-2.9 3.6-4.3 5-1.9 1.9-3 3.2-3.7 4.8-1 2.1-1.1 2.5-1.1 5.6v3.4l2.9 5.6c4.8 9.2 7.3 15.9 9.6 24.9 4.9 19.4 4.4 38.7-1.1 49.7-.5.9-.9 1.8-.9 1.9s6.3.2 14.1.2h14.1l.4-1.4c.2-.8.5-1.9.7-2.6.4-1.5 1.1-5.8 1.7-9.9.6-4.2.6-19.6 0-24.2-2.1-16.9-5.7-30.2-11.5-42.9-.6-1.4-1-2.7-.9-2.7.2-.1 1.1-1.4 2.1-2.9 7.2-10.9 11.7-24.7 13.9-42.9.6-5 .6-26.5 0-31.4-1.6-12.4-3.5-20.8-6.7-29.4-1.3-3.5-4.8-11-6.3-13.5l-.7-1.2 2.5-3.7c7.7-11.6 12.2-24.3 14-39.3.8-6.5.5-19.6-.5-26.1-2.6-16.5-8.7-31.6-18.1-45-4.9-7-12-14.7-18.3-19.6-1.8-1.5-3.5-2.8-3.6-2.9-.2-.1 0-2 .4-4 4-20.9 3.9-47-.3-67.4-3.6-17.8-10.3-31.9-18.8-40.1-6.8-6.5-13.8-9.3-22.2-8.8-19.2 1.1-34.6 23.2-40.7 58-1 5.6-1.9 12.2-1.9 14 0 .7-.1 1.3-.3 1.3s-1.5-.7-2.9-1.5C288.5 98.8 272 94.1 256 94.1s-32.5 4.7-47.3 13.4c-1.4.8-2.7 1.5-2.9 1.5s-.3-.6-.3-1.3c0-1.9-.9-8.6-1.9-14-5.5-31.2-18.2-51.9-35.1-57.1-2.2-.6-8.8-1.1-11.2-.7m5.6 27c4.8 3.8 10.1 14.6 13.1 26.7.6 2.2 1.2 4.7 1.3 5.6s.5 2.9.8 4.5c1.3 7 1.9 14.6 2 23.9v9.1l-2.3 3.4-2.3 3.4h-5.3c-6.2 0-12.4.8-18.4 2.4-2.1.5-4.2 1.1-4.6 1.2-.6.1-.7-.1-1.1-2.8-2-14.8-1.9-31.1.3-44.7 2.4-15.2 8-28.9 13.4-32.9 1.4-1 1.6-1 3.1.2m189.2-.2c3.3 2.4 6.9 8.9 9.6 17.1 5.4 16.5 6.9 39 4.1 60.5-.4 2.7-.5 2.9-1.1 2.8-.4-.1-2.5-.6-4.6-1.2-5.9-1.6-12.1-2.4-18.4-2.4h-5.3l-2.3-3.4-2.3-3.4v-9.1c.1-12.9 1.3-22.9 4.1-34.1 3-12 8.4-22.8 13.1-26.6 1.6-1.2 1.8-1.2 3.1-.2"/><path d="M250.9 229.6c-7.2.7-9.2 1-12.6 1.7-5.6 1.2-13.1 3.7-18.3 6.3-18.1 8.9-30.6 23.6-34.4 40.7-.8 3.4-.9 4.5-.9 10.2 0 5.6.1 6.9.8 10.1 5.1 22.3 25.6 38.8 52.3 41.8 5.8.6 30.7.6 36.5 0 21.4-2.4 39.7-14 48-30.3 2.2-4.3 3.3-7.2 4.2-11.6.7-3.2.8-4.4.8-10.1s-.1-6.8-.9-10.2c-5.5-24.8-29.6-44.4-59.2-48.1-3.7-.3-13.8-.7-16.3-.5m12.4 18.1c9.9 1.1 19.8 4.6 27.7 9.9 4.3 2.9 10.3 8.8 12.9 12.7 3.2 4.8 5 9.8 5.8 15.8.4 2.8.2 4.8-.8 9.3-1.6 6.6-6.4 13.6-12.9 18.4-3.1 2.2-9.4 5.4-13.3 6.7-7.4 2.4-12.2 2.8-29.4 2.7-11.2-.1-13.2-.2-16.4-.8-11-2.1-19.7-6.4-26-13.1-5.1-5.4-7.4-10.3-8.7-18.2-.6-3.7.5-9.8 2.7-14.9 2.6-6.3 9.4-14.1 16.1-18.5 7.8-5.2 18-8.9 27.4-9.9 3.6-.5 11.2-.5 14.9-.1"/><path d="M243.3 271.9c-2.5 1.4-4.3 4.8-3.7 7.4.6 2.8 3 5.5 6.8 7.8 2 1.2 2.2 1.4 2.3 2.6.1.7-.2 2.8-.6 4.7-.4 1.8-.7 3.7-.7 4.3 0 1.4 1.4 3.7 2.8 4.9 1.2 1 1.5 1 4.9 1.1 3.2.1 3.8 0 5.1-.6 3.3-1.6 4.1-4.5 2.9-10.1-1-4.7-.8-5.4 1.7-6.8 2.6-1.5 5.4-4.2 6.2-6 1.6-3.5.1-7.4-3.4-9.3-.9-.4-1.9-.6-3.5-.6-2.4 0-4 .6-6.8 2.4l-1.6 1-1-.6c-4.2-2.5-5-2.8-7.5-2.8-2 0-3 .1-3.9.6m-80.5-38.5c-5.9 1.9-10.3 6.2-12.5 12.3-1.1 2.9-1.6 7.5-1.2 10 1.1 5.9 6 11.3 11.5 12.8 7 1.8 12.2.6 16.8-3.9 2.7-2.6 4.1-4.9 5.6-8.6 1.1-2.6 1.1-3.1 1.1-6.8v-4l-1.4-2.9c-2.2-4.5-6.2-7.9-10.9-9.1-2.5-.6-6.7-.6-9 .2m177.2-.1c-4.5 1.2-8.6 4.6-10.7 9.1l-1.4 2.9v4c0 3.7.1 4.2 1.1 6.8 1.5 3.7 2.9 6 5.6 8.6 4.6 4.6 9.8 5.8 16.8 3.9 4-1.1 8-4.4 10-8.4 1.7-3.4 2.1-5.8 1.5-9.6-1.2-8.7-6.3-15.1-13.9-17.3-2.3-.7-6.6-.7-9 0"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -43,6 +43,7 @@ type OpenAICompatiblePreset = {
|
|||
baseUrl: string;
|
||||
model: string;
|
||||
note: string;
|
||||
apiKeyOptional?: boolean;
|
||||
};
|
||||
|
||||
type ClaudeCompatiblePreset = {
|
||||
|
|
@ -60,6 +61,7 @@ type ModelProviderCard = {
|
|||
model: string;
|
||||
badge: string;
|
||||
note: string;
|
||||
apiKeyOptional?: boolean;
|
||||
};
|
||||
|
||||
const CLAUDE_COMPATIBLE_PRESETS: ClaudeCompatiblePreset[] = [
|
||||
|
|
@ -107,6 +109,14 @@ const OPENAI_COMPATIBLE_PRESETS: OpenAICompatiblePreset[] = [
|
|||
model: "",
|
||||
note: "Zhipu BigModel chat completions endpoint.",
|
||||
},
|
||||
{
|
||||
id: "ollama",
|
||||
label: "Ollama",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
model: "",
|
||||
note: "Local Ollama OpenAI-compatible endpoint.",
|
||||
apiKeyOptional: true,
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
label: "Gemini OpenAI",
|
||||
|
|
@ -559,6 +569,9 @@ export function ClaudeSetup({
|
|||
? providerPreset
|
||||
: fallbackCardId;
|
||||
const activeCard = providerCards.find((card) => card.id === activeCardId);
|
||||
const apiKeyOptional =
|
||||
selectedProvider === "openai-compatible" && !!activeCard?.apiKeyOptional;
|
||||
const apiKeyRequired = !apiKeyOptional;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -637,7 +650,9 @@ export function ClaudeSetup({
|
|||
type="password"
|
||||
placeholder={
|
||||
selectedProvider === "openai-compatible"
|
||||
? "sk-..."
|
||||
? apiKeyOptional
|
||||
? "Optional for local Ollama"
|
||||
: "sk-..."
|
||||
: "sk-ant-... or provider key"
|
||||
}
|
||||
value={apiKey}
|
||||
|
|
@ -651,7 +666,9 @@ export function ClaudeSetup({
|
|||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{selectedProvider === "openai-compatible"
|
||||
? "Use the API key from your model provider."
|
||||
? apiKeyOptional
|
||||
? "Ollama runs locally and normally does not require an API key."
|
||||
: "Use the API key from your model provider."
|
||||
: "Anthropic keys start with sk-ant-. Claude-compatible proxies can use their own key format."}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -714,7 +731,7 @@ export function ClaudeSetup({
|
|||
disabled={
|
||||
isSavingApiKey ||
|
||||
isFetchingModels ||
|
||||
!apiKey.trim() ||
|
||||
(apiKeyRequired && !apiKey.trim()) ||
|
||||
!baseUrl.trim()
|
||||
}
|
||||
>
|
||||
|
|
@ -779,7 +796,7 @@ export function ClaudeSetup({
|
|||
size="sm"
|
||||
className="w-full gap-2"
|
||||
disabled={
|
||||
!apiKey.trim() ||
|
||||
(apiKeyRequired && !apiKey.trim()) ||
|
||||
isSavingApiKey ||
|
||||
(selectedProvider === "openai-compatible" &&
|
||||
(!baseUrl.trim() || !model.trim()))
|
||||
|
|
@ -795,7 +812,9 @@ export function ClaudeSetup({
|
|||
? "Verifying..."
|
||||
: "Saving..."
|
||||
: selectedProvider === "openai-compatible"
|
||||
? "Verify & Use API Key"
|
||||
? apiKeyOptional
|
||||
? "Verify & Use Local Provider"
|
||||
: "Verify & Use API Key"
|
||||
: "Use API Key"}
|
||||
</Button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import anthropicIcon from "@/assets/providers/anthropic.svg";
|
|||
import deepseekIcon from "@/assets/providers/deepseek.svg";
|
||||
import geminiIcon from "@/assets/providers/gemini-color.svg";
|
||||
import moonshotIcon from "@/assets/providers/moonshot.svg";
|
||||
import ollamaIcon from "@/assets/providers/ollama.svg";
|
||||
import openaiIcon from "@/assets/providers/openai.svg";
|
||||
import qwenIcon from "@/assets/providers/qwen.svg";
|
||||
import zhipuIcon from "@/assets/providers/zhipu-color.svg";
|
||||
|
|
@ -37,6 +38,14 @@ function isGenericOpenAiLabel(label?: string | null) {
|
|||
export function getProviderDisplayName(input: ProviderIconInput): string {
|
||||
const haystack = providerHaystack(input);
|
||||
|
||||
if (
|
||||
haystack.includes("ollama") ||
|
||||
haystack.includes("localhost:11434") ||
|
||||
haystack.includes("127.0.0.1:11434")
|
||||
) {
|
||||
return "Ollama";
|
||||
}
|
||||
|
||||
if (
|
||||
haystack.includes("qwen") ||
|
||||
haystack.includes("dashscope") ||
|
||||
|
|
@ -96,6 +105,14 @@ export function getProviderDisplayName(input: ProviderIconInput): string {
|
|||
export function getProviderIconSrc(input: ProviderIconInput): string | null {
|
||||
const haystack = providerHaystack(input);
|
||||
|
||||
if (
|
||||
haystack.includes("ollama") ||
|
||||
haystack.includes("localhost:11434") ||
|
||||
haystack.includes("127.0.0.1:11434")
|
||||
) {
|
||||
return ollamaIcon;
|
||||
}
|
||||
|
||||
if (
|
||||
haystack.includes("qwen") ||
|
||||
haystack.includes("dashscope") ||
|
||||
|
|
|
|||
|
|
@ -294,12 +294,12 @@ export const useClaudeSetupStore = create<ClaudeSetupState>((set, get) => ({
|
|||
const key = apiKey.trim();
|
||||
const url = baseUrl?.trim() ?? "";
|
||||
const modelName = model?.trim() ?? "";
|
||||
if (!key) {
|
||||
if (provider !== "openai-compatible" && !key) {
|
||||
set({ error: "API key is empty" });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/\s/.test(key)) {
|
||||
if (key && /\s/.test(key)) {
|
||||
set({ error: "API key cannot contain spaces or line breaks" });
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue