@@ -468,6 +468,55 @@ pub mod client {
468468 self . config . api_key . is_empty ( )
469469 }
470470
471+ /// Returns `true` when this client is configured to use a Claude Code
472+ /// OAuth Bearer token (Claude.ai Pro/Max). The query path and the
473+ /// request builders check this to enable stealth-impersonation.
474+ pub fn is_oauth ( & self ) -> bool {
475+ self . config . use_bearer_auth
476+ }
477+
478+ /// Mutate the outgoing request so it looks like Claude Code when the
479+ /// client is authenticated with an OAuth Bearer token:
480+ ///
481+ /// 1. Prepend the required `"You are Claude Code, …"` system block.
482+ /// Existing system content is preserved as a second block so the
483+ /// rest of Claurst's prompt assembly still reaches the model.
484+ ///
485+ /// No-op when `use_bearer_auth` is false (API-key flow).
486+ fn apply_oauth_stealth ( & self , request : & mut CreateMessageRequest ) {
487+ if !self . config . use_bearer_auth {
488+ return ;
489+ }
490+
491+ let identity_block = SystemBlock {
492+ block_type : "text" . to_string ( ) ,
493+ text : claurst_core:: oauth_config:: CLAUDE_CODE_SYSTEM_PROMPT_PREFIX . to_string ( ) ,
494+ cache_control : None ,
495+ } ;
496+
497+ request. system = match request. system . take ( ) {
498+ None => Some ( SystemPrompt :: Blocks ( vec ! [ identity_block] ) ) ,
499+ Some ( SystemPrompt :: Text ( existing) ) => {
500+ let existing_block = SystemBlock {
501+ block_type : "text" . to_string ( ) ,
502+ text : existing,
503+ cache_control : None ,
504+ } ;
505+ Some ( SystemPrompt :: Blocks ( vec ! [ identity_block, existing_block] ) )
506+ }
507+ Some ( SystemPrompt :: Blocks ( mut blocks) ) => {
508+ // Avoid duplicating the identity block on retries / re-sends.
509+ let already_first = blocks. first ( ) . is_some_and ( |b| {
510+ b. text == claurst_core:: oauth_config:: CLAUDE_CODE_SYSTEM_PROMPT_PREFIX
511+ } ) ;
512+ if !already_first {
513+ blocks. insert ( 0 , identity_block) ;
514+ }
515+ Some ( SystemPrompt :: Blocks ( blocks) )
516+ }
517+ } ;
518+ }
519+
471520 /// Build a new client. Panics if `config.api_key` is empty.
472521 pub fn new ( config : ClientConfig ) -> anyhow:: Result < Self > {
473522 // Allow empty key at construction — validation is deferred to
@@ -556,6 +605,7 @@ pub mod client {
556605 }
557606
558607 request. stream = false ;
608+ self . apply_oauth_stealth ( & mut request) ;
559609 let body = serde_json:: to_value ( & request) . map_err ( ClaudeError :: Json ) ?;
560610
561611 let resp = self . send_with_retry ( & body) . await ?;
@@ -661,6 +711,7 @@ pub mod client {
661711 }
662712
663713 request. stream = true ;
714+ self . apply_oauth_stealth ( & mut request) ;
664715 let body = serde_json:: to_value ( & request) . map_err ( ClaudeError :: Json ) ?;
665716
666717 let resp = self . send_with_retry ( & body) . await ?;
@@ -704,11 +755,19 @@ pub mod client {
704755 . get ( & url)
705756 . header ( "anthropic-version" , & self . config . api_version )
706757 . header ( "content-type" , "application/json" ) ;
707- req = if self . config . use_bearer_auth {
708- req. header ( "Authorization" , format ! ( "Bearer {}" , & self . config. api_key) )
758+ if self . config . use_bearer_auth {
759+ let ua = format ! (
760+ "claude-cli/{}" ,
761+ claurst_core:: oauth_config:: CLAUDE_CODE_VERSION_FOR_OAUTH
762+ ) ;
763+ req = req
764+ . header ( "anthropic-beta" , claurst_core:: oauth_config:: OAUTH_BETA_FLAGS . join ( "," ) )
765+ . header ( "user-agent" , ua)
766+ . header ( "x-app" , "cli" )
767+ . header ( "Authorization" , format ! ( "Bearer {}" , & self . config. api_key) ) ;
709768 } else {
710- req. header ( "x-api-key" , & self . config . api_key )
711- } ;
769+ req = req . header ( "x-api-key" , & self . config . api_key ) ;
770+ }
712771
713772 let resp = req. send ( ) . await ?;
714773
@@ -744,24 +803,57 @@ pub mod client {
744803 loop {
745804 attempts += 1 ;
746805
747- // Compute CCH hash and build billing header
748- let cch_hash = cch:: compute_cch ( body_bytes) ;
749- let billing_header = format ! ( "cc_version=0.1; cc_entrypoint=claude_code; {}; cc_workload=claude_code;" , cch_hash) ;
750-
751806 // Use Bearer auth for Claude.ai OAuth tokens; x-api-key for regular keys.
807+ let use_oauth = self . config . use_bearer_auth ;
808+
809+ // On the OAuth path we impersonate Claude Code: prepend the
810+ // required beta flags, advertise `claude-cli/<ver>` as the
811+ // user-agent, and drop the CCH billing header (the real
812+ // Claude Code client sends it but the API does not require
813+ // it for OAuth tokens, and emitting it would fingerprint
814+ // mismatch against the impersonated UA).
815+ let anthropic_beta = if use_oauth {
816+ let mut flags: Vec < & str > =
817+ claurst_core:: oauth_config:: OAUTH_BETA_FLAGS . to_vec ( ) ;
818+ if !self . config . beta_features . is_empty ( ) {
819+ flags. push ( & self . config . beta_features ) ;
820+ }
821+ flags. join ( "," )
822+ } else {
823+ self . config . beta_features . clone ( )
824+ } ;
825+
752826 let mut req = self
753827 . http
754828 . post ( & url)
755829 . header ( "anthropic-version" , & self . config . api_version )
756- . header ( "anthropic-beta" , & self . config . beta_features )
830+ . header ( "anthropic-beta" , & anthropic_beta )
757831 . header ( "content-type" , "application/json" )
758- . header ( "accept" , "text/event-stream" )
759- . header ( "x-anthropic-billing-header" , billing_header) ;
760- req = if self . config . use_bearer_auth {
761- req. header ( "Authorization" , format ! ( "Bearer {}" , & self . config. api_key) )
832+ . header ( "accept" , "text/event-stream" ) ;
833+
834+ if use_oauth {
835+ let ua = format ! (
836+ "claude-cli/{}" ,
837+ claurst_core:: oauth_config:: CLAUDE_CODE_VERSION_FOR_OAUTH
838+ ) ;
839+ req = req
840+ . header ( "user-agent" , ua)
841+ . header ( "x-app" , "cli" )
842+ . header ( "Authorization" , format ! ( "Bearer {}" , & self . config. api_key) ) ;
762843 } else {
763- req. header ( "x-api-key" , & self . config . api_key )
764- } ;
844+ // Compute CCH billing hash and attach on the API-key path
845+ // only — this is the codepath the official client uses
846+ // for direct API customers.
847+ let cch_hash = cch:: compute_cch ( body_bytes) ;
848+ let billing_header = format ! (
849+ "cc_version=0.1; cc_entrypoint=claude_code; {}; cc_workload=claude_code;" ,
850+ cch_hash
851+ ) ;
852+ req = req
853+ . header ( "x-anthropic-billing-header" , billing_header)
854+ . header ( "x-api-key" , & self . config . api_key ) ;
855+ }
856+
765857 let req = req. body ( body_str. clone ( ) ) ;
766858
767859 let resp = req. send ( ) . await . map_err ( ClaudeError :: Http ) ?;
0 commit comments