claude_codex/providers/cursor/
mod.rs1pub mod auth;
2pub mod client;
3pub mod connect;
4pub mod model;
5pub mod proto;
6pub mod request;
7pub mod response;
8pub mod sse;
9#[cfg(test)]
10pub(crate) mod test_frames;
11pub mod tool_bridge;
12pub mod tool_use_xml;
13
14use async_trait::async_trait;
15use axum::Json;
16use axum::response::{IntoResponse, Response};
17use http::StatusCode;
18
19use crate::anthropic::error::json_error;
20use crate::anthropic::schema::{CountTokensResponse, MessagesRequest};
21use crate::monitor::usage_from_anthropic_sse;
22use crate::provider::{CliHandlers, Provider, RequestContext};
23use crate::providers::cursor::auth::{
24 clear_cursor_auth, expired_auth_message, load_cursor_auth, missing_auth_message,
25 run_cursor_login,
26};
27use crate::providers::cursor::client::CursorHttpClient;
28use crate::providers::cursor::model::resolve_cursor_model;
29use crate::providers::cursor::request::render_cursor_prompt;
30use crate::providers::cursor::response::{
31 CursorDecodeError, decode_cursor_upstream, decode_upstream_response,
32};
33use crate::providers::cursor::tool_bridge::{
34 BridgeRegistry, advertised_tool_names, can_bridge_cursor_native_tools, find_tool_result,
35 resume_cursor_tool_bridge, start_cursor_tool_bridge,
36};
37
38pub struct CursorProvider;
43
44impl Default for CursorProvider {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl CursorProvider {
51 pub fn new() -> Self {
52 Self
53 }
54}
55
56#[async_trait]
57impl Provider for CursorProvider {
58 fn name(&self) -> &'static str {
59 "cursor"
60 }
61
62 fn supported_models(&self) -> Vec<String> {
63 model::cursor_supported_models()
64 }
65
66 fn cli(&self) -> &'static dyn CliHandlers {
67 &CURSOR_CLI
68 }
69
70 async fn handle_messages(&self, body: MessagesRequest, ctx: RequestContext) -> Response {
71 let message_id = format!("msg_{}", uuid::Uuid::new_v4().to_string().replace('-', ""));
72 let want_stream = body.stream;
73 let model = body.model.as_deref().unwrap_or("cursor");
74
75 let resolved = resolve_cursor_model(model);
76 if let Err(e) = resolved {
77 return json_error(
78 StatusCode::BAD_REQUEST,
79 "invalid_request_error",
80 format!("Model \"{model}\" is not supported: {e}"),
81 );
82 }
83
84 if let Some(ref session_id) = ctx.session_id {
85 if let Some(pending) = BridgeRegistry::pending_tool(session_id) {
86 if let Some(result) = find_tool_result(&body, pending.tool_use_id()) {
87 let (_result_messages, sse_bytes) =
88 resume_cursor_tool_bridge(session_id, &message_id, model, result, &pending);
89 if let Some(monitor) = ctx.monitor.as_ref() {
90 let (input_tokens, output_tokens) = usage_from_anthropic_sse(&sse_bytes);
91 monitor.stream_progress(
92 &ctx.req_id,
93 sse_bytes.len() as u64,
94 count_sse_events(&sse_bytes),
95 input_tokens,
96 output_tokens,
97 );
98 }
99 let headers = [
100 (http::header::CONTENT_TYPE, "text/event-stream"),
101 (http::header::CACHE_CONTROL, "no-cache"),
102 (http::header::CONNECTION, "keep-alive"),
103 ];
104 return (headers, sse_bytes).into_response();
105 }
106 }
107 }
108
109 let auth = match load_cursor_auth() {
110 Ok(Some(auth)) => auth,
111 Ok(None) => {
112 return json_error(
113 StatusCode::UNAUTHORIZED,
114 "authentication_error",
115 missing_auth_message(),
116 );
117 }
118 Err(err) => {
119 return json_error(
120 StatusCode::UNAUTHORIZED,
121 "authentication_error",
122 format!("Cursor auth failed: {err}"),
123 );
124 }
125 };
126
127 if matches!(auth.expires, Some(expires) if expires <= now_ms() + 60_000) {
128 return json_error(
129 StatusCode::UNAUTHORIZED,
130 "authentication_error",
131 expired_auth_message(&auth),
132 );
133 }
134
135 let token = auth.access_token;
136
137 let prompt = render_cursor_prompt(&body);
138 let images = request::cursor_selected_images(&body);
139
140 let client = CursorHttpClient::new();
141 if let Some(monitor) = ctx.monitor.as_ref() {
142 monitor.upstream_started(&ctx.req_id);
143 }
144 let upstream = match client.run_agent(&token, &prompt, &model, &images).await {
145 Ok(r) => r,
146 Err(e) => {
147 return map_cursor_error_to_response(&e);
148 }
149 };
150
151 if want_stream {
152 let session_id = ctx.session_id.as_deref();
153 let bridge_eligible = can_bridge_cursor_native_tools(&body, session_id);
154
155 if bridge_eligible {
156 let events = match decode_upstream_response(&upstream.body) {
157 Ok(e) => e,
158 Err(e) => return map_cursor_decode_error_to_response(&e),
159 };
160
161 let allowed = advertised_tool_names(&body);
162 let (sse_bytes, _paused) = start_cursor_tool_bridge(
163 &message_id,
164 model,
165 session_id.unwrap(),
166 &events,
167 allowed,
168 Box::new(|| uuid::Uuid::new_v4().to_string().replace('-', "")),
169 );
170 if let Some(monitor) = ctx.monitor.as_ref() {
171 let (input_tokens, output_tokens) = usage_from_anthropic_sse(&sse_bytes);
172 monitor.stream_progress(
173 &ctx.req_id,
174 sse_bytes.len() as u64,
175 count_sse_events(&sse_bytes),
176 input_tokens,
177 output_tokens,
178 );
179 }
180
181 let headers = [
182 (http::header::CONTENT_TYPE, "text/event-stream"),
183 (http::header::CACHE_CONTROL, "no-cache"),
184 (http::header::CONNECTION, "keep-alive"),
185 ];
186 (headers, sse_bytes).into_response()
187 } else {
188 let sse_bytes = sse::frame_cursor_stream(&upstream, &message_id, model);
189 if let Some(monitor) = ctx.monitor.as_ref() {
190 let (input_tokens, output_tokens) = usage_from_anthropic_sse(&sse_bytes);
191 monitor.stream_progress(
192 &ctx.req_id,
193 sse_bytes.len() as u64,
194 count_sse_events(&sse_bytes),
195 input_tokens,
196 output_tokens,
197 );
198 }
199 let headers = [
200 (http::header::CONTENT_TYPE, "text/event-stream"),
201 (http::header::CACHE_CONTROL, "no-cache"),
202 (http::header::CONNECTION, "keep-alive"),
203 ];
204 (headers, sse_bytes).into_response()
205 }
206 } else {
207 match decode_cursor_upstream(&upstream, &message_id, model) {
208 Ok(json) => {
209 if let Some(monitor) = ctx.monitor.as_ref() {
210 monitor.usage_updated(
211 &ctx.req_id,
212 json.pointer("/usage/input_tokens").and_then(|v| v.as_u64()),
213 json.pointer("/usage/output_tokens")
214 .and_then(|v| v.as_u64()),
215 );
216 }
217 (StatusCode::OK, Json(json)).into_response()
218 }
219 Err(e) => map_cursor_decode_error_to_response(&e),
220 }
221 }
222 }
223
224 async fn handle_count_tokens(&self, body: MessagesRequest, ctx: RequestContext) -> Response {
225 let prompt = render_cursor_prompt(&body);
226 let tokens = (prompt.len() / 4) as u64; if let Some(monitor) = ctx.monitor.as_ref() {
228 monitor.usage_updated(&ctx.req_id, Some(tokens), None);
229 }
230 (
231 StatusCode::OK,
232 Json(CountTokensResponse {
233 input_tokens: tokens,
234 }),
235 )
236 .into_response()
237 }
238}
239
240fn count_sse_events(bytes: &[u8]) -> u64 {
241 String::from_utf8_lossy(bytes).matches("event:").count() as u64
242}
243
244fn now_ms() -> u64 {
245 std::time::SystemTime::now()
246 .duration_since(std::time::UNIX_EPOCH)
247 .unwrap_or_default()
248 .as_millis() as u64
249}
250
251fn map_cursor_error_to_response(err: &client::CursorError) -> Response {
256 match err.status {
257 401 | 403 => json_error(
258 StatusCode::UNAUTHORIZED,
259 "authentication_error",
260 err.detail.as_deref().unwrap_or("Authentication failed"),
261 ),
262 429 => {
263 let retry_after = err.retry_after.as_deref().unwrap_or("5");
264 let resp = json_error(
265 StatusCode::TOO_MANY_REQUESTS,
266 "rate_limit_error",
267 &err.message,
268 );
269 let headers = [(http::header::RETRY_AFTER, retry_after)];
270 (headers, resp).into_response()
271 }
272 _ => json_error(
273 StatusCode::BAD_GATEWAY,
274 "api_error",
275 err.detail.as_deref().unwrap_or("Upstream error"),
276 ),
277 }
278}
279
280fn map_cursor_decode_error_to_response(err: &CursorDecodeError) -> Response {
281 match err.status() {
282 Some(401 | 403) => json_error(
283 StatusCode::UNAUTHORIZED,
284 "authentication_error",
285 err.to_string(),
286 ),
287 Some(429) => json_error(
288 StatusCode::TOO_MANY_REQUESTS,
289 "rate_limit_error",
290 err.to_string(),
291 ),
292 _ => json_error(
293 StatusCode::BAD_GATEWAY,
294 "api_error",
295 format!("Response decoding error: {err}"),
296 ),
297 }
298}
299
300pub(crate) struct CursorCli;
305
306impl CliHandlers for CursorCli {
307 fn login(&self) -> Result<(), anyhow::Error> {
308 let auth = run_cursor_login()?.ok_or_else(|| anyhow::anyhow!("Cursor login timed out"))?;
309 println!("Cursor auth saved in {}", auth.source);
310 if let Some(ref user_id) = auth.user_id {
311 println!("User: {user_id}");
312 }
313 if let Some(ref email) = auth.email {
314 println!("Email: {email}");
315 }
316 Ok(())
317 }
318
319 fn device(&self) -> Result<(), anyhow::Error> {
320 anyhow::bail!("cursor: device login not yet implemented");
321 }
322
323 fn status(&self) -> Result<(), anyhow::Error> {
324 match load_cursor_auth()? {
325 Some(auth) => {
326 println!("Auth source: {}", auth.source);
327 if let Some(ref user_id) = auth.user_id {
328 println!("User: {user_id}");
329 }
330 if let Some(ref email) = auth.email {
331 println!("Email: {email}");
332 }
333 if let Some(expires) = auth.expires {
334 let remaining = expires.saturating_sub(now_ms()) / 1000;
335 println!("Access token expires in: {remaining}s");
336 } else {
337 println!("Access token expiry: unknown");
338 }
339 Ok(())
340 }
341 None => {
342 anyhow::bail!("Not authenticated");
343 }
344 }
345 }
346
347 fn logout(&self) -> Result<(), anyhow::Error> {
348 clear_cursor_auth()?;
349 println!(
350 "Cursor persistent auth cleared. Unset CCP_CURSOR_AUTH_TOKEN or CURSOR_AUTH_TOKEN if using env auth."
351 );
352 Ok(())
353 }
354}
355
356pub(crate) static CURSOR_CLI: CursorCli = CursorCli;
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn supported_models_includes_legacy_and_agent() {
364 let provider = CursorProvider::new();
365 let models = provider.supported_models();
366 assert!(models.contains(&"cursor".to_string()));
367 assert!(models.contains(&"cursor-agent".to_string()));
368 assert!(models.contains(&"cursor-plan".to_string()));
369 assert!(models.contains(&"cursor-ask".to_string()));
370 }
371
372 #[test]
373 fn cursor_cli_logout_does_not_error() {
374 let result = CURSOR_CLI.logout();
375 assert!(result.is_ok());
376 }
377}