mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## Why Model catalog refreshes need to use the same registered actor and credential owner as model requests when runtime authentication is enabled. ## What changed - Add `ProviderAuthContext` to carry credentials and optional runtime authentication into model providers and catalog clients. - Select runtime authentication independently for inference and catalog destinations. Reject unsupported OpenAI origins while preserving provider authentication for other destinations. - Include API organization and project selectors in runtime registration. Reuse registered runtimes across projects within an organization and replace them when the organization changes. - Preserve transport restrictions and account bindings, and strip sensitive headers on cross-origin redirects. - Apply the catalog timeout to authentication and fetching while allowing credential refresh and registration to finish safely. ## Testing Add coverage for runtime-authenticated catalogs, provider isolation, resource selection, gateway authentication, cross-origin redirects, and timeout recovery that preserves registration and refreshed tokens. GitOrigin-RevId: c14f1a02159031ea0af1f9b26ea890f8dca7d6ad
156 lines
5.1 KiB
Rust
156 lines
5.1 KiB
Rust
//! Classify OpenAI API destinations without treating an unsupported origin as another provider.
|
|
|
|
use crate::is_allowed_chatgpt_host;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum OpenAiDestination {
|
|
Supported,
|
|
Unsupported,
|
|
Other,
|
|
}
|
|
|
|
pub fn classify_openai_api_url(destination: &str) -> OpenAiDestination {
|
|
let Ok(url) = reqwest::Url::parse(destination) else {
|
|
return OpenAiDestination::Other;
|
|
};
|
|
let Some(host) = url.host_str() else {
|
|
return OpenAiDestination::Other;
|
|
};
|
|
let host = host.strip_suffix('.').unwrap_or(host);
|
|
let openai_owned = [
|
|
"openai.com",
|
|
"openai.org",
|
|
"openai-staging.com",
|
|
"chatgpt.com",
|
|
"chatgpt-staging.com",
|
|
]
|
|
.iter()
|
|
.any(|domain| {
|
|
let host = host.trim_end_matches('.');
|
|
host == *domain
|
|
|| host
|
|
.strip_suffix(*domain)
|
|
.is_some_and(|prefix| prefix.ends_with('.'))
|
|
});
|
|
if !openai_owned {
|
|
return OpenAiDestination::Other;
|
|
}
|
|
let supported_host = matches!(
|
|
host,
|
|
"api.openai.com"
|
|
| "gov.api.openai.com"
|
|
| "us.api.openai.com"
|
|
| "eu.api.openai.com"
|
|
| "au.api.openai.com"
|
|
| "ca.api.openai.com"
|
|
| "jp.api.openai.com"
|
|
| "in.api.openai.com"
|
|
| "sg.api.openai.com"
|
|
| "kr.api.openai.com"
|
|
| "gb.api.openai.com"
|
|
| "ae.api.openai.com"
|
|
| "mtls.api.openai.com"
|
|
| "mtls-us.api.openai.com"
|
|
| "mtls-eu.api.openai.com"
|
|
| "api.openai.org"
|
|
| "api.openai-staging.com"
|
|
) || is_allowed_chatgpt_host(host);
|
|
if supported_host
|
|
&& url.scheme() == "https"
|
|
&& url.port_or_known_default() == Some(443)
|
|
&& url.username().is_empty()
|
|
&& url.password().is_none()
|
|
{
|
|
OpenAiDestination::Supported
|
|
} else {
|
|
OpenAiDestination::Unsupported
|
|
}
|
|
}
|
|
|
|
pub fn is_openai_api_url(destination: &str) -> bool {
|
|
classify_openai_api_url(destination) == OpenAiDestination::Supported
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::OpenAiDestination;
|
|
use super::classify_openai_api_url;
|
|
use super::is_openai_api_url;
|
|
|
|
#[test]
|
|
fn unsupported_openai_origins_are_distinct_from_other_providers() {
|
|
for destination in [
|
|
"https://unknown.api.openai.com/v1",
|
|
"https://api.openai.com../v1",
|
|
"https://api.openai.com:8443/v1",
|
|
"http://api.openai.com/v1",
|
|
"https://user@api.openai.com/v1",
|
|
"https://other.openai.org/v1",
|
|
] {
|
|
assert_eq!(
|
|
classify_openai_api_url(destination),
|
|
OpenAiDestination::Unsupported,
|
|
"{destination}"
|
|
);
|
|
}
|
|
for destination in [
|
|
"https://api.openai.com.example.test/v1",
|
|
"https://example.test/v1",
|
|
"http://127.0.0.1:8080/v1",
|
|
] {
|
|
assert_eq!(
|
|
classify_openai_api_url(destination),
|
|
OpenAiDestination::Other,
|
|
"{destination}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_credentials_require_a_trusted_https_origin() {
|
|
for destination in [
|
|
"https://api.openai.com/v1",
|
|
"https://api.openai.com.:443/v1",
|
|
"https://gov.api.openai.com/v1",
|
|
"https://eu.api.openai.com/v1",
|
|
"https://us.api.openai.com/v1",
|
|
"https://au.api.openai.com/v1",
|
|
"https://ca.api.openai.com/v1",
|
|
"https://jp.api.openai.com/v1",
|
|
"https://in.api.openai.com/v1",
|
|
"https://sg.api.openai.com/v1",
|
|
"https://kr.api.openai.com/v1",
|
|
"https://gb.api.openai.com/v1",
|
|
"https://ae.api.openai.com/v1",
|
|
"https://mtls.api.openai.com/v1",
|
|
"https://mtls-us.api.openai.com/v1",
|
|
"https://mtls-eu.api.openai.com/v1",
|
|
"https://api.openai.org/v1",
|
|
"https://api.openai-staging.com/v1",
|
|
"https://chatgpt.com/backend-api",
|
|
"https://chat.openai.com/backend-api",
|
|
"https://us.chatgpt.com/backend-api",
|
|
"https://chatgpt-staging.com/backend-api",
|
|
"https://us.chatgpt-staging.com/backend-api",
|
|
] {
|
|
assert!(is_openai_api_url(destination), "{destination}");
|
|
}
|
|
for destination in [
|
|
"http://api.openai.com/v1",
|
|
"https://api.openai.com:8443/v1",
|
|
"https://user@api.openai.com/v1",
|
|
"https://user:password@chatgpt.com/backend-api",
|
|
"https://api.openai.com.example.test/v1",
|
|
"https://eu.api.openai.com.example.test/v1",
|
|
"https://unknown.api.openai.com/v1",
|
|
"https://evilchatgpt.com/backend-api",
|
|
"https://chatgpt.com.example.test/backend-api",
|
|
"https://foo.chat.openai.com/backend-api",
|
|
"http://127.0.0.1:8080/v1",
|
|
"https://example.test/v1",
|
|
] {
|
|
assert!(!is_openai_api_url(destination), "{destination}");
|
|
}
|
|
}
|
|
}
|