feat(setup): separate env namespace for custom OpenAI-compat provider
The Custom (OpenAI-compat) /setup option was saving kind: openai and injecting OPENAI_API_KEY / OPENAI_BASE_URL. That collides with users who have real OpenAI/NeuralWatt credentials in their environment. Introduce a dedicated custom-openai provider kind that uses its own environment variables: - CLAWCUSTOMOPENAI_API_KEY - CLAWCUSTOMOPENAI_BASE_URL A new custom/ routing prefix selects the OpenAI-compatible client with those env vars and is stripped on the wire, so the proxy receives the bare model id. /setup now saves kind: custom-openai and prompts for the new env vars. Bare model names saved by /setup are normalized to custom/<model>. Manual verification against http://100.96.49.42:4001/v1 succeeds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
parent
3dde9be4ce
commit
b08c32d760
|
|
@ -47,6 +47,9 @@ impl ProviderClient {
|
||||||
Some(meta) if meta.auth_env == "DASHSCOPE_API_KEY" => {
|
Some(meta) if meta.auth_env == "DASHSCOPE_API_KEY" => {
|
||||||
OpenAiCompatConfig::dashscope()
|
OpenAiCompatConfig::dashscope()
|
||||||
}
|
}
|
||||||
|
Some(meta) if meta.auth_env == "CLAWCUSTOMOPENAI_API_KEY" => {
|
||||||
|
OpenAiCompatConfig::custom_openai()
|
||||||
|
}
|
||||||
_ => OpenAiCompatConfig::openai(),
|
_ => OpenAiCompatConfig::openai(),
|
||||||
};
|
};
|
||||||
Ok(Self::OpenAi(OpenAiCompatClient::from_env(config)?))
|
Ok(Self::OpenAi(OpenAiCompatClient::from_env(config)?))
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,14 @@ pub fn metadata_for_model(model: &str) -> Option<ProviderMetadata> {
|
||||||
default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
|
default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if canonical.starts_with("custom/") {
|
||||||
|
return Some(ProviderMetadata {
|
||||||
|
provider: ProviderKind::OpenAi,
|
||||||
|
auth_env: "CLAWCUSTOMOPENAI_API_KEY",
|
||||||
|
base_url_env: "CLAWCUSTOMOPENAI_BASE_URL",
|
||||||
|
default_base_url: openai_compat::DEFAULT_CUSTOM_OPENAI_BASE_URL,
|
||||||
|
});
|
||||||
|
}
|
||||||
// Alibaba DashScope compatible-mode endpoint. Routes qwen/* and bare
|
// Alibaba DashScope compatible-mode endpoint. Routes qwen/* and bare
|
||||||
// qwen-* model names (qwen-max, qwen-plus, qwen-turbo, qwen-qwq, etc.)
|
// qwen-* model names (qwen-max, qwen-plus, qwen-turbo, qwen-qwq, etc.)
|
||||||
// to the OpenAI-compat client pointed at DashScope's /compatible-mode/v1.
|
// to the OpenAI-compat client pointed at DashScope's /compatible-mode/v1.
|
||||||
|
|
@ -377,6 +385,11 @@ pub fn detect_provider_kind(model: &str) -> ProviderKind {
|
||||||
{
|
{
|
||||||
return ProviderKind::OpenAi;
|
return ProviderKind::OpenAi;
|
||||||
}
|
}
|
||||||
|
// Explicit `custom/` prefix selects the Claw custom OpenAI-compat provider
|
||||||
|
// even when no other credentials are present.
|
||||||
|
if resolved_model.starts_with("custom/") {
|
||||||
|
return ProviderKind::OpenAi;
|
||||||
|
}
|
||||||
if anthropic::has_auth_from_env_or_saved().unwrap_or(false) {
|
if anthropic::has_auth_from_env_or_saved().unwrap_or(false) {
|
||||||
return ProviderKind::Anthropic;
|
return ProviderKind::Anthropic;
|
||||||
}
|
}
|
||||||
|
|
@ -1138,6 +1151,21 @@ mod tests {
|
||||||
assert_eq!(super::resolve_model_alias("KIMI"), "kimi-k2.5"); // case insensitive
|
assert_eq!(super::resolve_model_alias("KIMI"), "kimi-k2.5"); // case insensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_prefix_routes_to_custom_openai_env_vars() {
|
||||||
|
let meta = super::metadata_for_model("custom/openclaw_3750")
|
||||||
|
.expect("custom/ prefix must resolve to custom OpenAI metadata");
|
||||||
|
assert_eq!(meta.provider, ProviderKind::OpenAi);
|
||||||
|
assert_eq!(meta.auth_env, "CLAWCUSTOMOPENAI_API_KEY");
|
||||||
|
assert_eq!(meta.base_url_env, "CLAWCUSTOMOPENAI_BASE_URL");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
detect_provider_kind("custom/openclaw_3750"),
|
||||||
|
ProviderKind::OpenAi,
|
||||||
|
"custom/ prefix must select OpenAi provider kind"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn provider_diagnostics_explain_openai_compatible_capabilities() {
|
fn provider_diagnostics_explain_openai_compatible_capabilities() {
|
||||||
let diagnostics = super::provider_diagnostics_for_model("openai/deepseek-v4-pro");
|
let diagnostics = super::provider_diagnostics_for_model("openai/deepseek-v4-pro");
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ use super::{preflight_message_request, resolve_model_alias, Provider, ProviderFu
|
||||||
pub const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";
|
pub const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";
|
||||||
pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
|
pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
|
||||||
pub const DEFAULT_DASHSCOPE_BASE_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
pub const DEFAULT_DASHSCOPE_BASE_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
||||||
|
/// Default base URL for Claw's custom OpenAI-compatible provider.
|
||||||
|
/// Intentionally left empty: a custom endpoint must set
|
||||||
|
/// `CLAWCUSTOMOPENAI_BASE_URL`; otherwise requests will fail at URL build
|
||||||
|
/// time rather than leaking credentials to the real OpenAI endpoint.
|
||||||
|
pub const DEFAULT_CUSTOM_OPENAI_BASE_URL: &str = "";
|
||||||
const REQUEST_ID_HEADER: &str = "request-id";
|
const REQUEST_ID_HEADER: &str = "request-id";
|
||||||
const ALT_REQUEST_ID_HEADER: &str = "x-request-id";
|
const ALT_REQUEST_ID_HEADER: &str = "x-request-id";
|
||||||
const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_secs(1);
|
const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_secs(1);
|
||||||
|
|
@ -43,6 +48,7 @@ pub struct OpenAiCompatConfig {
|
||||||
const XAI_ENV_VARS: &[&str] = &["XAI_API_KEY"];
|
const XAI_ENV_VARS: &[&str] = &["XAI_API_KEY"];
|
||||||
const OPENAI_ENV_VARS: &[&str] = &["OPENAI_API_KEY"];
|
const OPENAI_ENV_VARS: &[&str] = &["OPENAI_API_KEY"];
|
||||||
const DASHSCOPE_ENV_VARS: &[&str] = &["DASHSCOPE_API_KEY"];
|
const DASHSCOPE_ENV_VARS: &[&str] = &["DASHSCOPE_API_KEY"];
|
||||||
|
const CUSTOM_OPENAI_ENV_VARS: &[&str] = &["CLAWCUSTOMOPENAI_API_KEY"];
|
||||||
|
|
||||||
// Provider-specific request body size limits in bytes
|
// Provider-specific request body size limits in bytes
|
||||||
const XAI_MAX_REQUEST_BODY_BYTES: usize = 52_428_800; // 50MB
|
const XAI_MAX_REQUEST_BODY_BYTES: usize = 52_428_800; // 50MB
|
||||||
|
|
@ -95,12 +101,27 @@ impl OpenAiCompatConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Claw-specific custom OpenAI-compatible endpoint.
|
||||||
|
/// Reads `CLAWCUSTOMOPENAI_API_KEY` / `CLAWCUSTOMOPENAI_BASE_URL` so it
|
||||||
|
/// can coexist with real OpenAI/NeuralWatt `OPENAI_*` environment vars.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn custom_openai() -> Self {
|
||||||
|
Self {
|
||||||
|
provider_name: "Custom OpenAI",
|
||||||
|
api_key_env: "CLAWCUSTOMOPENAI_API_KEY",
|
||||||
|
base_url_env: "CLAWCUSTOMOPENAI_BASE_URL",
|
||||||
|
default_base_url: DEFAULT_CUSTOM_OPENAI_BASE_URL,
|
||||||
|
max_request_body_bytes: OPENAI_MAX_REQUEST_BODY_BYTES,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn credential_env_vars(self) -> &'static [&'static str] {
|
pub fn credential_env_vars(self) -> &'static [&'static str] {
|
||||||
match self.provider_name {
|
match self.provider_name {
|
||||||
"xAI" => XAI_ENV_VARS,
|
"xAI" => XAI_ENV_VARS,
|
||||||
"OpenAI" => OPENAI_ENV_VARS,
|
"OpenAI" => OPENAI_ENV_VARS,
|
||||||
"DashScope" => DASHSCOPE_ENV_VARS,
|
"DashScope" => DASHSCOPE_ENV_VARS,
|
||||||
|
"Custom OpenAI" => CUSTOM_OPENAI_ENV_VARS,
|
||||||
_ => &[],
|
_ => &[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1066,7 +1087,7 @@ fn wire_model_for_base_url<'a>(
|
||||||
if matches!(lowered_prefix.as_str(), "xai" | "grok" | "qwen" | "kimi") {
|
if matches!(lowered_prefix.as_str(), "xai" | "grok" | "qwen" | "kimi") {
|
||||||
return Cow::Borrowed(&model[pos + 1..]);
|
return Cow::Borrowed(&model[pos + 1..]);
|
||||||
}
|
}
|
||||||
if lowered_prefix == "local" {
|
if matches!(lowered_prefix.as_str(), "local" | "custom") {
|
||||||
return Cow::Borrowed(&model[pos + 1..]);
|
return Cow::Borrowed(&model[pos + 1..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2278,6 +2299,29 @@ mod tests {
|
||||||
assert_eq!(parse_tool_arguments("not-json"), json!({"raw": "not-json"}));
|
assert_eq!(parse_tool_arguments("not-json"), json!({"raw": "not-json"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_routing_prefix_strips_on_wire() {
|
||||||
|
let payload = build_chat_completion_request(
|
||||||
|
&MessageRequest {
|
||||||
|
model: "custom/openclaw_3750".to_string(),
|
||||||
|
max_tokens: 64,
|
||||||
|
messages: vec![InputMessage::user_text("hello")],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
OpenAiCompatConfig::custom_openai(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(payload["model"], json!("openclaw_3750"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_openai_config_uses_separate_env_vars() {
|
||||||
|
let config = OpenAiCompatConfig::custom_openai();
|
||||||
|
assert_eq!(config.provider_name, "Custom OpenAI");
|
||||||
|
assert_eq!(config.api_key_env, "CLAWCUSTOMOPENAI_API_KEY");
|
||||||
|
assert_eq!(config.base_url_env, "CLAWCUSTOMOPENAI_BASE_URL");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_xai_api_key_is_provider_specific() {
|
fn missing_xai_api_key_is_provider_specific() {
|
||||||
let _lock = env_lock();
|
let _lock = env_lock();
|
||||||
|
|
|
||||||
|
|
@ -3133,26 +3133,21 @@ fn config_model_for_current_dir() -> Option<String> {
|
||||||
let model = config.model()?;
|
let model = config.model()?;
|
||||||
|
|
||||||
// If the user configured a custom OpenAI-compatible endpoint with a bare
|
// If the user configured a custom OpenAI-compatible endpoint with a bare
|
||||||
// model name (e.g. "openclaw"), route it through the local/ prefix so it
|
// model name (e.g. "openclaw"), route it through the custom/ prefix so it
|
||||||
// passes validation, maps to the OpenAI-compat client, and gets stripped
|
// passes validation, maps to the custom OpenAI-compat client, and gets
|
||||||
// down to the bare model id on the wire (avoiding a proxy 404).
|
// stripped down to the bare model id on the wire (avoiding a proxy 404).
|
||||||
if config_has_custom_openai_base_url(&config) && !model.contains('/') {
|
if is_custom_openai_provider(&config) && !model.contains('/') {
|
||||||
return Some(format!("local/{model}"));
|
return Some(format!("custom/{model}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(model.to_owned())
|
Some(model.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether the loaded config has a custom OpenAI-compatible base URL.
|
/// Check whether the loaded config uses the Claw custom OpenAI-compatible
|
||||||
fn config_has_custom_openai_base_url(config: &runtime::RuntimeConfig) -> bool {
|
/// provider, which has its own env var namespace so it can coexist with real
|
||||||
let provider = config.provider();
|
/// OpenAI/NeuralWatt credentials.
|
||||||
if provider.kind() != Some("openai") && provider.kind() != Some("openai-compat") {
|
fn is_custom_openai_provider(config: &runtime::RuntimeConfig) -> bool {
|
||||||
return false;
|
config.provider().kind() == Some("custom-openai")
|
||||||
}
|
|
||||||
let Some(base_url) = provider.base_url() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
!base_url.is_empty() && base_url != "https://api.openai.com/v1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_repl_model(cli_model: String) -> Result<String, String> {
|
fn resolve_repl_model(cli_model: String) -> Result<String, String> {
|
||||||
|
|
@ -13078,6 +13073,7 @@ fn inject_config_as_env_fallbacks() {
|
||||||
"xai" => ("XAI_API_KEY", "XAI_BASE_URL"),
|
"xai" => ("XAI_API_KEY", "XAI_BASE_URL"),
|
||||||
"openai" => ("OPENAI_API_KEY", "OPENAI_BASE_URL"),
|
"openai" => ("OPENAI_API_KEY", "OPENAI_BASE_URL"),
|
||||||
"dashscope" => ("DASHSCOPE_API_KEY", "DASHSCOPE_BASE_URL"),
|
"dashscope" => ("DASHSCOPE_API_KEY", "DASHSCOPE_BASE_URL"),
|
||||||
|
"custom-openai" => ("CLAWCUSTOMOPENAI_API_KEY", "CLAWCUSTOMOPENAI_BASE_URL"),
|
||||||
_ => return, // unknown provider kind — don't inject
|
_ => return, // unknown provider kind — don't inject
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -15117,7 +15113,42 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_model_normalizes_bare_name_to_local_prefix_for_custom_openai_endpoint() {
|
fn config_model_normalizes_bare_name_to_custom_prefix_for_custom_openai_provider() {
|
||||||
|
let _guard = env_lock();
|
||||||
|
let root = temp_dir();
|
||||||
|
let cwd = root.join("project");
|
||||||
|
let config_home = root.join("config-home");
|
||||||
|
std::fs::create_dir_all(&cwd).expect("project dir should exist");
|
||||||
|
std::fs::create_dir_all(&config_home).expect("config home should exist");
|
||||||
|
std::fs::write(
|
||||||
|
config_home.join("settings.json"),
|
||||||
|
r#"{
|
||||||
|
"provider": {
|
||||||
|
"kind": "custom-openai",
|
||||||
|
"apiKey": "sk-test",
|
||||||
|
"baseUrl": "http://localhost:9999/v1"
|
||||||
|
},
|
||||||
|
"model": "openclaw"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("user settings should write");
|
||||||
|
|
||||||
|
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||||
|
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||||
|
|
||||||
|
let resolved = with_current_dir(&cwd, super::config_model_for_current_dir);
|
||||||
|
|
||||||
|
match original_config_home {
|
||||||
|
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||||
|
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||||
|
}
|
||||||
|
std::fs::remove_dir_all(root).expect("temp config root should clean up");
|
||||||
|
|
||||||
|
assert_eq!(resolved, Some("custom/openclaw".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_model_leaves_openai_kind_bare_model_unnormalized() {
|
||||||
let _guard = env_lock();
|
let _guard = env_lock();
|
||||||
let root = temp_dir();
|
let root = temp_dir();
|
||||||
let cwd = root.join("project");
|
let cwd = root.join("project");
|
||||||
|
|
@ -15138,9 +15169,7 @@ mod tests {
|
||||||
.expect("user settings should write");
|
.expect("user settings should write");
|
||||||
|
|
||||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||||
let original_base_url = std::env::var("OPENAI_BASE_URL").ok();
|
|
||||||
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||||
std::env::remove_var("OPENAI_BASE_URL");
|
|
||||||
|
|
||||||
let resolved = with_current_dir(&cwd, super::config_model_for_current_dir);
|
let resolved = with_current_dir(&cwd, super::config_model_for_current_dir);
|
||||||
|
|
||||||
|
|
@ -15148,13 +15177,9 @@ mod tests {
|
||||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||||
}
|
}
|
||||||
match original_base_url {
|
|
||||||
Some(value) => std::env::set_var("OPENAI_BASE_URL", value),
|
|
||||||
None => std::env::remove_var("OPENAI_BASE_URL"),
|
|
||||||
}
|
|
||||||
std::fs::remove_dir_all(root).expect("temp config root should clean up");
|
std::fs::remove_dir_all(root).expect("temp config root should clean up");
|
||||||
|
|
||||||
assert_eq!(resolved, Some("local/openclaw".to_string()));
|
assert_eq!(resolved, Some("openclaw".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -15208,6 +15233,57 @@ mod tests {
|
||||||
assert_eq!(base_url, Some("http://localhost:9999/v1".to_string()));
|
assert_eq!(base_url, Some("http://localhost:9999/v1".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inject_config_as_env_fallbacks_sets_custom_openai_provider_env_vars() {
|
||||||
|
let _guard = env_lock();
|
||||||
|
let root = temp_dir();
|
||||||
|
let cwd = root.join("project");
|
||||||
|
let config_home = root.join("config-home");
|
||||||
|
std::fs::create_dir_all(&cwd).expect("project dir should exist");
|
||||||
|
std::fs::create_dir_all(&config_home).expect("config home should exist");
|
||||||
|
std::fs::write(
|
||||||
|
config_home.join("settings.json"),
|
||||||
|
r#"{
|
||||||
|
"provider": {
|
||||||
|
"kind": "custom-openai",
|
||||||
|
"apiKey": "sk-from-config",
|
||||||
|
"baseUrl": "http://localhost:9999/v1"
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("user settings should write");
|
||||||
|
|
||||||
|
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||||
|
let original_api_key = std::env::var("CLAWCUSTOMOPENAI_API_KEY").ok();
|
||||||
|
let original_base_url = std::env::var("CLAWCUSTOMOPENAI_BASE_URL").ok();
|
||||||
|
|
||||||
|
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||||
|
std::env::remove_var("CLAWCUSTOMOPENAI_API_KEY");
|
||||||
|
std::env::remove_var("CLAWCUSTOMOPENAI_BASE_URL");
|
||||||
|
|
||||||
|
with_current_dir(&cwd, super::inject_config_as_env_fallbacks);
|
||||||
|
|
||||||
|
let api_key = std::env::var("CLAWCUSTOMOPENAI_API_KEY").ok();
|
||||||
|
let base_url = std::env::var("CLAWCUSTOMOPENAI_BASE_URL").ok();
|
||||||
|
|
||||||
|
match original_config_home {
|
||||||
|
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||||
|
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||||
|
}
|
||||||
|
match original_api_key {
|
||||||
|
Some(value) => std::env::set_var("CLAWCUSTOMOPENAI_API_KEY", value),
|
||||||
|
None => std::env::remove_var("CLAWCUSTOMOPENAI_API_KEY"),
|
||||||
|
}
|
||||||
|
match original_base_url {
|
||||||
|
Some(value) => std::env::set_var("CLAWCUSTOMOPENAI_BASE_URL", value),
|
||||||
|
None => std::env::remove_var("CLAWCUSTOMOPENAI_BASE_URL"),
|
||||||
|
}
|
||||||
|
std::fs::remove_dir_all(root).expect("temp config root should clean up");
|
||||||
|
|
||||||
|
assert_eq!(api_key, Some("sk-from-config".to_string()));
|
||||||
|
assert_eq!(base_url, Some("http://localhost:9999/v1".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_permission_mode_uses_project_config_when_env_is_unset() {
|
fn default_permission_mode_uses_project_config_when_env_is_unset() {
|
||||||
let _guard = env_lock();
|
let _guard = env_lock();
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const PROVIDERS: &[(&str, &str, &str)] = &[
|
||||||
("2", "xAI / Grok", "xai"),
|
("2", "xAI / Grok", "xai"),
|
||||||
("3", "OpenAI", "openai"),
|
("3", "OpenAI", "openai"),
|
||||||
("4", "DashScope (Qwen/Kimi)", "dashscope"),
|
("4", "DashScope (Qwen/Kimi)", "dashscope"),
|
||||||
("5", "Custom (OpenAI-compat)", "openai"),
|
("5", "Custom (OpenAI-compat)", "custom-openai"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDER_MODELS: &[(&str, &[&str])] = &[
|
const PROVIDER_MODELS: &[(&str, &[&str])] = &[
|
||||||
|
|
@ -27,6 +27,8 @@ const DEFAULT_BASE_URLS: &[(&str, &str)] = &[
|
||||||
"dashscope",
|
"dashscope",
|
||||||
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
),
|
),
|
||||||
|
// Custom OpenAI has no default base URL; the user must supply one.
|
||||||
|
("custom-openai", ""),
|
||||||
];
|
];
|
||||||
|
|
||||||
const API_KEY_ENV_VARS: &[(&str, &str)] = &[
|
const API_KEY_ENV_VARS: &[(&str, &str)] = &[
|
||||||
|
|
@ -34,6 +36,7 @@ const API_KEY_ENV_VARS: &[(&str, &str)] = &[
|
||||||
("xai", "XAI_API_KEY"),
|
("xai", "XAI_API_KEY"),
|
||||||
("openai", "OPENAI_API_KEY"),
|
("openai", "OPENAI_API_KEY"),
|
||||||
("dashscope", "DASHSCOPE_API_KEY"),
|
("dashscope", "DASHSCOPE_API_KEY"),
|
||||||
|
("custom-openai", "CLAWCUSTOMOPENAI_API_KEY"),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn run_setup_wizard() -> Result<(), Box<dyn std::error::Error>> {
|
pub fn run_setup_wizard() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
@ -177,6 +180,7 @@ fn prompt_base_url(
|
||||||
"xai" => "XAI_BASE_URL",
|
"xai" => "XAI_BASE_URL",
|
||||||
"openai" => "OPENAI_BASE_URL",
|
"openai" => "OPENAI_BASE_URL",
|
||||||
"dashscope" => "DASHSCOPE_BASE_URL",
|
"dashscope" => "DASHSCOPE_BASE_URL",
|
||||||
|
"custom-openai" => "CLAWCUSTOMOPENAI_BASE_URL",
|
||||||
_ => "BASE_URL",
|
_ => "BASE_URL",
|
||||||
};
|
};
|
||||||
let env_set = std::env::var(env_var).ok().is_some_and(|v| !v.is_empty());
|
let env_set = std::env::var(env_var).ok().is_some_and(|v| !v.is_empty());
|
||||||
|
|
@ -214,7 +218,11 @@ fn prompt_model(
|
||||||
if !aliases.is_empty() {
|
if !aliases.is_empty() {
|
||||||
println!(" Common: {}", aliases.join(", "));
|
println!(" Common: {}", aliases.join(", "));
|
||||||
}
|
}
|
||||||
println!(" Or enter any model name (e.g. openai/gpt-4.1-mini for custom routing)");
|
if kind == "custom-openai" {
|
||||||
|
println!(" Or enter any model name (e.g. custom/gpt-4.1 for custom routing)");
|
||||||
|
} else {
|
||||||
|
println!(" Or enter any model name (e.g. openai/gpt-4.1-mini for custom routing)");
|
||||||
|
}
|
||||||
|
|
||||||
let input = read_line(&format!(" Model [{current_model}]: "))?;
|
let input = read_line(&format!(" Model [{current_model}]: "))?;
|
||||||
if input.trim().is_empty() {
|
if input.trim().is_empty() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue