mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Allow selecting a configured provider in model/list (#59776)
## Why Clients need to list models for a configured provider other than the app-server's startup default. ## What changed Add an optional `modelProvider` parameter to `model/list` and expose it in the generated schemas and client types. Explicit selections use provider definitions loaded at startup and respect current managed provider requirements. Unknown providers return a structured `modelProviderNotConfigured` error. Use a temporary catalog when selecting another provider, preserving the default catalog and its disk cache while retaining runtime authentication. Requests without a selector keep the existing provider and caching behavior. ## Testing Add coverage for provider selection, preservation of the default catalog and disk cache, unknown-provider errors, managed requirement enforcement, and runtime authentication for temporary catalogs. GitOrigin-RevId: 6092df9f6141810c6ac215a8e1fb601f77e54747
This commit is contained in:
@@ -2459,6 +2459,13 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelProvider": {
|
||||
"description": "List models for this configured provider instead of the app-server default.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+7
@@ -16270,6 +16270,13 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelProvider": {
|
||||
"description": "List models for this configured provider instead of the app-server default.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ModelListParams",
|
||||
|
||||
+7
@@ -12231,6 +12231,13 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelProvider": {
|
||||
"description": "List models for this configured provider instead of the app-server default.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ModelListParams",
|
||||
|
||||
@@ -23,6 +23,13 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelProvider": {
|
||||
"description": "List models for this configured provider instead of the app-server default.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ModelListParams",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,6 +3,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModelListParams = {
|
||||
/**
|
||||
* List models for this configured provider instead of the app-server default.
|
||||
*/
|
||||
modelProvider?: string | null,
|
||||
/**
|
||||
* Opaque pagination cursor returned by a previous call.
|
||||
*/
|
||||
|
||||
@@ -3782,7 +3782,8 @@ mod tests {
|
||||
"params": {
|
||||
"limit": null,
|
||||
"cursor": null,
|
||||
"includeHidden": null
|
||||
"includeHidden": null,
|
||||
"modelProvider": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
|
||||
@@ -52,6 +52,9 @@ pub struct ModelProviderCapabilitiesReadResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ModelListParams {
|
||||
/// List models for this configured provider instead of the app-server default.
|
||||
#[ts(optional = nullable)]
|
||||
pub model_provider: Option<String>,
|
||||
/// Opaque pagination cursor returned by a previous call.
|
||||
#[ts(optional = nullable)]
|
||||
pub cursor: Option<String>,
|
||||
|
||||
@@ -121,12 +121,18 @@ denial limit, and interrupted status; neither emits a separate `Error` event.
|
||||
|
||||
# Model catalog provider requirements
|
||||
|
||||
`model/list` and periodic model catalog refreshes check the startup provider against
|
||||
current managed provider requirements before using the catalog. If that provider no longer
|
||||
`model/list` accepts an optional `modelProvider` to list models for a configured provider
|
||||
other than the startup default. For example, `{"modelProvider": "my_gateway"}` returns that
|
||||
provider's models after the user's default changes. Omit it to retain the startup provider
|
||||
and caching behavior. Explicit selections use the provider definitions loaded at startup. Restart the server
|
||||
to pick up edits to those definitions. An unknown selector returns JSON-RPC error
|
||||
`-32600` with `data.type = "modelProviderNotConfigured"` and the requested `modelProvider`.
|
||||
|
||||
`model/list` checks its selected provider, and periodic model catalog refreshes check the
|
||||
startup provider, against current managed provider requirements. If that provider no longer
|
||||
complies, `model/list` returns JSON-RPC error `-32600` asking the client to restart Codex,
|
||||
and background refreshes skip the old endpoint. Requirement load failures also block these
|
||||
operations. Checks apply even when the catalog is cached. Existing startup provider selection
|
||||
and caching behavior remain in effect while the provider satisfies current requirements.
|
||||
operations. Checks apply even when the catalog is cached.
|
||||
|
||||
# MCP App UI
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ use codex_app_server_protocol::MockExperimentalMethodParams;
|
||||
use codex_app_server_protocol::MockExperimentalMethodResponse;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::ModelProviderError;
|
||||
use codex_app_server_protocol::PermissionProfileListParams;
|
||||
use codex_app_server_protocol::PermissionProfileListResponse;
|
||||
use codex_app_server_protocol::PermissionProfileSummary;
|
||||
@@ -694,6 +695,25 @@ fn resolve_turn_environment_requests(
|
||||
Ok(Some(requests))
|
||||
}
|
||||
|
||||
/// Project-local layers cannot define providers, so explicit selectors can use the startup map.
|
||||
fn config_for_provider(
|
||||
startup: &Arc<Config>,
|
||||
provider_id: &str,
|
||||
) -> Result<Arc<Config>, JSONRPCErrorError> {
|
||||
if startup.model_provider_id == provider_id {
|
||||
return Ok(Arc::clone(startup));
|
||||
}
|
||||
let provider = startup.model_providers.get(provider_id).ok_or_else(|| {
|
||||
model_provider_error(&ModelProviderError::ModelProviderNotConfigured {
|
||||
model_provider: provider_id.to_owned(),
|
||||
})
|
||||
})?;
|
||||
let mut config = startup.as_ref().clone();
|
||||
config.model_provider_id = provider_id.to_owned();
|
||||
config.model_provider = provider.clone();
|
||||
Ok(Arc::new(config))
|
||||
}
|
||||
|
||||
fn resolve_runtime_workspace_roots(workspace_roots: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
|
||||
let mut resolved_roots = Vec::new();
|
||||
for root in workspace_roots {
|
||||
|
||||
@@ -179,11 +179,29 @@ impl CatalogRequestProcessor {
|
||||
&self,
|
||||
params: ModelListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let (config, models_manager) = self
|
||||
let (active_config, active_models_manager) = self
|
||||
.model_catalog
|
||||
.current()
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
let (config, models_manager) = match params.model_provider.as_deref() {
|
||||
None => (active_config, active_models_manager),
|
||||
Some(provider_id) => {
|
||||
let config = config_for_provider(&self.config, provider_id)?;
|
||||
if provider_id == active_config.model_provider_id
|
||||
&& config.model_provider == active_config.model_provider
|
||||
{
|
||||
(active_config, active_models_manager)
|
||||
} else {
|
||||
// Temporary catalogs must not replace the active provider's disk cache.
|
||||
let manager = self.thread_manager.temporary_models_manager_for_config(
|
||||
&config,
|
||||
&codex_models_manager::manager::ModelCatalogSettings::default(),
|
||||
);
|
||||
(config, manager)
|
||||
}
|
||||
}
|
||||
};
|
||||
// Gate the same provider used by the catalog, including when its model cache is warm.
|
||||
// Resolving credentials may refresh them, but explicit host policy prevents browser login.
|
||||
if let Some(gateway) = codex_model_provider::create_model_provider(
|
||||
@@ -204,9 +222,11 @@ impl CatalogRequestProcessor {
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
// A direct config file edit can leave the catalog using the previous gateway.
|
||||
if current.model_provider_id != config.model_provider_id
|
||||
|| current.model_provider != config.model_provider
|
||||
// An explicit selection need not match the current default.
|
||||
if current.model_providers.get(&config.model_provider_id)
|
||||
!= Some(&config.model_provider)
|
||||
|| (params.model_provider.is_none()
|
||||
&& current.model_provider_id != config.model_provider_id)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"Model provider settings changed. Restart Codex to apply them, then retry fetching the model list",
|
||||
@@ -293,6 +313,7 @@ impl CatalogRequestProcessor {
|
||||
limit,
|
||||
cursor,
|
||||
include_hidden,
|
||||
model_provider: _,
|
||||
} = params;
|
||||
let presets = self
|
||||
.model_catalog
|
||||
|
||||
@@ -13,17 +13,19 @@ fn cloud_config_bundle_load_error(err: &std::io::Error) -> Option<&CloudConfigBu
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn model_provider_error(provider: &ModelProviderError) -> JSONRPCErrorError {
|
||||
let mut error = invalid_request(provider.to_string());
|
||||
error.data = serde_json::to_value(provider).ok();
|
||||
error
|
||||
}
|
||||
|
||||
pub(crate) fn config_load_error(err: &std::io::Error) -> JSONRPCErrorError {
|
||||
let mut current = err
|
||||
.get_ref()
|
||||
.map(|error| error as &(dyn std::error::Error + 'static));
|
||||
while let Some(source) = current {
|
||||
if let Some(provider_error) =
|
||||
source.downcast_ref::<codex_app_server_protocol::ModelProviderError>()
|
||||
{
|
||||
let mut error = invalid_request(provider_error.to_string());
|
||||
error.data = serde_json::to_value(provider_error).ok();
|
||||
return error;
|
||||
if let Some(provider_error) = source.downcast_ref::<ModelProviderError>() {
|
||||
return model_provider_error(provider_error);
|
||||
}
|
||||
current = source.source();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ region = "us-west-2"
|
||||
cursor: None,
|
||||
limit: Some(100),
|
||||
include_hidden: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
@@ -118,6 +119,7 @@ region = "us-west-2"
|
||||
cursor: None,
|
||||
limit: Some(100),
|
||||
include_hidden: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -156,6 +156,7 @@ requires_openai_auth = true
|
||||
limit: Some(100),
|
||||
include_hidden: Some(true),
|
||||
cursor: None,
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
@@ -274,6 +275,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
||||
limit: Some(100),
|
||||
cursor: None,
|
||||
include_hidden: None,
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
@@ -304,6 +306,7 @@ async fn list_models_includes_hidden_models() -> Result<()> {
|
||||
limit: Some(100),
|
||||
cursor: None,
|
||||
include_hidden: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
@@ -426,6 +429,7 @@ model_catalog_url = "{server_uri}/v1/models"
|
||||
limit: Some(100),
|
||||
cursor: None,
|
||||
include_hidden: None,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let response = mcp
|
||||
@@ -509,6 +513,7 @@ async fn list_models_pagination_works() -> Result<()> {
|
||||
limit: Some(1),
|
||||
cursor: cursor.clone(),
|
||||
include_hidden: None,
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
@@ -545,6 +550,7 @@ async fn list_models_rejects_invalid_cursor() -> Result<()> {
|
||||
limit: None,
|
||||
cursor: Some("invalid".to_string()),
|
||||
include_hidden: None,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Exercises live catalog routing through the public model/list RPC.
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::LogoutAccountResponse;
|
||||
@@ -153,6 +155,22 @@ requires_openai_auth = true
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["gateway-model"]
|
||||
);
|
||||
// A provider selector cannot override a required selection, even before its catalog is cached.
|
||||
let id = server
|
||||
.send_raw_request("model/list", Some(json!({"modelProvider": "other"})))
|
||||
.await?;
|
||||
let error = server
|
||||
.read_stream_until_error_message(RequestId::Integer(id))
|
||||
.await?;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert!(
|
||||
other
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("request recording enabled")
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
let updated_providers = if required_selection == "gateway" {
|
||||
providers.replace(&gateway.uri(), &other.uri())
|
||||
} else {
|
||||
@@ -376,3 +394,83 @@ async fn model_list_preserves_invalid_startup_config_fallback() -> Result<()> {
|
||||
assert!(!list_models(&mut server).await?.data.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn model_list_selects_a_provider_without_replacing_the_startup_catalog() -> Result<()> {
|
||||
let first = MockServer::start().await;
|
||||
let second = MockServer::start().await;
|
||||
// Startup refresh and model/list can fetch concurrently from the default provider.
|
||||
for (server, slug) in [(&first, "first-model"), (&second, "second-model")] {
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(".*/models$"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(catalog(slug)?))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
let home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model_provider = "first"
|
||||
[model_providers.first]
|
||||
name = "First"
|
||||
base_url = "{}/v1"
|
||||
requires_openai_auth = true
|
||||
[model_providers.second]
|
||||
name = "Second"
|
||||
base_url = "{}/v1"
|
||||
requires_openai_auth = true
|
||||
"#,
|
||||
first.uri(),
|
||||
second.uri(),
|
||||
),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-access-token").plan_type("pro"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
let mut server = TestAppServer::builder()
|
||||
.with_codex_home(home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let initial = list_models(&mut server).await?;
|
||||
assert_eq!(initial.data.len(), 1);
|
||||
assert_eq!(initial.data[0].id, "first-model");
|
||||
let cache_path = home.path().join("models_cache.json");
|
||||
let initial_cache: ModelsResponse = serde_json::from_slice(&std::fs::read(&cache_path)?)?;
|
||||
|
||||
let select_second = |request_id| ClientRequest::ModelList {
|
||||
request_id,
|
||||
params: ModelListParams {
|
||||
model_provider: Some("second".into()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let selected: ModelListResponse = server.request(select_second).await?;
|
||||
assert_eq!(selected.data.len(), 1);
|
||||
assert_eq!(selected.data[0].id, "second-model");
|
||||
let disk_catalog: ModelsResponse = serde_json::from_slice(&std::fs::read(&cache_path)?)?;
|
||||
assert_eq!(disk_catalog, initial_cache);
|
||||
assert_eq!(list_models(&mut server).await?, initial);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_list_reports_an_unconfigured_provider() -> Result<()> {
|
||||
let mut server = TestAppServer::builder().build_initialized().await?;
|
||||
let id = server
|
||||
.send_raw_request("model/list", Some(json!({"modelProvider": "missing"})))
|
||||
.await?;
|
||||
let error = server
|
||||
.read_stream_until_error_message(RequestId::Integer(id))
|
||||
.await?;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert_eq!(
|
||||
error.error.data,
|
||||
Some(json!({"type": "modelProviderNotConfigured", "modelProvider": "missing"}))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_model_provider::ModelCatalogFactory;
|
||||
use codex_model_provider::ProviderAuthContext;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::OPENAI_PROVIDER_ID;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
@@ -1039,6 +1041,29 @@ impl ThreadManager {
|
||||
|
||||
/// Select the app-server catalog using the same owner as thread requests.
|
||||
pub fn models_manager_for_config(&self, config: &Config) -> SharedModelsManager {
|
||||
self.state.model_catalog.for_config(
|
||||
config,
|
||||
config.model_catalog.as_ref(),
|
||||
self.catalog_auth_context(config),
|
||||
/*same_owner*/ true,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a separate catalog with the harness's runtime identity and the caller's cache policy.
|
||||
pub fn temporary_models_manager_for_config(
|
||||
&self,
|
||||
config: &Config,
|
||||
factory: &dyn ModelCatalogFactory,
|
||||
) -> SharedModelsManager {
|
||||
factory.build(
|
||||
self.catalog_auth_context(config)
|
||||
.model_provider(config.model_provider.clone()),
|
||||
config.model_catalog.clone(),
|
||||
config.features.enabled(Feature::ApiKeyModelDiscovery),
|
||||
)
|
||||
}
|
||||
|
||||
fn catalog_auth_context(&self, config: &Config) -> ProviderAuthContext {
|
||||
let credentials = self.auth_manager();
|
||||
let runtime = crate::runtime_auth::runtime_for_config(
|
||||
&self.state.runtime_auth,
|
||||
@@ -1046,13 +1071,10 @@ impl ThreadManager {
|
||||
Arc::clone(&credentials),
|
||||
&self.state.installation_id,
|
||||
);
|
||||
self.state.model_catalog.for_config(
|
||||
config,
|
||||
config.model_catalog.as_ref(),
|
||||
ProviderAuthContext {
|
||||
credentials,
|
||||
runtime,
|
||||
/*same_owner*/ true,
|
||||
)
|
||||
runtime_auth: runtime.map(|runtime| (runtime, config.runtime_auth_mode)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_api_key_model_discovery_enabled(&self, enabled: bool) {
|
||||
|
||||
@@ -127,32 +127,30 @@ impl ThreadModelCatalog {
|
||||
.as_ref()
|
||||
.filter(|_| self.provider == config.model_provider)
|
||||
});
|
||||
self.for_config(config, models, credentials, runtime, same_owner)
|
||||
let auth = ProviderAuthContext {
|
||||
credentials,
|
||||
runtime_auth: runtime.map(|runtime| (runtime, config.runtime_auth_mode)),
|
||||
};
|
||||
self.for_config(config, models, auth, same_owner)
|
||||
}
|
||||
|
||||
pub(super) fn for_config(
|
||||
&self,
|
||||
config: &Config,
|
||||
models: Option<&ModelsResponse>,
|
||||
credentials: Arc<AuthManager>,
|
||||
runtime: Option<Arc<RuntimeAuthManager>>,
|
||||
auth: ProviderAuthContext,
|
||||
same_owner: bool,
|
||||
) -> SharedModelsManager {
|
||||
let runtime = runtime.map(|runtime| (runtime, config.runtime_auth_mode));
|
||||
let api_key_model_discovery_default =
|
||||
config.features.enabled(Feature::ApiKeyModelDiscovery);
|
||||
if same_owner
|
||||
&& self.runtime_mode == runtime.as_ref().map(|(_, mode)| *mode)
|
||||
&& self.runtime_mode == auth.runtime_auth.as_ref().map(|(_, mode)| *mode)
|
||||
&& self.provider == config.model_provider
|
||||
&& self.catalog.as_ref() == models
|
||||
&& self.api_key_model_discovery_default == api_key_model_discovery_default
|
||||
{
|
||||
return Arc::clone(&self.manager);
|
||||
}
|
||||
let auth = ProviderAuthContext {
|
||||
credentials,
|
||||
runtime_auth: runtime,
|
||||
};
|
||||
self.factory.build(
|
||||
auth.model_provider(config.model_provider.clone()),
|
||||
models.cloned(),
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_history::InitialHistory;
|
||||
use codex_history::ResumedHistory;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider::ProviderAuthContext;
|
||||
use codex_models_manager::manager::ModelCatalogSettings;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -44,6 +45,7 @@ use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::body_string_contains;
|
||||
use wiremock::matchers::header_regex;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
@@ -132,6 +134,14 @@ async fn harness_shares_runtime_and_honors_thread_auth_config() -> Result<()> {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/selected-catalog"))
|
||||
.and(header_regex("authorization", "^AgentAssertion "))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(catalog_fixture()))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
for mode in ["dual", "assertion", "disabled"] {
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/catalog-reload/{mode}")))
|
||||
@@ -365,6 +375,19 @@ async fn exercise_harness(server_uri: String) -> Result<()> {
|
||||
};
|
||||
catalog_config.model_provider.model_catalog_url =
|
||||
Some(format!("https://api.openai.com/catalog-reload/{label}").into());
|
||||
if mode == RuntimeAuthMode::AssertionOnly {
|
||||
let mut selected = catalog_config.clone();
|
||||
selected.model_provider.base_url = Some(server_uri.clone());
|
||||
selected.model_provider.model_catalog_url =
|
||||
Some("https://api.openai.com/selected-catalog".into());
|
||||
let models = test
|
||||
.thread_manager
|
||||
.temporary_models_manager_for_config(&selected, &ModelCatalogSettings::default());
|
||||
models
|
||||
.raw_model_catalog(RefreshStrategy::Online, selected.http_client_factory())
|
||||
.await;
|
||||
assert_eq!(models.get_remote_models().await, catalog_fixture().models);
|
||||
}
|
||||
let catalog_thread = test
|
||||
.thread_manager
|
||||
.start_thread(StartThreadOptions {
|
||||
|
||||
@@ -50,6 +50,7 @@ pub(crate) async fn program_for_turn(
|
||||
cursor,
|
||||
limit: None,
|
||||
include_hidden: Some(true),
|
||||
model_provider: None,
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -820,6 +820,7 @@ impl AppServerSession {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
include_hidden: Some(true),
|
||||
model_provider: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -33,6 +33,7 @@ impl AppServerSession {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
include_hidden: Some(true),
|
||||
model_provider: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -1290,7 +1290,8 @@ async fn background_turn_program(
|
||||
params: ModelListParams {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
include_hidden: Some(true)
|
||||
include_hidden: Some(true),
|
||||
model_provider: None,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2825,6 +2825,13 @@ class ModelListParams(BaseModel):
|
||||
int | None,
|
||||
Field(description="Optional page size; defaults to a reasonable server-side value.", ge=0),
|
||||
] = None
|
||||
model_provider: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
alias="modelProvider",
|
||||
description="List models for this configured provider instead of the app-server default.",
|
||||
),
|
||||
] = None
|
||||
|
||||
|
||||
class ModelProviderCapabilitiesReadParams(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user