Skip to content

Commit 188273a

Browse files
committed
ollama: default to Responses API for built-ins
This is an alternate PR to solving the same problem as <openai#8227>. In this PR, when Ollama is used via `--oss` (or via `model_provider = "ollama`), we default it to use the Responses format. At runtime, we do an Ollama version check, and if the version is older than when Responses support was added to Ollama, we print out a warning. Because there's no way of configuring the wire api for a built-in provider, we temporarily add a new `oss_provider`/`model_provider` called `"ollama-chat"` that will force the chat format. Once the `"chat"` format is fully removed (see <openai#7782>), `ollama-chat` can be removed as well
1 parent 188f79a commit 188273a

File tree

19 files changed

+258
-14
lines changed

19 files changed

+258
-14
lines changed

codex-rs/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ serde_yaml = "0.9"
192192
serial_test = "3.2.0"
193193
sha1 = "0.10.6"
194194
sha2 = "0.10"
195+
semver = "1.0"
195196
shlex = "1.3.0"
196197
similar = "2.7.0"
197198
socket2 = "0.6.1"

codex-rs/common/src/oss.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,52 @@
11
//! OSS provider utilities shared between TUI and exec.
22
33
use codex_core::LMSTUDIO_OSS_PROVIDER_ID;
4+
use codex_core::OLLAMA_CHAT_PROVIDER_ID;
45
use codex_core::OLLAMA_OSS_PROVIDER_ID;
6+
use codex_core::WireApi;
57
use codex_core::config::Config;
8+
use codex_core::protocol::DeprecationNoticeEvent;
9+
use std::io;
610

711
/// Returns the default model for a given OSS provider.
812
pub fn get_default_model_for_oss_provider(provider_id: &str) -> Option<&'static str> {
913
match provider_id {
1014
LMSTUDIO_OSS_PROVIDER_ID => Some(codex_lmstudio::DEFAULT_OSS_MODEL),
11-
OLLAMA_OSS_PROVIDER_ID => Some(codex_ollama::DEFAULT_OSS_MODEL),
15+
OLLAMA_OSS_PROVIDER_ID | OLLAMA_CHAT_PROVIDER_ID => Some(codex_ollama::DEFAULT_OSS_MODEL),
1216
_ => None,
1317
}
1418
}
1519

