|
|
|
@@ -399,6 +399,9 @@ pub struct AiConfig {
|
|
|
|
|
/// persist this field; it is attached to a runtime config clone.
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub runtime_effort: Option<AiEffortSelection>,
|
|
|
|
|
/// Optional output token budget configured for this provider.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub max_output_tokens: Option<u32>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub context_window: Option<u32>,
|
|
|
|
|
/// Maximum number of automatic retries on transient API errors
|
|
|
|
@@ -1214,8 +1217,12 @@ pub fn responses_stream_text(event: &serde_json::Value) -> Option<&str> {
|
|
|
|
|
event["delta"].as_str().filter(|s| !s.is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn responses_max_output_tokens(max_tokens: Option<u32>) -> u32 {
|
|
|
|
|
max_tokens.unwrap_or(2048).max(16)
|
|
|
|
|
fn output_token_limit(max_tokens: Option<u32>, config: &AiConfig, default: u32) -> u32 {
|
|
|
|
|
max_tokens.or(config.max_output_tokens).unwrap_or(default)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn responses_max_output_tokens(max_tokens: Option<u32>, config: &AiConfig) -> u32 {
|
|
|
|
|
output_token_limit(max_tokens, config, 2048).max(16)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn responses_token_usage(event: &serde_json::Value) -> Option<TokenUsage> {
|
|
|
|
@@ -1247,7 +1254,8 @@ fn uses_chat_completion_max_completion_tokens(config: &AiConfig) -> bool {
|
|
|
|
|
|| is_openai_api_config(config) && is_openai_reasoning_model(&config.model)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_chat_completion_token_limit(body: &mut serde_json::Value, config: &AiConfig, max_tokens: u32) {
|
|
|
|
|
fn set_chat_completion_token_limit(body: &mut serde_json::Value, config: &AiConfig, max_tokens: Option<u32>) {
|
|
|
|
|
let max_tokens = output_token_limit(max_tokens, config, 4096);
|
|
|
|
|
if uses_chat_completion_max_completion_tokens(config) {
|
|
|
|
|
body["max_completion_tokens"] = json!(max_tokens);
|
|
|
|
|
} else {
|
|
|
|
@@ -1261,7 +1269,7 @@ fn apply_minimax_chat_completion_fields(body: &mut serde_json::Value, config: &A
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn decorate_chat_completion_body(body: &mut serde_json::Value, config: &AiConfig, max_tokens: u32) {
|
|
|
|
|
fn decorate_chat_completion_body(body: &mut serde_json::Value, config: &AiConfig, max_tokens: Option<u32>) {
|
|
|
|
|
set_chat_completion_token_limit(body, config, max_tokens);
|
|
|
|
|
apply_minimax_chat_completion_fields(body, config);
|
|
|
|
|
apply_chat_completion_thinking_toggle(body, config);
|
|
|
|
@@ -2133,7 +2141,7 @@ pub async fn resolve_model_effort_core(config: &AiConfig, model_id: &str) -> Res
|
|
|
|
|
pub async fn call_claude(client: &reqwest::Client, request: AiCompletionRequest) -> Result<String, String> {
|
|
|
|
|
let mut body = json!({
|
|
|
|
|
"model": claude_http_model(&request.config.model),
|
|
|
|
|
"max_tokens": request.max_tokens.unwrap_or(2048),
|
|
|
|
|
"max_tokens": output_token_limit(request.max_tokens, &request.config, 2048),
|
|
|
|
|
"system": claude_system_prompt(&request.system_prompt),
|
|
|
|
|
"messages": claude_messages(&request.messages),
|
|
|
|
|
});
|
|
|
|
@@ -2213,7 +2221,7 @@ pub async fn call_openai_compatible(client: &reqwest::Client, request: AiComplet
|
|
|
|
|
"model": request.config.model,
|
|
|
|
|
"messages": messages,
|
|
|
|
|
});
|
|
|
|
|
decorate_chat_completion_body(&mut body_obj, &request.config, request.max_tokens.unwrap_or(2048));
|
|
|
|
|
decorate_chat_completion_body(&mut body_obj, &request.config, request.max_tokens);
|
|
|
|
|
|
|
|
|
|
let res = client
|
|
|
|
|
.post(resolve_endpoint(&request.config))
|
|
|
|
@@ -2238,7 +2246,7 @@ pub async fn call_responses_api(client: &reqwest::Client, request: AiCompletionR
|
|
|
|
|
let mut body = json!({
|
|
|
|
|
"model": request.config.model,
|
|
|
|
|
"input": build_responses_input(&request.system_prompt, &request.messages),
|
|
|
|
|
"max_output_tokens": responses_max_output_tokens(request.max_tokens),
|
|
|
|
|
"max_output_tokens": responses_max_output_tokens(request.max_tokens, &request.config),
|
|
|
|
|
});
|
|
|
|
|
crate::ai_effort::apply_runtime_effort(&mut body, &request.config);
|
|
|
|
|
|
|
|
|
@@ -2268,7 +2276,7 @@ pub async fn call_gemini(client: &reqwest::Client, request: AiCompletionRequest)
|
|
|
|
|
},
|
|
|
|
|
"contents": contents,
|
|
|
|
|
"generationConfig": {
|
|
|
|
|
"maxOutputTokens": request.max_tokens.unwrap_or(2048),
|
|
|
|
|
"maxOutputTokens": output_token_limit(request.max_tokens, &request.config, 2048),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
crate::ai_effort::apply_runtime_effort(&mut body, &request.config);
|
|
|
|
@@ -2787,7 +2795,7 @@ pub async fn test_connection_core(config: &AiConfig) -> Result<AiTestConnectionR
|
|
|
|
|
"messages": messages,
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
decorate_chat_completion_body(&mut body, &config_inner, 16);
|
|
|
|
|
decorate_chat_completion_body(&mut body, &config_inner, Some(16));
|
|
|
|
|
body
|
|
|
|
|
};
|
|
|
|
|
let headers = maybe_bearer_headers(&config_inner)?;
|
|
|
|
@@ -3167,7 +3175,7 @@ async fn stream_claude(
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let mut body = json!({
|
|
|
|
|
"model": claude_http_model(&request.config.model),
|
|
|
|
|
"max_tokens": request.max_tokens.unwrap_or(2048),
|
|
|
|
|
"max_tokens": output_token_limit(request.max_tokens, &request.config, 2048),
|
|
|
|
|
"system": claude_system_prompt(&request.system_prompt),
|
|
|
|
|
"messages": claude_messages(&request.messages),
|
|
|
|
|
"stream": true,
|
|
|
|
@@ -3275,7 +3283,7 @@ async fn stream_openai(
|
|
|
|
|
"messages": messages,
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
decorate_chat_completion_body(&mut body_obj, &request.config, request.max_tokens.unwrap_or(2048));
|
|
|
|
|
decorate_chat_completion_body(&mut body_obj, &request.config, request.max_tokens);
|
|
|
|
|
|
|
|
|
|
let endpoint = resolve_endpoint(&request.config);
|
|
|
|
|
let config = request.config.clone();
|
|
|
|
@@ -3406,7 +3414,7 @@ async fn stream_responses_api(
|
|
|
|
|
let mut body = json!({
|
|
|
|
|
"model": request.config.model,
|
|
|
|
|
"input": build_responses_input(&request.system_prompt, &request.messages),
|
|
|
|
|
"max_output_tokens": responses_max_output_tokens(request.max_tokens),
|
|
|
|
|
"max_output_tokens": responses_max_output_tokens(request.max_tokens, &request.config),
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
crate::ai_effort::apply_runtime_effort(&mut body, &request.config);
|
|
|
|
@@ -3498,7 +3506,7 @@ async fn stream_gemini(
|
|
|
|
|
},
|
|
|
|
|
"contents": contents,
|
|
|
|
|
"generationConfig": {
|
|
|
|
|
"maxOutputTokens": request.max_tokens.unwrap_or(2048),
|
|
|
|
|
"maxOutputTokens": output_token_limit(request.max_tokens, &request.config, 2048),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
crate::ai_effort::apply_runtime_effort(&mut body, &request.config);
|
|
|
|
@@ -3733,7 +3741,7 @@ async fn stream_claude_with_tools(
|
|
|
|
|
|
|
|
|
|
let mut body = json!({
|
|
|
|
|
"model": claude_http_model(&request.config.model),
|
|
|
|
|
"max_tokens": request.max_tokens.unwrap_or(4096),
|
|
|
|
|
"max_tokens": output_token_limit(request.max_tokens, &request.config, 4096),
|
|
|
|
|
"system": claude_system_prompt(&request.system_prompt),
|
|
|
|
|
"messages": messages,
|
|
|
|
|
"tools": tool_json,
|
|
|
|
@@ -3918,7 +3926,7 @@ async fn stream_openai_with_tools(
|
|
|
|
|
"stream": true,
|
|
|
|
|
"stream_options": { "include_usage": true },
|
|
|
|
|
});
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &request.config, request.max_tokens.unwrap_or(4096));
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &request.config, request.max_tokens);
|
|
|
|
|
apply_minimax_chat_completion_fields(&mut body, &request.config);
|
|
|
|
|
|
|
|
|
|
if request.config.runtime_effort.is_none() && !request.config.enable_thinking {
|
|
|
|
@@ -4094,7 +4102,7 @@ async fn stream_responses_with_tools(
|
|
|
|
|
let mut body = json!({
|
|
|
|
|
"model": request.config.model,
|
|
|
|
|
"input": build_responses_input_with_tools(&request.system_prompt, &request.messages),
|
|
|
|
|
"max_output_tokens": responses_max_output_tokens(request.max_tokens),
|
|
|
|
|
"max_output_tokens": responses_max_output_tokens(request.max_tokens, &request.config),
|
|
|
|
|
"tools": tool_json,
|
|
|
|
|
"tool_choice": "auto",
|
|
|
|
|
"stream": true,
|
|
|
|
@@ -4346,7 +4354,7 @@ async fn stream_gemini_with_tools(
|
|
|
|
|
"systemInstruction": { "parts": [{ "text": request.system_prompt }] },
|
|
|
|
|
"tools": [{ "functionDeclarations": tool_declarations }],
|
|
|
|
|
"generationConfig": {
|
|
|
|
|
"maxOutputTokens": request.max_tokens.unwrap_or(4096),
|
|
|
|
|
"maxOutputTokens": output_token_limit(request.max_tokens, &request.config, 4096),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
crate::ai_effort::apply_runtime_effort(&mut body, &request.config);
|
|
|
|
@@ -4603,6 +4611,27 @@ mod tests {
|
|
|
|
|
assert_eq!(openai_message_content(&message), serde_json::json!(marker));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ai_config_max_output_tokens_uses_camel_case_and_defaults_to_none() {
|
|
|
|
|
let configured: AiConfig = serde_json::from_value(serde_json::json!({
|
|
|
|
|
"provider": "deepseek",
|
|
|
|
|
"model": "deepseek-v4-flash",
|
|
|
|
|
"maxOutputTokens": 32_768
|
|
|
|
|
}))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(configured.max_output_tokens, Some(32_768));
|
|
|
|
|
|
|
|
|
|
let legacy: AiConfig = serde_json::from_value(serde_json::json!({
|
|
|
|
|
"provider": "deepseek",
|
|
|
|
|
"model": "deepseek-v4-flash"
|
|
|
|
|
}))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(legacy.max_output_tokens, None);
|
|
|
|
|
|
|
|
|
|
let serialized = serde_json::to_value(configured).unwrap();
|
|
|
|
|
assert_eq!(serialized["maxOutputTokens"], serde_json::json!(32_768));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ai_chat_selection_default_mode_serde() {
|
|
|
|
|
// Old blobs without a defaultMode field load as None (no migration needed).
|
|
|
|
@@ -4842,6 +4871,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: Some(1_000_000),
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5505,6 +5535,7 @@ mod tests {
|
|
|
|
|
proxy_url: "not a proxy url".to_string(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5546,6 +5577,7 @@ mod tests {
|
|
|
|
|
proxy_url: "127.0.0.1:7890".to_string(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5585,6 +5617,7 @@ mod tests {
|
|
|
|
|
proxy_url: "not a proxy url".to_string(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5624,6 +5657,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5667,6 +5701,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5707,6 +5742,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5731,7 +5767,7 @@ mod tests {
|
|
|
|
|
for provider in
|
|
|
|
|
[AiProvider::Ollama, AiProvider::AnthropicCompatible, AiProvider::OpenaiCompatible, AiProvider::Custom]
|
|
|
|
|
{
|
|
|
|
|
let config = AiConfig { provider, ..base.clone() };
|
|
|
|
|
let config = AiConfig { max_output_tokens: None, provider, ..base.clone() };
|
|
|
|
|
assert!(validate_config(&config).is_ok());
|
|
|
|
|
assert!(validate_model_list_config(&config).is_ok());
|
|
|
|
|
assert!(maybe_bearer_headers(&config).unwrap().get(AUTHORIZATION).is_none());
|
|
|
|
@@ -5745,7 +5781,7 @@ mod tests {
|
|
|
|
|
AiProvider::Qwen,
|
|
|
|
|
AiProvider::MiniMax,
|
|
|
|
|
] {
|
|
|
|
|
let config = AiConfig { provider, ..base.clone() };
|
|
|
|
|
let config = AiConfig { max_output_tokens: None, provider, ..base.clone() };
|
|
|
|
|
assert_eq!(validate_config(&config).unwrap_err(), "API key is required");
|
|
|
|
|
assert_eq!(validate_model_list_config(&config).unwrap_err(), "API key is required");
|
|
|
|
|
}
|
|
|
|
@@ -5766,6 +5802,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5801,6 +5838,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5839,6 +5877,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5864,19 +5903,24 @@ mod tests {
|
|
|
|
|
assert_eq!(resolve_endpoint(&config), "https://gateway.example.com/anthropic/v1/messages");
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&config).unwrap(), "https://gateway.example.com/anthropic/v1/models");
|
|
|
|
|
|
|
|
|
|
let full_messages =
|
|
|
|
|
AiConfig { endpoint: "https://gateway.example.com/anthropic/v1/messages".to_string(), ..config.clone() };
|
|
|
|
|
let full_messages = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
endpoint: "https://gateway.example.com/anthropic/v1/messages".to_string(),
|
|
|
|
|
..config.clone()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(resolve_endpoint(&full_messages), "https://gateway.example.com/anthropic/v1/messages");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolve_model_list_endpoint(&full_messages).unwrap(),
|
|
|
|
|
"https://gateway.example.com/anthropic/v1/models"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let bare_origin = AiConfig { endpoint: "https://gateway.example.com".to_string(), ..config.clone() };
|
|
|
|
|
let bare_origin =
|
|
|
|
|
AiConfig { max_output_tokens: None, endpoint: "https://gateway.example.com".to_string(), ..config.clone() };
|
|
|
|
|
assert_eq!(resolve_endpoint(&bare_origin), "https://gateway.example.com/v1/messages");
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&bare_origin).unwrap(), "https://gateway.example.com/v1/models");
|
|
|
|
|
|
|
|
|
|
let kimi_coding = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
endpoint: "https://api.kimi.com/coding/".to_string(),
|
|
|
|
|
model: "kimi-for-coding".to_string(),
|
|
|
|
|
..config.clone()
|
|
|
|
@@ -5893,16 +5937,26 @@ mod tests {
|
|
|
|
|
assert_eq!(resolve_endpoint(&config), "https://gateway.example.com/v1/messages");
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&config).unwrap(), "https://gateway.example.com/v1/models");
|
|
|
|
|
|
|
|
|
|
let v1 = AiConfig { endpoint: "https://gateway.example.com/v1".to_string(), ..config.clone() };
|
|
|
|
|
let v1 = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
endpoint: "https://gateway.example.com/v1".to_string(),
|
|
|
|
|
..config.clone()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(resolve_endpoint(&v1), "https://gateway.example.com/v1/messages");
|
|
|
|
|
|
|
|
|
|
let nested =
|
|
|
|
|
AiConfig { endpoint: "https://gateway.example.com/anthropic/v1/messages".to_string(), ..config.clone() };
|
|
|
|
|
let nested = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
endpoint: "https://gateway.example.com/anthropic/v1/messages".to_string(),
|
|
|
|
|
..config.clone()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(resolve_endpoint(&nested), "https://gateway.example.com/anthropic/v1/messages");
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&nested).unwrap(), "https://gateway.example.com/anthropic/v1/models");
|
|
|
|
|
|
|
|
|
|
let dashscope =
|
|
|
|
|
AiConfig { endpoint: "https://dashscope-intl.aliyuncs.com/apps/anthropic".to_string(), ..config.clone() };
|
|
|
|
|
let dashscope = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
endpoint: "https://dashscope-intl.aliyuncs.com/apps/anthropic".to_string(),
|
|
|
|
|
..config.clone()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(resolve_endpoint(&dashscope), "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolve_model_list_endpoint(&dashscope).unwrap(),
|
|
|
|
@@ -5929,6 +5983,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -5955,14 +6010,15 @@ mod tests {
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&config).unwrap(), "https://api.minimax.io/v1/models");
|
|
|
|
|
assert!(provider_requires_api_key(&config.provider));
|
|
|
|
|
|
|
|
|
|
let china = AiConfig { endpoint: "https://api.minimaxi.com/v1".to_string(), ..config.clone() };
|
|
|
|
|
let china =
|
|
|
|
|
AiConfig { max_output_tokens: None, endpoint: "https://api.minimaxi.com/v1".to_string(), ..config.clone() };
|
|
|
|
|
assert_eq!(resolve_endpoint(&china), "https://api.minimaxi.com/v1/chat/completions");
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&china).unwrap(), "https://api.minimaxi.com/v1/models");
|
|
|
|
|
|
|
|
|
|
let no_key = AiConfig { api_key: String::new(), ..config.clone() };
|
|
|
|
|
let no_key = AiConfig { max_output_tokens: None, api_key: String::new(), ..config.clone() };
|
|
|
|
|
assert_eq!(validate_config(&no_key).unwrap_err(), "API key is required");
|
|
|
|
|
|
|
|
|
|
let responses = AiConfig { api_style: AiApiStyle::Responses, ..config.clone() };
|
|
|
|
|
let responses = AiConfig { max_output_tokens: None, api_style: AiApiStyle::Responses, ..config.clone() };
|
|
|
|
|
assert_eq!(
|
|
|
|
|
validate_config(&responses).unwrap_err(),
|
|
|
|
|
"MiniMax currently supports the Chat Completions API style in DBX; select Completions and retry"
|
|
|
|
@@ -5976,6 +6032,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn kimi_provider_uses_openai_compatible_endpoints_and_requires_an_api_key() {
|
|
|
|
|
let config = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
provider: AiProvider::Kimi,
|
|
|
|
|
api_key: "key".to_string(),
|
|
|
|
|
auth_method: AiAuthMethod::Bearer,
|
|
|
|
@@ -6013,6 +6070,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6037,25 +6095,31 @@ mod tests {
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&config).unwrap(), "https://api.example.com/v1/models");
|
|
|
|
|
|
|
|
|
|
// Endpoint with /v1 already present — no change
|
|
|
|
|
let config_v1 = AiConfig { endpoint: "https://api.example.com/v1".to_string(), ..config.clone() };
|
|
|
|
|
let config_v1 =
|
|
|
|
|
AiConfig { max_output_tokens: None, endpoint: "https://api.example.com/v1".to_string(), ..config.clone() };
|
|
|
|
|
assert_eq!(resolve_endpoint(&config_v1), "https://api.example.com/v1/chat/completions");
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&config_v1).unwrap(), "https://api.example.com/v1/models");
|
|
|
|
|
|
|
|
|
|
// Endpoint with /v2 — no change
|
|
|
|
|
let config_v2 = AiConfig { endpoint: "https://api.example.com/v2".to_string(), ..config.clone() };
|
|
|
|
|
let config_v2 =
|
|
|
|
|
AiConfig { max_output_tokens: None, endpoint: "https://api.example.com/v2".to_string(), ..config.clone() };
|
|
|
|
|
assert_eq!(resolve_endpoint(&config_v2), "https://api.example.com/v2/chat/completions");
|
|
|
|
|
|
|
|
|
|
// Full path already specified — no change
|
|
|
|
|
let config_full =
|
|
|
|
|
AiConfig { endpoint: "https://api.openai.com/v1/chat/completions".to_string(), ..config.clone() };
|
|
|
|
|
let config_full = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
endpoint: "https://api.openai.com/v1/chat/completions".to_string(),
|
|
|
|
|
..config.clone()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(resolve_endpoint(&config_full), "https://api.openai.com/v1/chat/completions");
|
|
|
|
|
|
|
|
|
|
// Responses API style with /v1 missing
|
|
|
|
|
let config_responses = AiConfig { api_style: AiApiStyle::Responses, ..config.clone() };
|
|
|
|
|
let config_responses = AiConfig { max_output_tokens: None, api_style: AiApiStyle::Responses, ..config.clone() };
|
|
|
|
|
assert_eq!(resolve_endpoint(&config_responses), "https://api.example.com/v1/responses");
|
|
|
|
|
|
|
|
|
|
// Ollama preset already has /v1 — no change
|
|
|
|
|
let ollama = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
provider: AiProvider::Ollama,
|
|
|
|
|
endpoint: "http://localhost:11434/v1".to_string(),
|
|
|
|
|
..config.clone()
|
|
|
|
@@ -6063,12 +6127,14 @@ mod tests {
|
|
|
|
|
assert_eq!(resolve_endpoint(&ollama), "http://localhost:11434/v1/chat/completions");
|
|
|
|
|
|
|
|
|
|
// Custom path without /v1 — left alone (CC-Switch strategy: only bare origin gets auto /v1)
|
|
|
|
|
let custom_path = AiConfig { endpoint: "https://my-gateway.com/api".to_string(), ..config.clone() };
|
|
|
|
|
let custom_path =
|
|
|
|
|
AiConfig { max_output_tokens: None, endpoint: "https://my-gateway.com/api".to_string(), ..config.clone() };
|
|
|
|
|
assert_eq!(resolve_endpoint(&custom_path), "https://my-gateway.com/api/chat/completions");
|
|
|
|
|
assert_eq!(resolve_model_list_endpoint(&custom_path).unwrap(), "https://my-gateway.com/api/models");
|
|
|
|
|
|
|
|
|
|
// Bare host with port — add /v1
|
|
|
|
|
let bare_with_port = AiConfig { endpoint: "http://localhost:8080".to_string(), ..config.clone() };
|
|
|
|
|
let bare_with_port =
|
|
|
|
|
AiConfig { max_output_tokens: None, endpoint: "http://localhost:8080".to_string(), ..config.clone() };
|
|
|
|
|
assert_eq!(resolve_endpoint(&bare_with_port), "http://localhost:8080/v1/chat/completions");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -6087,6 +6153,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6111,6 +6178,7 @@ mod tests {
|
|
|
|
|
assert_eq!(resolve_endpoint(&config), "https://api.openai.com/v1/responses");
|
|
|
|
|
|
|
|
|
|
let completions = AiConfig {
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
endpoint: "https://api.openai.com/v1/responses".to_string(),
|
|
|
|
|
api_style: AiApiStyle::Completions,
|
|
|
|
|
custom_headers: Default::default(),
|
|
|
|
@@ -6134,6 +6202,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6304,6 +6373,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6326,7 +6396,8 @@ mod tests {
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(resolve_ollama_show_endpoint(&config).unwrap(), "http://localhost:11434/api/show");
|
|
|
|
|
|
|
|
|
|
let prefixed = AiConfig { endpoint: "https://example.com/ollama/v1".to_string(), ..config };
|
|
|
|
|
let prefixed =
|
|
|
|
|
AiConfig { max_output_tokens: None, endpoint: "https://example.com/ollama/v1".to_string(), ..config };
|
|
|
|
|
assert_eq!(resolve_ollama_show_endpoint(&prefixed).unwrap(), "https://example.com/ollama/api/show");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -6347,6 +6418,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6439,6 +6511,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6473,10 +6546,15 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn responses_api_clamps_tiny_output_token_requests() {
|
|
|
|
|
assert_eq!(responses_max_output_tokens(Some(1)), 16);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(Some(16)), 16);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(Some(2400)), 2400);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(None), 2048);
|
|
|
|
|
let mut config = minimax_test_config("https://api.minimaxi.com/v1");
|
|
|
|
|
assert_eq!(responses_max_output_tokens(Some(1), &config), 16);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(Some(16), &config), 16);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(Some(2400), &config), 2400);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(None, &config), 2048);
|
|
|
|
|
|
|
|
|
|
config.max_output_tokens = Some(32_768);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(None, &config), 32_768);
|
|
|
|
|
assert_eq!(responses_max_output_tokens(Some(2400), &config), 2400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
@@ -6717,6 +6795,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6743,7 +6822,7 @@ mod tests {
|
|
|
|
|
"messages": [{ "role": "user", "content": TEST_PROMPT }],
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, 1024);
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, Some(1024));
|
|
|
|
|
|
|
|
|
|
assert_eq!(body.get("max_completion_tokens"), Some(&serde_json::json!(1024)));
|
|
|
|
|
assert!(body.get("max_tokens").is_none());
|
|
|
|
@@ -6754,7 +6833,7 @@ mod tests {
|
|
|
|
|
"messages": [{ "role": "user", "content": TEST_PROMPT }],
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, 1024);
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, Some(1024));
|
|
|
|
|
|
|
|
|
|
assert_eq!(body.get("max_tokens"), Some(&serde_json::json!(1024)));
|
|
|
|
|
assert!(body.get("max_completion_tokens").is_none());
|
|
|
|
@@ -6766,7 +6845,7 @@ mod tests {
|
|
|
|
|
"messages": [{ "role": "user", "content": TEST_PROMPT }],
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, 1024);
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, Some(1024));
|
|
|
|
|
|
|
|
|
|
assert_eq!(body.get("max_completion_tokens"), Some(&serde_json::json!(1024)));
|
|
|
|
|
assert!(body.get("max_tokens").is_none());
|
|
|
|
@@ -6779,10 +6858,19 @@ mod tests {
|
|
|
|
|
"messages": [{ "role": "user", "content": TEST_PROMPT }],
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, 1024);
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, Some(1024));
|
|
|
|
|
|
|
|
|
|
assert_eq!(body.get("max_tokens"), Some(&serde_json::json!(1024)));
|
|
|
|
|
assert!(body.get("max_completion_tokens").is_none());
|
|
|
|
|
|
|
|
|
|
config.max_output_tokens = Some(32_768);
|
|
|
|
|
let mut body = serde_json::json!({
|
|
|
|
|
"model": &config.model,
|
|
|
|
|
"messages": [{ "role": "user", "content": TEST_PROMPT }],
|
|
|
|
|
"stream": true,
|
|
|
|
|
});
|
|
|
|
|
set_chat_completion_token_limit(&mut body, &config, None);
|
|
|
|
|
assert_eq!(body.get("max_tokens"), Some(&serde_json::json!(32_768)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
@@ -6939,6 +7027,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -6990,6 +7079,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -7044,6 +7134,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -7092,6 +7183,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -7127,7 +7219,7 @@ mod tests {
|
|
|
|
|
config.enable_thinking = false;
|
|
|
|
|
let mut body = serde_json::json!({ "model": &config.model });
|
|
|
|
|
|
|
|
|
|
decorate_chat_completion_body(&mut body, &config, 1024);
|
|
|
|
|
decorate_chat_completion_body(&mut body, &config, Some(1024));
|
|
|
|
|
|
|
|
|
|
assert_eq!(body["reasoning_split"], true);
|
|
|
|
|
assert_eq!(body["max_completion_tokens"], 1024);
|
|
|
|
@@ -7484,6 +7576,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::High,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: Some(AiEffortSelection::ProviderDefault),
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -7559,6 +7652,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: true,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
@@ -7629,6 +7723,7 @@ mod tests {
|
|
|
|
|
proxy_url: String::new(),
|
|
|
|
|
enable_thinking: false,
|
|
|
|
|
reasoning_level: AiReasoningLevel::Default,
|
|
|
|
|
max_output_tokens: None,
|
|
|
|
|
runtime_effort: None,
|
|
|
|
|
context_window: None,
|
|
|
|
|
max_retries: None,
|
|
|
|
|