feat: support the chat completions API in the Rust CLI (#862)
This is a substantial PR to add support for the chat completions API, which in turn makes it possible to use non-OpenAI model providers (just like in the TypeScript CLI): * It moves a number of structs from `client.rs` to `client_common.rs` so they can be shared. * It introduces support for the chat completions API in `chat_completions.rs`. * It updates `ModelProviderInfo` so that `env_key` is `Option<String>` instead of `String` (for e.g., ollama) and adds a `wire_api` field * It updates `client.rs` to choose between `stream_responses()` and `stream_chat_completions()` based on the `wire_api` for the `ModelProviderInfo` * It updates the `exec` and TUI CLIs to no longer fail if the `OPENAI_API_KEY` environment variable is not set * It updates the TUI so that `EventMsg::Error` is displayed more prominently when it occurs, particularly now that it is important to alert users to the `CodexErr::EnvVar` variant. * `CodexErr::EnvVar` was updated to include an optional `instructions` field so we can preserve the behavior where we direct users to https://platform.openai.com if `OPENAI_API_KEY` is not set. * Cleaned up the "welcome message" in the TUI to ensure the model provider is displayed. * Updated the docs in `codex-rs/README.md`. To exercise the chat completions API from OpenAI models, I added the following to my `config.toml`: ```toml model = "gpt-4o" model_provider = "openai-chat-completions" [model_providers.openai-chat-completions] name = "OpenAI using Chat Completions" base_url = "https://api.openai.com/v1" env_key = "OPENAI_API_KEY" wire_api = "chat" ``` Though to test a non-OpenAI provider, I installed ollama with mistral locally on my Mac because ChatGPT said that would be a good match for my hardware: ```shell brew install ollama ollama serve ollama pull mistral ``` Then I added the following to my `~/.codex/config.toml`: ```toml model = "mistral" model_provider = "ollama" ``` Note this code could certainly use more test coverage, but I want to get this in so folks can start playing with it. For reference, I believe https://github.com/openai/codex/pull/247 was roughly the comparable PR on the TypeScript side.
This commit is contained in:
@@ -32,6 +32,9 @@ pub(crate) enum PatchEventType {
|
||||
/// `Vec<Line<'static>>` representation to make it easier to display in a
|
||||
/// scrollable list.
|
||||
pub(crate) enum HistoryCell {
|
||||
/// Welcome message.
|
||||
WelcomeMessage { lines: Vec<Line<'static>> },
|
||||
|
||||
/// Message from the user.
|
||||
UserPrompt { lines: Vec<Line<'static>> },
|
||||
|
||||
@@ -69,6 +72,9 @@ pub(crate) enum HistoryCell {
|
||||
/// Background event
|
||||
BackgroundEvent { lines: Vec<Line<'static>> },
|
||||
|
||||
/// Error event from the backend.
|
||||
ErrorEvent { lines: Vec<Line<'static>> },
|
||||
|
||||
/// Info describing the newly‑initialized session.
|
||||
SessionInfo { lines: Vec<Line<'static>> },
|
||||
|
||||
@@ -85,6 +91,31 @@ pub(crate) enum HistoryCell {
|
||||
const TOOL_CALL_MAX_LINES: usize = 5;
|
||||
|
||||
impl HistoryCell {
|
||||
pub(crate) fn new_welcome_message(config: &Config) -> Self {
|
||||
let mut lines: Vec<Line<'static>> = vec![
|
||||
Line::from(vec![
|
||||
"OpenAI ".into(),
|
||||
"Codex".bold(),
|
||||
" (research preview)".dim(),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from("codex session:".magenta().bold()),
|
||||
];
|
||||
|
||||
let entries = vec![
|
||||
("workdir", config.cwd.display().to_string()),
|
||||
("model", config.model.clone()),
|
||||
("provider", config.model_provider_id.clone()),
|
||||
("approval", format!("{:?}", config.approval_policy)),
|
||||
("sandbox", format!("{:?}", config.sandbox_policy)),
|
||||
];
|
||||
for (key, value) in entries {
|
||||
lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()]));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
HistoryCell::WelcomeMessage { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_user_prompt(message: String) -> Self {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
lines.push(Line::from("user".cyan().bold()));
|
||||
@@ -245,26 +276,26 @@ impl HistoryCell {
|
||||
HistoryCell::BackgroundEvent { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_error_event(message: String) -> Self {
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
vec!["ERROR: ".red().bold(), message.into()].into(),
|
||||
"".into(),
|
||||
];
|
||||
HistoryCell::ErrorEvent { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_session_info(config: &Config, model: String) -> Self {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
|
||||
lines.push(Line::from("codex session:".magenta().bold()));
|
||||
lines.push(Line::from(vec!["↳ model: ".bold(), model.into()]));
|
||||
lines.push(Line::from(vec![
|
||||
"↳ cwd: ".bold(),
|
||||
config.cwd.display().to_string().into(),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
"↳ approval: ".bold(),
|
||||
format!("{:?}", config.approval_policy).into(),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
"↳ sandbox: ".bold(),
|
||||
format!("{:?}", config.sandbox_policy).into(),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
HistoryCell::SessionInfo { lines }
|
||||
if config.model == model {
|
||||
HistoryCell::SessionInfo { lines: vec![] }
|
||||
} else {
|
||||
let lines = vec![
|
||||
Line::from("model changed:".magenta().bold()),
|
||||
Line::from(format!("requested: {}", config.model)),
|
||||
Line::from(format!("used: {}", model)),
|
||||
Line::from(""),
|
||||
];
|
||||
HistoryCell::SessionInfo { lines }
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `PendingPatch` cell that lists the file‑level summary of
|
||||
@@ -329,9 +360,11 @@ impl HistoryCell {
|
||||
|
||||
pub(crate) fn lines(&self) -> &Vec<Line<'static>> {
|
||||
match self {
|
||||
HistoryCell::UserPrompt { lines, .. }
|
||||
HistoryCell::WelcomeMessage { lines, .. }
|
||||
| HistoryCell::UserPrompt { lines, .. }
|
||||
| HistoryCell::AgentMessage { lines, .. }
|
||||
| HistoryCell::BackgroundEvent { lines, .. }
|
||||
| HistoryCell::ErrorEvent { lines, .. }
|
||||
| HistoryCell::SessionInfo { lines, .. }
|
||||
| HistoryCell::ActiveExecCommand { lines, .. }
|
||||
| HistoryCell::CompletedExecCommand { lines, .. }
|
||||
|
||||
Reference in New Issue
Block a user