From 5e944ba3e7c1a477082c90f0a10b5d34595c9453 Mon Sep 17 00:00:00 2001 From: "kaijian.ding" Date: Sat, 30 May 2026 01:04:08 +0800 Subject: [PATCH] feat(provider): support ANTHROPIC_BASE_URL for custom endpoint override Allows routing Anthropic API requests through a local proxy by setting ANTHROPIC_BASE_URL (e.g. http://127.0.0.1:3456/v1). When set, the messages endpoint is derived from the env var instead of the hardcoded api.anthropic.com URL. OAuth vs non-OAuth path selection is preserved. Co-Authored-By: Claude Sonnet 4.6 --- crates/jcode-base/src/provider/anthropic.rs | 14 +++++++++++- .../src/provider/anthropic_tests.rs | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/jcode-base/src/provider/anthropic.rs b/crates/jcode-base/src/provider/anthropic.rs index 3ec394fcb..15af1c58d 100644 --- a/crates/jcode-base/src/provider/anthropic.rs +++ b/crates/jcode-base/src/provider/anthropic.rs @@ -44,6 +44,18 @@ const API_URL: &str = "https://api.anthropic.com/v1/messages"; /// OAuth endpoint (with beta=true query param) const API_URL_OAUTH: &str = "https://api.anthropic.com/v1/messages?beta=true"; +/// Returns the messages endpoint URL, respecting ANTHROPIC_BASE_URL if set. +fn messages_url(is_oauth: bool) -> String { + if let Ok(base) = std::env::var("ANTHROPIC_BASE_URL") { + let base = base.trim_end_matches('/'); + format!("{}/messages", base) + } else if is_oauth { + API_URL_OAUTH.to_string() + } else { + API_URL.to_string() + } +} + /// User-Agent for OAuth requests, matching the official Claude Code CLI. pub(crate) const CLAUDE_CLI_USER_AGENT: &str = "claude-cli/2.1.123 (external, sdk-cli)"; @@ -1769,7 +1781,7 @@ async fn stream_response( let connect_start = std::time::Instant::now(); // Build request with appropriate auth headers - let url = if is_oauth { API_URL_OAUTH } else { API_URL }; + let url = messages_url(is_oauth); let mut req = client .post(url) diff --git a/crates/jcode-base/src/provider/anthropic_tests.rs b/crates/jcode-base/src/provider/anthropic_tests.rs index 7f20be96f..8c05f37f0 100644 --- a/crates/jcode-base/src/provider/anthropic_tests.rs +++ b/crates/jcode-base/src/provider/anthropic_tests.rs @@ -1177,6 +1177,28 @@ async fn test_sanitize_dangling_tool_ids_with_dots() { } } +#[test] +fn test_messages_url_default() { + crate::env::remove_var("ANTHROPIC_BASE_URL"); + assert_eq!(messages_url(false), API_URL); + assert_eq!(messages_url(true), API_URL_OAUTH); +} + +#[test] +fn test_messages_url_base_url_override() { + crate::env::set_var("ANTHROPIC_BASE_URL", "http://127.0.0.1:3456/v1"); + let _guard = EnvVarGuard { key: "ANTHROPIC_BASE_URL", previous: None }; + assert_eq!(messages_url(false), "http://127.0.0.1:3456/v1/messages"); + assert_eq!(messages_url(true), "http://127.0.0.1:3456/v1/messages"); +} + +#[test] +fn test_messages_url_base_url_trailing_slash() { + crate::env::set_var("ANTHROPIC_BASE_URL", "http://127.0.0.1:3456/v1/"); + let _guard = EnvVarGuard { key: "ANTHROPIC_BASE_URL", previous: None }; + assert_eq!(messages_url(false), "http://127.0.0.1:3456/v1/messages"); +} + /// The runtime-provider identity that `set_credential_mode` writes must decode /// back to the exact same credential mode. This guards the model picker / header /// widget from reporting OAuth when an API key is in use (or vice versa): the