mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-04 11:36:44 +00:00
fix: normalize Anthropic model routing
This commit is contained in:
@@ -468,8 +468,7 @@ impl AnthropicClient {
|
||||
request: &MessageRequest,
|
||||
) -> Result<reqwest::Response, ApiError> {
|
||||
let request_url = format!("{}/v1/messages", self.base_url.trim_end_matches('/'));
|
||||
let mut request_body = self.request_profile.render_json_body(request)?;
|
||||
strip_unsupported_beta_body_fields(&mut request_body);
|
||||
let request_body = render_standard_messages_body(&self.request_profile, request)?;
|
||||
let request_builder = self.build_request(&request_url).json(&request_body);
|
||||
request_builder.send().await.map_err(ApiError::from)
|
||||
}
|
||||
@@ -529,8 +528,7 @@ impl AnthropicClient {
|
||||
"{}/v1/messages/count_tokens",
|
||||
self.base_url.trim_end_matches('/')
|
||||
);
|
||||
let mut request_body = self.request_profile.render_json_body(request)?;
|
||||
strip_unsupported_beta_body_fields(&mut request_body);
|
||||
let request_body = render_standard_messages_body(&self.request_profile, request)?;
|
||||
let response = self
|
||||
.build_request(&request_url)
|
||||
.json(&request_body)
|
||||
@@ -977,6 +975,21 @@ fn enrich_bearer_auth_error(error: ApiError, auth: &AuthSource) -> ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
fn anthropic_wire_model(model: &str) -> &str {
|
||||
model.strip_prefix("anthropic/").unwrap_or(model)
|
||||
}
|
||||
|
||||
fn render_standard_messages_body(
|
||||
request_profile: &AnthropicRequestProfile,
|
||||
request: &MessageRequest,
|
||||
) -> Result<Value, serde_json::Error> {
|
||||
let mut wire_request = request.clone();
|
||||
wire_request.model = anthropic_wire_model(&request.model).to_string();
|
||||
let mut body = request_profile.render_json_body(&wire_request)?;
|
||||
strip_unsupported_beta_body_fields(&mut body);
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Remove beta-only body fields that the standard `/v1/messages` and
|
||||
/// `/v1/messages/count_tokens` endpoints reject as `Extra inputs are not
|
||||
/// permitted`. The `betas` opt-in is communicated via the `anthropic-beta`
|
||||
@@ -1550,6 +1563,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_messages_body_strips_anthropic_routing_prefix() {
|
||||
let client = AnthropicClient::new("test-key");
|
||||
let request = MessageRequest {
|
||||
model: "anthropic/claude-opus-4-6".to_string(),
|
||||
max_tokens: 64,
|
||||
messages: vec![],
|
||||
system: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
stream: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let rendered = super::render_standard_messages_body(client.request_profile(), &request)
|
||||
.expect("body should render");
|
||||
|
||||
assert_eq!(rendered["model"], serde_json::json!("claude-opus-4-6"));
|
||||
assert!(rendered.get("betas").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_bearer_auth_error_appends_sk_ant_hint_on_401_with_pure_bearer_token() {
|
||||
// given
|
||||
|
||||
@@ -103,6 +103,58 @@ async fn send_message_posts_json_and_parses_response() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_message_strips_anthropic_routing_prefix_on_wire() {
|
||||
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
|
||||
let server = spawn_server(
|
||||
state.clone(),
|
||||
vec![
|
||||
http_response("200 OK", "application/json", "{\"input_tokens\":1}"),
|
||||
http_response(
|
||||
"200 OK",
|
||||
"application/json",
|
||||
concat!(
|
||||
"{",
|
||||
"\"id\":\"msg_prefixed\",",
|
||||
"\"type\":\"message\",",
|
||||
"\"role\":\"assistant\",",
|
||||
"\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],",
|
||||
"\"model\":\"claude-opus-4-6\",",
|
||||
"\"stop_reason\":\"end_turn\",",
|
||||
"\"stop_sequence\":null,",
|
||||
"\"usage\":{\"input_tokens\":1,\"output_tokens\":1}",
|
||||
"}"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = AnthropicClient::new("test-key").with_base_url(server.base_url());
|
||||
client
|
||||
.send_message(&MessageRequest {
|
||||
model: "anthropic/claude-opus-4-6".to_string(),
|
||||
..sample_request(false)
|
||||
})
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
let captured = state.lock().await;
|
||||
assert_eq!(
|
||||
captured.len(),
|
||||
2,
|
||||
"count_tokens and messages requests should be captured"
|
||||
);
|
||||
let count_tokens_body: serde_json::Value =
|
||||
serde_json::from_str(&captured[0].body).expect("count_tokens body should be json");
|
||||
let messages_body: serde_json::Value =
|
||||
serde_json::from_str(&captured[1].body).expect("request body should be json");
|
||||
assert_eq!(captured[0].path, "/v1/messages/count_tokens");
|
||||
assert_eq!(captured[1].path, "/v1/messages");
|
||||
assert_eq!(count_tokens_body["model"], json!("claude-opus-4-6"));
|
||||
assert_eq!(messages_body["model"], json!("claude-opus-4-6"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_message_blocks_oversized_requests_before_the_http_call() {
|
||||
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
|
||||
|
||||
@@ -2068,6 +2068,9 @@ fn validate_model_syntax(model: &str) -> Result<(), String> {
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
if is_bare_provider_model(trimmed) {
|
||||
return Ok(());
|
||||
}
|
||||
// Check provider/model format: provider_id/model_id
|
||||
let parts: Vec<&str> = trimmed.split('/').collect();
|
||||
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
|
||||
@@ -2094,6 +2097,10 @@ fn validate_model_syntax(model: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_bare_provider_model(model: &str) -> bool {
|
||||
model.starts_with("claude-") || model.starts_with("gpt-")
|
||||
}
|
||||
|
||||
fn config_alias_for_current_dir(alias: &str) -> Option<String> {
|
||||
if alias.is_empty() {
|
||||
return None;
|
||||
@@ -12449,6 +12456,12 @@ mod tests {
|
||||
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_model_alias_uses_anthropic_routing_prefix() {
|
||||
assert_eq!(DEFAULT_MODEL, "anthropic/claude-opus-4-6");
|
||||
assert_eq!(resolve_model_alias("opus"), "anthropic/claude-opus-4-6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_defined_aliases_resolve_before_provider_dispatch() {
|
||||
// given
|
||||
@@ -12956,6 +12969,19 @@ mod tests {
|
||||
}
|
||||
other => panic!("expected CliAction::Status, got: {other:?}"),
|
||||
}
|
||||
match parse_args(&["--model=claude-opus-4-6".to_string(), "status".to_string()])
|
||||
.expect("bare Anthropic model should parse")
|
||||
{
|
||||
CliAction::Status {
|
||||
model,
|
||||
model_flag_raw,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(model, "claude-opus-4-6");
|
||||
assert_eq!(model_flag_raw.as_deref(), Some("claude-opus-4-6"));
|
||||
}
|
||||
other => panic!("expected CliAction::Status, got: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -13481,22 +13507,19 @@ mod tests {
|
||||
!err_other.contains("--output-format json"),
|
||||
"unrelated args should not trigger --json hint: {err_other}"
|
||||
);
|
||||
// #154: model syntax error should hint at provider prefix when applicable
|
||||
let err_gpt = parse_args(&[
|
||||
// #424: bare canonical GPT model ids should parse and route via provider
|
||||
// detection instead of forcing the local-only `openai/` routing prefix.
|
||||
match parse_args(&[
|
||||
"prompt".to_string(),
|
||||
"test".to_string(),
|
||||
"--model".to_string(),
|
||||
"gpt-4".to_string(),
|
||||
])
|
||||
.expect_err("`--model gpt-4` should fail with OpenAI hint");
|
||||
assert!(
|
||||
err_gpt.contains("Did you mean `openai/gpt-4`?"),
|
||||
"GPT model error should hint openai/ prefix: {err_gpt}"
|
||||
);
|
||||
assert!(
|
||||
err_gpt.contains("OPENAI_API_KEY"),
|
||||
"GPT model error should mention env var: {err_gpt}"
|
||||
);
|
||||
.expect("`--model gpt-4` should parse as a bare OpenAI model")
|
||||
{
|
||||
CliAction::Prompt { model, .. } => assert_eq!(model, "gpt-4"),
|
||||
other => panic!("expected CliAction::Prompt, got: {other:?}"),
|
||||
}
|
||||
let err_qwen = parse_args(&[
|
||||
"prompt".to_string(),
|
||||
"test".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user