mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-08 05:06:44 +00:00
fix(providers): strip provider prefix from model names for openai_compat endpoints
This commit is contained in:
@@ -296,6 +296,20 @@ pub fn metadata_for_model(model: &str) -> Option<ProviderMetadata> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn strip_provider_prefix(canonical_model: &str) -> String {
|
||||||
|
if let Some(pos) = canonical_model.find('/') {
|
||||||
|
canonical_model[pos + 1..].to_string()
|
||||||
|
} else {
|
||||||
|
canonical_model.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn provider_diagnostics_for_model(model: &str) -> ProviderDiagnostics {
|
pub fn provider_diagnostics_for_model(model: &str) -> ProviderDiagnostics {
|
||||||
let resolved_model = resolve_model_alias(model);
|
let resolved_model = resolve_model_alias(model);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ use crate::types::{
|
|||||||
ToolChoice, ToolDefinition, ToolResultContentBlock, Usage,
|
ToolChoice, ToolDefinition, ToolResultContentBlock, Usage,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{preflight_message_request, Provider, ProviderFuture};
|
use super::{preflight_message_request, Provider, ProviderFuture, resolve_model_alias, strip_provider_prefix};
|
||||||
|
|
||||||
|
|
||||||
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";
|
||||||
@@ -215,14 +216,73 @@ impl OpenAiCompatClient {
|
|||||||
&self,
|
&self,
|
||||||
request: &MessageRequest,
|
request: &MessageRequest,
|
||||||
) -> Result<MessageResponse, ApiError> {
|
) -> Result<MessageResponse, ApiError> {
|
||||||
let request = MessageRequest {
|
// 1. Keep track of what Claw originally asked for
|
||||||
|
let original_model = request.model.clone();
|
||||||
|
let canonical = resolve_model_alias(&request.model);
|
||||||
|
|
||||||
|
// 2. Clean the model string (e.g., "openai/deepseek-v4-flash" -> "deepseek-v4-flash")
|
||||||
|
let downstream_model = strip_provider_prefix(&canonical);
|
||||||
|
|
||||||
|
let mut request = MessageRequest {
|
||||||
stream: false,
|
stream: false,
|
||||||
..request.clone()
|
..request.clone()
|
||||||
};
|
};
|
||||||
|
request.model = downstream_model; // Use the clean name for the API payload
|
||||||
|
|
||||||
preflight_message_request(&request)?;
|
preflight_message_request(&request)?;
|
||||||
let response = self.send_with_retry(&request).await?;
|
let response = self.send_with_retry(&request).await?;
|
||||||
let request_id = request_id_from_headers(response.headers());
|
let request_id = request_id_from_headers(response.headers());
|
||||||
let body = response.text().await.map_err(ApiError::from)?;
|
let body = response.text().await.map_err(ApiError::from)?;
|
||||||
|
|
||||||
|
// Some backends return {"error":{"message":"...","type":"...","code":...}}
|
||||||
|
// instead of a valid completion object. Check for this before attempting
|
||||||
|
// full deserialization so the user sees the actual error, not a cryptic.
|
||||||
|
if let Ok(raw) = serde_json::from_str::<serde_json::Value>(&body) {
|
||||||
|
if let Some(err_obj) = raw.get("error") {
|
||||||
|
let msg = err_obj
|
||||||
|
.get("message")
|
||||||
|
.and_then(|m| m.as_str())
|
||||||
|
.unwrap_or("provider returned an error")
|
||||||
|
.to_string();
|
||||||
|
let code = err_obj
|
||||||
|
.get("code")
|
||||||
|
.and_then(serde_json::Value::as_u64)
|
||||||
|
.map(|c| c as u16);
|
||||||
|
return Err(ApiError::Api {
|
||||||
|
status: reqwest::StatusCode::from_u16(code.unwrap_or(400))
|
||||||
|
.unwrap_or(reqwest::StatusCode::BAD_REQUEST),
|
||||||
|
error_type: err_obj
|
||||||
|
.get("type")
|
||||||
|
.and_then(|t| t.as_str())
|
||||||
|
.map(str::to_owned),
|
||||||
|
message: Some(msg),
|
||||||
|
request_id,
|
||||||
|
body,
|
||||||
|
retryable: false,
|
||||||
|
suggested_action: suggested_action_for_status(
|
||||||
|
reqwest::StatusCode::from_u16(code.unwrap_or(400))
|
||||||
|
.unwrap_or(reqwest::StatusCode::BAD_REQUEST),
|
||||||
|
),
|
||||||
|
retry_after: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass original_model to the deserializer error context so debugging logs are accurate
|
||||||
|
let payload = serde_json::from_str::<ChatCompletionResponse>(&body).map_err(|error| {
|
||||||
|
ApiError::json_deserialize(self.config.provider_name, &original_model, &body, error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut normalized = normalize_response(&request.model, payload)?;
|
||||||
|
if normalized.request_id.is_none() {
|
||||||
|
normalized.request_id = request_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CRITICAL: Put the original model string back so Claw's internal routing stays happy
|
||||||
|
normalized.model = original_model;
|
||||||
|
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
// Some backends return {"error":{"message":"...","type":"...","code":...}}
|
// Some backends return {"error":{"message":"...","type":"...","code":...}}
|
||||||
// instead of a valid completion object. Check for this before attempting
|
// instead of a valid completion object. Check for this before attempting
|
||||||
// full deserialization so the user sees the actual error, not a cryptic
|
// full deserialization so the user sees the actual error, not a cryptic
|
||||||
@@ -271,17 +331,26 @@ impl OpenAiCompatClient {
|
|||||||
&self,
|
&self,
|
||||||
request: &MessageRequest,
|
request: &MessageRequest,
|
||||||
) -> Result<MessageStream, ApiError> {
|
) -> Result<MessageStream, ApiError> {
|
||||||
preflight_message_request(request)?;
|
// 1. Keep track of the original model name
|
||||||
let response = self
|
let original_model = request.model.clone();
|
||||||
.send_with_retry(&request.clone().with_streaming())
|
let canonical = resolve_model_alias(&request.model);
|
||||||
.await?;
|
|
||||||
|
// 2. Clean it up for DeepSeek
|
||||||
|
let downstream_model = strip_provider_prefix(&canonical);
|
||||||
|
|
||||||
|
let mut streaming_request = request.clone().with_streaming();
|
||||||
|
streaming_request.model = downstream_model;
|
||||||
|
|
||||||
|
preflight_message_request(&streaming_request)?;
|
||||||
|
let response = self.send_with_retry(&streaming_request).await?;
|
||||||
|
|
||||||
Ok(MessageStream {
|
Ok(MessageStream {
|
||||||
request_id: request_id_from_headers(response.headers()),
|
request_id: request_id_from_headers(response.headers()),
|
||||||
response,
|
response,
|
||||||
parser: OpenAiSseParser::with_context(self.config.provider_name, request.model.clone()),
|
parser: OpenAiSseParser::with_context(self.config.provider_name, original_model.clone()),
|
||||||
pending: VecDeque::new(),
|
pending: VecDeque::new(),
|
||||||
done: false,
|
done: false,
|
||||||
state: StreamState::new(request.model.clone()),
|
state: StreamState::new(original_model), // 3. Use the original name here
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user