@@ -6,6 +6,7 @@ use axum::Extension;
66
77use sha2:: Digest ;
88
9+ use crate :: agent_loop;
910use crate :: auth:: RequireAuth ;
1011use crate :: encryption:: { self , Endpoint } ;
1112use crate :: error:: AppError ;
@@ -35,11 +36,8 @@ pub async fn chat_completions(
3536 // honored when authenticated with config.token (trusted gateway); sk- clients
3637 // always bind signatures to the wire body so they cannot forge a hash for a
3738 // different payload.
38- let original_request_hash = Some ( resolve_request_hash_for_signing (
39- & headers,
40- & request_body,
41- auth. cloud_api_key . is_none ( ) ,
42- ) ) ;
39+ let request_hash =
40+ resolve_request_hash_for_signing ( & headers, & request_body, auth. cloud_api_key . is_none ( ) ) ;
4341
4442 // Decrypt request fields if encryption is active
4543 if let Some ( ref ctx) = enc_ctx {
@@ -56,6 +54,50 @@ pub async fn chat_completions(
5654 . and_then ( |v| v. as_bool ( ) )
5755 . unwrap_or ( false ) ;
5856
57+ // Server-side agent loop opt-in: the request advertises exactly
58+ // `{"type":"web_context_search"}` and nothing else. Requires streaming
59+ // (we splice tool-result chunks between iterations) and Brave creds
60+ // configured on this CVM. Anything that doesn't match falls through to
61+ // the existing pass-through path below, byte-for-byte identical.
62+ if agent_loop:: is_web_context_search_request ( & request_json) {
63+ if !is_stream {
64+ return Err ( AppError :: BadRequest (
65+ "web_context_search requires stream:true" . to_string ( ) ,
66+ ) ) ;
67+ }
68+ if state. config . web_context_search_url . is_none ( )
69+ || state. config . web_context_search_api_key . is_none ( )
70+ {
71+ return Err ( AppError :: BadRequest (
72+ "web_context_search is not configured on this deployment" . to_string ( ) ,
73+ ) ) ;
74+ }
75+
76+ // Build chunk transform if E2EE is active. The agent loop is the
77+ // privacy-critical path — both our synthetic `nearai_tool_result`
78+ // chunks AND the model's own `tool_calls[].function.{name,arguments}`
79+ // (which contain the search query the model just generated from the
80+ // user's E2EE-decrypted prompt) must travel encrypted. Force
81+ // `encrypt_all_fields: true` on the context used to build this
82+ // transform so clients don't need to remember to send
83+ // `X-Encrypt-All-Fields: true` to get the full privacy guarantee.
84+ // This only affects the agent-loop path; the regular chat path
85+ // below still honors the client's `X-Encrypt-All-Fields` choice.
86+ let chunk_transform = enc_ctx. map ( |mut ctx| {
87+ ctx. encrypt_all_fields = true ;
88+ encryption:: make_chunk_transform ( Endpoint :: ChatCompletions , ctx, state. signing . clone ( ) )
89+ } ) ;
90+ return agent_loop:: run_chat_completion (
91+ state,
92+ auth,
93+ tracing_ids,
94+ request_hash,
95+ request_json,
96+ chunk_transform,
97+ )
98+ . await ;
99+ }
100+
59101 // For cloud API key requests with streaming, force include_usage
60102 // so the backend always sends token counts for billing.
61103 // (Non-streaming requests also stream internally via proxy_json_request,
@@ -102,7 +144,7 @@ pub async fn chat_completions(
102144 model_name : state. config . model_name . clone ( ) ,
103145 usage_reporter : make_usage_reporter ( & auth, & state) ,
104146 usage_type : UsageType :: ChatCompletion ,
105- request_hash : original_request_hash ,
147+ request_hash : Some ( request_hash ) ,
106148 response_transform,
107149 chunk_transform,
108150 backend_guard : Some ( guard) ,
0 commit comments