Add GPT-5.6 support (#10384)
Co-authored-by: Jack Amadeo <jackamadeo@squareup.com>
This commit is contained in:
@@ -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"))]);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user