Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15565,6 +15565,105 @@
"output": 128000
}
},
{
"id": "amazon-bedrock/openai.gpt-5.6-luna",
"name": "GPT-5.6 Luna",
"family": "gpt-luna",
"attachment": true,
"reasoning": true,
"tool_call": true,
"temperature": false,
"knowledge": "2026-02-16",
"release_date": "2026-07-09",
"last_updated": "2026-07-09",
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"open_weights": false,
"cost": {
"input": 1.0,
"output": 6.0,
"cache_read": 0.1,
"cache_write": 1.25
},
"limit": {
"context": 272000,
"output": 128000
}
},
{
"id": "amazon-bedrock/openai.gpt-5.6-sol",
Comment thread
jamadeo marked this conversation as resolved.
"name": "GPT-5.6 Sol",
"family": "gpt-sol",
"attachment": true,
"reasoning": true,
"tool_call": true,
"temperature": false,
"knowledge": "2026-02-16",
"release_date": "2026-07-09",
"last_updated": "2026-07-09",
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"open_weights": false,
"cost": {
"input": 5.0,
"output": 30.0,
"cache_read": 0.5,
"cache_write": 6.25
},
"limit": {
"context": 272000,
"output": 128000
}
},
{
"id": "amazon-bedrock/openai.gpt-5.6-terra",
"name": "GPT-5.6 Terra",
"family": "gpt-terra",
"attachment": true,
"reasoning": true,
"tool_call": true,
"temperature": false,
"knowledge": "2026-02-16",
"release_date": "2026-07-09",
"last_updated": "2026-07-09",
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"open_weights": false,
"cost": {
"input": 2.5,
"output": 15.0,
"cache_read": 0.25,
"cache_write": 3.125
},
"limit": {
"context": 272000,
"output": 128000
}
},
{
"id": "amazon-bedrock/openai.gpt-oss-120b",
"name": "gpt-oss-120b",
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-provider-types/src/formats/databricks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,7 +1158,7 @@ mod tests {
}

#[test]
fn test_create_request_off_effort_preserves_none() -> anyhow::Result<()> {
fn test_create_request_off_effort_uses_low() -> anyhow::Result<()> {
let mut params = std::collections::HashMap::new();
params.insert("thinking_effort".to_string(), serde_json::json!("off"));
let model_config = ModelConfig {
Expand All @@ -1172,7 +1172,7 @@ mod tests {
reasoning: None,
};
let request = create_request(&model_config, "system", &[], &[], &ImageFormat::OpenAi)?;
assert_eq!(request["reasoning_effort"], "none");
assert_eq!(request["reasoning_effort"], "low");
assert!(request.get("thinking_effort").is_none());
Ok(())
}
Expand Down
32 changes: 4 additions & 28 deletions crates/goose-provider-types/src/formats/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,13 +1515,10 @@ pub fn openai_reasoning_effort_for_thinking(
model_name: &str,
effort: ThinkingEffort,
) -> Option<String> {
if effort == ThinkingEffort::Off {
return Some("none".to_string());
}

let supported = openai_reasoning_efforts_for_model(model_name);

let preferred: &[&str] = match effort {
ThinkingEffort::Off => unreachable!(),
ThinkingEffort::Off => &["none", "low"],
ThinkingEffort::Low => &["low", "medium", "high", "xhigh"],
ThinkingEffort::Medium => &["medium", "high", "low", "xhigh"],
ThinkingEffort::High => &["high", "medium", "xhigh", "low"],
Expand All @@ -1534,7 +1531,7 @@ pub fn openai_reasoning_effort_for_thinking(
.map(|level| (*level).to_string())
}

fn openai_reasoning_efforts_for_model(model_name: &str) -> &'static [&'static str] {
pub(crate) fn openai_reasoning_efforts_for_model(model_name: &str) -> &'static [&'static str] {
let normalized = model_name.to_ascii_lowercase();

if normalized.contains("gpt-5") {
Expand All @@ -1547,7 +1544,7 @@ fn openai_reasoning_efforts_for_model(model_name: &str) -> &'static [&'static st
|| normalized.contains("gpt-5.6")
|| normalized.contains("gpt-5-6")
{
&["low", "medium", "high", "xhigh"]
&["none", "low", "medium", "high", "xhigh"]
Comment thread
angiejones marked this conversation as resolved.
} else {
&["low", "medium", "high"]
}
Expand Down Expand Up @@ -2502,27 +2499,6 @@ mod tests {
Ok(())
}

#[test]
fn test_create_request_o3_off_effort_preserves_none() -> anyhow::Result<()> {
let model_config = test_model_config("o3")
.with_max_tokens(Some(1024))
.with_thinking_effort(ThinkingEffort::Off);
let request = create_request(
&model_config,
"system",
&[],
&[],
&ImageFormat::OpenAi,
false,
)?;
let obj = request.as_object().unwrap();

assert_eq!(obj.get("reasoning_effort"), Some(&json!("none")));
assert!(obj.get("thinking_effort").is_none());

Ok(())
}

#[test]
fn test_create_request_gpt56_max_effort_uses_xhigh() -> anyhow::Result<()> {
let model_config = test_model_config("gpt-5.6-luna")
Expand Down
136 changes: 119 additions & 17 deletions crates/goose-provider-types/src/formats/openai_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,14 @@ fn add_message_items(input_items: &mut Vec<Value>, messages: &[Message]) {
}
}

fn is_gpt_5_6_model(model_name: &str) -> bool {
let normalized = model_name.to_ascii_lowercase();
normalized == "gpt-5.6"
|| normalized.starts_with("gpt-5.6-")
|| normalized == "gpt-5-6"
|| normalized.starts_with("gpt-5-6-")
}

pub fn create_responses_request(
model_config: &ModelConfig,
system: &str,
Expand Down Expand Up @@ -590,11 +598,15 @@ pub fn create_responses_request(
let is_reasoning_model = is_openai_responses_model(&model_name);
let reasoning_effort = if is_reasoning_model {
if let Some(effort) = legacy_reasoning_effort.as_deref() {
effort
.parse()
.ok()
.and_then(|effort| openai_reasoning_effort_for_thinking(&model_name, effort))
.or(legacy_reasoning_effort)
if effort.eq_ignore_ascii_case("none") {
legacy_reasoning_effort
} else {
effort
.parse()
.ok()
.and_then(|effort| openai_reasoning_effort_for_thinking(&model_name, effort))
.or(legacy_reasoning_effort)
}
} else {
model_config
.thinking_effort()
Expand All @@ -605,20 +617,43 @@ pub fn create_responses_request(
};

let store = model_config.request_param::<bool>("store").unwrap_or(false);
let reasoning_mode = model_config
.request_param::<String>("reasoning_mode")
.map(|mode| {
let normalized = mode.to_ascii_lowercase();
match normalized.as_str() {
"standard" | "pro" => Ok(normalized),
_ => Err(anyhow!(
"Invalid reasoning_mode '{}'. Supported values are: standard, pro",
mode
)),
}
})
.transpose()?;
if reasoning_mode.is_some() && !is_gpt_5_6_model(&model_name) {
return Err(anyhow!(
"reasoning_mode is only supported for GPT-5.6 models"
));
}
let mut payload = json!({
"model": model_name,
"input": input_items,
"store": store,
});

if let Some(effort) = reasoning_effort {
payload.as_object_mut().unwrap().insert(
"reasoning".to_string(),
json!({
"effort": effort,
"summary": "auto",
}),
);
if reasoning_effort.is_some() || reasoning_mode.is_some() {
let mut reasoning = serde_json::Map::new();
if let Some(effort) = reasoning_effort {
reasoning.insert("effort".to_string(), json!(effort));
reasoning.insert("summary".to_string(), json!("auto"));
}
if let Some(mode) = reasoning_mode {
reasoning.insert("mode".to_string(), json!(mode));
}
payload
.as_object_mut()
.unwrap()
.insert("reasoning".to_string(), Value::Object(reasoning));
}

if !tools.is_empty() {
Expand Down Expand Up @@ -650,10 +685,12 @@ pub fn create_responses_request(
}
}

payload.as_object_mut().unwrap().insert(
"max_output_tokens".to_string(),
json!(model_config.max_output_tokens()),
);
if let Some(max_tokens) = model_config.max_tokens {
payload
.as_object_mut()
.unwrap()
.insert("max_output_tokens".to_string(), json!(max_tokens));
}

Ok(payload)
}
Expand Down Expand Up @@ -1455,6 +1492,46 @@ mod tests {
assert_eq!(result["reasoning"]["summary"], "auto");
}

#[test]
fn test_responses_request_supports_gpt_5_6_xhigh_effort() {
let model_config = ModelConfig::new("gpt-5.6-sol-xhigh");

let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap();

assert_eq!(result["model"], "gpt-5.6-sol");
assert_eq!(result["reasoning"]["effort"], "xhigh");
assert_eq!(result["reasoning"]["summary"], "auto");
}

#[test]
fn test_responses_request_supports_gpt_5_6_reasoning_mode() {
let model_config = ModelConfig::new("gpt-5.6-sol").with_merged_request_params(
std::collections::HashMap::from([("reasoning_mode".to_string(), json!("pro"))]),
);

let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap();

assert_eq!(result["reasoning"]["mode"], "pro");
assert!(result["reasoning"].get("effort").is_none());
assert!(result["reasoning"].get("summary").is_none());
}

#[test]
fn test_responses_request_rejects_reasoning_mode_for_non_gpt_5_6_model() {
for model_name in ["gpt-5.5", "gpt-5.60"] {
let model_config = ModelConfig::new(model_name).with_merged_request_params(
std::collections::HashMap::from([("reasoning_mode".to_string(), json!("pro"))]),
);

let error = create_responses_request(&model_config, "You are helpful.", &[], &[])
.expect_err("reasoning mode should be gated to GPT-5.6 models");

assert!(error
.to_string()
.contains("reasoning_mode is only supported for GPT-5.6 models"));
}
}

#[test]
fn test_responses_request_without_effort_suffix_omits_reasoning() {
for model_name in ["gpt-5.4", "o3", "gpt-5-nano"] {
Expand All @@ -1480,6 +1557,31 @@ mod tests {
}
}

#[test]
fn test_responses_request_omits_default_max_output_tokens_for_unknown_model() {
let model_config = ModelConfig::new("gpt-5.6-sol");

let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap();

assert_eq!(result["model"], "gpt-5.6-sol");
assert!(
result.get("max_output_tokens").is_none(),
"unknown/new models should not receive Goose's fallback max_output_tokens"
);
}

#[test]
fn test_responses_request_includes_canonical_max_output_tokens() {
let model_config = ModelConfig::new("gpt-5.4").with_canonical_limits("openai");

let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap();

assert_eq!(
result["max_output_tokens"],
model_config.max_tokens.unwrap()
);
}

#[test]
fn test_responses_request_non_reasoning_model_ignores_global_thinking_effort() {
let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", Some("high"))]);
Expand Down
14 changes: 14 additions & 0 deletions crates/goose-provider-types/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,20 @@ mod tests {
let config = ModelConfig::new("gpt-5.4-xhigh").with_canonical_limits("openai");
assert_eq!(config.context_limit, Some(1_050_000));

// "gpt-5.6-sol-xhigh" should resolve via "gpt-5.6-sol"
let config = ModelConfig::new("gpt-5.6-sol-xhigh").with_canonical_limits("openai");
assert_eq!(config.context_limit, Some(1_050_000));
assert_eq!(config.max_tokens, Some(128_000));
assert_eq!(config.reasoning, Some(true));
let canonical = crate::canonical::maybe_get_canonical_model("openai", "gpt-5.6-sol")
.expect("gpt-5.6-sol should have canonical metadata");
assert_eq!(canonical.temperature, Some(false));

let config = ModelConfig::new("gpt-5.6-sol").with_canonical_limits("chatgpt_codex");
assert_eq!(config.context_limit, Some(1_050_000));
assert_eq!(config.max_tokens, Some(128_000));
assert_eq!(config.reasoning, Some(true));

// "gpt-5.4-nano-low" should resolve via "gpt-5.4-nano"
let config = ModelConfig::new("gpt-5.4-nano-low").with_canonical_limits("openai");
assert_eq!(config.context_limit, Some(400_000));
Expand Down
Loading
Loading