@@ -51,28 +51,61 @@ pub struct ProviderRequestContext {
5151 pub custom_headers : Option < std:: collections:: HashMap < String , String > > ,
5252}
5353
54- /// Resolve `api_host` into a usable base URL.
54+ /// Default version path for a given provider type.
55+ pub fn default_version_for_type ( provider_type : & ProviderType ) -> & ' static str {
56+ match provider_type {
57+ ProviderType :: Gemini => "/v1beta" ,
58+ _ => "/v1" ,
59+ }
60+ }
61+
62+ /// Resolve `api_host` into a usable base URL, using the provider type to
63+ /// determine the default version path (e.g. `/v1` for OpenAI, `/v1beta` for Gemini).
5564///
56- /// - Trailing `!` → force mode: strip `!`, return as-is (no auto `/v1`).
57- /// - Already ends with `/v1` → return as-is.
58- /// - Otherwise → append `/v1`.
65+ /// - Trailing `!` → force mode: strip `!`, return as-is.
66+ /// - Already ends with a versioned path (e.g. `/v1`, `/v1beta`) → return as-is.
67+ /// - Otherwise → append the default version path for this provider type.
68+ pub fn resolve_base_url_for_type ( api_host : & str , provider_type : & ProviderType ) -> String {
69+ let default_version = default_version_for_type ( provider_type) ;
70+ resolve_base_url_inner ( api_host, default_version)
71+ }
72+
73+ /// Resolve `api_host` into a usable base URL (defaults to `/v1`).
5974pub fn resolve_base_url ( api_host : & str ) -> String {
75+ resolve_base_url_inner ( api_host, "/v1" )
76+ }
77+
78+ fn resolve_base_url_inner ( api_host : & str , default_version : & str ) -> String {
6079 let trimmed = api_host. trim_end_matches ( '/' ) ;
6180 if let Some ( forced) = trimmed. strip_suffix ( '!' ) {
6281 forced. trim_end_matches ( '/' ) . to_string ( )
63- } else if trimmed . ends_with ( "/v1" ) {
82+ } else if has_version_suffix ( trimmed ) {
6483 trimmed. to_string ( )
6584 } else {
66- format ! ( "{}/v1" , trimmed)
85+ format ! ( "{}{}" , trimmed, default_version)
86+ }
87+ }
88+
89+ /// Check whether the URL already ends with a versioned path segment
90+ /// like `/v1`, `/v1beta`, `/v2`, `/v1beta1`, etc.
91+ fn has_version_suffix ( url : & str ) -> bool {
92+ let last_seg = url. rsplit ( '/' ) . next ( ) . unwrap_or ( "" ) ;
93+ // Match patterns like v1, v2, v1beta, v1beta1, v1alpha, etc.
94+ let bytes = last_seg. as_bytes ( ) ;
95+ if bytes. len ( ) < 2 || bytes[ 0 ] != b'v' {
96+ return false ;
6797 }
98+ // After 'v', must start with digit(s), optionally followed by alpha tag
99+ let rest = & last_seg[ 1 ..] ;
100+ rest. starts_with ( |c : char | c. is_ascii_digit ( ) )
68101}
69102
70103/// Build the full chat/completion URL from resolved `base_url` and optional `api_path`.
71104///
72105/// When `api_path` is provided:
73106/// - Trailing `!` on api_path → force: concat resolved base + raw path (strip `!`).
74- /// - No `!` → auto-dedup: if both resolved base ends with `/v1` and
75- /// api_path starts with `/v1`, strip the duplicate prefix from api_path.
107+ /// - No `!` → auto-dedup: if both resolved base and api_path share a common
108+ /// versioned prefix (e.g. `/v1`, `/v1beta`), strip the duplicate from api_path.
76109///
77110/// When `api_path` is absent, returns `resolved_base_url + default_suffix`
78111/// (e.g. `/chat/completions`).
@@ -98,18 +131,35 @@ pub fn resolve_chat_url(
98131 } else {
99132 format ! ( "/{}" , path)
100133 } ;
101- // Auto dedup: if both have /v1, strip from api_path
102- if base. ends_with ( "/v1" ) && p. starts_with ( "/v1" ) {
103- format ! ( "{}{}" , base, & p[ 3 ..] )
104- } else {
105- format ! ( "{}{}" , base, p)
134+ // Auto dedup: if base ends with a version prefix that matches
135+ // the start of api_path, strip it from api_path
136+ if let Some ( ver) = extract_version_prefix ( base) {
137+ if p. starts_with ( & ver) {
138+ return format ! ( "{}{}" , base, & p[ ver. len( ) ..] ) ;
139+ }
106140 }
141+ format ! ( "{}{}" , base, p)
107142 }
108143 }
109144 _ => format ! ( "{}{}" , base, default_suffix) ,
110145 }
111146}
112147
148+ /// Extract the trailing version prefix from a URL (e.g. "/v1", "/v1beta").
149+ fn extract_version_prefix ( url : & str ) -> Option < String > {
150+ let last_seg = url. rsplit ( '/' ) . next ( ) ?;
151+ let bytes = last_seg. as_bytes ( ) ;
152+ if bytes. len ( ) < 2 || bytes[ 0 ] != b'v' {
153+ return None ;
154+ }
155+ let rest = & last_seg[ 1 ..] ;
156+ if rest. starts_with ( |c : char | c. is_ascii_digit ( ) ) {
157+ Some ( format ! ( "/{}" , last_seg) )
158+ } else {
159+ None
160+ }
161+ }
162+
113163pub ( crate ) fn parse_base64_data_url ( url : & str ) -> Option < ( String , String ) > {
114164 let rest = url. strip_prefix ( "data:" ) ?;
115165 let ( mime_type, data) = rest. split_once ( ";base64," ) ?;
0 commit comments