Add GPT-5.6 support (#10384)

Co-authored-by: Jack Amadeo <jackamadeo@squareup.com>
This commit is contained in:
Angie Jones
2026-07-14 05:10:36 -07:00
committed by GitHub
parent 2ecb8c0894
commit 6e8f7108d5
7 changed files with 282 additions and 72 deletions
@@ -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",
"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",
@@ -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 {
@@ -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(())
}
@@ -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"],
@@ -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") {
@@ -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"]
} else {
&["low", "medium", "high"]
}
@@ -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")
@@ -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,
@@ -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()
@@ -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() {
@@ -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)
}
@@ -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"] {
@@ -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"))]);
+14
View File
@@ -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));
+4 -21
View File
@@ -62,9 +62,10 @@ pub const OPEN_AI_KNOWN_MODELS: &[(&str, usize)] = &[
("gpt-5.4-pro", 1_050_000),
("gpt-5.5", 1_050_000),
("gpt-5.5-pro", 1_050_000),
("gpt-5.6-luna", 1_050_000),
("gpt-5.6", 1_050_000),
("gpt-5.6-sol", 1_050_000),
("gpt-5.6-terra", 1_050_000),
("gpt-5.6-luna", 1_050_000),
];
pub const OPEN_AI_DOC_URL: &str = "https://platform.openai.com/docs/models";
@@ -844,26 +845,6 @@ mod tests {
use crate::api_client::AuthMethod;
use serde_json::json;
#[test]
fn gpt_5_5_and_5_6_models_have_expected_context_limits() {
for model in [
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
] {
assert_eq!(
OPEN_AI_KNOWN_MODELS
.iter()
.find(|(name, _)| *name == model)
.map(|(_, limit)| *limit),
Some(1_050_000),
"unexpected context limit for {model}"
);
}
}
fn make_provider(name: &str) -> OpenAiProvider {
OpenAiProvider {
api_client: ApiClient::new_with_tls(
@@ -1023,6 +1004,8 @@ mod tests {
for (model_name, base_path, expected) in [
("gpt-5.4", "v1/chat/completions", true),
("gpt-5.4-xhigh", "v1/chat/completions", true),
("gpt-5.6-sol", "v1/chat/completions", true),
("gpt-5.6-terra-xhigh", "v1/chat/completions", true),
("gpt-5.2-pro-2025-12-11", "v1/chat/completions", true),
("gpt-4o", "v1/chat/completions", false),
("gpt-5.2-codex", "openai/v1/chat/completions", false),
+40 -4
View File
@@ -52,6 +52,22 @@ pub struct ChatGptCodexModelAttrs {
}
pub const CHATGPT_CODEX_KNOWN_MODELS: &[ChatGptCodexModelAttrs] = &[
ChatGptCodexModelAttrs {
name: "gpt-5.6-sol",
reasoning_levels: &["none", "low", "medium", "high", "xhigh"],
},
ChatGptCodexModelAttrs {
name: "gpt-5.6-terra",
reasoning_levels: &["none", "low", "medium", "high", "xhigh"],
},
ChatGptCodexModelAttrs {
name: "gpt-5.6-luna",
reasoning_levels: &["none", "low", "medium", "high", "xhigh"],
},
ChatGptCodexModelAttrs {
name: "gpt-5.6",
reasoning_levels: &["none", "low", "medium", "high", "xhigh"],
},
ChatGptCodexModelAttrs {
name: "gpt-5.5",
reasoning_levels: &["low", "medium", "high", "xhigh"],
@@ -223,7 +239,13 @@ fn reasoning_effort_for_config(model_config: &ModelConfig) -> Option<String> {
.map(|effort| {
let valid_levels = reasoning_levels_for_model(&model_config.model_name);
let preferred_levels: &[&str] = match effort {
ThinkingEffort::Off => return None,
ThinkingEffort::Off => {
return Some(if valid_levels.contains(&"none") {
"none".to_string()
} else {
"low".to_string()
});
}
ThinkingEffort::Low => &["low", "medium", "high", "xhigh"],
ThinkingEffort::Medium => &["medium", "high", "low", "xhigh"],
ThinkingEffort::High => &["high", "medium", "xhigh", "low"],
@@ -1201,14 +1223,14 @@ mod tests {
}
#[test]
fn test_create_codex_request_off_omits_reasoning_for_codex_models() {
fn test_create_codex_request_off_sets_none_for_gpt_5_6_models() {
let mut params = std::collections::HashMap::new();
params.insert("thinking_effort".to_string(), json!("off"));
let mut config = ModelConfig::new("gpt-5.2-codex");
let mut config = ModelConfig::new("gpt-5.6-sol");
config.request_params = Some(params);
let payload = create_codex_request(&config, "sys", &[], &[]).unwrap();
assert!(payload.get("reasoning").is_none());
assert_eq!(payload["reasoning"]["effort"], "none");
assert!(payload.get("reasoning_effort").is_none());
}
@@ -1374,11 +1396,25 @@ mod tests {
assert_eq!(claims.chatgpt_account_id.as_deref(), Some("account-1"));
}
#[test_case("gpt-5.6-sol", &["none", "low", "medium", "high", "xhigh"]; "gpt 5.6 sol supports extended reasoning levels")]
#[test_case("gpt-5.6-terra", &["none", "low", "medium", "high", "xhigh"]; "gpt 5.6 terra supports extended reasoning levels")]
#[test_case("gpt-5.6-luna", &["none", "low", "medium", "high", "xhigh"]; "gpt 5.6 luna supports extended reasoning levels")]
#[test_case("gpt-5.6", &["none", "low", "medium", "high", "xhigh"]; "gpt 5.6 supports extended reasoning levels")]
#[test_case("unknown-model", &["medium", "high"]; "unknown model gets default reasoning levels")]
fn test_reasoning_levels_for_model(model: &str, expected: &[&str]) {
assert_eq!(reasoning_levels_for_model(model), expected);
}
#[test]
fn test_known_model_names_include_gpt_5_6_models() {
let names = known_model_names();
assert!(names.contains(&"gpt-5.6-sol"));
assert!(names.contains(&"gpt-5.6-terra"));
assert!(names.contains(&"gpt-5.6-luna"));
assert!(names.contains(&"gpt-5.6"));
}
#[test]
fn test_instructions_passed_through() {
let model = ModelConfig::new("gpt-5.4");