20+
/// Returns a deprecation notice if Ollama doesn't support the responses wire API.
21+
pub async fn ollama_chat_deprecation_notice(
22+
config: &Config,
23+
) -> io::Result<Option<DeprecationNoticeEvent>> {
24+
if config.model_provider_id != OLLAMA_OSS_PROVIDER_ID
25+
|| config.model_provider.wire_api != WireApi::Responses
26+
{
27+
return Ok(None);
28+
}
29+
30+
if let Ok(Some(detection)) = codex_ollama::detect_wire_api(&config.model_provider).await
31+
&& detection.wire_api == WireApi::Chat
32+
{
33+
let version_suffix = detection
34+
.version
35+
.as_ref()
36+
.map(|version| format!(" (version {version})"))
37+
.unwrap_or_default();
38+
let summary = format!(
39+
"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."
40+
);
41+
return Ok(Some(DeprecationNoticeEvent {
42+
summary,
43+
details: None,
44+
}));
45+
}
46+
47+
Ok(None)
48+
}
49+
1650
/// Ensures the specified OSS provider is ready (models downloaded, service reachable).
1751
pub async fn ensure_oss_provider_ready(
1852
provider_id: &str,
@@ -24,7 +58,7 @@ pub async fn ensure_oss_provider_ready(
2458
.await
2559
.map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;
2660
}
27-
OLLAMA_OSS_PROVIDER_ID => {
61+
OLLAMA_OSS_PROVIDER_ID | OLLAMA_CHAT_PROVIDER_ID => {
2862
codex_ollama::ensure_oss_ready(config)
2963
.await
3064
.map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;

codex-rs/core/src/config/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use crate::features::FeaturesToml;
2424
use crate::git_info::resolve_root_git_project_for_trust;
2525
use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
2626
use crate::model_provider_info::ModelProviderInfo;
27+
use crate::model_provider_info::OLLAMA_CHAT_PROVIDER_ID;
2728
use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID;
2829
use crate::model_provider_info::built_in_model_providers;
2930
use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
@@ -631,14 +632,14 @@ pub fn set_project_trust_level(
631632
pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> {
632633
// Validate that the provider is one of the known OSS providers
633634
match provider {
634-
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => {
635+
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID | OLLAMA_CHAT_PROVIDER_ID => {
635636
// Valid provider, continue
636637
}
637638
_ => {
638639
return Err(std::io::Error::new(
639640
std::io::ErrorKind::InvalidInput,
640641
format!(
641-
"Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}"
642+
"Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}, {OLLAMA_CHAT_PROVIDER_ID}"
642643
),
643644
));
644645
}
@@ -836,7 +837,7 @@ pub struct ConfigToml {
836837
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
837838
pub experimental_use_unified_exec_tool: Option<bool>,
838839
pub experimental_use_freeform_apply_patch: Option<bool>,
839-
/// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama".
840+
/// Preferred OSS provider for local models, e.g. "lmstudio", "ollama", or "ollama-chat".
840841
pub oss_provider: Option<String>,
841842
}
842843

codex-rs/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub use model_provider_info::DEFAULT_LMSTUDIO_PORT;
5656
pub use model_provider_info::DEFAULT_OLLAMA_PORT;
5757
pub use model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
5858
pub use model_provider_info::ModelProviderInfo;
59+
pub use model_provider_info::OLLAMA_CHAT_PROVIDER_ID;
5960
pub use model_provider_info::OLLAMA_OSS_PROVIDER_ID;
6061
pub use model_provider_info::WireApi;
6162
pub use model_provider_info::built_in_model_providers;

codex-rs/core/src/model_provider_info.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ pub const DEFAULT_OLLAMA_PORT: u16 = 11434;
260260

261261
pub const LMSTUDIO_OSS_PROVIDER_ID: &str = "lmstudio";
262262
pub const OLLAMA_OSS_PROVIDER_ID: &str = "ollama";
263+
pub const OLLAMA_CHAT_PROVIDER_ID: &str = "ollama-chat";
263264

264265
/// Built-in default provider list.
265266
pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
@@ -273,6 +274,10 @@ pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
273274
("openai", P::create_openai_provider()),
274275
(
275276
OLLAMA_OSS_PROVIDER_ID,
277+
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses),
278+
),
279+
(
280+
OLLAMA_CHAT_PROVIDER_ID,
276281
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Chat),
277282
),
278283
(

codex-rs/exec/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub struct Cli {
2828
#[arg(long = "oss", default_value_t = false)]
2929
pub oss: bool,
3030

31-
/// Specify which local provider to use (lmstudio or ollama).
31+
/// Specify which local provider to use (lmstudio, ollama, or ollama-chat).
3232
/// If not specified with --oss, will use config default or show selection.
3333
#[arg(long = "local-provider")]
3434
pub oss_provider: Option<String>,

codex-rs/exec/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ pub use cli::Command;
1515
pub use cli::ReviewArgs;
1616
use codex_common::oss::ensure_oss_provider_ready;
1717
use codex_common::oss::get_default_model_for_oss_provider;
18+
use codex_common::oss::ollama_chat_deprecation_notice;
1819
use codex_core::AuthManager;
1920
use codex_core::ConversationManager;
2021
use codex_core::LMSTUDIO_OSS_PROVIDER_ID;
2122
use codex_core::NewConversation;
23+
use codex_core::OLLAMA_CHAT_PROVIDER_ID;
2224
use codex_core::OLLAMA_OSS_PROVIDER_ID;
2325
use codex_core::auth::enforce_login_restrictions;
2426
use codex_core::config::Config;
@@ -176,7 +178,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
176178
Some(provider)
177179
} else {
178180
return Err(anyhow::anyhow!(
179-
"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"
181+
"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"
180182
));
181183
}
182184
} else {
@@ -223,6 +225,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
223225
std::process::exit(1);
224226
}
225227

228+
let ollama_chat_support_notice = ollama_chat_deprecation_notice(&config).await?;
229+
226230
let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"));
227231

228232
#[allow(clippy::print_stderr)]
@@ -252,6 +256,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
252256
last_message_file.clone(),
253257
)),
254258
};
259+
if let Some(notice) = ollama_chat_support_notice {
260+
event_processor.process_event(Event {
261+
id: String::new(),
262+
msg: EventMsg::DeprecationNotice(notice),
263+
});
264+
}
255265

256266
if oss {
257267
// We're in the oss section, so provider_id should be Some

codex-rs/ollama/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ bytes = { workspace = true }
1717
codex-core = { workspace = true }
1818
futures = { workspace = true }
1919
reqwest = { workspace = true, features = ["json", "stream"] }
20+
semver = { workspace = true }
2021
serde_json = { workspace = true }
2122
tokio = { workspace = true, features = [
2223
"io-std",
@@ -30,3 +31,4 @@ wiremock = { workspace = true }
3031

3132
[dev-dependencies]
3233
assert_matches = { workspace = true }
34+
pretty_assertions = { workspace = true }

codex-rs/ollama/src/client.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use bytes::BytesMut;
22
use futures::StreamExt;
33
use futures::stream::BoxStream;
4+
use semver::Version;
45
use serde_json::Value as JsonValue;
56
use std::collections::VecDeque;
67
use std::io;
@@ -53,7 +54,7 @@ impl OllamaClient {
5354
}
5455

5556
/// Build a client from a provider definition and verify the server is reachable.
56-
async fn try_from_provider(provider: &ModelProviderInfo) -> io::Result<Self> {
57+
pub(crate) async fn try_from_provider(provider: &ModelProviderInfo) -> io::Result<Self> {
5758
#![expect(clippy::expect_used)]
5859
let base_url = provider
5960
.base_url
@@ -125,6 +126,32 @@ impl OllamaClient {
125126
Ok(names)
126127
}
127128

129+
/// Query the server for its version string, returning `None` when unavailable.
130+
pub async fn fetch_version(&self) -> io::Result<Option<Version>> {
131+
let version_url = format!("{}/api/version", self.host_root.trim_end_matches('/'));
132+
let resp = self
133+
.client
134+
.get(version_url)
135+
.send()
136+
.await
137+
.map_err(io::Error::other)?;
138+
if !resp.status().is_success() {
139+
return Ok(None);
140+
}
141+
let val = resp.json::<JsonValue>().await.map_err(io::Error::other)?;
142+
let Some(version_str) = val.get("version").and_then(|v| v.as_str()).map(str::trim) else {
143+
return Ok(None);
144+
};
145+
let normalized = version_str.trim_start_matches('v');
146+
match Version::parse(normalized) {
147+
Ok(version) => Ok(Some(version)),
148+
Err(err) => {
149+
tracing::warn!("Failed to parse Ollama version `{version_str}`: {err}");
150+
Ok(None)
151+
}
152+
}
153+
}
154+
128155
/// Start a model pull and emit streaming events. The returned stream ends when
129156
/// a Success event is observed or the server closes the connection.
130157
pub async fn pull_model_stream(
@@ -236,6 +263,7 @@ impl OllamaClient {
236263
#[cfg(test)]
237264
mod tests {
238265
use super::*;
266+
use pretty_assertions::assert_eq;
239267

240268
// Happy-path tests using a mock HTTP server; skip if sandbox network is disabled.
241269
#[tokio::test]
@@ -269,6 +297,42 @@ mod tests {
269297
assert!(models.contains(&"mistral".to_string()));
270298
}
271299

300+
#[tokio::test]
301+
async fn test_fetch_version() {
302+
if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
303+
tracing::info!(
304+
"{} is set; skipping test_fetch_version",
305+
codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR
306+
);
307+
return;
308+
}
309+
310+
let server = wiremock::MockServer::start().await;
311+
wiremock::Mock::given(wiremock::matchers::method("GET"))
312+
.and(wiremock::matchers::path("/api/tags"))
313+
.respond_with(wiremock::ResponseTemplate::new(200).set_body_raw(
314+
serde_json::json!({ "models": [] }).to_string(),
315+
"application/json",
316+
))
317+
.mount(&server)
318+
.await;
319+
wiremock::Mock::given(wiremock::matchers::method("GET"))
320+
.and(wiremock::matchers::path("/api/version"))
321+
.respond_with(wiremock::ResponseTemplate::new(200).set_body_raw(
322+
serde_json::json!({ "version": "0.14.1" }).to_string(),
323+
"application/json",
324+
))
325+
.mount(&server)
326+
.await;
327+
328+
let client = OllamaClient::try_from_provider_with_base_url(server.uri().as_str())
329+
.await
330+
.expect("client");
331+
332+
let version = client.fetch_version().await.expect("version fetch");
333+
assert_eq!(version, Some(Version::new(0, 14, 1)));
334+
}
335+
272336
#[tokio::test]
273337
async fn test_probe_server_happy_path_openai_compat_and_native() {
274338
if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {

0 commit comments

Comments
 (0)