Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ serde_yaml = "0.9"
serial_test = "3.2.0"
sha1 = "0.10.6"
sha2 = "0.10"
semver = "1.0"
shlex = "1.3.0"
similar = "2.7.0"
socket2 = "0.6.1"
Expand Down
38 changes: 36 additions & 2 deletions codex-rs/common/src/oss.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
//! OSS provider utilities shared between TUI and exec.

use codex_core::LMSTUDIO_OSS_PROVIDER_ID;
use codex_core::OLLAMA_CHAT_PROVIDER_ID;
use codex_core::OLLAMA_OSS_PROVIDER_ID;
use codex_core::WireApi;
use codex_core::config::Config;
use codex_core::protocol::DeprecationNoticeEvent;
use std::io;

/// Returns the default model for a given OSS provider.
pub fn get_default_model_for_oss_provider(provider_id: &str) -> Option<&'static str> {
match provider_id {
LMSTUDIO_OSS_PROVIDER_ID => Some(codex_lmstudio::DEFAULT_OSS_MODEL),
OLLAMA_OSS_PROVIDER_ID => Some(codex_ollama::DEFAULT_OSS_MODEL),
OLLAMA_OSS_PROVIDER_ID | OLLAMA_CHAT_PROVIDER_ID => Some(codex_ollama::DEFAULT_OSS_MODEL),
_ => None,
}
}

/// Returns a deprecation notice if Ollama doesn't support the responses wire API.
pub async fn ollama_chat_deprecation_notice(
config: &Config,
) -> io::Result<Option<DeprecationNoticeEvent>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function returns Result, but it always returns the Ok variant, never Err.

If detect_wire_api returns Err, should that flow through?

Because if only Ok is possible, then the Result wrapper seems unnecessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just pushed a new commit that makes it flow through and logs a warning. I don't think it should propagate as far as preventing startup (because the call itself just a best-effort explanation for users on older versions).

if config.model_provider_id != OLLAMA_OSS_PROVIDER_ID
|| config.model_provider.wire_api != WireApi::Responses
{
return Ok(None);
}

if let Some(detection) = codex_ollama::detect_wire_api(&config.model_provider).await?
&& detection.wire_api == WireApi::Chat
{
let version_suffix = detection
.version
.as_ref()
.map(|version| format!(" (version {version})"))
.unwrap_or_default();
let summary = format!(
"Your Ollama server{version_suffix} doesn't support the Responses API. Either update Ollama or set `oss_provider = \"{OLLAMA_CHAT_PROVIDER_ID}\"` (or `model_provider = \"{OLLAMA_CHAT_PROVIDER_ID}\"`) in your config.toml to use the \"chat\" wire API. Support for the \"chat\" wire API is deprecated and will soon be removed."
);
return Ok(Some(DeprecationNoticeEvent {
summary,
details: None,
}));
}

Ok(None)
}

