use crate::Provider;
use crate::model_profile::capabilities::{EffortLevel, ModelCapabilities, ThinkingSupport};
use serde_json::{Value, json};
pub fn build_params_schema(caps: &ModelCapabilities) -> Value {
match caps.provider {
Provider::Anthropic => build_anthropic_schema(caps),
Provider::OpenAI => build_openai_schema(caps),
Provider::Gemini => build_gemini_schema(caps),
_ => json!({
"type": "object",
"additionalProperties": false,
"properties": {}
}),
}
}
fn build_anthropic_schema(caps: &ModelCapabilities) -> Value {
let mut props = serde_json::Map::new();
if let Some(thinking) = anthropic_thinking_schema(caps.thinking) {
props.insert("thinking".into(), thinking);
}
if caps.supports_thinking_budget_legacy && caps.thinking != ThinkingSupport::None {
props.insert("thinking_budget".into(), integer_nonneg_schema());
}
if caps.supports_top_k {
props.insert("top_k".into(), integer_nonneg_schema());
}
if !caps.effort_levels.is_empty() {
props.insert(
"effort".into(),
effort_enum_schema("Output effort level.", caps.effort_levels),
);
}
if caps.supports_inference_geo {
props.insert(
"inference_geo".into(),
json!({
"description": "Data residency region (e.g., \"us\" or \"global\").",
"type": "string"
}),
);
}
if caps.supports_compaction {
props.insert(
"compaction".into(),
json!({
"description": "Context compaction. \"auto\" or an object like {\"trigger\": 150000}.",
}),
);
}
object_schema(props)
}
fn anthropic_thinking_schema(mode: ThinkingSupport) -> Option<Value> {
match mode {
ThinkingSupport::None | ThinkingSupport::GeminiThinkingLevel => None,
ThinkingSupport::AnthropicEnabledOnly => Some(json!({
"description": "Extended thinking configuration. Format: {\"type\": \"enabled\", \"budget_tokens\": N}.",
"type": "object",
"required": ["type", "budget_tokens"],
"properties": {
"type": { "type": "string", "enum": ["enabled"] },
"budget_tokens": { "type": "integer", "minimum": 0 }
}
})),
ThinkingSupport::AnthropicAdaptiveOnly => Some(json!({
"description": "Extended thinking configuration. Format: {\"type\": \"adaptive\"}.",
"type": "object",
"required": ["type"],
"properties": {
"type": { "type": "string", "enum": ["adaptive"] }
}
})),
ThinkingSupport::AnthropicAdaptiveAndEnabled => Some(json!({
"description": "Extended thinking configuration. Format: {\"type\": \"adaptive\"} or {\"type\": \"enabled\", \"budget_tokens\": N}.",
"oneOf": [
{
"type": "object",
"required": ["type"],
"properties": {
"type": { "type": "string", "enum": ["adaptive"] }
}
},
{
"type": "object",
"required": ["type", "budget_tokens"],
"properties": {
"type": { "type": "string", "enum": ["enabled"] },
"budget_tokens": { "type": "integer", "minimum": 0 }
}
}
]
})),
}
}
fn build_openai_schema(caps: &ModelCapabilities) -> Value {
let mut props = serde_json::Map::new();
if caps.supports_reasoning && !caps.effort_levels.is_empty() {
props.insert(
"reasoning_effort".into(),
effort_enum_schema("Reasoning effort level.", caps.effort_levels),
);
}
if caps.supports_legacy_penalties {
props.insert(
"seed".into(),
json!({
"description": "Random seed for reproducibility.",
"type": "integer"
}),
);
props.insert(
"frequency_penalty".into(),
json!({
"description": "Frequency penalty (-2.0 to 2.0).",
"type": "number"
}),
);
props.insert(
"presence_penalty".into(),
json!({
"description": "Presence penalty (-2.0 to 2.0).",
"type": "number"
}),
);
}
object_schema(props)
}
fn build_gemini_schema(caps: &ModelCapabilities) -> Value {
let mut props = serde_json::Map::new();
if caps.thinking != ThinkingSupport::None {
let thinking_props = match caps.thinking {
ThinkingSupport::GeminiThinkingLevel => json!({
"thinking_level": gemini_thinking_level_schema(),
"thinking_budget": { "type": "integer", "minimum": 0 }
}),
_ => json!({
"thinking_budget": { "type": "integer", "minimum": 0 }
}),
};
props.insert(
"thinking".into(),
json!({
"description": "Thinking configuration.",
"type": "object",
"additionalProperties": false,
"properties": thinking_props
}),
);
if caps.thinking == ThinkingSupport::GeminiThinkingLevel {
props.insert("thinking_level".into(), gemini_thinking_level_schema());
}
if caps.supports_thinking_budget_legacy {
props.insert(
"thinking_budget".into(),
json!({
"description": "Legacy flat thinking budget (alternative to thinking.thinking_budget).",
"type": "integer",
"minimum": 0
}),
);
}
}
if caps.supports_top_k {
props.insert("top_k".into(), integer_nonneg_schema());
}
if caps.supports_top_p {
props.insert(
"top_p".into(),
json!({
"type": "number",
"minimum": 0.0,
"maximum": 1.0
}),
);
}
object_schema(props)
}
fn object_schema(properties: serde_json::Map<String, Value>) -> Value {
json!({
"type": "object",
"additionalProperties": false,
"properties": Value::Object(properties)
})
}
fn integer_nonneg_schema() -> Value {
json!({ "type": "integer", "minimum": 0 })
}
fn gemini_thinking_level_schema() -> Value {
json!({
"description": "Gemini 3 reasoning level.",
"type": "string",
"enum": ["minimal", "low", "medium", "high"]
})
}
fn effort_enum_schema(description: &str, levels: &[EffortLevel]) -> Value {
let vs: Vec<Value> = levels
.iter()
.map(|level| Value::String(level.as_wire_str().into()))
.collect();
json!({
"description": description,
"type": "string",
"enum": vs
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::model_profile::test_catalog::TEST_CATALOG;
fn property_keys(schema: &Value) -> std::collections::BTreeSet<String> {
schema
.get("properties")
.and_then(|p| p.as_object())
.map(|m| m.keys().cloned().collect())
.unwrap_or_default()
}
fn enum_values_for(schema: &Value, prop: &str) -> Option<std::collections::BTreeSet<String>> {
let val = schema
.get("properties")
.and_then(|p| p.get(prop))
.and_then(|v| v.get("enum"))
.and_then(|e| e.as_array())?;
Some(
val.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect(),
)
}
#[test]
fn builder_emits_object_schema_for_every_capability_row() {
for caps in TEST_CATALOG.capabilities {
let schema = build_params_schema(caps);
assert_eq!(
schema.get("type").and_then(|t| t.as_str()),
Some("object"),
"schema for {} must be type=object",
caps.id
);
assert!(
schema.get("properties").is_some(),
"schema for {} must have a properties map",
caps.id
);
}
}
#[test]
fn effort_enum_values_derive_from_typed_effort_levels() {
use crate::model_profile::capabilities::{EffortLevel, ModelCapabilities, ThinkingSupport};
let base = TEST_CATALOG
.capabilities_for(crate::Provider::Anthropic, "test-anthropic-default")
.expect("test anthropic row");
let caps = ModelCapabilities {
thinking: ThinkingSupport::AnthropicAdaptiveAndEnabled,
effort_levels: &[EffortLevel::Low, EffortLevel::High, EffortLevel::Max],
..*base
};
let schema = build_params_schema(&caps);
let values = enum_values_for(&schema, "effort").expect("effort enum");
let declared: std::collections::BTreeSet<String> = caps
.effort_levels
.iter()
.map(|level| level.as_wire_str().to_string())
.collect();
assert_eq!(
values, declared,
"effort enum must equal the row's typed effort_levels"
);
}
#[test]
fn empty_effort_levels_emit_no_effort_property() {
let caps = TEST_CATALOG
.capabilities_for(crate::Provider::Anthropic, "test-anthropic-default")
.expect("test anthropic row");
assert!(caps.effort_levels.is_empty());
let schema = build_params_schema(caps);
assert!(!property_keys(&schema).contains("effort"));
}
#[test]
fn gemini_thinking_level_rows_expose_thinking_level() {
let caps = TEST_CATALOG
.capabilities_for(crate::Provider::Gemini, "test-gemini-video")
.expect("test gemini row");
let schema = build_params_schema(caps);
let keys = property_keys(&schema);
assert!(
keys.contains("thinking_level"),
"GeminiThinkingLevel rows must advertise thinking_level"
);
let values = enum_values_for(&schema, "thinking_level").expect("thinking_level enum");
let expected: std::collections::BTreeSet<String> = ["high", "low", "medium", "minimal"]
.into_iter()
.map(str::to_string)
.collect();
assert_eq!(values, expected);
}
#[test]
fn gemini_rows_have_no_include_thoughts() {
let caps = TEST_CATALOG
.capabilities_for(crate::Provider::Gemini, "test-gemini-video")
.expect("test gemini row");
let schema = build_params_schema(caps);
assert!(!property_keys(&schema).contains("include_thoughts"));
let thinking = schema.get("properties").and_then(|p| p.get("thinking"));
if let Some(inner_props) = thinking.and_then(|t| t.get("properties")) {
let obj = inner_props.as_object().expect("inner properties");
assert!(!obj.contains_key("include_thoughts"));
}
}
}