mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
ollama_oss: default WireApi to Responses, fall back to Chat
In Ollama v0.13.4 `/v1/responses` support was added. When Ollama is used via `--oss`, it now defaults to Responses. At runtime we do a version check and if the version is older than when Responses support was added, we fall back to `/v1/chat/completions`.
This commit is contained in:
Generated
+1
@@ -1570,6 +1570,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"futures",
|
||||
"reqwest",
|
||||
"semver",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
||||
@@ -180,6 +180,7 @@ reqwest = "0.12"
|
||||
rmcp = { version = "0.10.0", default-features = false }
|
||||
schemars = "0.8.22"
|
||||
seccompiler = "0.5.0"
|
||||
semver = "1.0"
|
||||
sentry = "0.46.0"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use codex_core::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use codex_core::OLLAMA_OSS_PROVIDER_ID;
|
||||
use codex_core::WireApi;
|
||||
use codex_core::config::Config;
|
||||
|
||||
/// Returns the default model for a given OSS provider.
|
||||
@@ -16,7 +17,7 @@ pub fn get_default_model_for_oss_provider(provider_id: &str) -> Option<&'static
|
||||
/// Ensures the specified OSS provider is ready (models downloaded, service reachable).
|
||||
pub async fn ensure_oss_provider_ready(
|
||||
provider_id: &str,
|
||||
config: &Config,
|
||||
config: &mut Config,
|
||||
) -> Result<(), std::io::Error> {
|
||||
match provider_id {
|
||||
LMSTUDIO_OSS_PROVIDER_ID => {
|
||||
@@ -25,6 +26,15 @@ pub async fn ensure_oss_provider_ready(
|
||||
.map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;
|
||||
}
|
||||
OLLAMA_OSS_PROVIDER_ID => {
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
match codex_ollama::detect_wire_api(&config.model_provider).await {
|
||||
Ok(Some(detection)) if detection.wire_api == WireApi::Chat => {
|
||||
config.model_provider.wire_api = WireApi::Chat;
|
||||
}
|
||||
Ok(_) | Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
codex_ollama::ensure_oss_ready(config)
|
||||
.await
|
||||
.map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;
|
||||
|
||||
@@ -273,7 +273,7 @@ pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
|
||||
("openai", P::create_openai_provider()),
|
||||
(
|
||||
OLLAMA_OSS_PROVIDER_ID,
|
||||
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Chat),
|
||||
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses),
|
||||
),
|
||||
(
|
||||
LMSTUDIO_OSS_PROVIDER_ID,
|
||||
|
||||
@@ -202,7 +202,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
additional_writable_roots: add_dir,
|
||||
};
|
||||
|
||||
let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides).await?;
|
||||
let mut config = Config::load_with_cli_overrides(cli_kv_overrides, overrides).await?;
|
||||
|
||||
if let Err(err) = enforce_login_restrictions(&config).await {
|
||||
eprintln!("{err}");
|
||||
@@ -251,7 +251,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
));
|
||||
}
|
||||
};
|
||||
ensure_oss_provider_ready(provider_id, &config)
|
||||
ensure_oss_provider_ready(provider_id, &mut config)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("OSS setup failed: {e}"))?;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ tokio = { workspace = true, features = [
|
||||
] }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
wiremock = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use bytes::BytesMut;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::BoxStream;
|
||||
use semver::Version;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::VecDeque;
|
||||
use std::io;
|
||||
@@ -53,7 +54,7 @@ impl OllamaClient {
|
||||
}
|
||||
|
||||
/// Build a client from a provider definition and verify the server is reachable.
|
||||
async fn try_from_provider(provider: &ModelProviderInfo) -> io::Result<Self> {
|
||||
pub(crate) async fn try_from_provider(provider: &ModelProviderInfo) -> io::Result<Self> {
|
||||
#![expect(clippy::expect_used)]
|
||||
let base_url = provider
|
||||
.base_url
|
||||
@@ -125,6 +126,32 @@ impl OllamaClient {
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Query the server for its version string, returning `None` when unavailable.
|
||||
pub async fn fetch_version(&self) -> io::Result<Option<Version>> {
|
||||
let version_url = format!("{}/api/version", self.host_root.trim_end_matches('/'));
|
||||
let resp = self
|
||||
.client
|
||||
.get(version_url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
if !resp.status().is_success() {
|
||||
return Ok(None);
|
||||
}
|
||||
let val = resp.json::<JsonValue>().await.map_err(io::Error::other)?;
|
||||
let Some(version_str) = val.get("version").and_then(|v| v.as_str()).map(str::trim) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let normalized = version_str.trim_start_matches('v');
|
||||
match Version::parse(normalized) {
|
||||
Ok(version) => Ok(Some(version)),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse Ollama version `{version_str}`: {err}");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a model pull and emit streaming events. The returned stream ends when
|
||||
/// a Success event is observed or the server closes the connection.
|
||||
pub async fn pull_model_stream(
|
||||
@@ -269,6 +296,42 @@ mod tests {
|
||||
assert!(models.contains(&"mistral".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_version() {
|
||||
if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||||
tracing::info!(
|
||||
"{} is set; skipping test_fetch_version",
|
||||
codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/api/tags"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_raw(
|
||||
serde_json::json!({ "models": [] }).to_string(),
|
||||
"application/json",
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/api/version"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_raw(
|
||||
serde_json::json!({ "version": "0.14.1" }).to_string(),
|
||||
"application/json",
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = OllamaClient::try_from_provider_with_base_url(server.uri().as_str())
|
||||
.await
|
||||
.expect("client");
|
||||
|
||||
let version = client.fetch_version().await.expect("version fetch");
|
||||
assert_eq!(version, Some(Version::new(0, 14, 1)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_probe_server_happy_path_openai_compat_and_native() {
|
||||
if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||||
|
||||
@@ -4,15 +4,23 @@ mod pull;
|
||||
mod url;
|
||||
|
||||
pub use client::OllamaClient;
|
||||
use codex_core::ModelProviderInfo;
|
||||
use codex_core::WireApi;
|
||||
use codex_core::config::Config;
|
||||
pub use pull::CliProgressReporter;
|
||||
pub use pull::PullEvent;
|
||||
pub use pull::PullProgressReporter;
|
||||
pub use pull::TuiProgressReporter;
|
||||
use semver::Version;
|
||||
|
||||
/// Default OSS model to use when `--oss` is passed without an explicit `-m`.
|
||||
pub const DEFAULT_OSS_MODEL: &str = "gpt-oss:20b";
|
||||
|
||||
pub struct WireApiDetection {
|
||||
pub wire_api: WireApi,
|
||||
pub version: Option<Version>,
|
||||
}
|
||||
|
||||
/// Prepare the local OSS environment when `--oss` is selected.
|
||||
///
|
||||
/// - Ensures a local Ollama server is reachable.
|
||||
@@ -45,3 +53,58 @@ pub async fn ensure_oss_ready(config: &Config) -> std::io::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn min_responses_version() -> Version {
|
||||
Version::new(0, 13, 4)
|
||||
}
|
||||
|
||||
fn wire_api_for_version(version: &Version) -> WireApi {
|
||||
if *version == Version::new(0, 0, 0) {
|
||||
return WireApi::Responses;
|
||||
}
|
||||
if *version >= min_responses_version() {
|
||||
WireApi::Responses
|
||||
} else {
|
||||
WireApi::Chat
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect which wire API the running Ollama server supports based on its version.
|
||||
/// Returns `Ok(None)` when the version endpoint is missing or unparsable; callers
|
||||
/// should keep the configured default in that case.
|
||||
pub async fn detect_wire_api(
|
||||
provider: &ModelProviderInfo,
|
||||
) -> std::io::Result<Option<WireApiDetection>> {
|
||||
let client = crate::OllamaClient::try_from_provider(provider).await?;
|
||||
let Some(version) = client.fetch_version().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let wire_api = wire_api_for_version(&version);
|
||||
|
||||
Ok(Some(WireApiDetection {
|
||||
wire_api,
|
||||
version: Some(version),
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_wire_api_for_version_dev_zero_keeps_responses() {
|
||||
assert_eq!(wire_api_for_version(&Version::new(0, 0, 0)), WireApi::Responses);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wire_api_for_version_before_cutoff_is_chat() {
|
||||
assert_eq!(wire_api_for_version(&Version::new(0, 13, 3)), WireApi::Chat);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wire_api_for_version_at_or_after_cutoff_is_responses() {
|
||||
assert_eq!(wire_api_for_version(&Version::new(0, 13, 4)), WireApi::Responses);
|
||||
assert_eq!(wire_api_for_version(&Version::new(0, 14, 0)), WireApi::Responses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ pub async fn run_main(
|
||||
additional_writable_roots: additional_dirs,
|
||||
};
|
||||
|
||||
let config = load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await;
|
||||
let mut config = load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await;
|
||||
|
||||
if let Some(warning) = add_dir_warning_message(&cli.add_dir, &config.sandbox_policy) {
|
||||
#[allow(clippy::print_stderr)]
|
||||
@@ -294,7 +294,7 @@ pub async fn run_main(
|
||||
));
|
||||
}
|
||||
};
|
||||
ensure_oss_provider_ready(provider_id, &config).await?;
|
||||
ensure_oss_provider_ready(provider_id, &mut config).await?;
|
||||
}
|
||||
|
||||
let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"));
|
||||
|
||||
@@ -222,7 +222,7 @@ pub async fn run_main(
|
||||
additional_writable_roots: additional_dirs,
|
||||
};
|
||||
|
||||
let config = load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await;
|
||||
let mut config = load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await;
|
||||
|
||||
if let Some(warning) = add_dir_warning_message(&cli.add_dir, &config.sandbox_policy) {
|
||||
#[allow(clippy::print_stderr)]
|
||||
@@ -295,7 +295,7 @@ pub async fn run_main(
|
||||
));
|
||||
}
|
||||
};
|
||||
ensure_oss_provider_ready(provider_id, &config).await?;
|
||||
ensure_oss_provider_ready(provider_id, &mut config).await?;
|
||||
}
|
||||
|
||||
let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"));
|
||||
|
||||
Reference in New Issue
Block a user