mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Merge 5d8ec161e8 into sapling-pr-archive-bolinfest
This commit is contained in:
Generated
+1
@@ -600,6 +600,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codex-core",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"toml",
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ workspace = true
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
codex-core = { path = "../core" }
|
||||
codex-common = { path = "../common", features = ["cli"] }
|
||||
codex-common = { path = "../common", features = ["cli", "model-list"] }
|
||||
codex-exec = { path = "../exec" }
|
||||
codex-login = { path = "../login" }
|
||||
codex-linux-sandbox = { path = "../linux-sandbox" }
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use clap::Parser;
|
||||
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
|
||||
/// Print the list of models available for the configured (or overridden)
|
||||
/// provider.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ListModelsCli {
|
||||
/// Optional provider override. When set this value is used instead of the
|
||||
/// `model_provider_id` configured in `~/.codex/config.toml`.
|
||||
#[clap(long)]
|
||||
pub provider: Option<String>,
|
||||
|
||||
/// Arbitrary `-c key=value` overrides that apply **in addition** to the
|
||||
/// `--provider` flag.
|
||||
#[clap(flatten)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
}
|
||||
|
||||
impl ListModelsCli {
|
||||
pub async fn run(self) -> anyhow::Result<()> {
|
||||
// Compose strongly-typed overrides. The provider flag, if specified,
|
||||
// is translated into the corresponding field inside `ConfigOverrides`.
|
||||
let overrides = ConfigOverrides {
|
||||
model: None,
|
||||
config_profile: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
cwd: None,
|
||||
model_provider: self.provider.clone(),
|
||||
codex_linux_sandbox_exe: None,
|
||||
};
|
||||
|
||||
// Parse the raw `-c` overrides early so we can bail with a useful
|
||||
// error message if the user supplied an invalid value.
|
||||
let cli_kv_overrides = self
|
||||
.config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
// Load the merged configuration.
|
||||
let cfg = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?;
|
||||
|
||||
// Retrieve the model list.
|
||||
let models = codex_common::fetch_available_models(cfg.model_provider).await?;
|
||||
|
||||
for m in models {
|
||||
println!("{m}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use codex_tui::Cli as TuiCli;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::proto::ProtoCli;
|
||||
mod list_models;
|
||||
|
||||
/// Codex CLI
|
||||
///
|
||||
@@ -43,6 +44,10 @@ enum Subcommand {
|
||||
/// Experimental: run Codex as an MCP server.
|
||||
Mcp,
|
||||
|
||||
/// List models for the configured or specified provider.
|
||||
#[clap(name = "list-models", visible_alias = "lm")]
|
||||
ListModels(crate::list_models::ListModelsCli),
|
||||
|
||||
/// Run the Protocol stream via stdin/stdout
|
||||
#[clap(visible_alias = "p")]
|
||||
Proto(ProtoCli),
|
||||
@@ -121,6 +126,13 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
||||
.await?;
|
||||
}
|
||||
},
|
||||
Some(Subcommand::ListModels(list_cli)) => {
|
||||
// Combine root-level overrides with subcommand-specific ones so
|
||||
// that the latter take precedence.
|
||||
let mut list_cli = list_cli;
|
||||
prepend_config_flags(&mut list_cli.config_overrides, cli.config_overrides);
|
||||
list_cli.run().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -11,8 +11,15 @@ clap = { version = "4", features = ["derive", "wrap_help"], optional = true }
|
||||
codex-core = { path = "../core" }
|
||||
toml = { version = "0.8", optional = true }
|
||||
serde = { version = "1", optional = true }
|
||||
reqwest = { version = "0.12", features = ["json"], optional = true }
|
||||
|
||||
[features]
|
||||
# Separate feature so that `clap` is not a mandatory dependency.
|
||||
cli = ["clap", "toml", "serde"]
|
||||
elapsed = []
|
||||
|
||||
# Helper functionality for querying the list of available models from a model
|
||||
# provider. This is intentionally behind a separate opt-in feature so that
|
||||
# downstream crates that do not need it avoid pulling in the additional heavy
|
||||
# dependencies (`reqwest`, etc.).
|
||||
model-list = ["reqwest"]
|
||||
|
||||
@@ -14,3 +14,13 @@ mod config_override;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub use config_override::CliConfigOverrides;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Optional helpers for querying the list of available models.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "model-list")]
|
||||
mod model_list;
|
||||
|
||||
#[cfg(feature = "model-list")]
|
||||
pub use model_list::fetch_available_models;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Helper for fetching the list of models that are available for a given
|
||||
//! [`ModelProviderInfo`] instance.
|
||||
//!
|
||||
//! The implementation is intentionally lightweight and only covers the subset
|
||||
//! of the OpenAI-compatible REST API that is required to discover available
|
||||
//! model *identifiers*. At the time of writing all providers supported by
|
||||
//! Codex expose a `GET /models` endpoint that returns a JSON payload in the
|
||||
//! following canonical form:
|
||||
//!
|
||||
//! ```json
|
||||
//! {
|
||||
//! "object": "list",
|
||||
//! "data": [
|
||||
//! { "id": "o3", "object": "model" },
|
||||
//! { "id": "o4-mini", "object": "model" }
|
||||
//! ]
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! We purposefully parse *only* the `id` fields that callers care about and
|
||||
//! ignore any additional metadata so that the function keeps working even if
|
||||
//! upstream providers add new attributes.
|
||||
|
||||
use codex_core::error::{CodexErr, Result};
|
||||
use codex_core::ModelProviderInfo;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ModelsResponse {
|
||||
data: Vec<ModelId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ModelId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
/// Fetch the list of available model identifiers from the given provider.
|
||||
///
|
||||
/// The caller must ensure that the provider's API key can be resolved via
|
||||
/// [`ModelProviderInfo::api_key`] – if this fails the function returns a
|
||||
/// [`CodexErr::EnvVar`]. Any network or JSON parsing failures are forwarded
|
||||
/// to the caller.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn fetch_available_models(provider: ModelProviderInfo) -> Result<Vec<String>> {
|
||||
let api_key = provider.api_key()?;
|
||||
|
||||
let base_url = provider.base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/models");
|
||||
|
||||
// Build the request. For providers that require authentication we send
|
||||
// the token via the standard Bearer mechanism. Providers like Ollama do
|
||||
// not require a token – in that case we just omit the header.
|
||||
let client = reqwest::Client::new();
|
||||
let mut req = client.get(&url);
|
||||
if let Some(token) = api_key {
|
||||
req = req.bearer_auth(token);
|
||||
}
|
||||
|
||||
|
||||
|
||||
let resp = req.send().await?;
|
||||
|
||||
match resp.status() {
|
||||
StatusCode::OK => {
|
||||
let json: ModelsResponse = resp.json().await?;
|
||||
let mut models: Vec<String> = json.data.into_iter().map(|m| m.id).collect();
|
||||
models.sort();
|
||||
Ok(models)
|
||||
}
|
||||
_ => Err(CodexErr::Reqwest(resp.error_for_status().unwrap_err())),
|
||||
}
|
||||
}
|
||||
+22
-8
@@ -40,6 +40,9 @@ pub(crate) struct App<'a> {
|
||||
app_event_rx: Receiver<AppEvent>,
|
||||
app_state: AppState<'a>,
|
||||
|
||||
/// Config is stored here so we can recreate ChatWidgets as needed.
|
||||
config: Config,
|
||||
|
||||
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
||||
/// after dismissing the Git-repo warning.
|
||||
chat_args: Option<ChatWidgetArgs>,
|
||||
@@ -122,7 +125,7 @@ impl<'a> App<'a> {
|
||||
screen: LoginScreen::new(app_event_tx.clone(), config.codex_home.clone()),
|
||||
},
|
||||
Some(ChatWidgetArgs {
|
||||
config,
|
||||
config: config.clone(),
|
||||
initial_prompt,
|
||||
initial_images,
|
||||
}),
|
||||
@@ -133,14 +136,18 @@ impl<'a> App<'a> {
|
||||
screen: GitWarningScreen::new(),
|
||||
},
|
||||
Some(ChatWidgetArgs {
|
||||
config,
|
||||
config: config.clone(),
|
||||
initial_prompt,
|
||||
initial_images,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
let chat_widget =
|
||||
ChatWidget::new(config, app_event_tx.clone(), initial_prompt, initial_images);
|
||||
let chat_widget = ChatWidget::new(
|
||||
config.clone(),
|
||||
app_event_tx.clone(),
|
||||
initial_prompt,
|
||||
initial_images,
|
||||
);
|
||||
(
|
||||
AppState::Chat {
|
||||
widget: Box::new(chat_widget),
|
||||
@@ -153,6 +160,7 @@ impl<'a> App<'a> {
|
||||
app_event_tx,
|
||||
app_event_rx,
|
||||
app_state,
|
||||
config,
|
||||
chat_args,
|
||||
}
|
||||
}
|
||||
@@ -224,10 +232,16 @@ impl<'a> App<'a> {
|
||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
||||
},
|
||||
AppEvent::DispatchCommand(command) => match command {
|
||||
SlashCommand::Clear => match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.clear_conversation_history(),
|
||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
||||
},
|
||||
SlashCommand::New => {
|
||||
let new_widget = Box::new(ChatWidget::new(
|
||||
self.config.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
None,
|
||||
Vec::new(),
|
||||
));
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
SlashCommand::ToggleMouseMode => {
|
||||
if let Err(e) = mouse_capture.toggle() {
|
||||
tracing::error!("Failed to toggle mouse mode: {e}");
|
||||
|
||||
@@ -207,11 +207,6 @@ impl ChatWidget<'_> {
|
||||
self.conversation_history.scroll_to_bottom();
|
||||
}
|
||||
|
||||
pub(crate) fn clear_conversation_history(&mut self) {
|
||||
self.conversation_history.clear();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn handle_codex_event(&mut self, event: Event) {
|
||||
let Event { id, msg } = event;
|
||||
match msg {
|
||||
|
||||
@@ -245,12 +245,6 @@ impl ConversationHistoryWidget {
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove all history entries and reset scrolling.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
self.scroll_position = usize::MAX;
|
||||
}
|
||||
|
||||
pub fn record_completed_exec_command(
|
||||
&mut self,
|
||||
call_id: String,
|
||||
|
||||
@@ -12,7 +12,7 @@ use strum_macros::IntoStaticStr;
|
||||
)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum SlashCommand {
|
||||
Clear,
|
||||
New,
|
||||
ToggleMouseMode,
|
||||
Quit,
|
||||
}
|
||||
@@ -21,7 +21,7 @@ impl SlashCommand {
|
||||
/// User-visible description shown in the popup.
|
||||
pub fn description(self) -> &'static str {
|
||||
match self {
|
||||
SlashCommand::Clear => "Clear the chat history.",
|
||||
SlashCommand::New => "Start a new chat.",
|
||||
SlashCommand::ToggleMouseMode => {
|
||||
"Toggle mouse mode (enable for scrolling, disable for text selection)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user