mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-15 07:30:21 +08:00
fix(codex): restore upstream model discovery
This commit is contained in:
@@ -399,7 +399,8 @@ async fn provider_query_read_cached_models(
|
|||||||
let cache_key = format!("upstream_models:{provider_id}:{key_id}");
|
let cache_key = format!("upstream_models:{provider_id}:{key_id}");
|
||||||
let raw = state.runtime_state().kv_get(&cache_key).await.ok()??;
|
let raw = state.runtime_state().kv_get(&cache_key).await.ok()??;
|
||||||
let parsed = serde_json::from_str::<Vec<Value>>(&raw).ok()?;
|
let parsed = serde_json::from_str::<Vec<Value>>(&raw).ok()?;
|
||||||
Some(aggregate_models_for_cache(&parsed))
|
let models = aggregate_models_for_cache(&parsed);
|
||||||
|
(!models.is_empty()).then_some(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn provider_query_read_provider_cached_models(
|
async fn provider_query_read_provider_cached_models(
|
||||||
@@ -409,7 +410,8 @@ async fn provider_query_read_provider_cached_models(
|
|||||||
let cache_key = format!("{ANTIGRAVITY_PROVIDER_CACHE_KEY_PREFIX}{provider_id}");
|
let cache_key = format!("{ANTIGRAVITY_PROVIDER_CACHE_KEY_PREFIX}{provider_id}");
|
||||||
let raw = state.runtime_state().kv_get(&cache_key).await.ok()??;
|
let raw = state.runtime_state().kv_get(&cache_key).await.ok()??;
|
||||||
let parsed = serde_json::from_str::<Vec<Value>>(&raw).ok()?;
|
let parsed = serde_json::from_str::<Vec<Value>>(&raw).ok()?;
|
||||||
Some(aggregate_models_for_cache(&parsed))
|
let models = aggregate_models_for_cache(&parsed);
|
||||||
|
(!models.is_empty()).then_some(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn provider_query_write_provider_cached_models(
|
async fn provider_query_write_provider_cached_models(
|
||||||
@@ -417,7 +419,11 @@ async fn provider_query_write_provider_cached_models(
|
|||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
models: &[Value],
|
models: &[Value],
|
||||||
) {
|
) {
|
||||||
let Ok(serialized) = serde_json::to_string(&aggregate_models_for_cache(models)) else {
|
let models = aggregate_models_for_cache(models);
|
||||||
|
if models.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(serialized) = serde_json::to_string(&models) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let cache_key = format!("{ANTIGRAVITY_PROVIDER_CACHE_KEY_PREFIX}{provider_id}");
|
let cache_key = format!("{ANTIGRAVITY_PROVIDER_CACHE_KEY_PREFIX}{provider_id}");
|
||||||
@@ -577,7 +583,7 @@ async fn provider_query_fetch_models_for_key(
|
|||||||
};
|
};
|
||||||
|
|
||||||
all_errors.extend(outcome.errors);
|
all_errors.extend(outcome.errors);
|
||||||
let unique_models = aggregate_models_for_cache(&outcome.cached_models);
|
let unique_models = outcome.legacy_models;
|
||||||
if outcome.has_success && !unique_models.is_empty() {
|
if outcome.has_success && !unique_models.is_empty() {
|
||||||
<AppState as ModelFetchRuntimeState>::write_upstream_models_cache(
|
<AppState as ModelFetchRuntimeState>::write_upstream_models_cache(
|
||||||
state.app(),
|
state.app(),
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ async fn fetch_and_persist_key_models(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
state
|
state
|
||||||
.write_upstream_models_cache(&target.provider.id, &target.key.id, &result.cached_models)
|
.write_upstream_models_cache(&target.provider.id, &target.key.id, &result.legacy_models)
|
||||||
.await;
|
.await;
|
||||||
sync_provider_model_whitelist_associations(state, &target.provider.id, &filtered_models)
|
sync_provider_model_whitelist_associations(state, &target.provider.id, &filtered_models)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -469,8 +469,11 @@ impl ModelFetchRuntimeState for AppState {
|
|||||||
key_id: &str,
|
key_id: &str,
|
||||||
cached_models: &[Value],
|
cached_models: &[Value],
|
||||||
) {
|
) {
|
||||||
let Ok(serialized) = serde_json::to_string(&aggregate_models_for_cache(cached_models))
|
let models = aggregate_models_for_cache(cached_models);
|
||||||
else {
|
if models.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(serialized) = serde_json::to_string(&models) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let cache_key = format!("upstream_models:{provider_id}:{key_id}");
|
let cache_key = format!("upstream_models:{provider_id}:{key_id}");
|
||||||
|
|||||||
@@ -537,6 +537,159 @@ async fn gateway_handles_admin_provider_query_models_with_openai_responses_endpo
|
|||||||
execution_runtime_handle.abort();
|
execution_runtime_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gateway_recovers_codex_slug_only_models_from_an_empty_legacy_cache() {
|
||||||
|
run_provider_query_test(
|
||||||
|
"gateway_recovers_codex_slug_only_models_from_an_empty_legacy_cache",
|
||||||
|
gateway_recovers_codex_slug_only_models_from_an_empty_legacy_cache_impl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn gateway_recovers_codex_slug_only_models_from_an_empty_legacy_cache_impl() {
|
||||||
|
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
|
||||||
|
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
|
||||||
|
let execution_runtime = Router::new().route(
|
||||||
|
"/v1/execute/sync",
|
||||||
|
any(move |Json(plan): Json<ExecutionPlan>| {
|
||||||
|
let execution_runtime_hits_inner = Arc::clone(&execution_runtime_hits_clone);
|
||||||
|
async move {
|
||||||
|
*execution_runtime_hits_inner
|
||||||
|
.lock()
|
||||||
|
.expect("mutex should lock") += 1;
|
||||||
|
assert_eq!(
|
||||||
|
plan.url,
|
||||||
|
"https://chatgpt.com/backend-api/codex/models?client_version=0.144.1"
|
||||||
|
);
|
||||||
|
assert_eq!(plan.provider_api_format, "openai:responses");
|
||||||
|
Json(json!({
|
||||||
|
"request_id": "req-provider-query-codex-slug-only",
|
||||||
|
"status_code": 200,
|
||||||
|
"headers": {
|
||||||
|
"content-type": "application/json"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"json_body": {
|
||||||
|
"models": [{
|
||||||
|
"slug": "gpt-future-dynamic",
|
||||||
|
"display_name": "Future Dynamic",
|
||||||
|
"description": "A model unknown to this Aether build",
|
||||||
|
"model_messages": {
|
||||||
|
"instructions_template": "Follow the dynamic instructions."
|
||||||
|
},
|
||||||
|
"api_format": "opaque-upstream-protocol",
|
||||||
|
"future_capability": {
|
||||||
|
"opaque": true,
|
||||||
|
"schema_version": 7
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||||
|
let mut provider = sample_provider("provider-codex-dynamic", "Codex Dynamic", 10);
|
||||||
|
provider.provider_type = "codex".to_string();
|
||||||
|
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![provider],
|
||||||
|
vec![sample_endpoint(
|
||||||
|
"endpoint-codex-dynamic",
|
||||||
|
"provider-codex-dynamic",
|
||||||
|
"openai:responses",
|
||||||
|
"https://chatgpt.com/backend-api/codex",
|
||||||
|
)],
|
||||||
|
vec![sample_key(
|
||||||
|
"key-codex-dynamic",
|
||||||
|
"provider-codex-dynamic",
|
||||||
|
"openai:responses",
|
||||||
|
"codex-dynamic-token",
|
||||||
|
)],
|
||||||
|
));
|
||||||
|
|
||||||
|
let state = build_state_with_execution_runtime_override(execution_runtime_url)
|
||||||
|
.with_data_state_for_tests(GatewayDataState::with_provider_transport_reader_for_tests(
|
||||||
|
provider_catalog_repository,
|
||||||
|
DEVELOPMENT_ENCRYPTION_KEY.to_string(),
|
||||||
|
));
|
||||||
|
state
|
||||||
|
.runtime_state()
|
||||||
|
.kv_set(
|
||||||
|
"upstream_models:provider-codex-dynamic:key-codex-dynamic",
|
||||||
|
"[]".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("empty legacy cache should seed");
|
||||||
|
let cache_state = state.clone();
|
||||||
|
let gateway = build_router_with_state(state);
|
||||||
|
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||||
|
|
||||||
|
for (request_index, expected_from_cache) in [(0usize, false), (1usize, true)] {
|
||||||
|
let response = reqwest::Client::new()
|
||||||
|
.post(format!("{gateway_url}/api/admin/provider-query/models"))
|
||||||
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
|
.json(&json!({
|
||||||
|
"provider_id": "provider-codex-dynamic",
|
||||||
|
"api_key_id": "key-codex-dynamic"
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||||
|
assert_eq!(payload["success"], json!(true));
|
||||||
|
assert_eq!(payload["data"]["error"], serde_json::Value::Null);
|
||||||
|
assert_eq!(
|
||||||
|
payload["data"]["from_cache"],
|
||||||
|
json!(expected_from_cache),
|
||||||
|
"request {request_index} cache status"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["data"]["models"][0]["id"],
|
||||||
|
json!("gpt-future-dynamic")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["data"]["models"][0]["api_formats"],
|
||||||
|
json!(["openai:responses"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["data"]["models"][0]["api_format"],
|
||||||
|
json!("opaque-upstream-protocol")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["data"]["models"][0]["model_messages"]["instructions_template"],
|
||||||
|
json!("Follow the dynamic instructions.")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["data"]["models"][0]["future_capability"],
|
||||||
|
json!({"opaque": true, "schema_version": 7})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*execution_runtime_hits.lock().expect("mutex should lock"),
|
||||||
|
1,
|
||||||
|
"request {request_index} must not cause another upstream fetch"
|
||||||
|
);
|
||||||
|
if request_index == 0 {
|
||||||
|
<AppState as crate::model_fetch::ModelFetchRuntimeState>::write_upstream_models_cache(
|
||||||
|
&cache_state,
|
||||||
|
"provider-codex-dynamic",
|
||||||
|
"key-codex-dynamic",
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway_handle.abort();
|
||||||
|
execution_runtime_handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_when_token_invalidated() {
|
fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_when_token_invalidated() {
|
||||||
run_provider_query_test(
|
run_provider_query_test(
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ pub use logic::{
|
|||||||
endpoint_supports_rust_models_fetch, extract_error_message, json_string_list,
|
endpoint_supports_rust_models_fetch, extract_error_message, json_string_list,
|
||||||
merge_upstream_metadata, model_catalog_upstream_metadata, parse_models_response,
|
merge_upstream_metadata, model_catalog_upstream_metadata, parse_models_response,
|
||||||
parse_models_response_page, parse_windsurf_model_configs_response, preset_models_for_provider,
|
parse_models_response_page, parse_windsurf_model_configs_response, preset_models_for_provider,
|
||||||
provider_type_uses_preset_models, select_models_fetch_endpoint,
|
project_codex_models_for_legacy_cache, provider_type_uses_preset_models,
|
||||||
selected_models_fetch_endpoints, upstream_metadata_namespace_updates, ModelFetchRunSummary,
|
select_models_fetch_endpoint, selected_models_fetch_endpoints,
|
||||||
ModelsFetchPage, ModelsFetchSuccess,
|
upstream_metadata_namespace_updates, ModelFetchRunSummary, ModelsFetchPage, ModelsFetchSuccess,
|
||||||
};
|
};
|
||||||
pub use strategy::{
|
pub use strategy::{
|
||||||
fetch_models_from_transports, fetch_models_from_transports_for_client_version,
|
fetch_models_from_transports, fetch_models_from_transports_for_client_version,
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ fn codex_model_identities(model: &Value) -> Result<Vec<&str>, String> {
|
|||||||
|
|
||||||
pub(crate) fn codex_model_identity(model: &Value) -> Option<&str> {
|
pub(crate) fn codex_model_identity(model: &Value) -> Option<&str> {
|
||||||
let object = model.as_object()?;
|
let object = model.as_object()?;
|
||||||
["id", "slug"].iter().find_map(|field| {
|
["slug", "id"].iter().find_map(|field| {
|
||||||
object
|
object
|
||||||
.get(*field)
|
.get(*field)
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
@@ -312,6 +312,67 @@ pub(crate) fn codex_model_identity(model: &Value) -> Option<&str> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Projects opaque Codex cards into the legacy model-cache shape used by permission sync.
|
||||||
|
///
|
||||||
|
/// The source cards remain untouched. Only formats from transports that actually returned the
|
||||||
|
/// card are admitted into `api_formats`; an upstream `api_format` field is protocol data and is
|
||||||
|
/// preserved as-is rather than interpreted as an Aether endpoint format.
|
||||||
|
pub fn project_codex_models_for_legacy_cache<'a>(
|
||||||
|
successful_transports: impl IntoIterator<Item = (&'a str, &'a [Value])>,
|
||||||
|
) -> Vec<Value> {
|
||||||
|
let mut projected = BTreeMap::<String, serde_json::Map<String, Value>>::new();
|
||||||
|
|
||||||
|
for (endpoint_api_format, models) in successful_transports {
|
||||||
|
let api_format = normalize_api_format(endpoint_api_format);
|
||||||
|
if api_format.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for model in models {
|
||||||
|
let Some(model_id) = codex_model_identity(model).map(ToOwned::to_owned) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(source) = model.as_object() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = projected.entry(model_id.clone()).or_insert_with(|| {
|
||||||
|
let mut card = source.clone();
|
||||||
|
card.insert("id".to_string(), Value::String(model_id));
|
||||||
|
// `api_formats` is Aether's routing projection. Never inherit a similarly named
|
||||||
|
// opaque upstream field when constructing this legacy view.
|
||||||
|
card.insert("api_formats".to_string(), Value::Array(Vec::new()));
|
||||||
|
card
|
||||||
|
});
|
||||||
|
let mut formats = entry
|
||||||
|
.get("api_formats")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
formats.insert(api_format.clone());
|
||||||
|
entry.insert(
|
||||||
|
"api_formats".to_string(),
|
||||||
|
Value::Array(
|
||||||
|
sorted_api_formats(formats)
|
||||||
|
.into_iter()
|
||||||
|
.map(Value::String)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
projected.into_values().map(Value::Object).collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn valid_codex_model_identity(value: &str) -> bool {
|
fn valid_codex_model_identity(value: &str) -> bool {
|
||||||
!value.is_empty()
|
!value.is_empty()
|
||||||
&& value.len() <= 256
|
&& value.len() <= 256
|
||||||
@@ -715,9 +776,15 @@ pub fn aggregate_models_for_cache(models: &[Value]) -> Vec<Value> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let has_api_formats_array = object
|
||||||
|
.get("api_formats")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.is_some();
|
||||||
let entry = aggregated.entry(model_id.to_string()).or_insert_with(|| {
|
let entry = aggregated.entry(model_id.to_string()).or_insert_with(|| {
|
||||||
let mut cloned = object.clone();
|
let mut cloned = object.clone();
|
||||||
cloned.remove("api_format");
|
if !has_api_formats_array {
|
||||||
|
cloned.remove("api_format");
|
||||||
|
}
|
||||||
cloned
|
cloned
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -734,12 +801,16 @@ pub fn aggregate_models_for_cache(models: &[Value]) -> Vec<Value> {
|
|||||||
.collect::<BTreeSet<_>>()
|
.collect::<BTreeSet<_>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let legacy_api_format = object
|
let legacy_api_format = (!has_api_formats_array)
|
||||||
.get("api_format")
|
.then(|| {
|
||||||
.and_then(Value::as_str)
|
object
|
||||||
.map(str::trim)
|
.get("api_format")
|
||||||
.filter(|value| !value.is_empty())
|
.and_then(Value::as_str)
|
||||||
.map(ToOwned::to_owned);
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
let existing_formats = entry
|
let existing_formats = entry
|
||||||
.get("api_formats")
|
.get("api_formats")
|
||||||
.and_then(Value::as_array)
|
.and_then(Value::as_array)
|
||||||
@@ -767,7 +838,13 @@ pub fn aggregate_models_for_cache(models: &[Value]) -> Vec<Value> {
|
|||||||
entry.insert("api_formats".to_string(), Value::Array(merged_formats));
|
entry.insert("api_formats".to_string(), Value::Array(merged_formats));
|
||||||
|
|
||||||
for (key, value) in object {
|
for (key, value) in object {
|
||||||
if key == "api_format" || entry.contains_key(key) {
|
if key == "api_format" {
|
||||||
|
if has_api_formats_array && !entry.contains_key(key) {
|
||||||
|
entry.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if entry.contains_key(key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
entry.insert(key.clone(), value.clone());
|
entry.insert(key.clone(), value.clone());
|
||||||
@@ -1082,7 +1159,8 @@ mod tests {
|
|||||||
aggregate_models_for_cache, apply_model_filters, build_gemini_models_url,
|
aggregate_models_for_cache, apply_model_filters, build_gemini_models_url,
|
||||||
build_models_fetch_url, build_models_fetch_url_for_client_version, merge_upstream_metadata,
|
build_models_fetch_url, build_models_fetch_url_for_client_version, merge_upstream_metadata,
|
||||||
parse_codex_models_response_page, parse_models_response, parse_models_response_page,
|
parse_codex_models_response_page, parse_models_response, parse_models_response_page,
|
||||||
preset_models_for_provider, selected_models_fetch_endpoints,
|
preset_models_for_provider, project_codex_models_for_legacy_cache,
|
||||||
|
selected_models_fetch_endpoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn sample_endpoint(
|
fn sample_endpoint(
|
||||||
@@ -1200,6 +1278,27 @@ mod tests {
|
|||||||
assert!(aggregated[0].get("api_format").is_none());
|
assert!(aggregated[0].get("api_format").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregate_models_for_cache_preserves_opaque_api_format_on_projected_cards() {
|
||||||
|
let card = json!({
|
||||||
|
"slug": "gpt-slug-only-future",
|
||||||
|
"api_format": "opaque-upstream-protocol",
|
||||||
|
"model_messages": {"instructions_template": "Future instructions"},
|
||||||
|
"future_capability": {"opaque": true}
|
||||||
|
});
|
||||||
|
let cards = vec![card];
|
||||||
|
let projected =
|
||||||
|
project_codex_models_for_legacy_cache([("openai:responses", cards.as_slice())]);
|
||||||
|
|
||||||
|
let aggregated = aggregate_models_for_cache(&projected);
|
||||||
|
|
||||||
|
assert_eq!(aggregated.len(), 1);
|
||||||
|
assert_eq!(aggregated[0]["id"], "gpt-slug-only-future");
|
||||||
|
assert_eq!(aggregated[0]["api_format"], "opaque-upstream-protocol");
|
||||||
|
assert_eq!(aggregated[0]["api_formats"], json!(["openai:responses"]));
|
||||||
|
assert_eq!(aggregated[0]["future_capability"]["opaque"], true);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_gemini_models_url_preserves_base_query() {
|
fn build_gemini_models_url_preserves_base_query() {
|
||||||
let url =
|
let url =
|
||||||
@@ -1466,6 +1565,38 @@ mod tests {
|
|||||||
assert_eq!(parsed.cached_models, vec![card]);
|
assert_eq!(parsed.cached_models, vec![card]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codex_legacy_projector_adds_internal_identity_and_only_successful_endpoint_formats() {
|
||||||
|
let card = json!({
|
||||||
|
"id": "opaque-upstream-id",
|
||||||
|
"slug": "gpt-slug-only-future",
|
||||||
|
"api_format": "opaque-upstream-protocol",
|
||||||
|
"api_formats": ["opaque-upstream-format-list"],
|
||||||
|
"model_messages": {"instructions_template": "Future instructions"},
|
||||||
|
"future_capability": {"opaque": true}
|
||||||
|
});
|
||||||
|
let cards = vec![card.clone()];
|
||||||
|
|
||||||
|
let projected = project_codex_models_for_legacy_cache([
|
||||||
|
("openai:responses", cards.as_slice()),
|
||||||
|
("openai:chat", cards.as_slice()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(cards, vec![card]);
|
||||||
|
assert_eq!(projected.len(), 1);
|
||||||
|
assert_eq!(projected[0]["id"], "gpt-slug-only-future");
|
||||||
|
assert_eq!(
|
||||||
|
projected[0]["api_formats"],
|
||||||
|
json!(["openai:chat", "openai:responses"])
|
||||||
|
);
|
||||||
|
assert_eq!(projected[0]["api_format"], "opaque-upstream-protocol");
|
||||||
|
assert_eq!(
|
||||||
|
projected[0]["model_messages"]["instructions_template"],
|
||||||
|
"Future instructions"
|
||||||
|
);
|
||||||
|
assert_eq!(projected[0]["future_capability"]["opaque"], true);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strict_codex_parser_rejects_empty_models_array() {
|
fn strict_codex_parser_rejects_empty_models_array() {
|
||||||
let error = parse_codex_models_response_page(&json!({"models": []}))
|
let error = parse_codex_models_response_page(&json!({"models": []}))
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use crate::logic::{
|
|||||||
aggregate_models_for_cache, codex_model_identity, extract_error_message,
|
aggregate_models_for_cache, codex_model_identity, extract_error_message,
|
||||||
merge_codex_models_preserving_cards, parse_codex_models_response_page,
|
merge_codex_models_preserving_cards, parse_codex_models_response_page,
|
||||||
parse_models_response_page, parse_windsurf_model_configs_response, preset_models_for_provider,
|
parse_models_response_page, parse_windsurf_model_configs_response, preset_models_for_provider,
|
||||||
|
project_codex_models_for_legacy_cache,
|
||||||
};
|
};
|
||||||
use crate::transport::{
|
use crate::transport::{
|
||||||
build_antigravity_fetch_available_models_plan, build_antigravity_load_code_assist_plan,
|
build_antigravity_fetch_available_models_plan, build_antigravity_load_code_assist_plan,
|
||||||
@@ -46,7 +47,10 @@ const GOOGLE_CLOUD_PLATFORM_SCOPE: &str = "https://www.googleapis.com/auth/cloud
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ModelsFetchOutcome {
|
pub struct ModelsFetchOutcome {
|
||||||
pub fetched_model_ids: Vec<String>,
|
pub fetched_model_ids: Vec<String>,
|
||||||
|
/// Provider response cards. Versioned Codex catalogs remain byte-for-byte opaque here.
|
||||||
pub cached_models: Vec<Value>,
|
pub cached_models: Vec<Value>,
|
||||||
|
/// Provider cards projected into Aether's legacy admin/runtime-cache shape.
|
||||||
|
pub legacy_models: Vec<Value>,
|
||||||
pub errors: Vec<String>,
|
pub errors: Vec<String>,
|
||||||
pub has_success: bool,
|
pub has_success: bool,
|
||||||
pub upstream_metadata: Option<Value>,
|
pub upstream_metadata: Option<Value>,
|
||||||
@@ -253,15 +257,21 @@ async fn fetch_standard_models(
|
|||||||
codex_client_version: Option<&str>,
|
codex_client_version: Option<&str>,
|
||||||
) -> Result<ModelsFetchOutcome, String> {
|
) -> Result<ModelsFetchOutcome, String> {
|
||||||
let mut all_models = Vec::new();
|
let mut all_models = Vec::new();
|
||||||
|
let mut successful_codex_catalogs = Vec::<(String, Vec<Value>)>::new();
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut has_success = false;
|
let mut has_success = false;
|
||||||
let mut etag = ConsistentValue::default();
|
let mut etag = ConsistentValue::default();
|
||||||
let mut upstream_status = ConsistentValue::default();
|
let mut upstream_status = ConsistentValue::default();
|
||||||
|
let is_codex = provider_type.trim().eq_ignore_ascii_case("codex");
|
||||||
|
|
||||||
for transport in transports {
|
for transport in transports {
|
||||||
match fetch_standard_models_for_transport(runtime, transport, codex_client_version).await {
|
match fetch_standard_models_for_transport(runtime, transport, codex_client_version).await {
|
||||||
Ok(outcome) => {
|
Ok(outcome) => {
|
||||||
all_models.extend(outcome.cached_models);
|
all_models.extend(outcome.cached_models.iter().cloned());
|
||||||
|
if is_codex && outcome.has_success {
|
||||||
|
successful_codex_catalogs
|
||||||
|
.push((transport.endpoint.api_format.clone(), outcome.cached_models));
|
||||||
|
}
|
||||||
has_success |= outcome.has_success;
|
has_success |= outcome.has_success;
|
||||||
if outcome.has_success {
|
if outcome.has_success {
|
||||||
etag.observe(outcome.etag);
|
etag.observe(outcome.etag);
|
||||||
@@ -275,7 +285,6 @@ async fn fetch_standard_models(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_codex = provider_type.trim().eq_ignore_ascii_case("codex");
|
|
||||||
let merged_models = if is_codex {
|
let merged_models = if is_codex {
|
||||||
merge_codex_models_preserving_cards(&all_models)?
|
merge_codex_models_preserving_cards(&all_models)?
|
||||||
} else {
|
} else {
|
||||||
@@ -287,6 +296,11 @@ async fn fetch_standard_models(
|
|||||||
let mut outcome = build_success_outcome(merged_models, upstream_metadata, has_success);
|
let mut outcome = build_success_outcome(merged_models, upstream_metadata, has_success);
|
||||||
if let Some(model_ids) = codex_model_ids {
|
if let Some(model_ids) = codex_model_ids {
|
||||||
outcome.fetched_model_ids = model_ids;
|
outcome.fetched_model_ids = model_ids;
|
||||||
|
outcome.legacy_models = project_codex_models_for_legacy_cache(
|
||||||
|
successful_codex_catalogs
|
||||||
|
.iter()
|
||||||
|
.map(|(api_format, models)| (api_format.as_str(), models.as_slice())),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(outcome
|
Ok(outcome
|
||||||
.with_errors(errors)
|
.with_errors(errors)
|
||||||
@@ -328,7 +342,11 @@ async fn fetch_standard_models_for_transport(
|
|||||||
let body_json =
|
let body_json =
|
||||||
execution_result_json_body(&result).map_err(|err| (err, Some(result.status_code)))?;
|
execution_result_json_body(&result).map_err(|err| (err, Some(result.status_code)))?;
|
||||||
let parsed = if is_codex {
|
let parsed = if is_codex {
|
||||||
parse_codex_models_response_page(&body_json)
|
parse_codex_models_response_for_request(
|
||||||
|
&transport.endpoint.api_format,
|
||||||
|
&body_json,
|
||||||
|
codex_client_version,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
parse_models_response_page(&transport.endpoint.api_format, &body_json)
|
parse_models_response_page(&transport.endpoint.api_format, &body_json)
|
||||||
}
|
}
|
||||||
@@ -372,6 +390,19 @@ async fn fetch_standard_models_for_transport(
|
|||||||
.with_upstream_status(upstream_status.finish()))
|
.with_upstream_status(upstream_status.finish()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_codex_models_response_for_request(
|
||||||
|
endpoint_api_format: &str,
|
||||||
|
body: &Value,
|
||||||
|
codex_client_version: Option<&str>,
|
||||||
|
) -> Result<crate::logic::ModelsFetchPage, String> {
|
||||||
|
if codex_client_version.is_none()
|
||||||
|
&& (body.is_array() || body.get("data").and_then(Value::as_array).is_some())
|
||||||
|
{
|
||||||
|
return parse_models_response_page(endpoint_api_format, body);
|
||||||
|
}
|
||||||
|
parse_codex_models_response_page(body)
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_antigravity_models(
|
async fn fetch_antigravity_models(
|
||||||
runtime: &(impl ModelFetchTransportRuntime + ?Sized),
|
runtime: &(impl ModelFetchTransportRuntime + ?Sized),
|
||||||
transport: &GatewayProviderTransportSnapshot,
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
@@ -425,6 +456,7 @@ async fn fetch_antigravity_models(
|
|||||||
Ok(ModelsFetchOutcome {
|
Ok(ModelsFetchOutcome {
|
||||||
fetched_model_ids: Vec::new(),
|
fetched_model_ids: Vec::new(),
|
||||||
cached_models: Vec::new(),
|
cached_models: Vec::new(),
|
||||||
|
legacy_models: Vec::new(),
|
||||||
errors,
|
errors,
|
||||||
has_success: false,
|
has_success: false,
|
||||||
upstream_metadata: None,
|
upstream_metadata: None,
|
||||||
@@ -607,6 +639,7 @@ async fn fetch_vertex_api_key_models(
|
|||||||
return Ok(ModelsFetchOutcome {
|
return Ok(ModelsFetchOutcome {
|
||||||
fetched_model_ids: Vec::new(),
|
fetched_model_ids: Vec::new(),
|
||||||
cached_models: Vec::new(),
|
cached_models: Vec::new(),
|
||||||
|
legacy_models: Vec::new(),
|
||||||
errors: vec!["vertex_ai(api_key): missing api key".to_string()],
|
errors: vec!["vertex_ai(api_key): missing api key".to_string()],
|
||||||
has_success: false,
|
has_success: false,
|
||||||
upstream_metadata: None,
|
upstream_metadata: None,
|
||||||
@@ -666,6 +699,7 @@ async fn fetch_vertex_api_key_models(
|
|||||||
Ok(ModelsFetchOutcome {
|
Ok(ModelsFetchOutcome {
|
||||||
fetched_model_ids: Vec::new(),
|
fetched_model_ids: Vec::new(),
|
||||||
cached_models: Vec::new(),
|
cached_models: Vec::new(),
|
||||||
|
legacy_models: Vec::new(),
|
||||||
errors,
|
errors,
|
||||||
has_success,
|
has_success,
|
||||||
upstream_metadata: None,
|
upstream_metadata: None,
|
||||||
@@ -683,6 +717,7 @@ async fn fetch_vertex_service_account_models(
|
|||||||
return Ok(ModelsFetchOutcome {
|
return Ok(ModelsFetchOutcome {
|
||||||
fetched_model_ids: Vec::new(),
|
fetched_model_ids: Vec::new(),
|
||||||
cached_models: Vec::new(),
|
cached_models: Vec::new(),
|
||||||
|
legacy_models: Vec::new(),
|
||||||
errors: vec!["vertex_ai(service_account): missing auth_config".to_string()],
|
errors: vec!["vertex_ai(service_account): missing auth_config".to_string()],
|
||||||
has_success: false,
|
has_success: false,
|
||||||
upstream_metadata: None,
|
upstream_metadata: None,
|
||||||
@@ -753,6 +788,7 @@ async fn fetch_vertex_service_account_models(
|
|||||||
Ok(ModelsFetchOutcome {
|
Ok(ModelsFetchOutcome {
|
||||||
fetched_model_ids: Vec::new(),
|
fetched_model_ids: Vec::new(),
|
||||||
cached_models: Vec::new(),
|
cached_models: Vec::new(),
|
||||||
|
legacy_models: Vec::new(),
|
||||||
errors,
|
errors,
|
||||||
has_success,
|
has_success,
|
||||||
upstream_metadata: None,
|
upstream_metadata: None,
|
||||||
@@ -1367,9 +1403,11 @@ fn build_success_outcome(
|
|||||||
upstream_metadata: Option<Value>,
|
upstream_metadata: Option<Value>,
|
||||||
has_success: bool,
|
has_success: bool,
|
||||||
) -> ModelsFetchOutcome {
|
) -> ModelsFetchOutcome {
|
||||||
|
let legacy_models = cached_models.clone();
|
||||||
ModelsFetchOutcome {
|
ModelsFetchOutcome {
|
||||||
fetched_model_ids: collect_model_ids(&cached_models),
|
fetched_model_ids: collect_model_ids(&cached_models),
|
||||||
cached_models,
|
cached_models,
|
||||||
|
legacy_models,
|
||||||
errors: Vec::new(),
|
errors: Vec::new(),
|
||||||
has_success,
|
has_success,
|
||||||
upstream_metadata,
|
upstream_metadata,
|
||||||
@@ -1580,7 +1618,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
build_vertex_google_list_url, build_vertex_service_account_list_url,
|
build_vertex_google_list_url, build_vertex_service_account_list_url,
|
||||||
select_model_fetch_strategy, ModelFetchStrategy, ModelFetchStrategyKind,
|
parse_codex_models_response_for_request, select_model_fetch_strategy, ModelFetchStrategy,
|
||||||
|
ModelFetchStrategyKind,
|
||||||
};
|
};
|
||||||
use crate::transport::ModelFetchTransportRuntime;
|
use crate::transport::ModelFetchTransportRuntime;
|
||||||
use crate::{fetch_models_from_transports, fetch_models_from_transports_for_client_version};
|
use crate::{fetch_models_from_transports, fetch_models_from_transports_for_client_version};
|
||||||
@@ -1929,6 +1968,30 @@ mod tests {
|
|||||||
assert_eq!(strategy.kind(), ModelFetchStrategyKind::StandardTransport);
|
assert_eq!(strategy.kind(), ModelFetchStrategyKind::StandardTransport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unversioned_codex_parser_accepts_top_level_openai_compatible_array() {
|
||||||
|
let parsed = parse_codex_models_response_for_request(
|
||||||
|
"openai:responses",
|
||||||
|
&json!([{"id": "gpt-array-compatible"}]),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("unversioned admin fetch should retain the top-level array fallback");
|
||||||
|
|
||||||
|
assert_eq!(parsed.fetched_model_ids, vec!["gpt-array-compatible"]);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.cached_models[0]["api_formats"],
|
||||||
|
json!(["openai:responses"])
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = parse_codex_models_response_for_request(
|
||||||
|
"openai:responses",
|
||||||
|
&json!([{"id": "gpt-array-compatible"}]),
|
||||||
|
Some("0.145.2"),
|
||||||
|
)
|
||||||
|
.expect_err("versioned catalogs must use the opaque models-array schema");
|
||||||
|
assert!(error.contains("missing models array"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strategy_selection_uses_preset_catalog_for_claude_code() {
|
fn strategy_selection_uses_preset_catalog_for_claude_code() {
|
||||||
let mut transport = sample_custom_aiplatform_transport();
|
let mut transport = sample_custom_aiplatform_transport();
|
||||||
@@ -2044,6 +2107,7 @@ mod tests {
|
|||||||
vec!["responses-only", "shared-model"]
|
vec!["responses-only", "shared-model"]
|
||||||
);
|
);
|
||||||
assert_eq!(outcome.cached_models.len(), 2);
|
assert_eq!(outcome.cached_models.len(), 2);
|
||||||
|
assert_eq!(outcome.legacy_models, outcome.cached_models);
|
||||||
assert_eq!(outcome.errors.len(), 1);
|
assert_eq!(outcome.errors.len(), 1);
|
||||||
assert!(outcome.errors[0].contains("connection reset"));
|
assert!(outcome.errors[0].contains("connection reset"));
|
||||||
let shared_model = outcome
|
let shared_model = outcome
|
||||||
@@ -2209,6 +2273,16 @@ mod tests {
|
|||||||
"opaque-future-field"
|
"opaque-future-field"
|
||||||
);
|
);
|
||||||
assert!(outcome.cached_models[0].get("api_formats").is_none());
|
assert!(outcome.cached_models[0].get("api_formats").is_none());
|
||||||
|
assert_eq!(outcome.legacy_models.len(), 1);
|
||||||
|
assert_eq!(outcome.legacy_models[0]["id"], "gpt-5.6-future");
|
||||||
|
assert_eq!(
|
||||||
|
outcome.legacy_models[0]["api_formats"],
|
||||||
|
json!(["openai:responses"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
outcome.legacy_models[0]["api_format"],
|
||||||
|
"opaque-future-field"
|
||||||
|
);
|
||||||
let card = &outcome
|
let card = &outcome
|
||||||
.upstream_metadata
|
.upstream_metadata
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -2243,6 +2317,130 @@ mod tests {
|
|||||||
assert_eq!(outcome.fetched_model_ids, vec!["gpt-slug-only-future"]);
|
assert_eq!(outcome.fetched_model_ids, vec!["gpt-slug-only-future"]);
|
||||||
assert_eq!(outcome.cached_models, vec![card]);
|
assert_eq!(outcome.cached_models, vec![card]);
|
||||||
assert!(outcome.cached_models[0].get("id").is_none());
|
assert!(outcome.cached_models[0].get("id").is_none());
|
||||||
|
assert_eq!(outcome.legacy_models[0]["id"], "gpt-slug-only-future");
|
||||||
|
assert_eq!(
|
||||||
|
outcome.legacy_models[0]["api_formats"],
|
||||||
|
json!(["openai:responses"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
outcome.legacy_models[0]["future_capability"]["opaque"],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn codex_legacy_projection_merges_only_formats_from_successful_transports() {
|
||||||
|
let executed_urls = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let card = json!({
|
||||||
|
"slug": "gpt-multi-format-future",
|
||||||
|
"api_format": "opaque-upstream-protocol",
|
||||||
|
"future_capability": {"opaque": true}
|
||||||
|
});
|
||||||
|
let runtime = RoutingTestRuntime {
|
||||||
|
executed_urls,
|
||||||
|
routes: vec![
|
||||||
|
(
|
||||||
|
"chat.example.com/backend-api/codex/models".to_string(),
|
||||||
|
Ok((200, json!({"models": [card.clone()]}))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"responses.example.com/backend-api/codex/models".to_string(),
|
||||||
|
Ok((200, json!({"models": [card.clone()]}))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"compact.example.com/backend-api/codex/models".to_string(),
|
||||||
|
Err("compact endpoint unavailable".to_string()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut chat = sample_codex_transport_for_base(
|
||||||
|
"endpoint-chat",
|
||||||
|
"https://chat.example.com/backend-api/codex",
|
||||||
|
);
|
||||||
|
chat.endpoint.api_format = "openai:chat".to_string();
|
||||||
|
let responses = sample_codex_transport_for_base(
|
||||||
|
"endpoint-responses",
|
||||||
|
"https://responses.example.com/backend-api/codex",
|
||||||
|
);
|
||||||
|
let mut compact = sample_codex_transport_for_base(
|
||||||
|
"endpoint-compact",
|
||||||
|
"https://compact.example.com/backend-api/codex",
|
||||||
|
);
|
||||||
|
compact.endpoint.api_format = "openai:responses:compact".to_string();
|
||||||
|
|
||||||
|
let outcome = fetch_models_from_transports_for_client_version(
|
||||||
|
&runtime,
|
||||||
|
&[chat, responses, compact],
|
||||||
|
Some("0.145.2"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("successful endpoint catalogs should survive a sibling failure");
|
||||||
|
|
||||||
|
assert_eq!(outcome.cached_models, vec![card]);
|
||||||
|
assert_eq!(outcome.legacy_models.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
outcome.legacy_models[0]["api_formats"],
|
||||||
|
json!(["openai:chat", "openai:responses"])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
outcome.legacy_models[0]["api_format"],
|
||||||
|
"opaque-upstream-protocol"
|
||||||
|
);
|
||||||
|
assert_eq!(outcome.errors.len(), 1);
|
||||||
|
assert!(outcome.errors[0].contains("compact endpoint unavailable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unversioned_codex_admin_fetch_keeps_openai_compatible_data_fallback() {
|
||||||
|
let executed_urls = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let runtime = TestRuntime {
|
||||||
|
executed_urls,
|
||||||
|
response_body: json!({
|
||||||
|
"data": [{
|
||||||
|
"id": "gpt-legacy-compatible",
|
||||||
|
"future_capability": {"preserved": true}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
status_code: 200,
|
||||||
|
response_headers: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = fetch_models_from_transports(&runtime, &[sample_codex_transport()])
|
||||||
|
.await
|
||||||
|
.expect("unversioned admin fetch should retain the generic parser fallback");
|
||||||
|
|
||||||
|
assert!(outcome.has_success);
|
||||||
|
assert_eq!(outcome.fetched_model_ids, vec!["gpt-legacy-compatible"]);
|
||||||
|
assert_eq!(outcome.cached_models[0]["id"], "gpt-legacy-compatible");
|
||||||
|
assert_eq!(
|
||||||
|
outcome.legacy_models[0]["api_formats"],
|
||||||
|
json!(["openai:responses"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn versioned_codex_catalog_does_not_accept_openai_compatible_data_fallback() {
|
||||||
|
let executed_urls = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let runtime = TestRuntime {
|
||||||
|
executed_urls,
|
||||||
|
response_body: json!({"data": [{"id": "gpt-not-an-opaque-card"}]}),
|
||||||
|
status_code: 200,
|
||||||
|
response_headers: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = fetch_models_from_transports_for_client_version(
|
||||||
|
&runtime,
|
||||||
|
&[sample_codex_transport()],
|
||||||
|
Some("0.145.2"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("transport failures are returned as observable outcomes");
|
||||||
|
|
||||||
|
assert!(!outcome.has_success);
|
||||||
|
assert!(outcome.cached_models.is_empty());
|
||||||
|
assert!(outcome.legacy_models.is_empty());
|
||||||
|
assert_eq!(outcome.errors.len(), 1);
|
||||||
|
assert!(outcome.errors[0].contains("missing models array"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -973,8 +973,9 @@ mod tests {
|
|||||||
let mut transport = sample_transport("codex", "openai:chat", "oauth");
|
let mut transport = sample_transport("codex", "openai:chat", "oauth");
|
||||||
transport.endpoint.base_url = "https://chatgpt.com/backend-api/codex".to_string();
|
transport.endpoint.base_url = "https://chatgpt.com/backend-api/codex".to_string();
|
||||||
transport.endpoint.header_rules = Some(json!([
|
transport.endpoint.header_rules = Some(json!([
|
||||||
{"op": "set", "name": "chatgpt-account-id", "value": "spoofed-account"},
|
{"action": "set", "key": "authorization", "value": "Bearer spoofed-token"},
|
||||||
{"op": "set", "name": "x-openai-fedramp", "value": "false"}
|
{"action": "set", "key": "chatgpt-account-id", "value": "spoofed-account"},
|
||||||
|
{"action": "set", "key": "x-openai-fedramp", "value": "false"}
|
||||||
]));
|
]));
|
||||||
transport.key.decrypted_auth_config =
|
transport.key.decrypted_auth_config =
|
||||||
Some(r#"{"account_id":"account-1","chatgpt_account_is_fedramp":true}"#.to_string());
|
Some(r#"{"account_id":"account-1","chatgpt_account_is_fedramp":true}"#.to_string());
|
||||||
@@ -1028,8 +1029,8 @@ mod tests {
|
|||||||
let mut transport = sample_transport("codex", "openai:responses", "oauth");
|
let mut transport = sample_transport("codex", "openai:responses", "oauth");
|
||||||
transport.endpoint.base_url = "https://chatgpt.com/backend-api/codex".to_string();
|
transport.endpoint.base_url = "https://chatgpt.com/backend-api/codex".to_string();
|
||||||
transport.endpoint.header_rules = Some(json!([
|
transport.endpoint.header_rules = Some(json!([
|
||||||
{"op": "set", "name": "user-agent", "value": "codex_cli_rs/0.1.0"},
|
{"action": "set", "key": "user-agent", "value": "custom-codex-client/0.1.0"},
|
||||||
{"op": "remove", "name": "originator"}
|
{"action": "drop", "key": "originator"}
|
||||||
]));
|
]));
|
||||||
transport.key.decrypted_auth_config =
|
transport.key.decrypted_auth_config =
|
||||||
Some(r#"{"account_id":"account-1","chatgpt_account_is_fedramp":true}"#.to_string());
|
Some(r#"{"account_id":"account-1","chatgpt_account_is_fedramp":true}"#.to_string());
|
||||||
@@ -1054,10 +1055,6 @@ mod tests {
|
|||||||
plan.headers.get("originator").map(String::as_str),
|
plan.headers.get("originator").map(String::as_str),
|
||||||
Some("codex_cli_rs")
|
Some("codex_cli_rs")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
plan.headers.get("originator").map(String::as_str),
|
|
||||||
Some("codex_cli_rs")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plan.headers.get("chatgpt-account-id").map(String::as_str),
|
plan.headers.get("chatgpt-account-id").map(String::as_str),
|
||||||
Some("account-1")
|
Some("account-1")
|
||||||
|
|||||||
Reference in New Issue
Block a user