/// Ensures the specified OSS provider is ready (models downloaded, service reachable).
pub async fn ensure_oss_provider_ready(
provider_id: &str,
Expand All @@ -24,7 +58,7 @@ pub async fn ensure_oss_provider_ready(
.await
.map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;
}
OLLAMA_OSS_PROVIDER_ID => {
OLLAMA_OSS_PROVIDER_ID | OLLAMA_CHAT_PROVIDER_ID => {
codex_ollama::ensure_oss_ready(config)
.await
.map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;
Expand Down
7 changes: 4 additions & 3 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::features::FeaturesToml;
use crate::git_info::resolve_root_git_project_for_trust;
use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::OLLAMA_CHAT_PROVIDER_ID;
use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID;
use crate::model_provider_info::built_in_model_providers;
use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
Expand Down Expand Up @@ -634,14 +635,14 @@ pub fn set_project_trust_level(
pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> {
// Validate that the provider is one of the known OSS providers
match provider {
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => {
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID | OLLAMA_CHAT_PROVIDER_ID => {
// Valid provider, continue
}
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}"
"Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}, {OLLAMA_CHAT_PROVIDER_ID}"
),
));
}
Expand Down Expand Up @@ -843,7 +844,7 @@ pub struct ConfigToml {
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
pub experimental_use_unified_exec_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
/// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama".
/// Preferred OSS provider for local models, e.g. "lmstudio", "ollama", or "ollama-chat".
pub oss_provider: Option<String>,
}

Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub use model_provider_info::DEFAULT_LMSTUDIO_PORT;
pub use model_provider_info::DEFAULT_OLLAMA_PORT;
pub use model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
pub use model_provider_info::ModelProviderInfo;
pub use model_provider_info::OLLAMA_CHAT_PROVIDER_ID;
pub use model_provider_info::OLLAMA_OSS_PROVIDER_ID;
pub use model_provider_info::WireApi;
pub use model_provider_info::built_in_model_providers;
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/model_provider_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ pub const DEFAULT_OLLAMA_PORT: u16 = 11434;

pub const LMSTUDIO_OSS_PROVIDER_ID: &str = "lmstudio";
pub const OLLAMA_OSS_PROVIDER_ID: &str = "ollama";
pub const OLLAMA_CHAT_PROVIDER_ID: &str = "ollama-chat";

/// Built-in default provider list.
pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
Expand All @@ -273,6 +274,10 @@ 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::Responses),
),
(
OLLAMA_CHAT_PROVIDER_ID,
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Chat),
),
(
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub struct Cli {
#[arg(long = "oss", default_value_t = false)]
pub oss: bool,

/// Specify which local provider to use (lmstudio or ollama).
/// Specify which local provider to use (lmstudio, ollama, or ollama-chat).
/// If not specified with --oss, will use config default or show selection.
#[arg(long = "local-provider")]
pub oss_provider: Option<String>,
Expand Down
18 changes: 17 additions & 1 deletion codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ pub use cli::Command;
pub use cli::ReviewArgs;
use codex_common::oss::ensure_oss_provider_ready;
use codex_common::oss::get_default_model_for_oss_provider;
use codex_common::oss::ollama_chat_deprecation_notice;
use codex_core::AuthManager;
use codex_core::LMSTUDIO_OSS_PROVIDER_ID;
use codex_core::NewThread;
use codex_core::OLLAMA_CHAT_PROVIDER_ID;
use codex_core::OLLAMA_OSS_PROVIDER_ID;
use codex_core::ThreadManager;
use codex_core::auth::enforce_login_restrictions;
Expand Down Expand Up @@ -176,7 +178,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
Some(provider)
} else {
return Err(anyhow::anyhow!(
"No default OSS provider configured. Use --local-provider=provider or set oss_provider to either {LMSTUDIO_OSS_PROVIDER_ID} or {OLLAMA_OSS_PROVIDER_ID} in config.toml"
"No default OSS provider configured. Use --local-provider=provider or set oss_provider to one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}, {OLLAMA_CHAT_PROVIDER_ID} in config.toml"
));
}
} else {
Expand Down Expand Up @@ -223,6 +225,14 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
std::process::exit(1);
}

let ollama_chat_support_notice = match ollama_chat_deprecation_notice(&config).await {
Ok(notice) => notice,
Err(err) => {
tracing::warn!(?err, "Failed to detect Ollama wire API");
None
}
};

let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"), false);

#[allow(clippy::print_stderr)]
Expand Down Expand Up @@ -252,6 +262,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
last_message_file.clone(),
)),
};
if let Some(notice) = ollama_chat_support_notice {
event_processor.process_event(Event {
id: String::new(),
msg: EventMsg::DeprecationNotice(notice),
});
}

if oss {
// We're in the oss section, so provider_id should be Some
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/ollama/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ bytes = { workspace = true }
codex-core = { workspace = true }
futures = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream"] }
semver = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
Expand All @@ -30,3 +31,4 @@ wiremock = { workspace = true }

[dev-dependencies]
assert_matches = { workspace = true }
pretty_assertions = { workspace = true }
66 changes: 65 additions & 1 deletion codex-rs/ollama/src/client.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -236,6 +263,7 @@ impl OllamaClient {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;

// Happy-path tests using a mock HTTP server; skip if sandbox network is disabled.
#[tokio::test]
Expand Down Expand Up @@ -269,6 +297,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() {
Expand Down
Loading
Loading