Files
codex/codex-rs/core/tests/suite/gcp.rs
T
BOZHANG-OAI 126a0ee92e Add GPT-6.1 Terra to model catalogs (#57293)
## What changed

- Add `gpt-6.1-terra` to the bundled catalog and GCP Vertex model picker.
- Add `openai.gpt-6.1-terra` to Amazon Bedrock catalogs, including Global and US cross-region runtime variants.
- Use `gpt-6.1-terra` as GCP's preferred memory consolidation model.

## Testing

Extend catalog, model picker, thread startup, and Vertex request tests to cover GPT-6.1 Terra.

GitOrigin-RevId: edb3be3c4286c191349cdee742005ed3c67029e8
2026-10-26 22:13:14 +00:00

422 lines
15 KiB
Rust

use anyhow::Context;
use anyhow::Result;
use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained;
use codex_features::Feature;
use codex_model_provider_info::GCP_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec;
use codex_protocol::dynamic_tools::DynamicToolNamespaceTool;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use test_case::test_case;
use wiremock::Mock;
use wiremock::ResponseTemplate;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
use super::client::ProviderAuthCommandFixture;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn vertex_credential_file_is_retained_when_refreshing_after_401() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let auth_fixture = ProviderAuthCommandFixture::new(&["first-token", "second-token"])?;
let auth = auth_fixture.auth();
let credential_file = auth.cwd.join("adc.json").into_path_buf();
let credential_log = auth.cwd.join("credential-env.txt");
let inherited_credential_file = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS");
Mock::given(method("POST"))
.and(path("/v1/responses"))
.and(header("authorization", "Bearer first-token"))
.respond_with(ResponseTemplate::new(401).set_body_string("unauthorized"))
.expect(1)
.mount(&server)
.await;
let response = responses::mount_sse_once_match(
&server,
header("authorization", "Bearer second-token"),
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_completed("resp-1"),
]),
)
.await;
let mut provider = ModelProviderInfo::create_gcp_provider();
provider.base_url = Some(format!("{}/v1", server.uri()));
provider.auth = Some(auth);
provider.credential_file = Some(credential_file.clone());
let test = test_codex()
.with_config(move |config| {
config.model_provider_id = GCP_PROVIDER_ID.to_string();
config.model_provider = provider;
})
.build_with_auto_env(&server)
.await?;
test.submit_turn("say hello").await?;
assert_eq!(
response.single_request().header("authorization").as_deref(),
Some("Bearer second-token"),
);
let observed = std::fs::read_to_string(credential_log)?;
let expected = credential_file.to_string_lossy();
assert_eq!(
observed.lines().collect::<Vec<_>>(),
vec![expected.as_ref(); 2]
);
assert_eq!(
std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS"),
inherited_credential_file,
);
Ok(())
}
#[test_case("gpt-6.1-sol"; "sol")]
#[test_case("gpt-6.1-terra"; "terra")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn vertex_requests_use_command_auth_with_namespaces_and_tool_search(
model: &str,
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response = responses::mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_completed("resp-1"),
]),
)
.await;
let auth_fixture = ProviderAuthCommandFixture::fixed_token("vertex-access-token")?;
let mut provider = ModelProviderInfo::create_gcp_provider();
provider.base_url = Some(format!(
"{}/v1/projects/example-project/locations/global/publishers/openai/v1",
server.uri()
));
provider.auth = Some(auth_fixture.auth());
let mut test = test_codex()
.with_model_info_override(model, |model| {
model.use_responses_lite = true;
model.tool_mode = Some(ToolMode::CodeModeOnly);
model.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
model.supports_search_tool = true;
model.multi_agent_version = Some(MultiAgentVersion::V2);
})
.with_model(model)
.with_config(move |config| {
config.model_provider_id = GCP_PROVIDER_ID.to_string();
config.model_provider = provider;
config.base_instructions = Some("Vertex test instructions".to_string());
})
.build_with_auto_env(&server)
.await?;
let input_schema = json!({
"type": "object",
"properties": {},
"additionalProperties": false,
});
let dynamic_tool = DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
name: "records".to_string(),
description: "Record tools.".to_string(),
tools: vec![
DynamicToolNamespaceTool::Function(DynamicToolFunctionSpec {
name: "status".to_string(),
description: "Get record service status.".to_string(),
input_schema: input_schema.clone(),
defer_loading: false,
}),
DynamicToolNamespaceTool::Function(DynamicToolFunctionSpec {
name: "lookup".to_string(),
description: "Look up a record.".to_string(),
input_schema: input_schema.clone(),
defer_loading: true,
}),
],
});
let thread = test
.thread_manager
.start_thread(StartThreadOptions {
dynamic_tools: vec![dynamic_tool],
..test.start_thread_options().await
})
.await?;
test.codex = thread.thread;
test.session_configured = thread.session_configured;
test.submit_turn("Look up a record").await?;
let request = response.single_request();
assert_eq!(
request.path(),
"/v1/projects/example-project/locations/global/publishers/openai/v1/responses"
);
assert_eq!(
request.header("authorization").as_deref(),
Some("Bearer vertex-access-token")
);
assert_eq!(
request.header("x-openai-internal-codex-responses-lite"),
None
);
assert!(
request.inputs_of_type("additional_tools").is_empty(),
"Vertex request must omit Responses Lite additional_tools inputs"
);
let body = request.body_json();
assert_eq!(body["model"], model);
assert_eq!(request.instructions_text(), "Vertex test instructions");
let tools = body["tools"]
.as_array()
.context("Vertex request should include model tools")?;
assert_eq!(
test.codex.multi_agent_version(),
Some(MultiAgentVersion::V2)
);
assert!(tools.iter().any(|tool| tool["name"] == "collaboration"));
let namespace = tools.iter().find(|tool| tool["name"] == "records");
assert_eq!(
namespace,
Some(&json!({
"type": "namespace",
"name": "records",
"description": "Record tools.",
"tools": [{
"type": "function",
"name": "status",
"description": "Get record service status.",
"strict": false,
"parameters": input_schema,
}],
}))
);
assert!(
tools
.iter()
.any(|tool| tool["type"] == "tool_search" && tool["execution"] == "client"),
"Vertex request should expose client-side tool search: {tools:?}"
);
assert!(
tools
.iter()
.any(|tool| tool.get("name").and_then(Value::as_str) == Some("exec_command")),
"Vertex request should preserve the exec_command function tool: {tools:?}"
);
Ok(())
}
#[test_case("gpt-6.1-sol"; "catalog model")]
#[test_case("vertex-test-model"; "fallback model")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn vertex_requests_omit_reasoning_summaries(model: &str) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response = responses::mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_completed("resp-1"),
]),
)
.await;
let auth_fixture = ProviderAuthCommandFixture::new(&["vertex-access-token"])?;
let mut provider = ModelProviderInfo::create_gcp_provider();
provider.base_url = Some(format!("{}/v1", server.uri()));
provider.auth = Some(auth_fixture.auth());
let test = test_codex()
.with_model_info_override("gpt-6.1-sol", |model_info| {
model_info.supports_reasoning_summary_parameter = true;
model_info.default_reasoning_summary = ReasoningSummary::Detailed;
})
.with_model(model)
.with_config(move |config| {
config.model_provider_id = GCP_PROVIDER_ID.to_string();
config.model_provider = provider;
config.model_reasoning_effort = Some(ReasoningEffort::High);
config.model_reasoning_summary = Some(ReasoningSummary::None);
config
.features
.enable(Feature::ConcurrentReasoningSummaries)
.expect("test config should allow feature update");
})
.build_with_auto_env(&server)
.await?;
test.codex
.start_or_steer_turn(
TurnInputRequest::user_input(vec![UserInput::Text {
text: "say hello".into(),
text_elements: Vec::new(),
}])
.with_thread_settings(ThreadSettingsOverrides {
summary: Some(ReasoningSummary::Concise),
..Default::default()
}),
)
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
let body = response.single_request().body_json();
assert_eq!(body["model"], model);
assert_eq!(body["reasoning"], json!({"effort": "high"}));
assert_eq!(body["include"], json!(["reasoning.encrypted_content"]));
assert_eq!(body.get("stream_options"), None);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn missing_vertex_credentials_explain_how_to_reauthenticate() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response = responses::mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("unused-response"),
responses::ev_completed("unused-response"),
]),
)
.await;
let auth_fixture = ProviderAuthCommandFixture::new(&[])?;
let mut provider = ModelProviderInfo::create_gcp_provider();
provider.base_url = Some(format!(
"{}/v1/projects/example-project/locations/global/publishers/openai/v1",
server.uri()
));
provider.auth = Some(auth_fixture.auth());
let test = test_codex()
.with_model("vertex-test-model")
.with_config(move |config| {
config.model_provider_id = GCP_PROVIDER_ID.to_string();
config.model_provider = provider;
})
.build_with_auto_env(&server)
.await?;
test.codex
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
text: "say hello".into(),
text_elements: Vec::new(),
}]))
.await?;
let EventMsg::Error(error) =
wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await
else {
unreachable!();
};
assert_eq!(
error.message,
"Google Cloud credentials are unavailable or expired. Run \
`gcloud auth application-default login` to reauthenticate."
);
assert!(response.requests().is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn vertex_guardian_requests_use_luna() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response = responses::mount_sse_sequence(
&server,
vec![
responses::sse(vec![
responses::ev_response_created("parent-tool"),
responses::ev_function_call(
"reviewed-tool",
"exec_command",
r#"{"cmd":"true","sandbox_permissions":"require_escalated","justification":"test"}"#,
),
responses::ev_completed("parent-tool"),
]),
responses::sse(vec![
responses::ev_response_created("guardian-review"),
responses::ev_assistant_message("guardian", r#"{"outcome":"deny"}"#),
responses::ev_completed("guardian-review"),
]),
responses::sse(vec![responses::ev_completed("parent-complete")]),
],
)
.await;
let auth_fixture = ProviderAuthCommandFixture::fixed_token("vertex-access-token")?;
let mut provider = ModelProviderInfo::create_gcp_provider();
provider.base_url = Some(format!("{}/v1", server.uri()));
provider.auth = Some(auth_fixture.auth());
let test = test_codex()
.with_model("gpt-6.1-sol")
.with_config(move |config| {
config.model_provider_id = GCP_PROVIDER_ID.to_string();
config.model_provider = provider;
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
})
.build_with_auto_env(&server)
.await?;
let models = test.thread_manager.get_models_manager().try_list_models()?;
insta::assert_debug_snapshot!(
"gcp_model_picker",
models
.iter()
.map(|model| (&model.model, model.show_in_picker, model.is_default))
.collect::<Vec<_>>()
);
test.submit_text_turn("run a command").await?;
let requests = response.requests();
assert_eq!(
requests
.iter()
.map(|request| request.body_json()["model"].clone())
.collect::<Vec<_>>(),
vec![
json!("gpt-6.1-sol"),
json!("gpt-6-luna"),
json!("gpt-6.1-sol")
]
);
let guardian_request = &requests[1];
assert_eq!(guardian_request.path(), "/v1/responses");
assert_eq!(
guardian_request.header("authorization").as_deref(),
Some("Bearer vertex-access-token")
);
Ok(())
}