#[cfg(feature = "http")]
use crate::driver_registry::LlmCallConfig;
#[cfg(feature = "http")]
use crate::error::{AgentLoopError, Result};
#[cfg(feature = "http")]
use crate::model_profiles::get_model_profile;
#[cfg(feature = "http")]
use crate::openai_protocol::{is_azure_openai_api_url, is_openai_api_url};
use crate::provider::DriverId;
#[cfg(feature = "http")]
use crate::runtime_provider::ProviderEndpoint;
#[cfg(feature = "http")]
use serde_json::Value;
pub fn supports_cache_options(model: &str) -> bool {
crate::model_profiles::get_model_profile_key(&DriverId::OpenAI, model).is_some_and(|key| {
matches!(
key.as_str(),
"openai/gpt-6-astra"
| "openai/gpt-6-sol"
| "openai/gpt-6-luna"
| "openai/gpt-6.1-sol"
| "openai/gpt-5.6-sol"
| "openai/gpt-5.6-terra"
| "openai/gpt-5.6-luna"
)
})
}
#[cfg(feature = "http")]
pub(crate) fn validate_config(config: &LlmCallConfig) -> Result<()> {
if let Some(profile) = get_model_profile(&DriverId::OpenAI, &config.model) {
if let (Some(effort), Some(allowed)) = (config.reasoning_effort, profile.reasoning_effort)
&& !allowed.values.iter().any(|option| option.value == effort)
{
return Err(AgentLoopError::Configuration(format!(
"Reasoning effort '{}' is unsupported by {}",
effort.as_str(),
config.model
)));
}
if let Some(speed) = config.speed.as_deref().filter(|speed| *speed != "default") {
let offered = profile.speed.as_ref().is_some_and(|tiers| {
tiers
.values
.iter()
.any(|tier| tier.value.matches_tier(speed))
});
if !offered {
return Err(AgentLoopError::Configuration(format!(
"Speed '{speed}' is unsupported by {}",
config.model
)));
}
}
if config.temperature.is_some() && profile.family == "gpt-6-astra" {
return Err(AgentLoopError::Configuration(format!(
"temperature is unsupported by {}",
config.model
)));
}
}
Ok(())
}
#[cfg(feature = "http")]
pub(crate) fn validate_body(
body: &Value,
endpoint: &ProviderEndpoint,
responses: bool,
) -> Result<()> {
let astra = body
.get("model")
.and_then(Value::as_str)
.and_then(|model| crate::model_profiles::get_model_profile_key(&DriverId::OpenAI, model))
.is_some_and(|key| key == "openai/gpt-6-astra");
if !astra {
return Ok(());
}
let invalid = |message: &str| AgentLoopError::Configuration(message.to_string());
for key in ["temperature", "top_p", "top_logprobs"] {
if body.get(key).is_some_and(|v| !v.is_null()) {
return Err(invalid(&format!("GPT-6 Astra does not support {key}")));
}
}
if !responses {
let has_tools = body
.get("tools")
.and_then(Value::as_array)
.is_some_and(|tools| !tools.is_empty());
let has_tool_history =
body.get("messages")
.and_then(Value::as_array)
.is_some_and(|messages| {
messages.iter().any(|m| {
m.get("role").and_then(Value::as_str) == Some("tool")
|| m.get("tool_calls").is_some_and(|v| !v.is_null())
})
});
if has_tools || has_tool_history {
return Err(invalid(
"GPT-6 Astra tool calling requires the Responses API",
));
}
if body.get("logprobs").is_some_and(|v| !v.is_null()) {
return Err(invalid("GPT-6 Astra does not support logprobs"));
}
} else if body
.get("include")
.and_then(Value::as_array)
.is_some_and(|items| items.iter().any(|v| v == "message.output_text.logprobs"))
{
return Err(invalid(
"GPT-6 Astra does not support message.output_text.logprobs",
));
}
let eu = endpoint
.base_url()
.and_then(|u| url::Url::parse(u).ok())
.is_some_and(|u| {
u.host_str().is_some_and(|host| {
host.trim_end_matches('.')
.eq_ignore_ascii_case("eu.api.openai.com")
})
});
if eu
&& matches!(
body.get("service_tier").and_then(Value::as_str),
Some("fast" | "priority" | "ultrafast")
)
{
return Err(invalid(
"GPT-6 Astra requires Standard processing with EU data residency",
));
}
Ok(())
}
#[cfg(all(test, feature = "http"))]
mod tests {
use super::*;
use crate::{OpenAIProtocolChatDriver, Provider, ReasoningEffort};
use serde_json::json;
#[test]
fn astra_compatibility_checks_endpoint_and_wire_shape() {
let eu = Provider::new("openai", OpenAIProtocolChatDriver::new())
.base_url("https://eu.api.openai.com/v1");
let eu_absolute = Provider::new("openai", OpenAIProtocolChatDriver::new())
.base_url("https://eu.api.openai.com./v1");
let global = Provider::new("openai", OpenAIProtocolChatDriver::new())
.base_url("https://api.openai.com/v1");
let unrelated = Provider::new("custom", OpenAIProtocolChatDriver::new())
.base_url("https://eu.api.openai.com.example/v1");
for tier in ["fast", "priority", "ultrafast"] {
let body = json!({"model":"gpt-6-astra", "service_tier":tier});
assert!(validate_body(&body, eu.endpoint(), true).is_err());
assert!(validate_body(&body, eu_absolute.endpoint(), true).is_err());
assert!(validate_body(&body, global.endpoint(), true).is_ok());
assert!(validate_body(&body, unrelated.endpoint(), true).is_ok());
}
for body in [
json!({"model":"gpt-6-astra","tools":[{"type":"function"}]}),
json!({"model":"gpt-6-astra","messages":[{"role":"tool","content":"done"}]}),
] {
assert!(validate_body(&body, global.endpoint(), false).is_err());
assert!(validate_body(&body, global.endpoint(), true).is_ok());
}
for field in ["temperature", "top_p", "top_logprobs", "logprobs"] {
let mut body = json!({"model":"gpt-6-astra"});
body[field] = json!(0);
assert!(validate_body(&body, global.endpoint(), false).is_err());
}
assert!(
validate_body(
&json!({"model":"gpt-6-astra","include":["message.output_text.logprobs"]}),
global.endpoint(),
true
)
.is_err()
);
assert!(
validate_body(
&json!({"model":"gpt-6-astra","service_tier":"default"}),
eu.endpoint(),
true
)
.is_ok()
);
assert!(
validate_body(
&json!({"model":"gpt-5.5","service_tier":"priority","tools":[{}]}),
eu.endpoint(),
false
)
.is_ok()
);
}
#[test]
fn reasoning_effort_is_validated_before_none_is_filtered() {
let mut config = LlmCallConfig {
model: "gpt-6-astra".to_string(),
..Default::default()
};
for effort in [ReasoningEffort::None, ReasoningEffort::Minimal] {
config.reasoning_effort = Some(effort);
assert!(validate_config(&config).is_err());
}
for effort in [
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::Xhigh,
ReasoningEffort::Max,
] {
config.reasoning_effort = Some(effort);
assert!(validate_config(&config).is_ok());
}
config.temperature = Some(1.0);
assert!(validate_config(&config).is_err());
config.model = "gpt-5.5".into();
config.reasoning_effort = Some(ReasoningEffort::None);
config.temperature = None;
assert!(validate_config(&config).is_ok());
}
#[test]
fn speed_is_gated_by_the_model_profile() {
let check = |model: &str, speed: &str| {
validate_config(&LlmCallConfig {
model: model.to_string(),
speed: Some(speed.to_string()),
..Default::default()
})
};
for speed in ["flex", "default", "priority", "fast", "ultrafast"] {
assert!(check("gpt-6-astra", speed).is_ok(), "{speed}");
}
for speed in ["flex", "default", "priority", "fast"] {
assert!(check("gpt-6.1-sol", speed).is_ok(), "{speed}");
}
let err = check("gpt-6.1-sol", "ultrafast").unwrap_err().to_string();
assert!(
err.contains("Speed 'ultrafast' is unsupported by gpt-6.1-sol"),
"{err}"
);
assert!(check("gpt-5.5-pro", "fast").is_err());
assert!(check("gpt-5-nano", "default").is_ok());
assert!(check("gpt-5-nano", "flex").is_err());
assert!(check("some-gateway-model", "ultrafast").is_ok());
}
}
#[cfg(feature = "http")]
pub fn chat_completions_url(endpoint: &ProviderEndpoint) -> Result<String> {
endpoint.url("chat/completions").ok_or_else(|| {
AgentLoopError::Configuration(
"OpenAI Chat Completions provider has no base URL".to_string(),
)
})
}
#[cfg(feature = "http")]
pub fn max_output_fields(api_url: &str, max_tokens: Option<u32>) -> (Option<u32>, Option<u32>) {
if is_openai_api_url(api_url) || is_azure_openai_api_url(api_url) {
(None, max_tokens)
} else {
(max_tokens, None)
}
}
#[cfg(all(test, feature = "http"))]
mod output_cap_tests {
use super::max_output_fields;
#[test]
fn the_output_cap_uses_the_field_name_the_endpoint_accepts() {
for url in [
"https://api.openai.com/v1/chat/completions",
"https://my-resource.openai.azure.com/openai/v1/chat/completions",
"https://my-resource.services.ai.azure.com/openai/v1/chat/completions",
] {
assert_eq!(
max_output_fields(url, Some(128)),
(None, Some(128)),
"OpenAI-family host should use max_completion_tokens: {url}"
);
}
for url in [
"https://api.fireworks.ai/inference/v1/chat/completions",
"https://api.cloudflare.com/client/v4/accounts/acct/ai/v1/chat/completions",
"http://localhost:8000/v1/chat/completions",
] {
assert_eq!(
max_output_fields(url, Some(128)),
(Some(128), None),
"non-OpenAI host should keep max_tokens: {url}"
);
}
}
#[test]
fn an_absent_cap_sends_neither_field() {
assert_eq!(
max_output_fields("https://api.openai.com/v1/chat/completions", None),
(None, None)
);
assert_eq!(
max_output_fields(
"https://api.fireworks.ai/inference/v1/chat/completions",
None
),
(None, None)
);
}
}