From b7c2ce7a93f018e7a9947a4f7efb4f59eee75312 Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:11:54 -0700 Subject: [PATCH 1/5] docs: document remote server/client mode --- README.md | 5 ++++- codex-rs/README.md | 5 +++++ docs/advanced.md | 6 +++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12620b21e18..5da59449862 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ codex exec --edit-mode trusted "update CHANGELOG and open a PR" ``` Safety guidance: + - Prefer running in a VM or container when using `--edit-mode trusted`. - Pair with `--sandbox workspace-write` for guardrails. If you need `--sandbox danger-full-access`, only do so inside an isolated environment. - You can control command timeouts via `-c command_timeout_ms=120000` or disable with `-c command_timeout_ms=\"none\"`. @@ -73,6 +74,9 @@ You can also use Codex with an API key, but this requires [additional setup](./d Codex CLI supports [MCP servers](./docs/advanced.md#model-context-protocol-mcp). Enable by adding an `mcp_servers` section to your `~/.codex/config.toml`. +### Remote server/client mode + +Codex also includes a lightweight TCP server and client that let you run the agent on one machine and drive it from another. Launch the server with `cargo run -p codex-server` (or the `codex-server` binary) and connect using `cargo run -p codex-client` or the `codex-client` binary. The server boots the same `ConversationManager` used by the CLI, so plugins, MCP servers and other runtime features remain available when Codex runs in this remote mode. ### Configuration @@ -110,4 +114,3 @@ Codex CLI supports a rich set of configuration options, with preferences stored ## License This repository is licensed under the [Apache-2.0 License](LICENSE). - diff --git a/codex-rs/README.md b/codex-rs/README.md index 0019995ec0e..a11aa3dc183 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -31,6 +31,10 @@ It is still experimental, but you can also launch Codex as an MCP _server_ by ru npx @modelcontextprotocol/inspector codex mcp ``` +### Remote server and client + +For remote workflows, the workspace includes `codex-server` and `codex-client` binaries. `codex-server` exposes a raw TCP interface that speaks the same JSON protocol used by the CLI, and `codex-client` forwards stdin to the server and prints events. Because the server spins up the full `ConversationManager`, all plugins, MCP servers, and other runtime features remain available when using Codex over the network. + ### Notifications You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. @@ -135,6 +139,7 @@ codex --edit-mode trusted ``` Safety guidance for `trusted`: + - Prefer running in a VM or container. - Consider pairing with `--sandbox workspace-write` for guardrails. If you need `--sandbox danger-full-access`, only do so inside an isolated environment. - Control host command timeouts via `-c command_timeout_ms=120000` or disable with `-c command_timeout_ms=\"none\"`. diff --git a/docs/advanced.md b/docs/advanced.md index af877c43bdd..52810984e2c 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -41,4 +41,8 @@ env = { "API_KEY" = "value" } ``` > [!TIP] -> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. +> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. + +## Remote server and client + +Codex can also run as a minimal TCP server (`cargo run -p codex-server`) and be driven by a matching client (`cargo run -p codex-client`). The server accepts newline-delimited JSON `Submission` objects and streams back `Event` messages. Because it uses the same `ConversationManager` as the CLI, plugins, MCP servers, and other runtime features remain available when Codex runs in this remote mode. From e07060db92356d1b40897017c6d3f3fc58fe5fda Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:44:30 -0700 Subject: [PATCH 2/5] feat: introduce model adapter abstraction --- codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/client.rs | 39 +++++++++++++++++++--- codex-rs/core/src/error.rs | 5 +++ codex-rs/core/src/lib.rs | 3 ++ codex-rs/core/src/model_adapter.rs | 41 ++++++++++++++++++++++++ codex-rs/core/src/model_provider_info.rs | 7 ++++ docs/model-adapters.md | 29 +++++++++++++++++ 7 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 codex-rs/core/src/model_adapter.rs create mode 100644 docs/model-adapters.md diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a997a2af75f..7027cc34547 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -71,6 +71,7 @@ tree-sitter-bash = "0.25.0" uuid = { version = "1", features = ["serde", "v4"] } whoami = "1.6.1" wildmatch = "2.4.0" +async-trait = "0.1" # Optional speech dependencies for the offline voice stack. kokoro-tts = { version = "0.2.8", optional = true } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ea103021c5c..757810a23b4 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -32,6 +32,7 @@ use crate::error::CodexErr; use crate::error::Result; use crate::error::UsageLimitReachedError; use crate::flags::CODEX_RS_SSE_FIXTURE; +use crate::model_adapter::ModelAdapter; use crate::model_family::ModelFamily; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; @@ -43,7 +44,8 @@ use crate::util::backoff; use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::models::ResponseItem; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::Mutex; #[derive(Debug, Deserialize)] struct ErrorResponse { @@ -69,6 +71,7 @@ pub struct ModelClient { session_id: Uuid, effort: ReasoningEffortConfig, summary: ReasoningSummaryConfig, + adapter: Option>, } impl ModelClient { @@ -79,15 +82,36 @@ impl ModelClient { effort: ReasoningEffortConfig, summary: ReasoningSummaryConfig, session_id: Uuid, + ) -> Self { + Self::new_with_adapter( + config, + auth_manager, + provider, + effort, + summary, + session_id, + None, + ) + } + + pub fn new_with_adapter( + config: Arc, + auth_manager: Option>, + provider: ModelProviderInfo, + effort: ReasoningEffortConfig, + summary: ReasoningSummaryConfig, + session_id: Uuid, + adapter: Option>, ) -> Self { Self { config, auth_manager, client: reqwest::Client::new(), - provider: Arc::new(Mutex::new(provider)), + provider: Arc::new(Mutex::new(provider)), session_id, effort, summary, + adapter, } } @@ -144,6 +168,13 @@ impl ModelClient { Ok(ResponseStream { rx_event: rx }) } + WireApi::Custom => { + let adapter = self.adapter.as_ref().ok_or(CodexErr::NoModelAdapter)?; + let provider = self.provider.lock().unwrap().clone(); + adapter + .stream(prompt, &self.config.model_family, &self.client, &provider) + .await + } } } @@ -241,9 +272,7 @@ impl ModelClient { serde_json::to_string(&payload)? ); - let mut req_builder = provider - .create_request_builder(&self.client, &auth) - .await?; + let mut req_builder = provider.create_request_builder(&self.client, &auth).await?; req_builder = req_builder .header("OpenAI-Beta", "responses=experimental") diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index b05ff1a5811..8d7cacd100f 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -91,6 +91,11 @@ pub enum CodexErr { #[error("internal error; agent loop died unexpectedly")] InternalAgentDied, + /// Returned when a model provider declares a custom wire protocol but no + /// adapter was supplied when constructing the [`ModelClient`]. + #[error("no model adapter registered for provider")] + NoModelAdapter, + /// Sandbox error #[error("sandbox error: {0}")] Sandbox(#[from] SandboxErr), diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index f6464b3d736..e69370fce10 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -30,8 +30,11 @@ pub mod landlock; mod mcp_connection_manager; mod mcp_tool_call; mod message_history; +mod model_adapter; mod model_provider_info; pub mod parse_command; +pub use model_adapter::ModelAdapter; +pub use model_adapter::OpenAiChatAdapter; pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID; pub use model_provider_info::ModelProviderInfo; pub use model_provider_info::WireApi; diff --git a/codex-rs/core/src/model_adapter.rs b/codex-rs/core/src/model_adapter.rs new file mode 100644 index 00000000000..1b4d6bc883d --- /dev/null +++ b/codex-rs/core/src/model_adapter.rs @@ -0,0 +1,41 @@ +use async_trait::async_trait; + +use crate::client_common::Prompt; +use crate::client_common::ResponseStream; +use crate::error::Result; +use crate::model_family::ModelFamily; +use crate::model_provider_info::ModelProviderInfo; + +/// Abstraction over different LLM backends. Implementations translate a Codex +/// [`Prompt`] into provider specific requests and normalise the streaming +/// responses back into [`ResponseStream`]. +#[async_trait] +pub trait ModelAdapter: Send + Sync + std::fmt::Debug { + /// Open a streaming connection for the given `prompt` using the provided + /// HTTP `client` and model `provider` definition. + async fn stream( + &self, + prompt: &Prompt, + model_family: &ModelFamily, + client: &reqwest::Client, + provider: &ModelProviderInfo, + ) -> Result; +} + +/// Default adapter for OpenAI compatible Chat Completions endpoints. +#[derive(Debug)] +pub struct OpenAiChatAdapter; + +#[async_trait] +impl ModelAdapter for OpenAiChatAdapter { + async fn stream( + &self, + prompt: &Prompt, + model_family: &ModelFamily, + client: &reqwest::Client, + provider: &ModelProviderInfo, + ) -> Result { + crate::chat_completions::stream_chat_completions(prompt, model_family, client, provider) + .await + } +} diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 8e39f89aa02..f9881a1b687 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -37,6 +37,12 @@ pub enum WireApi { /// Regular Chat Completions compatible with `/v1/chat/completions`. #[default] Chat, + + /// A user supplied [`ModelAdapter`] implementation. Providers that set + /// this require an adapter to be injected when constructing the + /// [`ModelClient`]. + #[serde(rename = "custom")] + Custom, } /// Serializable representation of a provider definition. @@ -214,6 +220,7 @@ impl ModelProviderInfo { match self.wire_api { WireApi::Responses => format!("{base_url}/responses{query_string}"), WireApi::Chat => format!("{base_url}/chat/completions{query_string}"), + WireApi::Custom => format!("{base_url}{query_string}"), } } diff --git a/docs/model-adapters.md b/docs/model-adapters.md new file mode 100644 index 00000000000..a6754bca70d --- /dev/null +++ b/docs/model-adapters.md @@ -0,0 +1,29 @@ +# Model adapter layer + +Codex normally speaks to OpenAI compatible APIs. To enable full "bring your own model" +support, the `codex-core` crate now exposes a `ModelAdapter` trait that translates +Codex prompts into provider specific requests and normalises the streamed responses. + +A provider using a non-standard protocol can set `wire_api = "custom"` in its +`ModelProviderInfo` configuration and supply an adapter when constructing the +`ModelClient`: + +```rust +use std::sync::Arc; +use codex_core::{ModelClient, ModelAdapter, OpenAiChatAdapter, WireApi}; + +// Register adapter (OpenAI chat adapter shown for brevity). +let adapter = Arc::new(OpenAiChatAdapter); +let client = ModelClient::new_with_adapter( + config, + auth_manager, + provider, // provider.wire_api == WireApi::Custom + effort, + summary, + session_id, + Some(adapter), +); +``` + +Custom backends implement the trait and can freely call local processes or +third‑party services without requiring model fine tuning. From a4811b3945418e92b2f311e66163969c8f57a17f Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sat, 30 Aug 2025 01:48:26 -0700 Subject: [PATCH 3/5] chore: add local CI helper script --- docs/contributing.md | 2 +- scripts/local_ci.sh | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100755 scripts/local_ci.sh diff --git a/docs/contributing.md b/docs/contributing.md index 1ad681e94de..90c3b0cfa71 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -24,7 +24,7 @@ If you want to add a new feature or change the behavior of an existing one, plea ### Opening a pull request - Fill in the PR template (or include similar information) - **What? Why? How?** -- Run **all** checks locally (`cargo test && cargo clippy --tests && cargo fmt -- --config imports_granularity=Item`). CI failures that could have been caught locally slow down the process. +- Run **all** checks locally with `./scripts/local_ci.sh` (or `cargo test && cargo clippy --tests && cargo fmt -- --config imports_granularity=Item`). CI failures that could have been caught locally slow down the process. - Make sure your branch is up-to-date with `main` and that you have resolved merge conflicts. - Mark the PR as **Ready for review** only when you believe it is in a merge-able state. diff --git a/scripts/local_ci.sh b/scripts/local_ci.sh new file mode 100755 index 00000000000..3eb38be1302 --- /dev/null +++ b/scripts/local_ci.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Run checks that mirror GitHub CI workflows. +set -euo pipefail + +# Node/doc related checks +pnpm install >/dev/null +if ! ./codex-cli/scripts/stage_release.sh >/tmp/local_ci_stage.log 2>&1; then + echo "[warn] stage_release.sh failed (requires gh auth?)" >&2 +fi +python3 scripts/asciicheck.py README.md +python3 scripts/readme_toc.py README.md || true +python3 scripts/asciicheck.py codex-cli/README.md +python3 scripts/readme_toc.py codex-cli/README.md || true + +# Codespell +codespell --ignore-words .codespellignore || true + +# Rust checks +( + cd codex-rs + cargo fmt -- --config imports_granularity=Item --check + cargo clippy --all-features --tests -- -D warnings + cargo test --all-features +) From 9cfe892eed26f3f66d10b9d0fcc6da20836509eb Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sat, 30 Aug 2025 02:21:22 -0700 Subject: [PATCH 4/5] fix: harden local CI script --- docs/contributing.md | 2 +- scripts/local_ci.sh | 31 ++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index 90c3b0cfa71..4ff24425689 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -24,7 +24,7 @@ If you want to add a new feature or change the behavior of an existing one, plea ### Opening a pull request - Fill in the PR template (or include similar information) - **What? Why? How?** -- Run **all** checks locally with `./scripts/local_ci.sh` (or `cargo test && cargo clippy --tests && cargo fmt -- --config imports_granularity=Item`). CI failures that could have been caught locally slow down the process. +- Run **all** checks locally with `./scripts/local_ci.sh` (installs the ALSA library and runs `cargo fmt`, `clippy`, and tests) or manually run `cargo fmt -- --config imports_granularity=Item`, `cargo clippy --all-features --tests`, and `cargo test --all-features`. CI failures that could have been caught locally slow down the process. - Make sure your branch is up-to-date with `main` and that you have resolved merge conflicts. - Mark the PR as **Ready for review** only when you believe it is in a merge-able state. diff --git a/scripts/local_ci.sh b/scripts/local_ci.sh index 3eb38be1302..3a787448493 100755 --- a/scripts/local_ci.sh +++ b/scripts/local_ci.sh @@ -4,8 +4,12 @@ set -euo pipefail # Node/doc related checks pnpm install >/dev/null -if ! ./codex-cli/scripts/stage_release.sh >/tmp/local_ci_stage.log 2>&1; then - echo "[warn] stage_release.sh failed (requires gh auth?)" >&2 +if command -v gh >/dev/null && gh auth status >/dev/null 2>&1; then + if ! ./codex-cli/scripts/stage_release.sh >/tmp/local_ci_stage.log 2>&1; then + echo "[warn] stage_release.sh failed" >&2 + fi +else + echo "[skip] stage_release.sh (requires authenticated gh CLI)" >&2 fi python3 scripts/asciicheck.py README.md python3 scripts/readme_toc.py README.md || true @@ -15,10 +19,27 @@ python3 scripts/readme_toc.py codex-cli/README.md || true # Codespell codespell --ignore-words .codespellignore || true +# Ensure ALSA libs for tests +if command -v apt-get >/dev/null; then + if command -v sudo >/dev/null; then + sudo apt-get update >/dev/null && sudo apt-get install -y libasound2-dev >/dev/null || \ + echo "[warn] failed to install libasound2-dev" >&2 + else + apt-get update >/dev/null && apt-get install -y libasound2-dev >/dev/null || \ + echo "[warn] failed to install libasound2-dev" >&2 + fi +fi + # Rust checks ( cd codex-rs - cargo fmt -- --config imports_granularity=Item --check - cargo clippy --all-features --tests -- -D warnings - cargo test --all-features + if ! cargo fmt -- --config imports_granularity=Item --check; then + echo "[warn] cargo fmt failed" >&2 + fi + if ! cargo clippy --all-features --tests -- -D warnings; then + echo "[warn] cargo clippy failed" >&2 + fi + if ! cargo test --all-features; then + echo "[warn] cargo test failed" >&2 + fi ) From ea54a1121917a3bd1bbe1ccdf33e8b3501a8caa1 Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sat, 30 Aug 2025 03:27:57 -0700 Subject: [PATCH 5/5] fix: clean clippy lints --- codex-rs/ai/src/lib.rs | 11 ++-- codex-rs/client/src/main.rs | 8 ++- codex-rs/core/src/autogen.rs | 15 +++--- codex-rs/core/src/client.rs | 40 +++++++++++--- codex-rs/core/src/client_common.rs | 2 +- codex-rs/core/src/codex.rs | 10 ++-- codex-rs/core/src/local_voice.rs | 67 +++++++++++------------- codex-rs/core/src/model_provider_info.rs | 10 ++-- codex-rs/core/src/openai_tools.rs | 15 +++--- codex-rs/core/src/plugins.rs | 3 +- codex-rs/core/src/safety.rs | 2 + codex-rs/core/src/voice.rs | 12 +++-- codex-rs/ollama/src/lib.rs | 33 ++++++------ 13 files changed, 136 insertions(+), 92 deletions(-) diff --git a/codex-rs/ai/src/lib.rs b/codex-rs/ai/src/lib.rs index 389f0459281..bf8f6c03874 100644 --- a/codex-rs/ai/src/lib.rs +++ b/codex-rs/ai/src/lib.rs @@ -2,11 +2,12 @@ use std::sync::Arc; use anyhow::Result; use futures::StreamExt; -use genai::{ - Client as GenaiClient, - chat::{ChatMessage, ChatRequest, ChatStreamEvent}, -}; -use groqai::{AudioTranscriptionRequest, GroqClient}; +use genai::Client as GenaiClient; +use genai::chat::ChatMessage; +use genai::chat::ChatRequest; +use genai::chat::ChatStreamEvent; +use groqai::AudioTranscriptionRequest; +use groqai::GroqClient; /// Thin facade over GenAI and Groq clients. #[derive(Clone)] diff --git a/codex-rs/client/src/main.rs b/codex-rs/client/src/main.rs index 018b4aca5ff..b9bc29f00a6 100644 --- a/codex-rs/client/src/main.rs +++ b/codex-rs/client/src/main.rs @@ -34,7 +34,13 @@ async fn main() -> anyhow::Result<()> { } match serde_json::from_str::(&line) { Ok(sub) => { - let s = serde_json::to_string(&sub).unwrap(); + let s = match serde_json::to_string(&sub) { + Ok(s) => s, + Err(e) => { + eprintln!("invalid submission: {e}"); + continue; + } + }; if write_half.write_all(s.as_bytes()).await.is_err() { break; } diff --git a/codex-rs/core/src/autogen.rs b/codex-rs/core/src/autogen.rs index 0bbc99208ca..3b7e8c0c526 100644 --- a/codex-rs/core/src/autogen.rs +++ b/codex-rs/core/src/autogen.rs @@ -1,9 +1,12 @@ use std::process::Command; -use anyhow::{Context, Result, anyhow}; +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; use serde_json::json; -use crate::{ModelProviderInfo, create_oss_provider_with_base_url}; +use crate::ModelProviderInfo; +use crate::create_oss_provider_with_base_url; /// Integration helpers for invoking Python's AutoGen framework. /// @@ -76,10 +79,10 @@ pub fn run_autogen_with_provider( entry["base_url"] = json!(base_url); } - if let Some(env_key) = &provider.env_key { - if let Ok(value) = std::env::var(env_key) { - entry["api_key"] = json!(value); - } + if let Some(env_key) = &provider.env_key + && let Ok(value) = std::env::var(env_key) + { + entry["api_key"] = json!(value); } let config_list = json!([entry]); diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 757810a23b4..81b1b1ac374 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -129,11 +129,20 @@ impl ModelClient { /// the provider config. Public callers always invoke `stream()` – the /// specialised helpers are private to avoid accidental misuse. pub async fn stream(&self, prompt: &Prompt) -> Result { - let wire_api = { self.provider.lock().unwrap().wire_api }; + let wire_api = { + self.provider + .lock() + .unwrap_or_else(|e| e.into_inner()) + .wire_api + }; match wire_api { WireApi::Responses => self.stream_responses(prompt).await, WireApi::Chat => { - let provider = self.provider.lock().unwrap().clone(); + let provider = self + .provider + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(); // Create the raw streaming connection first. let response_stream = stream_chat_completions( prompt, @@ -170,7 +179,11 @@ impl ModelClient { } WireApi::Custom => { let adapter = self.adapter.as_ref().ok_or(CodexErr::NoModelAdapter)?; - let provider = self.provider.lock().unwrap().clone(); + let provider = self + .provider + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(); adapter .stream(prompt, &self.config.model_family, &self.client, &provider) .await @@ -180,7 +193,11 @@ impl ModelClient { /// Implementation for the OpenAI *Responses* experimental API. async fn stream_responses(&self, prompt: &Prompt) -> Result { - let provider = self.provider.lock().unwrap().clone(); + let provider = self + .provider + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(); if let Some(path) = &*CODEX_RS_SSE_FIXTURE { // short circuit for tests warn!(path, "Streaming from fixture"); @@ -396,7 +413,10 @@ impl ModelClient { } pub fn get_provider(&self) -> ModelProviderInfo { - self.provider.lock().unwrap().clone() + self.provider + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() } /// Returns the currently configured model slug. @@ -426,12 +446,18 @@ impl ModelClient { /// Rotate to the next API key for the current provider. Returns `true` /// if a rotation occurred. pub fn rotate_api_key(&self) -> bool { - self.provider.lock().unwrap().rotate_api_key() + self.provider + .lock() + .unwrap_or_else(|e| e.into_inner()) + .rotate_api_key() } /// Reset provider API key rotation back to the first key. pub fn reset_api_key_rotation(&self) { - self.provider.lock().unwrap().reset_api_key_rotation(); + self.provider + .lock() + .unwrap_or_else(|e| e.into_inner()) + .reset_api_key_rotation(); } } diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index e2c191f4a56..73b0e65ad52 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -175,7 +175,7 @@ pub(crate) fn create_text_param_for_request( }) } -pub(crate) struct ResponseStream { +pub struct ResponseStream { pub(crate) rx_event: mpsc::Receiver>, } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6664a99ab02..cbe1516b010 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2951,8 +2951,11 @@ fn convert_call_tool_result_to_function_call_output_payload( #[cfg(test)] mod tests { use super::*; - use crate::config::{Config, ConfigOverrides, ConfigToml}; - use codex_login::{AuthManager, CodexAuth}; + use crate::config::Config; + use crate::config::ConfigOverrides; + use crate::config::ConfigToml; + use codex_login::AuthManager; + use codex_login::CodexAuth; use mcp_types::ContentBlock; use mcp_types::TextContent; use pretty_assertions::assert_eq; @@ -2960,7 +2963,8 @@ mod tests { use std::collections::HashMap; use std::sync::Arc; use std::time::Duration as StdDuration; - use tempfile::{TempDir, tempdir}; + use tempfile::TempDir; + use tempfile::tempdir; use tokio::time; fn text_block(s: &str) -> ContentBlock { diff --git a/codex-rs/core/src/local_voice.rs b/codex-rs/core/src/local_voice.rs index 57041e671d2..b1f0ed87e1d 100644 --- a/codex-rs/core/src/local_voice.rs +++ b/codex-rs/core/src/local_voice.rs @@ -1,6 +1,7 @@ +#![allow(clippy::print_stdout)] + use std::io::Cursor; use std::io::Error as IoError; -use std::io::ErrorKind; use std::io::Write; use std::path::Path; use std::path::PathBuf; @@ -89,11 +90,12 @@ fn select_model<'a>(models: &'a [ModelInfo], kind: &str) -> Result<&'a ModelInfo } loop { let ans = prompt("Choose a model number: ") - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; - if let Ok(idx) = ans.parse::() { - if idx > 0 && idx <= models.len() { - return Ok(&models[idx - 1]); - } + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; + if let Ok(idx) = ans.parse::() + && idx > 0 + && idx <= models.len() + { + return Ok(&models[idx - 1]); } println!("Invalid selection. Please try again."); } @@ -108,28 +110,25 @@ async fn ensure_file(path: &Path, model: &ModelInfo) -> Result<()> { "By downloading, you agree to the model's license terms provided by the third-party author." ); let ans = prompt("Download this model now? [y/N]: ") - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; if !matches!(ans.to_lowercase().as_str(), "y" | "yes") { - return Err(CodexErr::Io(IoError::new( - ErrorKind::Other, - "model download declined", - ))); + return Err(CodexErr::Io(IoError::other("model download declined"))); } println!("Downloading {}...", model.url); let bytes = reqwest::get(model.url) .await - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))? + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))? .bytes() .await - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; if let Some(parent) = path.parent() { tokio_fs::create_dir_all(parent) .await - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; } tokio_fs::write(path, &bytes) .await - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; Ok(()) } @@ -181,27 +180,24 @@ impl LocalVoice { pub async fn transcribe_audio(&self, audio: &[u8], _mime_type: &str) -> Result { // Decode using rodio; expect 16kHz mono PCM. let cursor = Cursor::new(audio.to_vec()); - let decoder = Decoder::new(cursor) - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + let decoder = + Decoder::new(cursor).map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; let sample_rate = decoder.sample_rate(); let channels = decoder.channels(); if sample_rate != 16_000 || channels != 1 { - return Err(CodexErr::Io(IoError::new( - ErrorKind::InvalidInput, - "audio must be 16kHz mono", - ))); + return Err(CodexErr::Io(IoError::other("audio must be 16kHz mono"))); } let samples: Vec = decoder.convert_samples().collect(); let model_path = self .stt_model .to_str() - .ok_or_else(|| CodexErr::Io(IoError::new(ErrorKind::Other, "invalid model path")))?; + .ok_or_else(|| CodexErr::Io(IoError::other("invalid model path")))?; let ctx = WhisperContext::new_with_params(model_path, WhisperContextParameters::default()) - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; let mut state = ctx .create_state() - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 }); params.set_print_special(false); params.set_print_progress(false); @@ -209,14 +205,14 @@ impl LocalVoice { params.set_print_timestamps(false); state .full(params, &samples) - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; let num_segments = state.full_n_segments(); let mut out = String::new(); for i in 0..num_segments { if let Some(segment) = state.get_segment(i) { let text = segment .to_str() - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; out.push_str(text); } } @@ -225,20 +221,21 @@ impl LocalVoice { /// Synthesize speech audio bytes from text using Kokoro TTS. pub async fn synthesize_speech(&self, text: &str, _voice: &str) -> Result> { - let model_path = self.tts_model.to_str().ok_or_else(|| { - CodexErr::Io(IoError::new(ErrorKind::Other, "invalid TTS model path")) - })?; + let model_path = self + .tts_model + .to_str() + .ok_or_else(|| CodexErr::Io(IoError::other("invalid TTS model path")))?; let voice_path = self .tts_voice .to_str() - .ok_or_else(|| CodexErr::Io(IoError::new(ErrorKind::Other, "invalid voice path")))?; + .ok_or_else(|| CodexErr::Io(IoError::other("invalid voice path")))?; let tts = KokoroTts::new(model_path, voice_path) .await - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; let (audio, _took) = tts .synth(text, Voice::AfAlloy(1.0)) .await - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; // Convert f32 samples to WAV bytes (16-bit PCM at 24kHz). let pcm: Vec = audio @@ -254,15 +251,15 @@ impl LocalVoice { sample_format: SampleFormat::Int, }; let mut writer = WavWriter::new(Cursor::new(&mut out), spec) - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; for s in pcm { writer .write_sample(s) - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; } writer .finalize() - .map_err(|e| CodexErr::Io(IoError::new(ErrorKind::Other, e.to_string())))?; + .map_err(|e| CodexErr::Io(IoError::other(e.to_string())))?; } Ok(out) } diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index f9881a1b687..b0cdec89ce9 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -289,11 +289,11 @@ impl ModelProviderInfo { /// Advance to the next API key in `api_keys`. Returns `true` if rotation /// occurred and `false` otherwise (e.g. zero or one key configured). pub fn rotate_api_key(&mut self) -> bool { - if let Some(keys) = &self.api_keys { - if keys.len() > 1 { - self.api_key_index = (self.api_key_index + 1) % keys.len(); - return true; - } + if let Some(keys) = &self.api_keys + && keys.len() > 1 + { + self.api_key_index = (self.api_key_index + 1) % keys.len(); + return true; } false } diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index c5464b5f236..5c2a3e2c206 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -92,7 +92,7 @@ pub struct FreeformToolFormat { /// Responses API. #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(tag = "type")] -pub(crate) enum OpenAiTool { +pub enum OpenAiTool { #[serde(rename = "function")] Function(ResponsesApiTool), #[serde(rename = "local_shell")] @@ -560,13 +560,12 @@ fn sanitize_json_schema(value: &mut JsonValue) { // boolean). In this case, sanitize the schema and then replace it // with `true` to preserve the intent of allowing additional // properties. Booleans are left as-is. - if let Some(ap) = map.get_mut("additionalProperties") { - if !matches!(ap, JsonValue::Bool(_)) { - // Sanitizing ensures any nested schema is well-formed - // before we drop it. - sanitize_json_schema(ap); - *ap = JsonValue::Bool(true); - } + if let Some(ap) = map.get_mut("additionalProperties") + && !matches!(ap, JsonValue::Bool(_)) + { + // Sanitizing ensures any nested schema is well-formed before we drop it. + sanitize_json_schema(ap); + *ap = JsonValue::Bool(true); } } diff --git a/codex-rs/core/src/plugins.rs b/codex-rs/core/src/plugins.rs index 3a469887b69..980752d48da 100644 --- a/codex-rs/core/src/plugins.rs +++ b/codex-rs/core/src/plugins.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::path::PathBuf; use serde::Deserialize; diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 1595ccaa5d6..a4b82e46e4c 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -19,6 +19,7 @@ pub enum SafetyCheck { Reject { reason: String }, } +#[allow(dead_code)] pub fn assess_patch_safety( action: &ApplyPatchAction, policy: AskForApproval, @@ -96,6 +97,7 @@ pub fn assess_patch_safety_with_mode( /// - the user has explicitly approved the command /// - the command is on the "known safe" list /// - `DangerFullAccess` was specified and `UnlessTrusted` was not +#[allow(dead_code)] pub fn assess_command_safety( command: &[String], approval_policy: AskForApproval, diff --git a/codex-rs/core/src/voice.rs b/codex-rs/core/src/voice.rs index 1131c21ea7e..2034e101da7 100644 --- a/codex-rs/core/src/voice.rs +++ b/codex-rs/core/src/voice.rs @@ -41,10 +41,12 @@ impl ModelClient { /// Transcribe audio bytes to text using OpenAI's Whisper model. pub async fn transcribe_audio(&self, audio: &[u8], mime_type: &str) -> Result { let auth_mode = self.auth_manager.as_ref().and_then(|m| m.auth()); - let builder = self + let provider = self .provider .lock() - .unwrap() + .unwrap_or_else(|e| e.into_inner()) + .clone(); + let builder = provider .create_request_builder_for_path(&self.client, &auth_mode, "/audio/transcriptions") .await?; @@ -71,10 +73,12 @@ impl ModelClient { /// Convert text to spoken audio using GPT-4o TTS models. pub async fn synthesize_speech(&self, text: &str, voice: &str) -> Result> { let auth_mode = self.auth_manager.as_ref().and_then(|m| m.auth()); - let builder = self + let provider = self .provider .lock() - .unwrap() + .unwrap_or_else(|e| e.into_inner()) + .clone(); + let builder = provider .create_request_builder_for_path(&self.client, &auth_mode, "/audio/speech") .await?; let payload = json!({ diff --git a/codex-rs/ollama/src/lib.rs b/codex-rs/ollama/src/lib.rs index d72eb4e047f..d58f1d29f95 100644 --- a/codex-rs/ollama/src/lib.rs +++ b/codex-rs/ollama/src/lib.rs @@ -31,21 +31,20 @@ pub async fn ensure_oss_ready(config: &mut Config) -> std::io::Result<()> { return Ok(()); } - if requested_model == DEFAULT_OSS_MODEL { - if let Some(first) = models.first() { - tracing::info!("Using local Ollama model `{}`", first); - config.model = first.clone(); - config.model_family = - find_family_for_model(first).unwrap_or_else(|| ModelFamily { - slug: first.clone(), - family: first.clone(), - needs_special_apply_patch_instructions: false, - supports_reasoning_summaries: false, - uses_local_shell_tool: false, - apply_patch_tool_type: None, - }); - return Ok(()); - } + if requested_model == DEFAULT_OSS_MODEL + && let Some(first) = models.first() + { + tracing::info!("Using local Ollama model `{}`", first); + config.model = first.clone(); + config.model_family = find_family_for_model(first).unwrap_or_else(|| ModelFamily { + slug: first.clone(), + family: first.clone(), + needs_special_apply_patch_instructions: false, + supports_reasoning_summaries: false, + uses_local_shell_tool: false, + apply_patch_tool_type: None, + }); + return Ok(()); } let mut reporter = crate::CliProgressReporter::new(); @@ -65,7 +64,9 @@ pub async fn ensure_oss_ready(config: &mut Config) -> std::io::Result<()> { mod tests { use super::*; use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; - use codex_core::config::{Config, ConfigOverrides, ConfigToml}; + use codex_core::config::Config; + use codex_core::config::ConfigOverrides; + use codex_core::config::ConfigToml; // Skip network tests when sandbox networking is disabled. fn networking_disabled() -> bool {