2025-10-22 11:31:21 -07:00
|
|
|
use hermesllm::clients::endpoints::SupportedUpstreamAPIs;
|
2024-10-17 10:16:40 -07:00
|
|
|
use http::StatusCode;
|
2026-01-28 17:47:33 -08:00
|
|
|
use log::{debug, error, info, warn};
|
2024-12-09 10:46:46 -08:00
|
|
|
use proxy_wasm::hostcalls::get_current_time;
|
2024-10-17 10:16:40 -07:00
|
|
|
use proxy_wasm::traits::*;
|
|
|
|
|
use proxy_wasm::types::*;
|
|
|
|
|
use std::num::NonZero;
|
|
|
|
|
use std::rc::Rc;
|
2026-01-28 17:47:33 -08:00
|
|
|
use std::sync::Arc;
|
2024-11-15 10:44:01 -08:00
|
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
2024-10-17 10:16:40 -07:00
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
use crate::metrics::Metrics;
|
|
|
|
|
use common::configuration::{LlmProvider, LlmProviderType, Overrides};
|
|
|
|
|
use common::consts::{
|
2025-10-22 11:31:21 -07:00
|
|
|
ARCH_IS_STREAMING_HEADER, ARCH_PROVIDER_HINT_HEADER, ARCH_ROUTING_HEADER, HEALTHZ_PATH,
|
|
|
|
|
RATELIMIT_SELECTOR_HEADER_KEY, REQUEST_ID_HEADER, TRACE_PARENT_HEADER,
|
2025-09-10 07:40:30 -07:00
|
|
|
};
|
|
|
|
|
use common::errors::ServerError;
|
|
|
|
|
use common::llm_providers::LlmProviders;
|
|
|
|
|
use common::ratelimit::Header;
|
|
|
|
|
use common::stats::{IncrementingMetric, RecordingMetric};
|
|
|
|
|
use common::{ratelimit, routing, tokenizer};
|
2025-12-03 14:58:26 -08:00
|
|
|
use hermesllm::apis::streaming_shapes::amazon_bedrock_binary_frame::BedrockBinaryFrameDecoder;
|
2025-12-18 11:02:59 -08:00
|
|
|
use hermesllm::apis::streaming_shapes::sse::{SseEvent, SseStreamBuffer, SseStreamBufferTrait};
|
|
|
|
|
use hermesllm::apis::streaming_shapes::sse_chunk_processor::SseChunkProcessor;
|
2025-12-03 14:58:26 -08:00
|
|
|
use hermesllm::clients::endpoints::SupportedAPIsFromClient;
|
2025-10-22 11:31:21 -07:00
|
|
|
use hermesllm::providers::response::ProviderResponse;
|
2025-12-03 14:58:26 -08:00
|
|
|
use hermesllm::providers::streaming_response::ProviderStreamResponse;
|
2025-10-22 11:31:21 -07:00
|
|
|
use hermesllm::{
|
|
|
|
|
DecodedFrame, ProviderId, ProviderRequest, ProviderRequestType, ProviderResponseType,
|
|
|
|
|
ProviderStreamResponseType,
|
|
|
|
|
};
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2024-10-18 12:53:44 -07:00
|
|
|
pub struct StreamContext {
|
2024-12-09 10:46:46 -08:00
|
|
|
metrics: Rc<Metrics>,
|
2024-10-17 10:16:40 -07:00
|
|
|
ratelimit_selector: Option<Header>,
|
|
|
|
|
streaming_response: bool,
|
|
|
|
|
response_tokens: usize,
|
2025-09-10 07:40:30 -07:00
|
|
|
/// The API that is requested by the client (before compatibility mapping)
|
2025-12-03 14:58:26 -08:00
|
|
|
client_api: Option<SupportedAPIsFromClient>,
|
2025-09-10 07:40:30 -07:00
|
|
|
/// The API that should be used for the upstream provider (after compatibility mapping)
|
2025-10-22 11:31:21 -07:00
|
|
|
resolved_api: Option<SupportedUpstreamAPIs>,
|
2024-10-17 10:16:40 -07:00
|
|
|
llm_providers: Rc<LlmProviders>,
|
2026-01-28 17:47:33 -08:00
|
|
|
llm_provider: Option<Arc<LlmProvider>>,
|
2024-10-17 10:16:40 -07:00
|
|
|
request_id: Option<String>,
|
2024-11-18 17:55:39 -08:00
|
|
|
start_time: SystemTime,
|
2024-11-15 10:44:01 -08:00
|
|
|
ttft_duration: Option<Duration>,
|
2024-11-18 17:55:39 -08:00
|
|
|
ttft_time: Option<u128>,
|
|
|
|
|
traceparent: Option<String>,
|
|
|
|
|
request_body_sent_time: Option<u128>,
|
2025-12-22 18:05:49 -08:00
|
|
|
_overrides: Rc<Option<Overrides>>,
|
2025-08-20 12:55:29 -07:00
|
|
|
user_message: Option<String>,
|
2025-09-25 17:00:37 -07:00
|
|
|
upstream_status_code: Option<StatusCode>,
|
2025-10-22 11:31:21 -07:00
|
|
|
binary_frame_decoder: Option<BedrockBinaryFrameDecoder<bytes::BytesMut>>,
|
2025-10-24 14:07:05 -07:00
|
|
|
http_method: Option<String>,
|
|
|
|
|
http_protocol: Option<String>,
|
2025-12-03 14:58:26 -08:00
|
|
|
sse_buffer: Option<SseStreamBuffer>,
|
2025-12-18 11:02:59 -08:00
|
|
|
sse_chunk_processor: Option<SseChunkProcessor>,
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-18 12:53:44 -07:00
|
|
|
impl StreamContext {
|
2024-11-18 17:55:39 -08:00
|
|
|
pub fn new(
|
2024-12-09 10:46:46 -08:00
|
|
|
metrics: Rc<Metrics>,
|
2024-11-18 17:55:39 -08:00
|
|
|
llm_providers: Rc<LlmProviders>,
|
2025-03-19 15:21:34 -07:00
|
|
|
overrides: Rc<Option<Overrides>>,
|
2024-11-18 17:55:39 -08:00
|
|
|
) -> Self {
|
2024-10-18 12:53:44 -07:00
|
|
|
StreamContext {
|
2024-10-17 10:16:40 -07:00
|
|
|
metrics,
|
2025-12-22 18:05:49 -08:00
|
|
|
_overrides: overrides,
|
2024-10-17 10:16:40 -07:00
|
|
|
ratelimit_selector: None,
|
|
|
|
|
streaming_response: false,
|
|
|
|
|
response_tokens: 0,
|
2025-09-10 07:40:30 -07:00
|
|
|
client_api: None,
|
|
|
|
|
resolved_api: None,
|
2024-10-17 10:16:40 -07:00
|
|
|
llm_providers,
|
|
|
|
|
llm_provider: None,
|
|
|
|
|
request_id: None,
|
2024-11-18 17:55:39 -08:00
|
|
|
start_time: SystemTime::now(),
|
2024-11-12 15:03:26 -08:00
|
|
|
ttft_duration: None,
|
2024-11-15 10:44:01 -08:00
|
|
|
traceparent: None,
|
|
|
|
|
ttft_time: None,
|
2024-11-17 17:01:19 -08:00
|
|
|
request_body_sent_time: None,
|
2025-08-20 12:55:29 -07:00
|
|
|
user_message: None,
|
2025-09-25 17:00:37 -07:00
|
|
|
upstream_status_code: None,
|
2025-10-22 11:31:21 -07:00
|
|
|
binary_frame_decoder: None,
|
2025-10-24 14:07:05 -07:00
|
|
|
http_method: None,
|
|
|
|
|
http_protocol: None,
|
2025-12-03 14:58:26 -08:00
|
|
|
sse_buffer: None,
|
2025-12-18 11:02:59 -08:00
|
|
|
sse_chunk_processor: None,
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
|
|
|
|
|
/// Returns the appropriate request identifier for logging.
|
|
|
|
|
/// Uses request_id (from x-request-id header) when available, otherwise returns a literal indicating no request ID.
|
|
|
|
|
fn request_identifier(&self) -> String {
|
|
|
|
|
self.request_id
|
|
|
|
|
.as_ref()
|
2025-12-25 21:08:37 -08:00
|
|
|
.filter(|id| !id.is_empty())
|
|
|
|
|
.cloned()
|
2026-02-09 13:33:27 -08:00
|
|
|
.unwrap_or_else(|| "no_request_id".to_string())
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
2024-10-17 10:16:40 -07:00
|
|
|
fn llm_provider(&self) -> &LlmProvider {
|
|
|
|
|
self.llm_provider
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("the provider should be set when asked for it")
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 12:55:29 -07:00
|
|
|
fn get_provider_id(&self) -> ProviderId {
|
|
|
|
|
self.llm_provider().to_provider_id()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
//This function assumes that the provider has been set.
|
|
|
|
|
fn update_upstream_path(&mut self, request_path: &str) {
|
|
|
|
|
let hermes_provider_id = self.llm_provider().to_provider_id();
|
|
|
|
|
if let Some(api) = &self.client_api {
|
2025-09-18 18:36:30 -07:00
|
|
|
let target_endpoint = api.target_endpoint_for_provider(
|
|
|
|
|
&hermes_provider_id,
|
|
|
|
|
request_path,
|
|
|
|
|
self.llm_provider()
|
|
|
|
|
.model
|
|
|
|
|
.as_ref()
|
|
|
|
|
.unwrap_or(&"".to_string()),
|
2025-10-22 11:31:21 -07:00
|
|
|
self.streaming_response,
|
2025-10-29 17:08:07 -07:00
|
|
|
self.llm_provider().base_url_path_prefix.as_deref(),
|
2026-03-31 20:40:42 -04:00
|
|
|
self.llm_provider().name.starts_with("perplexity/"),
|
2025-09-18 18:36:30 -07:00
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
if target_endpoint != request_path {
|
|
|
|
|
self.set_http_request_header(":path", Some(&target_endpoint));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 17:47:33 -08:00
|
|
|
fn select_llm_provider(&mut self) -> Result<(), String> {
|
2024-10-17 10:16:40 -07:00
|
|
|
let provider_hint = self
|
|
|
|
|
.get_http_request_header(ARCH_PROVIDER_HINT_HEADER)
|
2025-01-17 18:25:55 -08:00
|
|
|
.map(|llm_name| llm_name.into());
|
2024-10-17 10:16:40 -07:00
|
|
|
|
2026-01-28 17:47:33 -08:00
|
|
|
// Try to get provider with hint, fallback to default if error
|
|
|
|
|
// This handles prompt_gateway requests which don't set ARCH_PROVIDER_HINT_HEADER
|
|
|
|
|
// since prompt_gateway doesn't have access to model configuration.
|
|
|
|
|
// brightstaff (model proxy) always validates and sets the provider hint.
|
|
|
|
|
let provider = match routing::get_llm_provider(&self.llm_providers, provider_hint) {
|
|
|
|
|
Ok(provider) => provider,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
// Try default provider as fallback
|
|
|
|
|
match self.llm_providers.default() {
|
|
|
|
|
Some(default_provider) => {
|
|
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: provider selection failed, using default provider",
|
2026-01-28 17:47:33 -08:00
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
default_provider
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
error!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: provider selection failed, error='{}' and no default provider configured",
|
2026-01-28 17:47:33 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
return Err(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.llm_provider = Some(provider);
|
2025-01-31 10:37:53 -08:00
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: provider selected, hint='{}' selected='{}'",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
2025-03-05 14:08:06 -08:00
|
|
|
self.get_http_request_header(ARCH_PROVIDER_HINT_HEADER)
|
2025-09-10 07:40:30 -07:00
|
|
|
.unwrap_or("none".to_string()),
|
2025-07-11 16:42:16 -07:00
|
|
|
self.llm_provider.as_ref().unwrap().name
|
2025-01-31 10:37:53 -08:00
|
|
|
);
|
2026-01-28 17:47:33 -08:00
|
|
|
|
|
|
|
|
Ok(())
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn modify_auth_headers(&mut self) -> Result<(), ServerError> {
|
2026-04-17 17:23:05 -07:00
|
|
|
// Determine the credential to forward upstream. Either the client
|
|
|
|
|
// supplied one (passthrough_auth) or it's configured on the provider.
|
|
|
|
|
let credential: String = if self.llm_provider().passthrough_auth == Some(true) {
|
|
|
|
|
// Client auth may arrive in either Anthropic-style (`x-api-key`)
|
|
|
|
|
// or OpenAI-style (`Authorization: Bearer ...`). Accept both so
|
|
|
|
|
// clients using Anthropic SDKs (which default to `x-api-key`)
|
|
|
|
|
// work when the upstream is OpenAI-compatible, and vice versa.
|
|
|
|
|
let authorization = self.get_http_request_header("Authorization");
|
|
|
|
|
let x_api_key = self.get_http_request_header("x-api-key");
|
|
|
|
|
match extract_client_credential(authorization.as_deref(), x_api_key.as_deref()) {
|
|
|
|
|
Some(key) => {
|
|
|
|
|
debug!(
|
|
|
|
|
"request_id={}: forwarding client credential to provider '{}'",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
self.llm_provider().name
|
|
|
|
|
);
|
|
|
|
|
key
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
warn!(
|
|
|
|
|
"request_id={}: passthrough_auth enabled but no Authorization / x-api-key header present in client request",
|
|
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
2026-01-15 00:06:28 +01:00
|
|
|
}
|
2026-04-17 17:23:05 -07:00
|
|
|
} else {
|
2024-10-17 10:16:40 -07:00
|
|
|
self.llm_provider()
|
|
|
|
|
.access_key
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or(ServerError::BadRequest {
|
|
|
|
|
why: format!(
|
|
|
|
|
"No access key configured for selected LLM Provider \"{}\"",
|
|
|
|
|
self.llm_provider()
|
|
|
|
|
),
|
2026-04-17 17:23:05 -07:00
|
|
|
})?
|
|
|
|
|
.clone()
|
|
|
|
|
};
|
2024-10-17 10:16:40 -07:00
|
|
|
|
2026-04-17 17:23:05 -07:00
|
|
|
// Normalize the credential into whichever header the upstream expects.
|
|
|
|
|
// This lets an Anthropic-SDK client reach an OpenAI-compatible upstream
|
|
|
|
|
// (and vice versa) without the caller needing to know what format the
|
|
|
|
|
// upstream uses.
|
2025-09-10 07:40:30 -07:00
|
|
|
match self.resolved_api.as_ref() {
|
2025-10-22 11:31:21 -07:00
|
|
|
Some(SupportedUpstreamAPIs::AnthropicMessagesAPI(_)) => {
|
2026-04-17 17:23:05 -07:00
|
|
|
// Anthropic expects `x-api-key` + `anthropic-version`.
|
2025-09-10 07:40:30 -07:00
|
|
|
self.remove_http_request_header("Authorization");
|
2026-04-17 17:23:05 -07:00
|
|
|
self.set_http_request_header("x-api-key", Some(&credential));
|
2025-09-10 07:40:30 -07:00
|
|
|
self.set_http_request_header("anthropic-version", Some("2023-06-01"));
|
|
|
|
|
}
|
2025-10-22 11:31:21 -07:00
|
|
|
Some(
|
|
|
|
|
SupportedUpstreamAPIs::OpenAIChatCompletions(_)
|
|
|
|
|
| SupportedUpstreamAPIs::AmazonBedrockConverse(_)
|
2025-12-03 14:58:26 -08:00
|
|
|
| SupportedUpstreamAPIs::AmazonBedrockConverseStream(_)
|
|
|
|
|
| SupportedUpstreamAPIs::OpenAIResponsesAPI(_),
|
2025-10-22 11:31:21 -07:00
|
|
|
)
|
|
|
|
|
| None => {
|
2026-04-17 17:23:05 -07:00
|
|
|
// OpenAI (and default): `Authorization: Bearer ...`.
|
2025-09-10 07:40:30 -07:00
|
|
|
self.remove_http_request_header("x-api-key");
|
2026-04-17 17:23:05 -07:00
|
|
|
let authorization_header_value = format!("Bearer {}", credential);
|
2025-09-10 07:40:30 -07:00
|
|
|
self.set_http_request_header("Authorization", Some(&authorization_header_value));
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-17 10:16:40 -07:00
|
|
|
|
2026-04-23 15:34:44 -07:00
|
|
|
// Apply any extra headers configured on the provider (e.g., ChatGPT-Account-Id, originator)
|
|
|
|
|
let headers = self.llm_provider().headers.clone();
|
|
|
|
|
if let Some(headers) = headers {
|
|
|
|
|
for (key, value) in &headers {
|
|
|
|
|
self.set_http_request_header(key, Some(value));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn delete_content_length_header(&mut self) {
|
|
|
|
|
// Remove the Content-Length header because further body manipulations in the gateway logic will invalidate it.
|
|
|
|
|
// Server's generally throw away requests whose body length do not match the Content-Length header.
|
|
|
|
|
// However, a missing Content-Length header is not grounds for bad requests given that intermediary hops could
|
|
|
|
|
// manipulate the body in benign ways e.g., compression.
|
|
|
|
|
self.set_http_request_header("content-length", None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn save_ratelimit_header(&mut self) {
|
|
|
|
|
self.ratelimit_selector = self
|
|
|
|
|
.get_http_request_header(RATELIMIT_SELECTOR_HEADER_KEY)
|
|
|
|
|
.and_then(|key| {
|
|
|
|
|
self.get_http_request_header(&key)
|
|
|
|
|
.map(|value| Header { key, value })
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn send_server_error(&self, error: ServerError, override_status_code: Option<StatusCode>) {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: server error occurred: {}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
error
|
|
|
|
|
);
|
2024-10-17 10:16:40 -07:00
|
|
|
self.send_http_response(
|
|
|
|
|
override_status_code
|
|
|
|
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
|
|
|
|
.as_u16()
|
|
|
|
|
.into(),
|
|
|
|
|
vec![],
|
|
|
|
|
Some(format!("{error}").as_bytes()),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn enforce_ratelimits(
|
|
|
|
|
&mut self,
|
|
|
|
|
model: &str,
|
|
|
|
|
json_string: &str,
|
|
|
|
|
) -> Result<(), ratelimit::Error> {
|
2024-11-12 15:03:26 -08:00
|
|
|
// Tokenize and record token count.
|
|
|
|
|
let token_count = tokenizer::token_count(model, json_string).unwrap_or(0);
|
|
|
|
|
|
2025-09-30 18:46:13 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: token count, model='{}' input_tokens={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
model,
|
|
|
|
|
token_count
|
|
|
|
|
);
|
|
|
|
|
|
2024-11-12 15:03:26 -08:00
|
|
|
// Record the token count to metrics.
|
|
|
|
|
self.metrics
|
|
|
|
|
.input_sequence_length
|
|
|
|
|
.record(token_count as u64);
|
|
|
|
|
|
|
|
|
|
// Check if rate limiting needs to be applied.
|
2024-10-17 10:16:40 -07:00
|
|
|
if let Some(selector) = self.ratelimit_selector.take() {
|
2025-09-10 07:40:30 -07:00
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: ratelimit check, model='{}' selector='{}:{}'",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
model,
|
|
|
|
|
selector.key,
|
|
|
|
|
selector.value
|
|
|
|
|
);
|
2024-11-12 15:03:26 -08:00
|
|
|
ratelimit::ratelimits(None).read().unwrap().check_limit(
|
|
|
|
|
model.to_owned(),
|
|
|
|
|
selector,
|
|
|
|
|
NonZero::new(token_count as u32).unwrap(),
|
|
|
|
|
)?;
|
|
|
|
|
} else {
|
2025-09-10 07:40:30 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: ratelimit skip, model='{}' (no selector)",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
model
|
|
|
|
|
);
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
2024-11-12 15:03:26 -08:00
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
|
|
|
|
|
// === Helper methods extracted from on_http_response_body (no behavior change) ===
|
|
|
|
|
#[inline]
|
|
|
|
|
fn record_ttft_if_needed(&mut self) {
|
|
|
|
|
if self.ttft_duration.is_none() {
|
|
|
|
|
let current_time = get_current_time().unwrap();
|
|
|
|
|
self.ttft_time = Some(current_time_ns());
|
|
|
|
|
match current_time.duration_since(self.start_time) {
|
|
|
|
|
Ok(duration) => {
|
|
|
|
|
let duration_ms = duration.as_millis();
|
|
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: time to first token {}ms",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
duration_ms
|
|
|
|
|
);
|
|
|
|
|
self.ttft_duration = Some(duration);
|
|
|
|
|
self.metrics.time_to_first_token.record(duration_ms as u64);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: time measurement error: {:?}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 14:07:05 -07:00
|
|
|
fn handle_end_of_request_metrics_and_traces(&mut self, current_time: SystemTime) {
|
2025-09-10 07:40:30 -07:00
|
|
|
// All streaming responses end with bytes=0 and end_stream=true
|
|
|
|
|
// Record the latency for the request
|
|
|
|
|
match current_time.duration_since(self.start_time) {
|
|
|
|
|
Ok(duration) => {
|
|
|
|
|
// Convert the duration to milliseconds
|
|
|
|
|
let duration_ms = duration.as_millis();
|
|
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: request complete, latency={}ms tokens={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
duration_ms,
|
|
|
|
|
self.response_tokens
|
|
|
|
|
);
|
|
|
|
|
// Record the latency to the latency histogram
|
|
|
|
|
self.metrics.request_latency.record(duration_ms as u64);
|
|
|
|
|
|
|
|
|
|
if self.response_tokens > 0 {
|
|
|
|
|
// Compute the time per output token
|
|
|
|
|
let tpot = duration_ms as u64 / self.response_tokens as u64;
|
|
|
|
|
|
|
|
|
|
// Record the time per output token
|
|
|
|
|
self.metrics.time_per_output_token.record(tpot);
|
|
|
|
|
|
|
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: token throughput, time_per_token={}ms tokens_per_second={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
tpot,
|
|
|
|
|
1000 / tpot
|
|
|
|
|
);
|
|
|
|
|
// Record the tokens per second
|
|
|
|
|
self.metrics.tokens_per_second.record(1000 / tpot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: system time error: {:?}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Record the output sequence length
|
|
|
|
|
self.metrics
|
|
|
|
|
.output_sequence_length
|
|
|
|
|
.record(self.response_tokens as u64);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_raw_response_body(&mut self, body_size: usize) -> Result<Vec<u8>, Action> {
|
|
|
|
|
if self.streaming_response {
|
|
|
|
|
let chunk_size = body_size;
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream response chunk, streaming=true chunk_size={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
chunk_size
|
|
|
|
|
);
|
|
|
|
|
let streaming_chunk = match self.get_http_response_body(0, chunk_size) {
|
|
|
|
|
Some(chunk) => chunk,
|
|
|
|
|
None => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream response error, empty chunk size={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
chunk_size
|
|
|
|
|
);
|
|
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if streaming_chunk.len() != chunk_size {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream response size mismatch, expected={} actual={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
chunk_size,
|
|
|
|
|
streaming_chunk.len()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Ok(streaming_chunk)
|
|
|
|
|
} else {
|
|
|
|
|
if body_size == 0 {
|
|
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream response complete, streaming=false body_size={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
body_size
|
|
|
|
|
);
|
|
|
|
|
match self.get_http_response_body(0, body_size) {
|
|
|
|
|
Some(body) => Ok(body),
|
|
|
|
|
None => {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: non streaming response body empty",
|
|
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
Err(Action::Continue)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle_streaming_response(
|
|
|
|
|
&mut self,
|
|
|
|
|
body: &[u8],
|
|
|
|
|
provider_id: ProviderId,
|
|
|
|
|
) -> Result<Vec<u8>, Action> {
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: streaming process, client={:?} provider_id={:?} chunk_size={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
2025-09-29 19:23:08 -07:00
|
|
|
self.client_api,
|
2025-09-10 07:40:30 -07:00
|
|
|
provider_id,
|
|
|
|
|
body.len()
|
|
|
|
|
);
|
|
|
|
|
match self.client_api.as_ref() {
|
|
|
|
|
Some(client_api) => {
|
|
|
|
|
let client_api = client_api.clone(); // Clone to avoid borrowing issues
|
2025-10-22 11:31:21 -07:00
|
|
|
let upstream_api =
|
|
|
|
|
provider_id.compatible_api_for_client(&client_api, self.streaming_response);
|
|
|
|
|
|
|
|
|
|
// Check if this is Bedrock binary stream
|
|
|
|
|
if matches!(
|
|
|
|
|
upstream_api,
|
|
|
|
|
SupportedUpstreamAPIs::AmazonBedrockConverseStream(_)
|
|
|
|
|
) {
|
|
|
|
|
return self.handle_bedrock_binary_stream(body, &client_api, &upstream_api);
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2025-12-18 11:02:59 -08:00
|
|
|
// Initialize SSE chunk processor if not present
|
|
|
|
|
if self.sse_chunk_processor.is_none() {
|
|
|
|
|
self.sse_chunk_processor = Some(SseChunkProcessor::new());
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Initialize SSE buffer if not present
|
|
|
|
|
if self.sse_buffer.is_none() {
|
|
|
|
|
self.sse_buffer = match SseStreamBuffer::try_from((&client_api, &upstream_api))
|
|
|
|
|
{
|
|
|
|
|
Ok(buffer) => Some(buffer),
|
|
|
|
|
Err(e) => {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: failed to create sse buffer: {}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
2025-12-03 14:58:26 -08:00
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2025-12-18 11:02:59 -08:00
|
|
|
// Process chunk through SSE processor (handles incomplete events)
|
|
|
|
|
let transformed_events = match self.sse_chunk_processor.as_mut() {
|
|
|
|
|
Some(processor) => {
|
|
|
|
|
let result = processor.process_chunk(body, &client_api, &upstream_api);
|
|
|
|
|
let has_buffered = processor.has_buffered_data();
|
|
|
|
|
let buffered_size = processor.buffered_size();
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(events) => {
|
|
|
|
|
if has_buffered {
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: sse incomplete buffered, {} bytes buffered for next chunk",
|
2025-12-18 11:02:59 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
buffered_size
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
events
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
Err(e) => {
|
2025-12-18 11:02:59 -08:00
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: sse chunk process error: {}",
|
2025-12-18 11:02:59 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
2025-12-18 11:02:59 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: sse chunk processor unexpectedly missing",
|
|
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
2025-12-18 11:02:59 -08:00
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2025-12-18 11:02:59 -08:00
|
|
|
// Process each successfully transformed SSE event
|
|
|
|
|
for transformed_event in transformed_events {
|
2025-09-10 07:40:30 -07:00
|
|
|
// Extract ProviderStreamResponse for processing (token counting, etc.)
|
2025-10-24 14:07:05 -07:00
|
|
|
if !transformed_event.is_done() && !transformed_event.is_event_only() {
|
2025-09-10 07:40:30 -07:00
|
|
|
match transformed_event.provider_response() {
|
|
|
|
|
Ok(provider_response) => {
|
|
|
|
|
self.record_ttft_if_needed();
|
|
|
|
|
|
|
|
|
|
if provider_response.is_final() {
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: streaming final chunk, total_tokens={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
self.response_tokens
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(content) = provider_response.content_delta() {
|
|
|
|
|
let estimated_tokens = content.len() / 4;
|
|
|
|
|
self.response_tokens += estimated_tokens.max(1);
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: streaming token update, delta_chars={} estimated_tokens={} total_tokens={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
content.len(),
|
|
|
|
|
estimated_tokens.max(1),
|
|
|
|
|
self.response_tokens
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: streaming chunk error: {}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Add transformed event to buffer (buffer may inject lifecycle events)
|
|
|
|
|
if let Some(buffer) = self.sse_buffer.as_mut() {
|
|
|
|
|
buffer.add_transformed_event(transformed_event);
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Get accumulated bytes from buffer and return
|
|
|
|
|
match self.sse_buffer.as_mut() {
|
|
|
|
|
Some(buffer) => {
|
2025-12-25 21:08:37 -08:00
|
|
|
let bytes = buffer.to_bytes();
|
2025-12-03 14:58:26 -08:00
|
|
|
if !bytes.is_empty() {
|
|
|
|
|
let content = String::from_utf8_lossy(&bytes);
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream transformed client response, size={} content={}",
|
2025-12-03 14:58:26 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
bytes.len(),
|
|
|
|
|
content
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
}
|
|
|
|
|
None => {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: sse buffer unexpectedly missing after initialization",
|
|
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
2025-12-03 14:58:26 -08:00
|
|
|
Err(Action::Continue)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
None => {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: missing client_api for non-streaming response",
|
|
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
Err(Action::Continue)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-22 11:31:21 -07:00
|
|
|
fn handle_bedrock_binary_stream(
|
|
|
|
|
&mut self,
|
|
|
|
|
body: &[u8],
|
2025-12-03 14:58:26 -08:00
|
|
|
client_api: &SupportedAPIsFromClient,
|
2025-10-22 11:31:21 -07:00
|
|
|
upstream_api: &SupportedUpstreamAPIs,
|
|
|
|
|
) -> Result<Vec<u8>, Action> {
|
|
|
|
|
// Initialize decoder if not present
|
|
|
|
|
if self.binary_frame_decoder.is_none() {
|
|
|
|
|
self.binary_frame_decoder = Some(BedrockBinaryFrameDecoder::from_bytes(&[]));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Initialize SSE buffer if not present
|
|
|
|
|
if self.sse_buffer.is_none() {
|
|
|
|
|
self.sse_buffer = match SseStreamBuffer::try_from((client_api, upstream_api)) {
|
|
|
|
|
Ok(buffer) => Some(buffer),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: bedrock buffer init error: {}",
|
2025-12-03 14:58:26 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add incoming bytes to decoder buffer
|
2025-10-22 11:31:21 -07:00
|
|
|
let decoder = self.binary_frame_decoder.as_mut().unwrap();
|
|
|
|
|
decoder.buffer_mut().extend_from_slice(body);
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Process all complete frames
|
2025-10-22 11:31:21 -07:00
|
|
|
loop {
|
|
|
|
|
let decoded_frame = self.binary_frame_decoder.as_mut().unwrap().decode_frame();
|
|
|
|
|
match decoded_frame {
|
|
|
|
|
Some(DecodedFrame::Complete(ref frame_ref)) => {
|
|
|
|
|
let frame = DecodedFrame::Complete(frame_ref.clone());
|
2025-12-03 14:58:26 -08:00
|
|
|
|
|
|
|
|
// Convert frame to provider response type
|
2025-10-22 11:31:21 -07:00
|
|
|
match ProviderStreamResponseType::try_from((&frame, client_api, upstream_api)) {
|
|
|
|
|
Ok(provider_response) => {
|
|
|
|
|
self.record_ttft_if_needed();
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Track token usage
|
|
|
|
|
if let Some(content) = provider_response.content_delta() {
|
|
|
|
|
let estimated_tokens = content.len() / 4;
|
|
|
|
|
self.response_tokens += estimated_tokens.max(1);
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: bedrock token update, delta_chars={} estimated_tokens={} total_tokens={}",
|
2025-12-03 14:58:26 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
content.len(),
|
|
|
|
|
estimated_tokens.max(1),
|
|
|
|
|
self.response_tokens
|
|
|
|
|
);
|
2025-10-22 11:31:21 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Create SseEvent from provider response
|
|
|
|
|
let event = SseEvent::from_provider_response(provider_response);
|
|
|
|
|
|
|
|
|
|
// Add to buffer (buffer handles all shim logic including ContentBlockStart injection)
|
|
|
|
|
if let Some(buffer) = self.sse_buffer.as_mut() {
|
|
|
|
|
buffer.add_transformed_event(event);
|
|
|
|
|
}
|
2025-10-22 11:31:21 -07:00
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: bedrock frame conversion error: {}",
|
2025-10-22 11:31:21 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some(DecodedFrame::Incomplete) => {
|
|
|
|
|
// Incomplete frame - buffer retains partial data, wait for more bytes
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: bedrock incomplete frame, waiting for more data",
|
2025-10-22 11:31:21 -07:00
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
// Decode error
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: bedrock decode error",
|
2025-10-22 11:31:21 -07:00
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 14:58:26 -08:00
|
|
|
// Get accumulated bytes from buffer and return
|
|
|
|
|
match self.sse_buffer.as_mut() {
|
|
|
|
|
Some(buffer) => {
|
2025-12-25 21:08:37 -08:00
|
|
|
let bytes = buffer.to_bytes();
|
2025-12-03 14:58:26 -08:00
|
|
|
if !bytes.is_empty() {
|
|
|
|
|
let content = String::from_utf8_lossy(&bytes);
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream transformed client response, size={} content={}",
|
2025-12-03 14:58:26 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
bytes.len(),
|
|
|
|
|
content
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: bedrock buffer missing",
|
2025-12-03 14:58:26 -08:00
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
Err(Action::Continue)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-22 11:31:21 -07:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
fn handle_non_streaming_response(
|
|
|
|
|
&mut self,
|
|
|
|
|
body: &[u8],
|
|
|
|
|
provider_id: ProviderId,
|
|
|
|
|
) -> Result<Vec<u8>, Action> {
|
2025-09-30 18:46:13 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: non-streaming process, provider_id={:?} body_size={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
provider_id,
|
|
|
|
|
body.len()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response: ProviderResponseType = match self.client_api.as_ref() {
|
|
|
|
|
Some(client_api) => {
|
|
|
|
|
match ProviderResponseType::try_from((body, client_api, &provider_id)) {
|
|
|
|
|
Ok(response) => response,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream response parse error: {} | body: {}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
e,
|
|
|
|
|
String::from_utf8_lossy(body)
|
|
|
|
|
);
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError(format!("Response parsing error: {}", e)),
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream response error, missing client_api",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
return Err(Action::Continue);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Use provider interface to extract usage information
|
|
|
|
|
if let Some((prompt_tokens, completion_tokens, total_tokens)) =
|
|
|
|
|
response.extract_usage_counts()
|
|
|
|
|
{
|
2025-09-30 18:46:13 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: response usage, prompt_tokens={} completion_tokens={} total_tokens={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
prompt_tokens,
|
|
|
|
|
completion_tokens,
|
|
|
|
|
total_tokens
|
|
|
|
|
);
|
|
|
|
|
self.response_tokens = completion_tokens;
|
|
|
|
|
} else {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: response usage, no usage information found",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
// Serialize the normalized response back to JSON bytes
|
|
|
|
|
match serde_json::to_vec(&response) {
|
|
|
|
|
Ok(bytes) => {
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: client response payload: {}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
String::from_utf8_lossy(&bytes)
|
|
|
|
|
);
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2026-02-09 13:33:27 -08:00
|
|
|
warn!(
|
|
|
|
|
"request_id={}: failed to serialize normalized response: {}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError(format!("Response serialization error: {}", e)),
|
|
|
|
|
Some(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
);
|
|
|
|
|
Err(Action::Continue)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HttpContext is the trait that allows the Rust code to interact with HTTP objects.
|
2024-10-18 12:53:44 -07:00
|
|
|
impl HttpContext for StreamContext {
|
2024-10-17 10:16:40 -07:00
|
|
|
// Envoy's HTTP model is event driven. The WASM ABI has given implementors events to hook onto
|
|
|
|
|
// the lifecycle of the http request and response.
|
|
|
|
|
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
|
2025-03-03 13:11:57 -08:00
|
|
|
let request_path = self.get_http_request_header(":path").unwrap_or_default();
|
|
|
|
|
if request_path == HEALTHZ_PATH {
|
|
|
|
|
self.send_http_response(200, vec![], None);
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 14:07:05 -07:00
|
|
|
// Capture HTTP method and protocol for tracing
|
|
|
|
|
self.http_method = self.get_http_request_header(":method");
|
|
|
|
|
self.http_protocol = self.get_http_request_header(":scheme");
|
|
|
|
|
|
2025-10-22 11:31:21 -07:00
|
|
|
self.streaming_response = self
|
|
|
|
|
.get_http_request_header(ARCH_IS_STREAMING_HEADER)
|
|
|
|
|
.map(|val| val == "true")
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
2025-12-22 18:05:49 -08:00
|
|
|
// let routing_header_value = self.get_http_request_header(ARCH_ROUTING_HEADER);
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2026-01-28 17:47:33 -08:00
|
|
|
if let Err(err) = self.select_llm_provider() {
|
|
|
|
|
self.send_http_response(
|
|
|
|
|
400,
|
|
|
|
|
vec![],
|
|
|
|
|
Some(format!(r#"{{"error": "{}"}}"#, err).as_bytes()),
|
|
|
|
|
);
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 18:05:49 -08:00
|
|
|
// Check if this is a supported API endpoint
|
|
|
|
|
if SupportedAPIsFromClient::from_endpoint(&request_path).is_none() {
|
|
|
|
|
self.send_http_response(404, vec![], Some(b"Unsupported endpoint"));
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2025-12-22 18:05:49 -08:00
|
|
|
// Get the SupportedApi for routing decisions
|
|
|
|
|
let supported_api: Option<SupportedAPIsFromClient> =
|
|
|
|
|
SupportedAPIsFromClient::from_endpoint(&request_path);
|
|
|
|
|
self.client_api = supported_api;
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2025-12-22 18:05:49 -08:00
|
|
|
// Debug: log provider, client API, resolved API, and request path
|
|
|
|
|
if let (Some(api), Some(provider)) = (self.client_api.as_ref(), self.llm_provider.as_ref())
|
|
|
|
|
{
|
|
|
|
|
let provider_id = provider.to_provider_id();
|
|
|
|
|
self.resolved_api =
|
|
|
|
|
Some(provider_id.compatible_api_for_client(api, self.streaming_response));
|
2025-10-22 11:31:21 -07:00
|
|
|
|
2025-12-22 18:05:49 -08:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: routing info, provider='{}' client_api={:?} resolved_api={:?} request_path='{}'",
|
2025-12-22 18:05:49 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
provider.to_provider_id(),
|
|
|
|
|
api,
|
|
|
|
|
self.resolved_api,
|
|
|
|
|
request_path
|
|
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
|
|
|
|
|
//We need to update the upstream path if there is a variation for a provider like Gemini/Groq, etc.
|
|
|
|
|
self.update_upstream_path(&request_path);
|
|
|
|
|
|
2026-01-15 00:06:28 +01:00
|
|
|
// Clone cluster_name to avoid borrowing self while calling add_http_request_header (which requires mut self)
|
|
|
|
|
let cluster_name_opt = self.llm_provider().cluster_name.clone();
|
|
|
|
|
|
|
|
|
|
if let Some(cluster_name) = cluster_name_opt {
|
|
|
|
|
self.add_http_request_header(ARCH_ROUTING_HEADER, &cluster_name);
|
2025-03-26 11:01:32 -07:00
|
|
|
} else {
|
|
|
|
|
self.add_http_request_header(
|
|
|
|
|
ARCH_ROUTING_HEADER,
|
|
|
|
|
&self.llm_provider().provider_interface.to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-03-19 15:21:34 -07:00
|
|
|
if let Err(error) = self.modify_auth_headers() {
|
|
|
|
|
// ensure that the provider has an endpoint if the access key is missing else return a bad request
|
2025-07-08 00:33:40 -07:00
|
|
|
if self.llm_provider.as_ref().unwrap().endpoint.is_none()
|
|
|
|
|
&& self.llm_provider.as_ref().unwrap().provider_interface
|
2026-03-15 09:36:11 -07:00
|
|
|
!= LlmProviderType::Plano
|
2025-03-19 15:21:34 -07:00
|
|
|
{
|
|
|
|
|
self.send_server_error(error, Some(StatusCode::BAD_REQUEST));
|
|
|
|
|
}
|
2025-01-17 18:25:55 -08:00
|
|
|
}
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
2025-03-19 15:21:34 -07:00
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
self.delete_content_length_header();
|
|
|
|
|
self.save_ratelimit_header();
|
|
|
|
|
|
|
|
|
|
self.request_id = self.get_http_request_header(REQUEST_ID_HEADER);
|
2024-11-15 10:44:01 -08:00
|
|
|
self.traceparent = self.get_http_request_header(TRACE_PARENT_HEADER);
|
2024-11-12 15:03:26 -08:00
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
Action::Continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
2025-03-27 10:40:20 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: request body chunk, bytes={} end_stream={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
body_size,
|
|
|
|
|
end_of_stream
|
2025-03-27 10:40:20 -07:00
|
|
|
);
|
|
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
// Let the client send the gateway all the data before sending to the LLM_provider.
|
|
|
|
|
// TODO: consider a streaming API.
|
2024-11-17 17:01:19 -08:00
|
|
|
|
|
|
|
|
if self.request_body_sent_time.is_none() {
|
2024-11-18 17:55:39 -08:00
|
|
|
self.request_body_sent_time = Some(current_time_ns());
|
2024-11-17 17:01:19 -08:00
|
|
|
}
|
|
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
if !end_of_stream {
|
|
|
|
|
return Action::Pause;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if body_size == 0 {
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 15:21:34 -07:00
|
|
|
let body_bytes = match self.get_http_request_body(0, body_size) {
|
|
|
|
|
Some(body_bytes) => body_bytes,
|
|
|
|
|
None => {
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError(format!(
|
|
|
|
|
"Failed to obtain body bytes even though body_size is {}",
|
|
|
|
|
body_size
|
|
|
|
|
)),
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
return Action::Pause;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
//We need to deserialize the request body based on the resolved API
|
|
|
|
|
let mut deserialized_client_request: ProviderRequestType = match self.client_api.as_ref() {
|
|
|
|
|
Some(the_client_api) => {
|
2025-10-22 11:31:21 -07:00
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: client request received, api={:?} body_size={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
the_client_api,
|
|
|
|
|
body_bytes.len()
|
|
|
|
|
);
|
2024-10-17 10:16:40 -07:00
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: client request payload: {}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
String::from_utf8_lossy(&body_bytes)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
match ProviderRequestType::try_from((&body_bytes[..], the_client_api)) {
|
|
|
|
|
Ok(deserialized) => deserialized,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: client request parse error: {} | body: {}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
e,
|
|
|
|
|
String::from_utf8_lossy(&body_bytes)
|
|
|
|
|
);
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError(format!("Request parsing error: {}", e)),
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Action::Pause;
|
|
|
|
|
}
|
2025-08-20 12:55:29 -07:00
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError("No resolved API for provider".to_string()),
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Action::Pause;
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-15 10:44:01 -08:00
|
|
|
|
2025-03-19 15:21:34 -07:00
|
|
|
let model_name = match self.llm_provider.as_ref() {
|
2026-01-08 15:11:05 -08:00
|
|
|
Some(llm_provider) => llm_provider.model.clone(),
|
2025-03-21 15:56:17 -07:00
|
|
|
None => None,
|
2025-03-19 15:21:34 -07:00
|
|
|
};
|
|
|
|
|
|
2025-08-20 12:55:29 -07:00
|
|
|
// Store the original model for logging
|
2025-09-10 07:40:30 -07:00
|
|
|
let model_requested = deserialized_client_request.model().to_string();
|
2025-08-20 12:55:29 -07:00
|
|
|
|
|
|
|
|
// Apply model name resolution logic using the trait method
|
|
|
|
|
let resolved_model = match model_name {
|
2026-01-08 15:11:05 -08:00
|
|
|
Some(model_name) => model_name,
|
2025-05-19 09:59:22 -07:00
|
|
|
None => {
|
2025-12-22 18:05:49 -08:00
|
|
|
warn!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: model resolution error, no model specified | req_model='{}' provider='{}' config_model={:?}",
|
2025-12-22 18:05:49 -08:00
|
|
|
self.request_identifier(),
|
|
|
|
|
model_requested,
|
|
|
|
|
self.llm_provider().name,
|
|
|
|
|
self.llm_provider().model
|
|
|
|
|
);
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::BadRequest {
|
|
|
|
|
why: format!(
|
Rename all arch references to plano (#745)
* Rename all arch references to plano across the codebase
Complete rebrand from "Arch"/"archgw" to "Plano" including:
- Config files: arch_config_schema.yaml, workflow, demo configs
- Environment variables: ARCH_CONFIG_* → PLANO_CONFIG_*
- Python CLI: variables, functions, file paths, docker mounts
- Rust crates: config paths, log messages, metadata keys
- Docker/build: Dockerfile, supervisord, .dockerignore, .gitignore
- Docker Compose: volume mounts and env vars across all demos/tests
- GitHub workflows: job/step names
- Shell scripts: log messages
- Demos: Python code, READMEs, VS Code configs, Grafana dashboard
- Docs: RST includes, code comments, config references
- Package metadata: package.json, pyproject.toml, uv.lock
External URLs (docs.archgw.com, github.com/katanemo/archgw) left as-is.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update remaining arch references in docs
- Rename RST cross-reference labels: arch_access_logging, arch_overview_tracing, arch_overview_threading → plano_*
- Update label references in request_lifecycle.rst
- Rename arch_config_state_storage_example.yaml → plano_config_state_storage_example.yaml
- Update config YAML comments: "Arch creates/uses" → "Plano creates/uses"
- Update "the Arch gateway" → "the Plano gateway" in configuration_reference.rst
- Update arch_config_schema.yaml reference in provider_models.py
- Rename arch_agent_router → plano_agent_router in config example
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix remaining arch references found in second pass
- config/docker-compose.dev.yaml: ARCH_CONFIG_FILE → PLANO_CONFIG_FILE,
arch_config.yaml → plano_config.yaml, archgw_logs → plano_logs
- config/test_passthrough.yaml: container mount path
- tests/e2e/docker-compose.yaml: source file path (was still arch_config.yaml)
- cli/planoai/core.py: comment and log message
- crates/brightstaff/src/tracing/constants.rs: doc comment
- tests/{e2e,archgw}/common.py: get_arch_messages → get_plano_messages,
arch_state/arch_messages variables renamed
- tests/{e2e,archgw}/test_prompt_gateway.py: updated imports and usages
- demos/shared/test_runner/{common,test_demos}.py: same renames
- tests/e2e/test_model_alias_routing.py: docstring
- .dockerignore: archgw_modelserver → plano_modelserver
- demos/use_cases/claude_code_router/pretty_model_resolution.sh: container name
Note: x-arch-* HTTP header values and Rust constant names intentionally
preserved for backwards compatibility with existing deployments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 15:16:56 -08:00
|
|
|
"No model specified in request and couldn't determine model name from plano_config. Model name in req: {}, plano_config, provider: {}, model: {:?}",
|
2025-12-22 18:05:49 -08:00
|
|
|
model_requested,
|
|
|
|
|
self.llm_provider().name,
|
|
|
|
|
self.llm_provider().model
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Action::Continue;
|
2025-03-21 15:56:17 -07:00
|
|
|
}
|
2025-05-19 09:59:22 -07:00
|
|
|
};
|
2024-10-17 10:16:40 -07:00
|
|
|
|
2025-08-20 12:55:29 -07:00
|
|
|
// Set the resolved model using the trait method
|
2025-09-10 07:40:30 -07:00
|
|
|
deserialized_client_request.set_model(resolved_model.clone());
|
2025-08-20 12:55:29 -07:00
|
|
|
|
|
|
|
|
// Extract user message for tracing
|
2025-09-10 07:40:30 -07:00
|
|
|
self.user_message = deserialized_client_request.get_recent_user_message();
|
2025-08-20 12:55:29 -07:00
|
|
|
|
2025-10-04 17:06:05 -07:00
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: model resolved, req_model='{}' -> resolved_model='{}' provider='{}' streaming={}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
2025-03-21 15:56:17 -07:00
|
|
|
model_requested,
|
2025-09-10 07:40:30 -07:00
|
|
|
resolved_model,
|
|
|
|
|
self.llm_provider().name,
|
|
|
|
|
deserialized_client_request.is_streaming()
|
2024-10-28 20:05:06 -04:00
|
|
|
);
|
|
|
|
|
|
2025-08-20 12:55:29 -07:00
|
|
|
// Use provider interface for streaming detection and setup
|
2025-10-22 11:31:21 -07:00
|
|
|
// If streaming_response is not already set from headers, get it from the parsed request
|
|
|
|
|
if !self.streaming_response {
|
|
|
|
|
self.streaming_response = deserialized_client_request.is_streaming();
|
|
|
|
|
}
|
2024-10-28 20:05:06 -04:00
|
|
|
|
2025-08-20 12:55:29 -07:00
|
|
|
// Use provider interface for text extraction (after potential mutation)
|
2025-09-10 07:40:30 -07:00
|
|
|
let input_tokens_str = deserialized_client_request.extract_messages_text();
|
2024-10-17 10:16:40 -07:00
|
|
|
// enforce ratelimits on ingress
|
2025-08-20 12:55:29 -07:00
|
|
|
if let Err(e) = self.enforce_ratelimits(&resolved_model, input_tokens_str.as_str()) {
|
2024-10-17 10:16:40 -07:00
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::ExceededRatelimit(e),
|
|
|
|
|
Some(StatusCode::TOO_MANY_REQUESTS),
|
|
|
|
|
);
|
|
|
|
|
self.metrics.ratelimited_rq.increment(1);
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 12:55:29 -07:00
|
|
|
// Convert chat completion request to llm provider specific request using provider interface
|
2026-02-09 13:33:27 -08:00
|
|
|
let serialized_body_bytes_upstream = match self.resolved_api.as_ref() {
|
|
|
|
|
Some(upstream) => {
|
|
|
|
|
info!(
|
|
|
|
|
"request_id={}: upstream transform, client_api={:?} -> upstream_api={:?}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
self.client_api,
|
|
|
|
|
upstream
|
2025-08-20 12:55:29 -07:00
|
|
|
);
|
2025-06-10 12:53:27 -07:00
|
|
|
|
2026-02-09 13:33:27 -08:00
|
|
|
match ProviderRequestType::try_from((deserialized_client_request, upstream)) {
|
2026-03-10 20:54:14 -07:00
|
|
|
Ok(mut request) => {
|
2026-04-23 15:34:44 -07:00
|
|
|
if let Err(e) =
|
|
|
|
|
request.normalize_for_upstream(self.get_provider_id(), upstream)
|
|
|
|
|
{
|
|
|
|
|
warn!(
|
|
|
|
|
"request_id={}: normalize_for_upstream failed: {}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError(e.message),
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Action::Pause;
|
|
|
|
|
}
|
2026-02-09 13:33:27 -08:00
|
|
|
debug!(
|
|
|
|
|
"request_id={}: upstream request payload: {}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
String::from_utf8_lossy(&request.to_bytes().unwrap_or_default())
|
|
|
|
|
);
|
2025-09-10 07:40:30 -07:00
|
|
|
|
2026-02-09 13:33:27 -08:00
|
|
|
match request.to_bytes() {
|
|
|
|
|
Ok(bytes) => bytes,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
|
|
|
|
"request_id={}: failed to serialize request body: {}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError(format!(
|
|
|
|
|
"Request serialization error: {}",
|
|
|
|
|
e
|
|
|
|
|
)),
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Action::Pause;
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 13:33:27 -08:00
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
|
|
|
|
"request_id={}: failed to create provider request: {}",
|
|
|
|
|
self.request_identifier(),
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError(format!("Provider request error: {}", e)),
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Action::Pause;
|
2025-09-10 07:40:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 13:33:27 -08:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
warn!(
|
|
|
|
|
"request_id={}: no upstream api resolved",
|
|
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
|
|
|
|
self.send_server_error(
|
|
|
|
|
ServerError::LogicError("No upstream API resolved".into()),
|
|
|
|
|
Some(StatusCode::BAD_REQUEST),
|
|
|
|
|
);
|
|
|
|
|
return Action::Pause;
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-10-17 10:16:40 -07:00
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
self.set_http_request_body(0, body_size, &serialized_body_bytes_upstream);
|
2024-10-17 10:16:40 -07:00
|
|
|
Action::Continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
fn on_http_response_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
|
2025-09-25 17:00:37 -07:00
|
|
|
// Capture the upstream response status code to handle errors appropriately
|
|
|
|
|
if let Some(status_str) = self.get_http_response_header(":status") {
|
|
|
|
|
if let Ok(status_code) = status_str.parse::<u16>() {
|
|
|
|
|
self.upstream_status_code = StatusCode::from_u16(status_code).ok();
|
|
|
|
|
|
2025-09-30 18:46:13 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream response status: {}",
|
2025-09-25 17:00:37 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
status_code
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
self.remove_http_response_header("content-length");
|
|
|
|
|
self.remove_http_response_header("content-encoding");
|
2024-11-18 17:55:39 -08:00
|
|
|
|
|
|
|
|
self.set_property(
|
|
|
|
|
vec!["metadata", "filter_metadata", "llm_filter", "user_prompt"],
|
|
|
|
|
Some("hello world from filter".as_bytes()),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Action::Continue
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
2025-03-27 10:40:20 -07:00
|
|
|
if self.request_body_sent_time.is_none() {
|
2026-02-09 13:33:27 -08:00
|
|
|
debug!(
|
|
|
|
|
"request_id={}: request body not sent, skipping processing in llm filter",
|
|
|
|
|
self.request_identifier()
|
|
|
|
|
);
|
2025-03-27 10:40:20 -07:00
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 14:07:05 -07:00
|
|
|
let current_time = get_current_time().unwrap();
|
|
|
|
|
if end_of_stream && body_size == 0 {
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: response body complete, total_bytes={}",
|
2025-10-24 14:07:05 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
body_size
|
|
|
|
|
);
|
|
|
|
|
self.handle_end_of_request_metrics_and_traces(current_time);
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 17:00:37 -07:00
|
|
|
// Check if this is an error response from upstream
|
|
|
|
|
if let Some(status_code) = &self.upstream_status_code {
|
|
|
|
|
if status_code.is_client_error() || status_code.is_server_error() {
|
|
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream error response, status={} body_size={}",
|
2025-09-25 17:00:37 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
status_code.as_u16(),
|
|
|
|
|
body_size
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// For error responses, forward the upstream error directly without parsing
|
|
|
|
|
if body_size > 0 {
|
|
|
|
|
if let Ok(body) = self.read_raw_response_body(body_size) {
|
|
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream error body: {}",
|
2025-09-25 17:00:37 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
String::from_utf8_lossy(&body)
|
|
|
|
|
);
|
|
|
|
|
// Forward the error response as-is
|
|
|
|
|
self.set_http_response_body(0, body_size, &body);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
match self.client_api {
|
2025-12-03 14:58:26 -08:00
|
|
|
Some(SupportedAPIsFromClient::OpenAIChatCompletions(_)) => {}
|
|
|
|
|
Some(SupportedAPIsFromClient::AnthropicMessagesAPI(_)) => {}
|
|
|
|
|
Some(SupportedAPIsFromClient::OpenAIResponsesAPI(_)) => {}
|
2025-09-10 07:40:30 -07:00
|
|
|
_ => {
|
|
|
|
|
let api_info = match &self.client_api {
|
|
|
|
|
Some(api) => format!("{}", api),
|
|
|
|
|
None => "None".to_string(),
|
|
|
|
|
};
|
|
|
|
|
info!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: unsupported api: {}",
|
2025-09-10 07:40:30 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
api_info
|
|
|
|
|
);
|
|
|
|
|
return Action::Continue;
|
|
|
|
|
}
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
let body = match self.read_raw_response_body(body_size) {
|
|
|
|
|
Ok(bytes) => bytes,
|
|
|
|
|
Err(action) => return action,
|
2024-10-28 20:05:06 -04:00
|
|
|
};
|
|
|
|
|
|
2025-09-29 19:23:08 -07:00
|
|
|
debug!(
|
2026-02-09 13:33:27 -08:00
|
|
|
"request_id={}: upstream raw response, body_size={} content={}",
|
2025-09-29 19:23:08 -07:00
|
|
|
self.request_identifier(),
|
|
|
|
|
body.len(),
|
|
|
|
|
String::from_utf8_lossy(&body)
|
|
|
|
|
);
|
2025-06-11 15:15:00 -07:00
|
|
|
|
2025-09-10 07:40:30 -07:00
|
|
|
let provider_id = self.get_provider_id();
|
2024-10-28 20:05:06 -04:00
|
|
|
if self.streaming_response {
|
2025-09-10 07:40:30 -07:00
|
|
|
match self.handle_streaming_response(&body, provider_id) {
|
|
|
|
|
Ok(serialized_body) => {
|
|
|
|
|
self.set_http_response_body(0, body_size, &serialized_body);
|
2024-11-12 15:03:26 -08:00
|
|
|
}
|
2025-09-10 07:40:30 -07:00
|
|
|
Err(action) => return action,
|
2024-11-12 15:03:26 -08:00
|
|
|
}
|
2024-10-17 10:16:40 -07:00
|
|
|
} else {
|
2025-09-10 07:40:30 -07:00
|
|
|
match self.handle_non_streaming_response(&body, provider_id) {
|
|
|
|
|
Ok(serialized_body) => {
|
|
|
|
|
self.set_http_response_body(0, body_size, &serialized_body);
|
|
|
|
|
}
|
|
|
|
|
Err(action) => return action,
|
2024-10-17 10:16:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 14:07:05 -07:00
|
|
|
|
2024-10-17 10:16:40 -07:00
|
|
|
Action::Continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-18 17:55:39 -08:00
|
|
|
fn current_time_ns() -> u128 {
|
|
|
|
|
SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.as_nanos()
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-18 12:53:44 -07:00
|
|
|
impl Context for StreamContext {}
|
2026-04-17 17:23:05 -07:00
|
|
|
|
|
|
|
|
/// Extract the credential a client sent in either an OpenAI-style
|
|
|
|
|
/// `Authorization` header or an Anthropic-style `x-api-key` header.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `None` when neither header is present or both are empty/whitespace.
|
|
|
|
|
/// The `Bearer ` prefix on the `Authorization` value is stripped if present;
|
|
|
|
|
/// otherwise the value is taken verbatim (some clients send a raw token).
|
|
|
|
|
fn extract_client_credential(
|
|
|
|
|
authorization: Option<&str>,
|
|
|
|
|
x_api_key: Option<&str>,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
// Strip the optional "Bearer " / "Bearer" prefix (case-sensitive, matches
|
|
|
|
|
// OpenAI SDK behavior) and trim surrounding whitespace before validating
|
|
|
|
|
// non-empty.
|
|
|
|
|
let from_authorization = authorization
|
|
|
|
|
.map(|v| {
|
|
|
|
|
v.strip_prefix("Bearer ")
|
|
|
|
|
.or_else(|| v.strip_prefix("Bearer"))
|
|
|
|
|
.unwrap_or(v)
|
|
|
|
|
.trim()
|
|
|
|
|
.to_string()
|
|
|
|
|
})
|
|
|
|
|
.filter(|s| !s.is_empty());
|
|
|
|
|
if from_authorization.is_some() {
|
|
|
|
|
return from_authorization;
|
|
|
|
|
}
|
|
|
|
|
x_api_key
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::extract_client_credential;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn authorization_bearer_strips_prefix() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
extract_client_credential(Some("Bearer sk-abc"), None),
|
|
|
|
|
Some("sk-abc".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn authorization_raw_token_preserved() {
|
|
|
|
|
// Some clients send the raw token without "Bearer " — accept it.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
extract_client_credential(Some("sk-abc"), None),
|
|
|
|
|
Some("sk-abc".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn x_api_key_used_when_authorization_absent() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
extract_client_credential(None, Some("sk-ant-api-key")),
|
|
|
|
|
Some("sk-ant-api-key".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn authorization_wins_when_both_present() {
|
|
|
|
|
// If a client is particularly exotic and sends both, prefer the
|
|
|
|
|
// OpenAI-style Authorization header.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
extract_client_credential(Some("Bearer openai-key"), Some("anthropic-key")),
|
|
|
|
|
Some("openai-key".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn returns_none_when_neither_present() {
|
|
|
|
|
assert!(extract_client_credential(None, None).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn empty_and_whitespace_headers_are_ignored() {
|
|
|
|
|
assert!(extract_client_credential(Some(""), None).is_none());
|
|
|
|
|
assert!(extract_client_credential(Some("Bearer "), None).is_none());
|
|
|
|
|
assert!(extract_client_credential(Some(" "), Some(" ")).is_none());
|
|
|
|
|
}
|
|
|
|
|
}
|