@@ -40,6 +40,8 @@ const OPENROUTER_PROVIDER_NAME: &str = "OpenRouter";
4040pub const OPENROUTER_PROVIDER_ID : & str = "openrouter" ;
4141const OPENPATHS_PROVIDER_NAME : & str = "OpenPaths" ;
4242pub const OPENPATHS_PROVIDER_ID : & str = "openpaths" ;
43+ pub const CURSOR_PROVIDER_NAME : & str = "Cursor" ;
44+ pub const CURSOR_PROVIDER_ID : & str = "cursor" ;
4345const ZHIPU_PROVIDER_NAME : & str = "Z.AI (Zhipu)" ;
4446pub const ZHIPU_PROVIDER_ID : & str = "zhipu" ;
4547const DEEPSEEK_PROVIDER_NAME : & str = "DeepSeek" ;
@@ -405,7 +407,7 @@ impl ModelProviderInfo {
405407 pub fn create_openpaths_provider ( ) -> ModelProviderInfo {
406408 ModelProviderInfo {
407409 name : OPENPATHS_PROVIDER_NAME . into ( ) ,
408- base_url : Some ( "https://openpaths.io/v1" . into ( ) ) ,
410+ base_url : Some ( openpaths_base_url ( ) ) ,
409411 env_key : Some ( "OPENPATHS_API_KEY" . into ( ) ) ,
410412 env_key_instructions : Some (
411413 "Get your API key from https://openpaths.io and set OPENPATHS_API_KEY" . into ( ) ,
@@ -426,6 +428,31 @@ impl ModelProviderInfo {
426428 }
427429 }
428430
431+ pub fn create_cursor_provider ( ) -> ModelProviderInfo {
432+ ModelProviderInfo {
433+ name : CURSOR_PROVIDER_NAME . into ( ) ,
434+ base_url : Some ( cursor_base_url ( ) ) ,
435+ env_key : Some ( "CURSOR_API_KEY" . into ( ) ) ,
436+ env_key_instructions : Some (
437+ "Get your API key from https://cursor.com/dashboard/integrations and set CURSOR_API_KEY"
438+ . into ( ) ,
439+ ) ,
440+ experimental_bearer_token : None ,
441+ auth : None ,
442+ wire_api : WireApi :: Responses ,
443+ query_params : None ,
444+ http_headers : None ,
445+ env_http_headers : None ,
446+ request_max_retries : None ,
447+ stream_max_retries : None ,
448+ stream_idle_timeout_ms : None ,
449+ websocket_connect_timeout_ms : None ,
450+ requires_openai_auth : false ,
451+ supports_websockets : false ,
452+ ..Default :: default ( )
453+ }
454+ }
455+
429456 pub fn create_zhipu_provider ( ) -> ModelProviderInfo {
430457 ModelProviderInfo {
431458 name : ZHIPU_PROVIDER_NAME . into ( ) ,
@@ -511,6 +538,8 @@ impl ModelProviderInfo {
511538 slug. strip_prefix ( "google/" ) . unwrap_or ( slug)
512539 } else if self . name == OPENPATHS_PROVIDER_NAME {
513540 slug. strip_prefix ( "openpaths/" ) . unwrap_or ( slug)
541+ } else if self . name == CURSOR_PROVIDER_NAME {
542+ slug. strip_prefix ( "cursor/" ) . unwrap_or ( slug)
514543 } else if self . name == ZHIPU_PROVIDER_NAME {
515544 slug. strip_prefix ( "zhipu/" )
516545 . or_else ( || slug. strip_prefix ( "z-ai/" ) )
@@ -556,6 +585,7 @@ pub fn built_in_model_providers(
556585 ( OPENAI_PROVIDER_ID , openai_provider) ,
557586 ( OPENROUTER_PROVIDER_ID , P :: create_openrouter_provider ( ) ) ,
558587 ( OPENPATHS_PROVIDER_ID , P :: create_openpaths_provider ( ) ) ,
588+ ( CURSOR_PROVIDER_ID , P :: create_cursor_provider ( ) ) ,
559589 ( GEMINI_PROVIDER_ID , P :: create_gemini_provider ( ) ) ,
560590 ( ZHIPU_PROVIDER_ID , P :: create_zhipu_provider ( ) ) ,
561591 ( DEEPSEEK_PROVIDER_ID , P :: create_deepseek_provider ( ) ) ,
@@ -580,6 +610,39 @@ fn non_empty_env_var(name: &str) -> bool {
580610 . is_some_and ( |value| !value. trim ( ) . is_empty ( ) )
581611}
582612
613+ fn openpaths_base_url ( ) -> String {
614+ std:: env:: var ( "OPENPATHS_BASE_URL" )
615+ . ok ( )
616+ . filter ( |value| !value. trim ( ) . is_empty ( ) )
617+ . map ( |value| {
618+ let trimmed = value. trim ( ) . trim_end_matches ( '/' ) ;
619+ if trimmed. ends_with ( "/v1" ) {
620+ trimmed. to_string ( )
621+ } else {
622+ format ! ( "{trimmed}/v1" )
623+ }
624+ } )
625+ . unwrap_or_else ( || "https://openpaths.io/v1" . to_string ( ) )
626+ }
627+
628+ fn cursor_base_url ( ) -> String {
629+ std:: env:: var ( "CURSOR_BASE_URL" )
630+ . ok ( )
631+ . filter ( |value| !value. trim ( ) . is_empty ( ) )
632+ . map ( |value| value. trim ( ) . trim_end_matches ( '/' ) . to_string ( ) )
633+ . unwrap_or_else ( || "https://api.cursor.com" . to_string ( ) )
634+ }
635+
636+ fn is_composer_model_slug ( lower : & str ) -> bool {
637+ let slug = lower
638+ . strip_prefix ( "openpaths/" )
639+ . or_else ( || lower. strip_prefix ( "cursor/" ) )
640+ . unwrap_or ( lower) ;
641+ slug == "composer-2.5"
642+ || slug == "composer-2.5-fast"
643+ || slug. starts_with ( "composer-2.5-" )
644+ }
645+
583646pub fn infer_builtin_provider_id_for_model ( model : & str ) -> Option < & ' static str > {
584647 let lower = model. to_lowercase ( ) ;
585648 if ( lower. starts_with ( "glm-" )
@@ -611,6 +674,14 @@ pub fn infer_builtin_provider_id_for_model(model: &str) -> Option<&'static str>
611674 {
612675 return Some ( OPENPATHS_PROVIDER_ID ) ;
613676 }
677+ if is_composer_model_slug ( & lower) {
678+ if non_empty_env_var ( "OPENPATHS_API_KEY" ) {
679+ return Some ( OPENPATHS_PROVIDER_ID ) ;
680+ }
681+ if non_empty_env_var ( "CURSOR_API_KEY" ) {
682+ return Some ( CURSOR_PROVIDER_ID ) ;
683+ }
684+ }
614685 match model. split_once ( '/' ) {
615686 Some ( ( "google" , _) ) if non_empty_env_var ( "GEMINI_API_KEY" ) => Some ( GEMINI_PROVIDER_ID ) ,
616687 Some ( ( "google" , _) ) if non_empty_env_var ( "OPENROUTER_API_KEY" ) => {
@@ -623,6 +694,7 @@ pub fn infer_builtin_provider_id_for_model(model: &str) -> Option<&'static str>
623694 Some ( ( "openpaths" , _) ) if non_empty_env_var ( "OPENPATHS_API_KEY" ) => {
624695 Some ( OPENPATHS_PROVIDER_ID )
625696 }
697+ Some ( ( "cursor" , _) ) if non_empty_env_var ( "CURSOR_API_KEY" ) => Some ( CURSOR_PROVIDER_ID ) ,
626698 Some ( ( "deepseek" , _) ) if non_empty_env_var ( "DEEPSEEK_API_KEY" ) => {
627699 Some ( DEEPSEEK_PROVIDER_ID )
628700 }
0 commit comments