From a603feafbd548c29b15723f3e400778a92f1fb8f Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 6 Apr 2026 13:45:25 -0400 Subject: [PATCH] openai_subscribed: Show all OpenAI models in ChatGPT Subscription provider - Replace the hardcoded CodexModel enum (3 models) with open_ai::Model, so all standard OpenAI models appear in the dropdown after signing in. - Add o4-mini as a proper variant in open_ai::Model. - Keep codex-mini-latest as a Custom model entry. - Add instructions field to responses::Request and extract system messages into it for the Codex backend (fixes 'Instructions are required' error). - Delegate to open_ai::Model for supports_images, max_output_tokens, count_tokens, and supports_parallel_tool_calls instead of hardcoding. --- .../language_models/src/provider/open_ai.rs | 5 +- .../src/provider/openai_subscribed.rs | 137 ++++++++++-------- crates/open_ai/src/open_ai.rs | 14 +- crates/open_ai/src/responses.rs | 2 + 4 files changed, 96 insertions(+), 62 deletions(-) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index fca5cd1d97ba7dd1b3f26ee2d4f114a7c62a7532..6ed2c3e098cb4029f2b390dbee67402975f02cee 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -337,7 +337,8 @@ impl LanguageModel for OpenAiLanguageModel { | Model::FivePointFour | Model::FivePointFourPro | Model::O1 - | Model::O3 => true, + | Model::O3 + | Model::O4Mini => true, Model::ThreePointFiveTurbo | Model::Four | Model::FourTurbo @@ -609,6 +610,7 @@ pub fn into_open_ai_response( ResponseRequest { model: model_id.into(), + instructions: None, input: input_items, stream, temperature, @@ -1265,6 +1267,7 @@ pub fn count_open_ai_tokens( | Model::O1 | Model::O3 | Model::O3Mini + | Model::O4Mini | Model::Five | Model::FiveCodex | Model::FiveMini diff --git a/crates/language_models/src/provider/openai_subscribed.rs b/crates/language_models/src/provider/openai_subscribed.rs index 8f3f7da156df2ca6beedd9329838d2ef81ab3ea5..545bed3373725b7b4ac022269032d95fb071091d 100644 --- a/crates/language_models/src/provider/openai_subscribed.rs +++ b/crates/language_models/src/provider/openai_subscribed.rs @@ -11,17 +11,20 @@ use language_model::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, RateLimiter, }; -use open_ai::{ReasoningEffort, responses::stream_response}; +use open_ai::responses::stream_response; use rand::RngCore as _; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; +use strum::IntoEnumIterator as _; use ui::{ConfiguredApiCard, prelude::*}; use util::ResultExt as _; -use crate::provider::open_ai::{OpenAiResponseEventMapper, into_open_ai_response}; +use crate::provider::open_ai::{ + OpenAiResponseEventMapper, count_open_ai_tokens, into_open_ai_response, +}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openai-subscribed"); const PROVIDER_NAME: LanguageModelProviderName = @@ -191,8 +194,9 @@ impl OpenAiSubscribedProvider { .detach(); } - fn create_language_model(&self, model: CodexModel) -> Arc { + fn create_language_model(&self, model: open_ai::Model) -> Arc { Arc::new(OpenAiSubscribedLanguageModel { + id: LanguageModelId::from(model.id().to_string()), model, state: self.state.clone(), http_client: self.http_client.clone(), @@ -223,18 +227,30 @@ impl LanguageModelProvider for OpenAiSubscribedProvider { } fn default_model(&self, _cx: &App) -> Option> { - Some(self.create_language_model(CodexModel::O4Mini)) + Some(self.create_language_model(open_ai::Model::default())) } fn default_fast_model(&self, _cx: &App) -> Option> { - Some(self.create_language_model(CodexModel::CodexMini)) + Some(self.create_language_model(open_ai::Model::default_fast())) } fn provided_models(&self, _cx: &App) -> Vec> { - CodexModel::all() - .into_iter() + let mut models: Vec> = open_ai::Model::iter() + .filter(|m| !matches!(m, open_ai::Model::Custom { .. })) .map(|m| self.create_language_model(m)) - .collect() + .collect(); + + models.push(self.create_language_model(open_ai::Model::Custom { + name: "codex-mini-latest".into(), + display_name: Some("Codex Mini".into()), + max_tokens: 200_000, + max_output_tokens: None, + max_completion_tokens: None, + reasoning_effort: None, + supports_chat_completions: false, + })); + + models } fn is_authenticated(&self, cx: &App) -> bool { @@ -269,52 +285,11 @@ impl LanguageModelProvider for OpenAiSubscribedProvider { } } -// --- Models --- - -#[derive(Clone, Debug, PartialEq)] -pub enum CodexModel { - CodexMini, - O4Mini, - O3, -} - -impl CodexModel { - pub fn all() -> Vec { - vec![Self::CodexMini, Self::O4Mini, Self::O3] - } - - fn id(&self) -> &str { - match self { - Self::CodexMini => "codex-mini-latest", - Self::O4Mini => "o4-mini", - Self::O3 => "o3", - } - } - - fn display_name(&self) -> &str { - match self { - Self::CodexMini => "Codex Mini", - Self::O4Mini => "o4-mini", - Self::O3 => "o3", - } - } - - fn max_token_count(&self) -> u64 { - 200_000 - } - - fn reasoning_effort(&self) -> Option { - match self { - Self::CodexMini => None, - Self::O4Mini | Self::O3 => Some(ReasoningEffort::Medium), - } - } -} - // --- Language model --- struct OpenAiSubscribedLanguageModel { - model: CodexModel, + id: LanguageModelId, + model: open_ai::Model, state: Entity, http_client: Arc, request_limiter: RateLimiter, @@ -322,7 +297,7 @@ struct OpenAiSubscribedLanguageModel { impl LanguageModel for OpenAiSubscribedLanguageModel { fn id(&self) -> LanguageModelId { - LanguageModelId::from(self.model.id().to_string()) + self.id.clone() } fn name(&self) -> LanguageModelName { @@ -342,7 +317,29 @@ impl LanguageModel for OpenAiSubscribedLanguageModel { } fn supports_images(&self) -> bool { - false + use open_ai::Model; + match &self.model { + Model::FourOmniMini + | Model::FourPointOneNano + | Model::Five + | Model::FiveCodex + | Model::FiveMini + | Model::FiveNano + | Model::FivePointOne + | Model::FivePointTwo + | Model::FivePointTwoCodex + | Model::FivePointThreeCodex + | Model::FivePointFour + | Model::FivePointFourPro + | Model::O1 + | Model::O3 + | Model::O4Mini => true, + Model::ThreePointFiveTurbo + | Model::Four + | Model::FourTurbo + | Model::O3Mini + | Model::Custom { .. } => false, + } } fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool { @@ -365,12 +362,16 @@ impl LanguageModel for OpenAiSubscribedLanguageModel { self.model.max_token_count() } + fn max_output_tokens(&self) -> Option { + self.model.max_output_tokens() + } + fn count_tokens( &self, - _request: LanguageModelRequest, - _cx: &App, + request: LanguageModelRequest, + cx: &App, ) -> BoxFuture<'static, Result> { - futures::future::ready(Ok(0)).boxed() + count_open_ai_tokens(request, self.model.clone(), cx) } fn stream_completion( @@ -390,7 +391,7 @@ impl LanguageModel for OpenAiSubscribedLanguageModel { let mut responses_request = into_open_ai_response( request, self.model.id(), - true, // supports_parallel_tool_calls + self.model.supports_parallel_tool_calls(), false, // supports_prompt_cache_key None, // max_output_tokens — not supported by Codex backend self.model.reasoning_effort(), @@ -398,6 +399,26 @@ impl LanguageModel for OpenAiSubscribedLanguageModel { responses_request.store = Some(false); responses_request.max_output_tokens = None; + // The Codex backend requires system messages to be in the top-level + // `instructions` field rather than as input items. + let mut instructions = Vec::new(); + responses_request.input.retain(|item| { + if let open_ai::responses::ResponseInputItem::Message(msg) = item { + if msg.role == open_ai::Role::System { + for part in &msg.content { + if let open_ai::responses::ResponseInputContent::Text { text } = part { + instructions.push(text.clone()); + } + } + return false; + } + } + true + }); + if !instructions.is_empty() { + responses_request.instructions = Some(instructions.join("\n\n")); + } + let state = self.state.downgrade(); let http_client = self.http_client.clone(); let request_limiter = self.request_limiter.clone(); @@ -783,7 +804,7 @@ impl Render for ConfigurationView { v_flex() .gap_2() .child(Label::new( - "Sign in with your ChatGPT Plus or Pro subscription to use o3, o4-mini, and Codex models in Zed's agent.", + "Sign in with your ChatGPT Plus or Pro subscription to use OpenAI models in Zed's agent.", )) .child( Button::new("sign-in", "Sign in with ChatGPT") diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index c4a3e078d76eb028b90e5b80fe95b1281b795f34..161326467e3cb4956329b0086a9aea40ad942ba1 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -73,6 +73,8 @@ pub enum Model { O3Mini, #[serde(rename = "o3")] O3, + #[serde(rename = "o4-mini")] + O4Mini, #[serde(rename = "gpt-5")] Five, #[serde(rename = "gpt-5-codex")] @@ -127,6 +129,7 @@ impl Model { "o1" => Ok(Self::O1), "o3-mini" => Ok(Self::O3Mini), "o3" => Ok(Self::O3), + "o4-mini" => Ok(Self::O4Mini), "gpt-5" => Ok(Self::Five), "gpt-5-codex" => Ok(Self::FiveCodex), "gpt-5-mini" => Ok(Self::FiveMini), @@ -151,6 +154,7 @@ impl Model { Self::O1 => "o1", Self::O3Mini => "o3-mini", Self::O3 => "o3", + Self::O4Mini => "o4-mini", Self::Five => "gpt-5", Self::FiveCodex => "gpt-5-codex", Self::FiveMini => "gpt-5-mini", @@ -175,6 +179,7 @@ impl Model { Self::O1 => "o1", Self::O3Mini => "o3-mini", Self::O3 => "o3", + Self::O4Mini => "o4-mini", Self::Five => "gpt-5", Self::FiveCodex => "gpt-5-codex", Self::FiveMini => "gpt-5-mini", @@ -199,6 +204,7 @@ impl Model { Self::O1 => 200_000, Self::O3Mini => 200_000, Self::O3 => 200_000, + Self::O4Mini => 200_000, Self::Five => 272_000, Self::FiveCodex => 272_000, Self::FiveMini => 400_000, @@ -226,6 +232,7 @@ impl Model { Self::O1 => Some(100_000), Self::O3Mini => Some(100_000), Self::O3 => Some(100_000), + Self::O4Mini => Some(100_000), Self::Five => Some(128_000), Self::FiveCodex => Some(128_000), Self::FiveMini => Some(128_000), @@ -244,7 +251,7 @@ impl Model { Self::Custom { reasoning_effort, .. } => reasoning_effort.to_owned(), - Self::FivePointThreeCodex | Self::FivePointFourPro => Some(ReasoningEffort::Medium), + Self::O4Mini | Self::FivePointThreeCodex | Self::FivePointFourPro => Some(ReasoningEffort::Medium), _ => None, } } @@ -255,7 +262,8 @@ impl Model { supports_chat_completions, .. } => *supports_chat_completions, - Self::FiveCodex + Self::O4Mini + | Self::FiveCodex | Self::FivePointTwoCodex | Self::FivePointThreeCodex | Self::FivePointFourPro => false, @@ -283,7 +291,7 @@ impl Model { | Self::FivePointFour | Self::FivePointFourPro | Self::FiveNano => true, - Self::O1 | Self::O3 | Self::O3Mini | Model::Custom { .. } => false, + Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false, } } diff --git a/crates/open_ai/src/responses.rs b/crates/open_ai/src/responses.rs index fce923c494de3d6bb4a172f9b4b02db1e4d74174..aeeae8c81124a93ad2e3983880a22a3004bba5c1 100644 --- a/crates/open_ai/src/responses.rs +++ b/crates/open_ai/src/responses.rs @@ -9,6 +9,8 @@ use crate::{ReasoningEffort, RequestError, Role, ToolChoice}; #[derive(Serialize, Debug)] pub struct Request { pub model: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub input: Vec, #[serde(default)]