Skip to main content

starweaver_model/oauth/
codex_model.rs

1//! Codex OAuth model adapter.
2
3use std::{collections::BTreeMap, sync::Arc};
4
5use async_trait::async_trait;
6use serde_json::json;
7use starweaver_oauth::OAuthTokenSource;
8
9use crate::{
10    ModelAdapter, ModelError, ModelProfile, ModelRequestContext, ModelRequestParameters,
11    ModelResponse, ModelResponseEventStream, ModelResponseStreamEvent, ModelRunSession,
12    ModelSettings, ProtocolFamily,
13    oauth::OAuthBearerHttpClient,
14    providers::client::ProtocolModelClient,
15    transport::{DynHttpClient, HttpModelConfig},
16};
17
18/// `Codex` OAuth-backed `OpenAI` Responses model.
19pub struct CodexOAuthResponsesModel {
20    inner: ProtocolModelClient,
21}
22
23impl CodexOAuthResponsesModel {
24    /// Create a Codex OAuth-backed `OpenAI` Responses model.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error when the OAuth HTTP client cannot be built.
29    pub fn new(
30        model_name: impl Into<String>,
31        http_config: HttpModelConfig,
32        token_source: Arc<dyn OAuthTokenSource>,
33        extra_headers: BTreeMap<String, String>,
34    ) -> Result<Self, ModelError> {
35        Self::new_with_profile(
36            model_name,
37            http_config,
38            token_source,
39            extra_headers,
40            codex_model_profile(),
41        )
42    }
43
44    /// Create a Codex OAuth-backed model with an explicit capability profile.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error when the OAuth HTTP client cannot be built.
49    pub fn new_with_profile(
50        model_name: impl Into<String>,
51        http_config: HttpModelConfig,
52        token_source: Arc<dyn OAuthTokenSource>,
53        extra_headers: BTreeMap<String, String>,
54        profile: ModelProfile,
55    ) -> Result<Self, ModelError> {
56        let client =
57            OAuthBearerHttpClient::with_default_http_client(token_source, "codex", extra_headers)?;
58        Ok(Self {
59            inner: ProtocolModelClient::new(
60                "codex",
61                model_name,
62                profile,
63                http_config,
64                Arc::new(client),
65            ),
66        })
67    }
68
69    /// Create a model with an injected HTTP client.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error when `extra_headers` contains reserved headers.
74    pub fn with_http_client(
75        model_name: impl Into<String>,
76        http_config: HttpModelConfig,
77        token_source: Arc<dyn OAuthTokenSource>,
78        extra_headers: BTreeMap<String, String>,
79        inner_http_client: DynHttpClient,
80    ) -> Result<Self, ModelError> {
81        Self::with_http_client_and_profile(
82            model_name,
83            http_config,
84            token_source,
85            extra_headers,
86            inner_http_client,
87            codex_model_profile(),
88        )
89    }
90
91    /// Create a model with an injected HTTP client and explicit capability profile.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error when `extra_headers` contains reserved headers.
96    pub fn with_http_client_and_profile(
97        model_name: impl Into<String>,
98        http_config: HttpModelConfig,
99        token_source: Arc<dyn OAuthTokenSource>,
100        extra_headers: BTreeMap<String, String>,
101        inner_http_client: DynHttpClient,
102        profile: ModelProfile,
103    ) -> Result<Self, ModelError> {
104        let client =
105            OAuthBearerHttpClient::new(inner_http_client, token_source, "codex", extra_headers)?;
106        Ok(Self {
107            inner: ProtocolModelClient::new(
108                "codex",
109                model_name,
110                profile,
111                http_config,
112                Arc::new(client),
113            ),
114        })
115    }
116}
117
118#[async_trait]
119impl ModelAdapter for CodexOAuthResponsesModel {
120    fn model_name(&self) -> &str {
121        self.inner.model_name()
122    }
123
124    fn provider_name(&self) -> Option<&str> {
125        Some("codex")
126    }
127
128    fn profile(&self) -> &ModelProfile {
129        self.inner.profile()
130    }
131
132    fn default_settings(&self) -> Option<&ModelSettings> {
133        self.inner.default_settings()
134    }
135
136    fn start_run_session(&self) -> Box<dyn ModelRunSession + '_> {
137        self.inner.start_run_session()
138    }
139
140    async fn request(
141        &self,
142        _messages: Vec<crate::ModelMessage>,
143        _settings: Option<ModelSettings>,
144        _params: ModelRequestParameters,
145        _context: ModelRequestContext,
146    ) -> Result<ModelResponse, ModelError> {
147        Err(ModelError::Transport(
148            "Codex OAuth Responses API requires streaming. Use request_stream_incremental or a streaming agent run."
149                .to_string(),
150        ))
151    }
152
153    async fn request_stream(
154        &self,
155        messages: Vec<crate::ModelMessage>,
156        settings: Option<ModelSettings>,
157        params: ModelRequestParameters,
158        context: ModelRequestContext,
159    ) -> Result<Vec<ModelResponseStreamEvent>, ModelError> {
160        self.inner
161            .request_stream(messages, settings, params, context)
162            .await
163    }
164
165    async fn request_stream_incremental(
166        &self,
167        messages: Vec<crate::ModelMessage>,
168        settings: Option<ModelSettings>,
169        params: ModelRequestParameters,
170        context: ModelRequestContext,
171    ) -> Result<ModelResponseEventStream, ModelError> {
172        self.inner
173            .request_stream_incremental(messages, settings, params, context)
174            .await
175    }
176}
177
178/// Build the Codex model capability profile.
179#[must_use]
180pub fn codex_model_profile() -> ModelProfile {
181    let mut profile = ModelProfile::for_protocol(ProtocolFamily::OpenAiResponses);
182    profile.supports_tools = true;
183    profile.supports_json_schema_output = true;
184    profile.supports_thinking = true;
185    profile.thinking_always_enabled = true;
186    profile
187}
188
189/// Build a Codex OAuth model from model name and token source.
190///
191/// # Errors
192///
193/// Returns an error when OAuth HTTP client construction fails.
194pub fn build_codex_model(
195    model_name: impl Into<String>,
196    token_source: Arc<dyn OAuthTokenSource>,
197    mut http_config: HttpModelConfig,
198    extra_headers: BTreeMap<String, String>,
199) -> Result<CodexOAuthResponsesModel, ModelError> {
200    http_config
201        .metadata
202        .insert("oauth_provider".to_string(), json!("codex"));
203    CodexOAuthResponsesModel::new(model_name, http_config, token_source, extra_headers)
204}
205
206/// Build a Codex OAuth model with an explicit capability profile.
207///
208/// # Errors
209///
210/// Returns an error when OAuth HTTP client construction fails.
211pub fn build_codex_model_with_profile(
212    model_name: impl Into<String>,
213    token_source: Arc<dyn OAuthTokenSource>,
214    mut http_config: HttpModelConfig,
215    extra_headers: BTreeMap<String, String>,
216    profile: ModelProfile,
217) -> Result<CodexOAuthResponsesModel, ModelError> {
218    http_config
219        .metadata
220        .insert("oauth_provider".to_string(), json!("codex"));
221    CodexOAuthResponsesModel::new_with_profile(
222        model_name,
223        http_config,
224        token_source,
225        extra_headers,
226        profile,
227    )
228}