mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
## What changed Return `CatalogModel` from model lookups, keeping metadata together with its `Bundled`, `Provider`, `ExplicitConfig`, or `Synthetic` origin through config overrides and runtime snapshots. Replace `used_fallback_model_metadata` checks with explicit `Synthetic` origin checks. Share the underlying `Arc<ModelInfo>` with extension stores and use copy-on-write for metadata changes. Keep origin tracking outside serialized model metadata. ## Testing Extend coverage for origin retention across cache reloads, namespaced model lookups, catalog fallback, and config overrides. Verify that step snapshots and extension stores share the same model metadata allocation. GitOrigin-RevId: bb6130c94a7a795aa168ee24664a1dd453579fba
216 lines
7.5 KiB
Rust
216 lines
7.5 KiB
Rust
use codex_api::AuthProvider;
|
|
use codex_api::ModelsClient;
|
|
use codex_api::Provider;
|
|
use codex_api::RetryConfig;
|
|
use codex_client::ReqwestTransport;
|
|
use codex_http_client::HttpClientBuilder;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::openai_models::ConfigShellToolType;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_protocol::openai_models::ModelVisibility;
|
|
use codex_protocol::openai_models::ModelsResponse;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::openai_models::ReasoningEffortPreset;
|
|
use codex_protocol::openai_models::TruncationPolicyConfig;
|
|
use codex_protocol::openai_models::default_input_modalities;
|
|
use http::HeaderMap;
|
|
use http::Method;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::sync::Arc;
|
|
use wiremock::Mock;
|
|
use wiremock::MockServer;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
|
|
#[derive(Clone, Default)]
|
|
struct DummyAuth;
|
|
|
|
impl AuthProvider for DummyAuth {
|
|
fn add_auth_headers(&self, _headers: &mut HeaderMap) -> Result<(), codex_api::AuthError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn provider(base_url: &str) -> Provider {
|
|
Provider {
|
|
name: "test".to_string(),
|
|
base_url: base_url.to_string(),
|
|
query_params: None,
|
|
headers: HeaderMap::new(),
|
|
retry: RetryConfig {
|
|
max_attempts: 1,
|
|
base_delay: std::time::Duration::from_millis(1),
|
|
retry_429: false,
|
|
retry_5xx: true,
|
|
retry_transport: true,
|
|
},
|
|
stream_idle_timeout: std::time::Duration::from_secs(1),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn invalid_models_response_reports_bounded_decode_metadata() {
|
|
let server = MockServer::start().await;
|
|
for (body, category, column_offset) in [
|
|
(
|
|
json!({ "data": [{ "id": "private-model" }] }).to_string(),
|
|
"Data",
|
|
0,
|
|
),
|
|
(
|
|
json!({ "models": "private-value".repeat(4096) }).to_string(),
|
|
"Data",
|
|
1,
|
|
),
|
|
(r#"{"models":]}"#.to_string(), "Syntax", 1),
|
|
(r#"{"models":["#.to_string(), "Eof", 0),
|
|
] {
|
|
let response = Mock::given(method("GET"))
|
|
.and(path("/models"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_string(&body))
|
|
.expect(1)
|
|
.mount_as_scoped(&server)
|
|
.await;
|
|
let transport = ReqwestTransport::from_http_client(
|
|
HttpClientBuilder::new()
|
|
.build_direct()
|
|
.expect("test HTTP client should build"),
|
|
);
|
|
let provider = provider(&server.uri());
|
|
let request_url = ModelsClient::<ReqwestTransport>::request_url(&provider, "0.1.0");
|
|
let client = ModelsClient::new(transport, provider, Arc::new(DummyAuth));
|
|
|
|
let error = client
|
|
.list_models(
|
|
request_url,
|
|
HeaderMap::new(),
|
|
/*response_body_limit_bytes*/ None,
|
|
)
|
|
.await
|
|
.expect_err("invalid catalog should fail to decode");
|
|
|
|
assert_eq!(
|
|
error.to_string(),
|
|
format!(
|
|
"stream error: failed to decode models response: {category} at line 1 column {} (body: {} bytes)",
|
|
body.len() - column_offset,
|
|
body.len()
|
|
)
|
|
);
|
|
drop(response);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn models_client_hits_models_endpoint() {
|
|
let server = MockServer::start().await;
|
|
let base_url = format!("{}/api/codex", server.uri());
|
|
|
|
let response = ModelsResponse {
|
|
models: vec![ModelInfo {
|
|
slug: "gpt-test".to_string(),
|
|
display_name: "gpt-test".to_string(),
|
|
description: Some("desc".to_string()),
|
|
default_reasoning_level: Some(ReasoningEffort::Medium),
|
|
supported_reasoning_levels: vec![
|
|
ReasoningEffortPreset {
|
|
effort: ReasoningEffort::Low,
|
|
description: ReasoningEffort::Low.to_string(),
|
|
},
|
|
ReasoningEffortPreset {
|
|
effort: ReasoningEffort::Medium,
|
|
description: ReasoningEffort::Medium.to_string(),
|
|
},
|
|
ReasoningEffortPreset {
|
|
effort: ReasoningEffort::High,
|
|
description: ReasoningEffort::High.to_string(),
|
|
},
|
|
],
|
|
shell_type: ConfigShellToolType::UnifiedExec,
|
|
visibility: ModelVisibility::List,
|
|
supported_in_api: true,
|
|
priority: 1,
|
|
additional_speed_tiers: Vec::new(),
|
|
service_tiers: Vec::new(),
|
|
default_service_tier: None,
|
|
available_access_programs: None,
|
|
upgrade: None,
|
|
model_messages: None,
|
|
include_skills_usage_instructions: false,
|
|
include_plugin_usage_instructions: false,
|
|
include_apps_usage_instructions: false,
|
|
supports_reasoning_summary_parameter: true,
|
|
default_reasoning_summary: ReasoningSummary::Auto,
|
|
support_verbosity: false,
|
|
default_verbosity: None,
|
|
availability_nux: None,
|
|
apply_patch_tool_type: None,
|
|
web_search_tool_type: Default::default(),
|
|
truncation_policy: TruncationPolicyConfig::bytes(/*limit*/ 10_000),
|
|
supports_image_detail_original: false,
|
|
context_window: Some(272_000),
|
|
max_context_window: None,
|
|
auto_compact_token_limit: None,
|
|
comp_hash: None,
|
|
effective_context_window_percent: 95,
|
|
experimental_supported_tools: Vec::new(),
|
|
input_modalities: default_input_modalities(),
|
|
supports_search_tool: false,
|
|
supports_experimental_context: false,
|
|
use_responses_lite: false,
|
|
supports_reasoning_effort_updates: true,
|
|
guardian: None,
|
|
node_repl_auto_review_required: true,
|
|
node_repl_disabled: true,
|
|
auto_review_model_override: None,
|
|
model_specialty: None,
|
|
tool_mode: None,
|
|
multi_agent_version: None,
|
|
multi_agent_reasoning_effort: None,
|
|
}],
|
|
};
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/api/codex/models"))
|
|
.respond_with(
|
|
ResponseTemplate::new(200)
|
|
.insert_header("content-type", "application/json")
|
|
.set_body_json(&response),
|
|
)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let transport = ReqwestTransport::from_http_client(
|
|
HttpClientBuilder::new()
|
|
.build_direct()
|
|
.expect("test HTTP client should build"),
|
|
);
|
|
let provider = provider(&base_url);
|
|
let request_url = ModelsClient::<ReqwestTransport>::request_url(&provider, "0.1.0");
|
|
let client = ModelsClient::new(transport, provider, Arc::new(DummyAuth));
|
|
|
|
let (models, _) = client
|
|
.list_models(
|
|
request_url,
|
|
HeaderMap::new(),
|
|
/*response_body_limit_bytes*/ None,
|
|
)
|
|
.await
|
|
.expect("models request should succeed");
|
|
|
|
assert_eq!(models.len(), 1);
|
|
assert_eq!(models[0].slug, "gpt-test");
|
|
assert!(models[0].node_repl_auto_review_required);
|
|
assert!(models[0].node_repl_disabled);
|
|
|
|
let received = server
|
|
.received_requests()
|
|
.await
|
|
.expect("should capture requests");
|
|
assert_eq!(received.len(), 1);
|
|
assert_eq!(received[0].method, Method::GET.as_str());
|
|
assert_eq!(received[0].url.path(), "/api/codex/models");
|
|
}
|