@@ -106,7 +106,7 @@ impl Llm {
106106 . await
107107 . and_then ( parse_openai_with_reasoning_details)
108108 }
109- Provider :: OpenAi | Provider :: Databricks => {
109+ Provider :: OpenAi | Provider :: OpenAiCompat | Provider :: Databricks => {
110110 self . openai_request ( cfg, effective_model, |use_responses, request_model| {
111111 // Normalize effort for model-specific availability. Startup no longer rejects
112112 // `max` for pure OpenAI/Databricks; this per-model table is the single authority
@@ -246,7 +246,7 @@ impl Llm {
246246 let v = self . post_openrouter ( cfg, & body) . await ?;
247247 Ok ( parse_openai ( v) ?. text )
248248 }
249- Provider :: OpenAi | Provider :: Databricks => {
249+ Provider :: OpenAi | Provider :: OpenAiCompat | Provider :: Databricks => {
250250 let r = self
251251 . openai_request ( cfg, effective_model, |use_responses, request_model| {
252252 if use_responses {
@@ -471,14 +471,19 @@ impl Llm {
471471 // statuses map to `LlmAuth` in `post`: a 403 is indistinguishable from
472472 // an expired-token 403 here, so we refresh once and let it propagate.
473473 let mut bearer = self . auth . bearer ( ) . await . map_err ( PostError :: from) ?;
474+ let use_bearer = cfg. provider != Provider :: OpenAiCompat || !bearer. is_empty ( ) ;
474475 let mut refreshed = false ;
475476 loop {
476- match post ( & self . http , & url, body_ref, cfg. llm_timeout , |r| {
477- r. bearer_auth ( & bearer)
477+ match post ( & self . http , & url, body_ref, cfg. llm_timeout , |request| {
478+ if use_bearer {
479+ request. bearer_auth ( & bearer)
480+ } else {
481+ request
482+ }
478483 } )
479484 . await
480485 {
481- Err ( PostError :: Agent ( AgentError :: LlmAuth ( _) ) ) if !refreshed => {
486+ Err ( PostError :: Agent ( AgentError :: LlmAuth ( _) ) ) if use_bearer && !refreshed => {
482487 refreshed = true ;
483488 bearer = self
484489 . auth
@@ -2071,7 +2076,7 @@ pub(crate) fn databricks_pkce_config(host: &str) -> PkceOAuthConfig {
20712076/// flow; subsequent requests use the cache + refresh transparently.
20722077pub ( crate ) fn build_token_source ( cfg : & Config ) -> Result < Arc < dyn TokenSource > , AgentError > {
20732078 match cfg. provider {
2074- Provider :: Anthropic | Provider :: OpenAi | Provider :: OpenRouter => {
2079+ Provider :: Anthropic | Provider :: OpenAi | Provider :: OpenAiCompat | Provider :: OpenRouter => {
20752080 Ok ( Arc :: new ( StaticTokenSource :: new ( cfg. api_key . clone ( ) ) ) )
20762081 }
20772082 Provider :: Databricks | Provider :: DatabricksV2 => {
@@ -2097,9 +2102,11 @@ pub(crate) fn build_token_source(cfg: &Config) -> Result<Arc<dyn TokenSource>, A
20972102pub ( crate ) fn summary_completion_cap ( provider : Provider , max_output_tokens : u32 ) -> u32 {
20982103 match provider {
20992104 Provider :: OpenRouter => max_output_tokens. saturating_mul ( 2 ) ,
2100- Provider :: Anthropic | Provider :: OpenAi | Provider :: Databricks | Provider :: DatabricksV2 => {
2101- max_output_tokens
2102- }
2105+ Provider :: Anthropic
2106+ | Provider :: OpenAi
2107+ | Provider :: OpenAiCompat
2108+ | Provider :: Databricks
2109+ | Provider :: DatabricksV2 => max_output_tokens,
21032110 }
21042111}
21052112
@@ -5725,6 +5732,49 @@ mod tests {
57255732 }
57265733 }
57275734
5735+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
5736+ async fn post_openai_compat_omits_authorization_when_key_is_empty ( ) {
5737+ use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
5738+ use tokio:: net:: TcpListener ;
5739+
5740+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
5741+ let base = format ! ( "http://{}" , listener. local_addr( ) . unwrap( ) ) ;
5742+ let captured = tokio:: spawn ( async move {
5743+ let ( mut socket, _) = listener. accept ( ) . await . unwrap ( ) ;
5744+ let mut bytes = Vec :: new ( ) ;
5745+ let mut buffer = [ 0u8 ; 4096 ] ;
5746+ while !bytes. windows ( 4 ) . any ( |window| window == b"\r \n \r \n " ) {
5747+ let count = socket. read ( & mut buffer) . await . unwrap ( ) ;
5748+ if count == 0 {
5749+ break ;
5750+ }
5751+ bytes. extend_from_slice ( & buffer[ ..count] ) ;
5752+ }
5753+ let body = "{\" ok\" :true}" ;
5754+ socket
5755+ . write_all (
5756+ format ! (
5757+ "HTTP/1.1 200 OK\r \n Content-Type: application/json\r \n Content-Length: {}\r \n Connection: close\r \n \r \n {}" ,
5758+ body. len( ) , body
5759+ )
5760+ . as_bytes ( ) ,
5761+ )
5762+ . await
5763+ . unwrap ( ) ;
5764+ String :: from_utf8_lossy ( & bytes) . to_ascii_lowercase ( )
5765+ } ) ;
5766+
5767+ let llm = llm_with ( Arc :: new ( StaticTokenSource :: new ( "" ) ) ) ;
5768+ let mut config = cfg ( Provider :: OpenAiCompat ) ;
5769+ config. base_url = base;
5770+ llm. post_openai ( & config, "/v1/x" , & json ! ( { } ) , "model" )
5771+ . await
5772+ . unwrap ( ) ;
5773+
5774+ let headers = captured. await . unwrap ( ) ;
5775+ assert ! ( !headers. contains( "authorization:" ) , "{headers}" ) ;
5776+ }
5777+
57285778 /// A single 401 forces exactly one refresh, the retry with the fresh
57295779 /// token succeeds, and a *later* call gets its own refresh — proving the
57305780 /// one-shot guard is per-call, not stored on the source.
0 commit comments