mirror of
https://github.com/katanemo/plano.git
synced 2026-07-14 16:22:12 +02:00
update code in both llm and gateway filter
This commit is contained in:
parent
3bd2ffe9fb
commit
cd315e3c27
10 changed files with 573 additions and 2160 deletions
|
|
@ -45,34 +45,12 @@ static_resources:
|
||||||
domains:
|
domains:
|
||||||
- "*"
|
- "*"
|
||||||
routes:
|
routes:
|
||||||
{% for provider in arch_llm_providers %}
|
|
||||||
- match:
|
- match:
|
||||||
prefix: "/"
|
prefix: "/"
|
||||||
headers:
|
|
||||||
- name: "x-arch-llm-provider"
|
|
||||||
string_match:
|
|
||||||
exact: {{ provider.name }}
|
|
||||||
route:
|
|
||||||
auto_host_rewrite: true
|
|
||||||
cluster: {{ provider.provider }}
|
|
||||||
timeout: 60s
|
|
||||||
{% endfor %}
|
|
||||||
- match:
|
|
||||||
prefix: "/"
|
|
||||||
headers:
|
|
||||||
- name: "x-arch-upstream"
|
|
||||||
string_match:
|
|
||||||
exact: arch_llm_listener
|
|
||||||
route:
|
route:
|
||||||
auto_host_rewrite: true
|
auto_host_rewrite: true
|
||||||
cluster: arch_llm_listener
|
cluster: arch_llm_listener
|
||||||
timeout: 60s
|
timeout: 60s
|
||||||
- match:
|
|
||||||
prefix: "/"
|
|
||||||
direct_response:
|
|
||||||
status: 400
|
|
||||||
body:
|
|
||||||
inline_string: "x-arch-llm-provider or x-arch-upstream header not set, cannot perform routing\n"
|
|
||||||
http_filters:
|
http_filters:
|
||||||
- name: envoy.filters.http.wasm
|
- name: envoy.filters.http.wasm
|
||||||
typed_config:
|
typed_config:
|
||||||
|
|
@ -232,7 +210,7 @@ static_resources:
|
||||||
direct_response:
|
direct_response:
|
||||||
status: 400
|
status: 400
|
||||||
body:
|
body:
|
||||||
inline_string: "x-arch-llm-provider header not set, cannot perform routing\n"
|
inline_string: "x-arch-llm-provider header not set, llm gateway cannot perform routing\n"
|
||||||
http_filters:
|
http_filters:
|
||||||
- name: envoy.filters.http.wasm
|
- name: envoy.filters.http.wasm
|
||||||
typed_config:
|
typed_config:
|
||||||
|
|
@ -250,7 +228,7 @@ static_resources:
|
||||||
runtime: "envoy.wasm.runtime.v8"
|
runtime: "envoy.wasm.runtime.v8"
|
||||||
code:
|
code:
|
||||||
local:
|
local:
|
||||||
filename: "/etc/envoy/proxy-wasm-plugins/prompt_gateway.wasm"
|
filename: "/etc/envoy/proxy-wasm-plugins/llm_gateway.wasm"
|
||||||
- name: envoy.filters.http.router
|
- name: envoy.filters.http.router
|
||||||
typed_config:
|
typed_config:
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use filter_context::FilterContext;
|
use llm_filter_context::LlmGatewayFilterContext;
|
||||||
use proxy_wasm::traits::*;
|
use proxy_wasm::traits::*;
|
||||||
use proxy_wasm::types::*;
|
use proxy_wasm::types::*;
|
||||||
|
|
||||||
mod filter_context;
|
mod llm_filter_context;
|
||||||
mod stream_context;
|
mod llm_stream_context;
|
||||||
|
|
||||||
proxy_wasm::main! {{
|
proxy_wasm::main! {{
|
||||||
proxy_wasm::set_log_level(LogLevel::Trace);
|
proxy_wasm::set_log_level(LogLevel::Trace);
|
||||||
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
|
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
|
||||||
Box::new(FilterContext::new())
|
Box::new(LlmGatewayFilterContext::new())
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
108
crates/llm_gateway/src/llm_filter_context.rs
Normal file
108
crates/llm_gateway/src/llm_filter_context.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
use crate::llm_stream_context::LlmGatewayStreamContext;
|
||||||
|
use common::configuration::Configuration;
|
||||||
|
use common::http::Client;
|
||||||
|
use common::llm_providers::LlmProviders;
|
||||||
|
use common::ratelimit;
|
||||||
|
use common::stats::Counter;
|
||||||
|
use common::stats::Gauge;
|
||||||
|
use log::debug;
|
||||||
|
use proxy_wasm::traits::*;
|
||||||
|
use proxy_wasm::types::*;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct WasmMetrics {
|
||||||
|
pub active_http_calls: Gauge,
|
||||||
|
pub ratelimited_rq: Counter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WasmMetrics {
|
||||||
|
fn new() -> WasmMetrics {
|
||||||
|
WasmMetrics {
|
||||||
|
active_http_calls: Gauge::new(String::from("active_http_calls")),
|
||||||
|
ratelimited_rq: Counter::new(String::from("ratelimited_rq")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FilterCallContext {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LlmGatewayFilterContext {
|
||||||
|
metrics: Rc<WasmMetrics>,
|
||||||
|
// callouts stores token_id to request mapping that we use during #on_http_call_response to match the response to the request.
|
||||||
|
callouts: RefCell<HashMap<u32, FilterCallContext>>,
|
||||||
|
llm_providers: Option<Rc<LlmProviders>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LlmGatewayFilterContext {
|
||||||
|
pub fn new() -> LlmGatewayFilterContext {
|
||||||
|
LlmGatewayFilterContext {
|
||||||
|
callouts: RefCell::new(HashMap::new()),
|
||||||
|
metrics: Rc::new(WasmMetrics::new()),
|
||||||
|
llm_providers: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client for LlmGatewayFilterContext {
|
||||||
|
type CallContext = FilterCallContext;
|
||||||
|
|
||||||
|
fn callouts(&self) -> &RefCell<HashMap<u32, Self::CallContext>> {
|
||||||
|
&self.callouts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_http_calls(&self) -> &Gauge {
|
||||||
|
&self.metrics.active_http_calls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for LlmGatewayFilterContext {}
|
||||||
|
|
||||||
|
// RootContext allows the Rust code to reach into the Envoy Config
|
||||||
|
impl RootContext for LlmGatewayFilterContext {
|
||||||
|
fn on_configure(&mut self, _: usize) -> bool {
|
||||||
|
let config_bytes = self
|
||||||
|
.get_plugin_configuration()
|
||||||
|
.expect("Arch config cannot be empty");
|
||||||
|
|
||||||
|
let config: Configuration = match serde_yaml::from_slice(&config_bytes) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(err) => panic!("Invalid arch config \"{:?}\"", err),
|
||||||
|
};
|
||||||
|
|
||||||
|
ratelimit::ratelimits(Some(config.ratelimits.unwrap_or_default()));
|
||||||
|
|
||||||
|
match config.llm_providers.try_into() {
|
||||||
|
Ok(llm_providers) => self.llm_providers = Some(Rc::new(llm_providers)),
|
||||||
|
Err(err) => panic!("{err}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
|
||||||
|
debug!(
|
||||||
|
"||| create_http_context called with context_id: {:?} |||",
|
||||||
|
context_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// No StreamContext can be created until the Embedding Store is fully initialized.
|
||||||
|
Some(Box::new(LlmGatewayStreamContext::new(
|
||||||
|
context_id,
|
||||||
|
Rc::clone(&self.metrics),
|
||||||
|
Rc::clone(
|
||||||
|
self.llm_providers
|
||||||
|
.as_ref()
|
||||||
|
.expect("LLM Providers must exist when Streams are being created"),
|
||||||
|
),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_type(&self) -> Option<ContextType> {
|
||||||
|
Some(ContextType::HttpContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
427
crates/llm_gateway/src/llm_stream_context.rs
Normal file
427
crates/llm_gateway/src/llm_stream_context.rs
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
use crate::llm_filter_context::WasmMetrics;
|
||||||
|
use common::common_types::open_ai::{
|
||||||
|
ArchState, ChatCompletionChunkResponse, ChatCompletionsRequest, ChatCompletionsResponse,
|
||||||
|
Message, ToolCall, ToolCallState,
|
||||||
|
};
|
||||||
|
use common::configuration::LlmProvider;
|
||||||
|
use common::consts::{
|
||||||
|
ARCH_PROVIDER_HINT_HEADER, ARCH_ROUTING_HEADER, ARCH_STATE_HEADER, CHAT_COMPLETIONS_PATH,
|
||||||
|
RATELIMIT_SELECTOR_HEADER_KEY, REQUEST_ID_HEADER, USER_ROLE,
|
||||||
|
};
|
||||||
|
use common::llm_providers::LlmProviders;
|
||||||
|
use common::ratelimit::Header;
|
||||||
|
use common::{ratelimit, routing, tokenizer};
|
||||||
|
use http::StatusCode;
|
||||||
|
use log::debug;
|
||||||
|
use proxy_wasm::traits::*;
|
||||||
|
use proxy_wasm::types::*;
|
||||||
|
use serde_json::Value;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::num::NonZero;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use common::stats::IncrementingMetric;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ServerError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Deserialization(serde_json::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
LogicError(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
ExceededRatelimit(ratelimit::Error),
|
||||||
|
#[error("{why}")]
|
||||||
|
BadRequest { why: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LlmGatewayStreamContext {
|
||||||
|
context_id: u32,
|
||||||
|
metrics: Rc<WasmMetrics>,
|
||||||
|
tool_calls: Option<Vec<ToolCall>>,
|
||||||
|
tool_call_response: Option<String>,
|
||||||
|
arch_state: Option<Vec<ArchState>>,
|
||||||
|
request_body_size: usize,
|
||||||
|
ratelimit_selector: Option<Header>,
|
||||||
|
streaming_response: bool,
|
||||||
|
user_prompt: Option<Message>,
|
||||||
|
response_tokens: usize,
|
||||||
|
is_chat_completions_request: bool,
|
||||||
|
chat_completions_request: Option<ChatCompletionsRequest>,
|
||||||
|
llm_providers: Rc<LlmProviders>,
|
||||||
|
llm_provider: Option<Rc<LlmProvider>>,
|
||||||
|
request_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LlmGatewayStreamContext {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(context_id: u32, metrics: Rc<WasmMetrics>, llm_providers: Rc<LlmProviders>) -> Self {
|
||||||
|
LlmGatewayStreamContext {
|
||||||
|
context_id,
|
||||||
|
metrics,
|
||||||
|
chat_completions_request: None,
|
||||||
|
tool_calls: None,
|
||||||
|
tool_call_response: None,
|
||||||
|
arch_state: None,
|
||||||
|
request_body_size: 0,
|
||||||
|
ratelimit_selector: None,
|
||||||
|
streaming_response: false,
|
||||||
|
user_prompt: None,
|
||||||
|
response_tokens: 0,
|
||||||
|
is_chat_completions_request: false,
|
||||||
|
llm_providers,
|
||||||
|
llm_provider: None,
|
||||||
|
request_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn llm_provider(&self) -> &LlmProvider {
|
||||||
|
self.llm_provider
|
||||||
|
.as_ref()
|
||||||
|
.expect("the provider should be set when asked for it")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_llm_provider(&mut self) {
|
||||||
|
let provider_hint = self
|
||||||
|
.get_http_request_header(ARCH_PROVIDER_HINT_HEADER)
|
||||||
|
.map(|provider_name| provider_name.into());
|
||||||
|
|
||||||
|
debug!("llm provider hint: {:?}", provider_hint);
|
||||||
|
self.llm_provider = Some(routing::get_llm_provider(
|
||||||
|
&self.llm_providers,
|
||||||
|
provider_hint,
|
||||||
|
));
|
||||||
|
debug!("selected llm: {}", self.llm_provider.as_ref().unwrap().name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_auth_headers(&mut self) -> Result<(), ServerError> {
|
||||||
|
let llm_provider_api_key_value =
|
||||||
|
self.llm_provider()
|
||||||
|
.access_key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ServerError::BadRequest {
|
||||||
|
why: format!(
|
||||||
|
"No access key configured for selected LLM Provider \"{}\"",
|
||||||
|
self.llm_provider()
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let authorization_header_value = format!("Bearer {}", llm_provider_api_key_value);
|
||||||
|
|
||||||
|
self.set_http_request_header("Authorization", Some(&authorization_header_value));
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
debug!("server error occurred: {}", error);
|
||||||
|
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> {
|
||||||
|
if let Some(selector) = self.ratelimit_selector.take() {
|
||||||
|
// Tokenize and Ratelimit.
|
||||||
|
if let Ok(token_count) = tokenizer::token_count(model, json_string) {
|
||||||
|
ratelimit::ratelimits(None).read().unwrap().check_limit(
|
||||||
|
model.to_owned(),
|
||||||
|
selector,
|
||||||
|
NonZero::new(token_count as u32).unwrap(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HttpContext is the trait that allows the Rust code to interact with HTTP objects.
|
||||||
|
impl HttpContext for LlmGatewayStreamContext {
|
||||||
|
// 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 {
|
||||||
|
debug!("on_http_request_headers 1");
|
||||||
|
self.select_llm_provider();
|
||||||
|
debug!("on_http_request_headers 2");
|
||||||
|
self.add_http_request_header(ARCH_ROUTING_HEADER, &self.llm_provider().name);
|
||||||
|
debug!("on_http_request_headers 3");
|
||||||
|
|
||||||
|
if let Err(error) = self.modify_auth_headers() {
|
||||||
|
self.send_server_error(error, Some(StatusCode::BAD_REQUEST));
|
||||||
|
}
|
||||||
|
debug!("on_http_request_headers 4");
|
||||||
|
self.delete_content_length_header();
|
||||||
|
self.save_ratelimit_header();
|
||||||
|
|
||||||
|
self.is_chat_completions_request =
|
||||||
|
self.get_http_request_header(":path").unwrap_or_default() == CHAT_COMPLETIONS_PATH;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"on_http_request_headers S[{}] req_headers={:?}",
|
||||||
|
self.context_id,
|
||||||
|
self.get_http_request_headers()
|
||||||
|
);
|
||||||
|
|
||||||
|
self.request_id = self.get_http_request_header(REQUEST_ID_HEADER);
|
||||||
|
|
||||||
|
Action::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
||||||
|
// Let the client send the gateway all the data before sending to the LLM_provider.
|
||||||
|
// TODO: consider a streaming API.
|
||||||
|
if !end_of_stream {
|
||||||
|
return Action::Pause;
|
||||||
|
}
|
||||||
|
|
||||||
|
if body_size == 0 {
|
||||||
|
return Action::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request_body_size = body_size;
|
||||||
|
|
||||||
|
// Deserialize body into spec.
|
||||||
|
// Currently OpenAI API.
|
||||||
|
let mut deserialized_body: ChatCompletionsRequest =
|
||||||
|
match self.get_http_request_body(0, body_size) {
|
||||||
|
Some(body_bytes) => match serde_json::from_slice(&body_bytes) {
|
||||||
|
Ok(deserialized) => deserialized,
|
||||||
|
Err(e) => {
|
||||||
|
self.send_server_error(
|
||||||
|
ServerError::Deserialization(e),
|
||||||
|
Some(StatusCode::BAD_REQUEST),
|
||||||
|
);
|
||||||
|
return Action::Pause;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
self.send_server_error(
|
||||||
|
ServerError::LogicError(format!(
|
||||||
|
"Failed to obtain body bytes even though body_size is {}",
|
||||||
|
body_size
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
return Action::Pause;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.is_chat_completions_request = true;
|
||||||
|
|
||||||
|
debug!("llm gateway mode, skipping over all prompt targets");
|
||||||
|
|
||||||
|
// remove metadata from the request body
|
||||||
|
deserialized_body.metadata = None;
|
||||||
|
// delete model key from message array
|
||||||
|
for message in deserialized_body.messages.iter_mut() {
|
||||||
|
message.model = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// override model name from the llm provider
|
||||||
|
deserialized_body
|
||||||
|
.model
|
||||||
|
.clone_from(&self.llm_provider.as_ref().unwrap().model);
|
||||||
|
let chat_completion_request_str = serde_json::to_string(&deserialized_body).unwrap();
|
||||||
|
|
||||||
|
// enforce ratelimits on ingress
|
||||||
|
if let Err(e) =
|
||||||
|
self.enforce_ratelimits(&deserialized_body.model, &chat_completion_request_str)
|
||||||
|
{
|
||||||
|
self.send_server_error(
|
||||||
|
ServerError::ExceededRatelimit(e),
|
||||||
|
Some(StatusCode::TOO_MANY_REQUESTS),
|
||||||
|
);
|
||||||
|
self.metrics.ratelimited_rq.increment(1);
|
||||||
|
return Action::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"arch => {:?}, body: {}",
|
||||||
|
deserialized_body.model, chat_completion_request_str
|
||||||
|
);
|
||||||
|
self.set_http_request_body(0, body_size, chat_completion_request_str.as_bytes());
|
||||||
|
|
||||||
|
Action::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
||||||
|
debug!(
|
||||||
|
"recv [S={}] bytes={} end_stream={}",
|
||||||
|
self.context_id, body_size, end_of_stream
|
||||||
|
);
|
||||||
|
|
||||||
|
if !self.is_chat_completions_request {
|
||||||
|
if let Some(body_str) = self
|
||||||
|
.get_http_response_body(0, body_size)
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
{
|
||||||
|
debug!("recv [S={}] body_str={}", self.context_id, body_str);
|
||||||
|
}
|
||||||
|
return Action::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !end_of_stream {
|
||||||
|
return Action::Pause;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = self
|
||||||
|
.get_http_response_body(0, body_size)
|
||||||
|
.expect("cant get response body");
|
||||||
|
|
||||||
|
if self.streaming_response {
|
||||||
|
let body_str = String::from_utf8(body).expect("body is not utf-8");
|
||||||
|
debug!("streaming response");
|
||||||
|
let chat_completions_data = match body_str.split_once("data: ") {
|
||||||
|
Some((_, chat_completions_data)) => chat_completions_data,
|
||||||
|
None => {
|
||||||
|
self.send_server_error(
|
||||||
|
ServerError::LogicError(String::from("parsing error in streaming data")),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
return Action::Pause;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let chat_completions_chunk_response: ChatCompletionChunkResponse =
|
||||||
|
match serde_json::from_str(chat_completions_data) {
|
||||||
|
Ok(de) => de,
|
||||||
|
Err(_) => {
|
||||||
|
if chat_completions_data != "[NONE]" {
|
||||||
|
self.send_server_error(
|
||||||
|
ServerError::LogicError(String::from(
|
||||||
|
"error in streaming response",
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
return Action::Continue;
|
||||||
|
}
|
||||||
|
return Action::Continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(content) = chat_completions_chunk_response
|
||||||
|
.choices
|
||||||
|
.first()
|
||||||
|
.unwrap()
|
||||||
|
.delta
|
||||||
|
.content
|
||||||
|
.as_ref()
|
||||||
|
{
|
||||||
|
let model = &chat_completions_chunk_response.model;
|
||||||
|
let token_count = tokenizer::token_count(model, content).unwrap_or(0);
|
||||||
|
self.response_tokens += token_count;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("non streaming response");
|
||||||
|
let chat_completions_response: ChatCompletionsResponse =
|
||||||
|
match serde_json::from_slice(&body) {
|
||||||
|
Ok(de) => de,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("invalid response: {}", String::from_utf8_lossy(&body));
|
||||||
|
self.send_server_error(ServerError::Deserialization(e), None);
|
||||||
|
return Action::Pause;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if chat_completions_response.usage.is_some() {
|
||||||
|
self.response_tokens += chat_completions_response
|
||||||
|
.usage
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.completion_tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tool_calls) = self.tool_calls.as_ref() {
|
||||||
|
if !tool_calls.is_empty() {
|
||||||
|
if self.arch_state.is_none() {
|
||||||
|
self.arch_state = Some(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute sha hash from message history
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
let prompts: Vec<String> = self
|
||||||
|
.chat_completions_request
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.filter(|msg| msg.role == USER_ROLE)
|
||||||
|
.map(|msg| msg.content.clone().unwrap())
|
||||||
|
.collect();
|
||||||
|
let prompts_merged = prompts.join("#.#");
|
||||||
|
hasher.update(prompts_merged.clone());
|
||||||
|
let hash_key = hasher.finalize();
|
||||||
|
// conver hash to hex string
|
||||||
|
let hash_key_str = format!("{:x}", hash_key);
|
||||||
|
debug!("hash key: {}, prompts: {}", hash_key_str, prompts_merged);
|
||||||
|
|
||||||
|
// create new tool call state
|
||||||
|
let tool_call_state = ToolCallState {
|
||||||
|
key: hash_key_str,
|
||||||
|
message: self.user_prompt.clone(),
|
||||||
|
tool_call: tool_calls[0].function.clone(),
|
||||||
|
tool_response: self.tool_call_response.clone().unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// push tool call state to arch state
|
||||||
|
self.arch_state
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(ArchState::ToolCall(vec![tool_call_state]));
|
||||||
|
|
||||||
|
let mut data: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
// use serde::Value to manipulate the json object and ensure that we don't lose any data
|
||||||
|
if let Value::Object(ref mut map) = data {
|
||||||
|
// serialize arch state and add to metadata
|
||||||
|
let arch_state_str = serde_json::to_string(&self.arch_state).unwrap();
|
||||||
|
debug!("arch_state: {}", arch_state_str);
|
||||||
|
let metadata = map
|
||||||
|
.entry("metadata")
|
||||||
|
.or_insert(Value::Object(serde_json::Map::new()));
|
||||||
|
metadata.as_object_mut().unwrap().insert(
|
||||||
|
ARCH_STATE_HEADER.to_string(),
|
||||||
|
serde_json::Value::String(arch_state_str),
|
||||||
|
);
|
||||||
|
|
||||||
|
let data_serialized = serde_json::to_string(&data).unwrap();
|
||||||
|
debug!("arch => user: {}", data_serialized);
|
||||||
|
self.set_http_response_body(0, body_size, data_serialized.as_bytes());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"recv [S={}] total_tokens={} end_stream={}",
|
||||||
|
self.context_id, self.response_tokens, end_of_stream
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO:: ratelimit based on response tokens.
|
||||||
|
Action::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for LlmGatewayStreamContext {}
|
||||||
|
|
@ -1,322 +0,0 @@
|
||||||
use crate::stream_context::StreamContext;
|
|
||||||
use common::common_types::EmbeddingType;
|
|
||||||
use common::configuration::{Configuration, GatewayMode, Overrides, PromptGuards, PromptTarget};
|
|
||||||
use common::consts::ARCH_INTERNAL_CLUSTER_NAME;
|
|
||||||
use common::consts::ARCH_UPSTREAM_HOST_HEADER;
|
|
||||||
use common::consts::DEFAULT_EMBEDDING_MODEL;
|
|
||||||
use common::consts::MODEL_SERVER_NAME;
|
|
||||||
use common::embeddings::{
|
|
||||||
CreateEmbeddingRequest, CreateEmbeddingRequestInput, CreateEmbeddingResponse,
|
|
||||||
};
|
|
||||||
use common::http::CallArgs;
|
|
||||||
use common::http::Client;
|
|
||||||
use common::llm_providers::LlmProviders;
|
|
||||||
use common::ratelimit;
|
|
||||||
use common::stats::Counter;
|
|
||||||
use common::stats::Gauge;
|
|
||||||
use common::stats::IncrementingMetric;
|
|
||||||
use log::debug;
|
|
||||||
use proxy_wasm::traits::*;
|
|
||||||
use proxy_wasm::types::*;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub struct WasmMetrics {
|
|
||||||
pub active_http_calls: Gauge,
|
|
||||||
pub ratelimited_rq: Counter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasmMetrics {
|
|
||||||
fn new() -> WasmMetrics {
|
|
||||||
WasmMetrics {
|
|
||||||
active_http_calls: Gauge::new(String::from("active_http_calls")),
|
|
||||||
ratelimited_rq: Counter::new(String::from("ratelimited_rq")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type EmbeddingTypeMap = HashMap<EmbeddingType, Vec<f64>>;
|
|
||||||
pub type EmbeddingsStore = HashMap<String, EmbeddingTypeMap>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FilterCallContext {
|
|
||||||
pub prompt_target_name: String,
|
|
||||||
pub embedding_type: EmbeddingType,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FilterContext {
|
|
||||||
metrics: Rc<WasmMetrics>,
|
|
||||||
// callouts stores token_id to request mapping that we use during #on_http_call_response to match the response to the request.
|
|
||||||
callouts: RefCell<HashMap<u32, FilterCallContext>>,
|
|
||||||
overrides: Rc<Option<Overrides>>,
|
|
||||||
system_prompt: Rc<Option<String>>,
|
|
||||||
prompt_targets: Rc<HashMap<String, PromptTarget>>,
|
|
||||||
mode: GatewayMode,
|
|
||||||
prompt_guards: Rc<PromptGuards>,
|
|
||||||
llm_providers: Option<Rc<LlmProviders>>,
|
|
||||||
embeddings_store: Option<Rc<EmbeddingsStore>>,
|
|
||||||
temp_embeddings_store: EmbeddingsStore,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FilterContext {
|
|
||||||
pub fn new() -> FilterContext {
|
|
||||||
FilterContext {
|
|
||||||
callouts: RefCell::new(HashMap::new()),
|
|
||||||
metrics: Rc::new(WasmMetrics::new()),
|
|
||||||
system_prompt: Rc::new(None),
|
|
||||||
prompt_targets: Rc::new(HashMap::new()),
|
|
||||||
overrides: Rc::new(None),
|
|
||||||
prompt_guards: Rc::new(PromptGuards::default()),
|
|
||||||
mode: GatewayMode::Prompt,
|
|
||||||
llm_providers: None,
|
|
||||||
embeddings_store: Some(Rc::new(HashMap::new())),
|
|
||||||
temp_embeddings_store: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_prompt_targets(&self) {
|
|
||||||
for values in self.prompt_targets.iter() {
|
|
||||||
let prompt_target = values.1;
|
|
||||||
self.schedule_embeddings_call(
|
|
||||||
&prompt_target.name,
|
|
||||||
&prompt_target.description,
|
|
||||||
EmbeddingType::Description,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schedule_embeddings_call(
|
|
||||||
&self,
|
|
||||||
prompt_target_name: &str,
|
|
||||||
input: &str,
|
|
||||||
embedding_type: EmbeddingType,
|
|
||||||
) {
|
|
||||||
let embeddings_input = CreateEmbeddingRequest {
|
|
||||||
input: Box::new(CreateEmbeddingRequestInput::String(String::from(input))),
|
|
||||||
model: String::from(DEFAULT_EMBEDDING_MODEL),
|
|
||||||
encoding_format: None,
|
|
||||||
dimensions: None,
|
|
||||||
user: None,
|
|
||||||
};
|
|
||||||
let json_data = serde_json::to_string(&embeddings_input).unwrap();
|
|
||||||
|
|
||||||
let call_args = CallArgs::new(
|
|
||||||
ARCH_INTERNAL_CLUSTER_NAME,
|
|
||||||
"/embeddings",
|
|
||||||
vec![
|
|
||||||
(ARCH_UPSTREAM_HOST_HEADER, MODEL_SERVER_NAME),
|
|
||||||
(":method", "POST"),
|
|
||||||
(":path", "/embeddings"),
|
|
||||||
(":authority", MODEL_SERVER_NAME),
|
|
||||||
("content-type", "application/json"),
|
|
||||||
("x-envoy-upstream-rq-timeout-ms", "60000"),
|
|
||||||
],
|
|
||||||
Some(json_data.as_bytes()),
|
|
||||||
vec![],
|
|
||||||
Duration::from_secs(60),
|
|
||||||
);
|
|
||||||
|
|
||||||
let call_context = crate::filter_context::FilterCallContext {
|
|
||||||
prompt_target_name: String::from(prompt_target_name),
|
|
||||||
embedding_type,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(error) = self.http_call(call_args, call_context) {
|
|
||||||
panic!("{error}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn embedding_response_handler(
|
|
||||||
&mut self,
|
|
||||||
body_size: usize,
|
|
||||||
embedding_type: EmbeddingType,
|
|
||||||
prompt_target_name: String,
|
|
||||||
) {
|
|
||||||
let prompt_target = self
|
|
||||||
.prompt_targets
|
|
||||||
.get(&prompt_target_name)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
panic!(
|
|
||||||
"Received embeddings response for unknown prompt target name={}",
|
|
||||||
prompt_target_name
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let body = self
|
|
||||||
.get_http_call_response_body(0, body_size)
|
|
||||||
.expect("No body in response");
|
|
||||||
if !body.is_empty() {
|
|
||||||
let mut embedding_response: CreateEmbeddingResponse =
|
|
||||||
match serde_json::from_slice(&body) {
|
|
||||||
Ok(response) => response,
|
|
||||||
Err(e) => {
|
|
||||||
panic!(
|
|
||||||
"Error deserializing embedding response. body: {:?}: {:?}",
|
|
||||||
String::from_utf8(body).unwrap(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let embeddings = embedding_response.data.remove(0).embedding;
|
|
||||||
debug!(
|
|
||||||
"Adding embeddings for prompt target name: {:?}, description: {:?}, embedding type: {:?}",
|
|
||||||
prompt_target.name,
|
|
||||||
prompt_target.description,
|
|
||||||
embedding_type
|
|
||||||
);
|
|
||||||
|
|
||||||
let entry = self.temp_embeddings_store.entry(prompt_target_name);
|
|
||||||
match entry {
|
|
||||||
Entry::Occupied(_) => {
|
|
||||||
entry.and_modify(|e| {
|
|
||||||
if let Entry::Vacant(e) = e.entry(embedding_type) {
|
|
||||||
e.insert(embeddings);
|
|
||||||
} else {
|
|
||||||
panic!(
|
|
||||||
"Duplicate {:?} for prompt target with name=\"{}\"",
|
|
||||||
&embedding_type, prompt_target.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Entry::Vacant(_) => {
|
|
||||||
entry.or_insert(HashMap::from([(embedding_type, embeddings)]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.prompt_targets.len() == self.temp_embeddings_store.len() {
|
|
||||||
self.embeddings_store =
|
|
||||||
Some(Rc::new(std::mem::take(&mut self.temp_embeddings_store)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client for FilterContext {
|
|
||||||
type CallContext = FilterCallContext;
|
|
||||||
|
|
||||||
fn callouts(&self) -> &RefCell<HashMap<u32, Self::CallContext>> {
|
|
||||||
&self.callouts
|
|
||||||
}
|
|
||||||
|
|
||||||
fn active_http_calls(&self) -> &Gauge {
|
|
||||||
&self.metrics.active_http_calls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Context for FilterContext {
|
|
||||||
fn on_http_call_response(
|
|
||||||
&mut self,
|
|
||||||
token_id: u32,
|
|
||||||
_num_headers: usize,
|
|
||||||
body_size: usize,
|
|
||||||
_num_trailers: usize,
|
|
||||||
) {
|
|
||||||
debug!(
|
|
||||||
"filter_context: on_http_call_response called with token_id: {:?}",
|
|
||||||
token_id
|
|
||||||
);
|
|
||||||
let callout_data = self
|
|
||||||
.callouts
|
|
||||||
.borrow_mut()
|
|
||||||
.remove(&token_id)
|
|
||||||
.expect("invalid token_id");
|
|
||||||
|
|
||||||
self.metrics.active_http_calls.increment(-1);
|
|
||||||
|
|
||||||
self.embedding_response_handler(
|
|
||||||
body_size,
|
|
||||||
callout_data.embedding_type,
|
|
||||||
callout_data.prompt_target_name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootContext allows the Rust code to reach into the Envoy Config
|
|
||||||
impl RootContext for FilterContext {
|
|
||||||
fn on_configure(&mut self, _: usize) -> bool {
|
|
||||||
let config_bytes = self
|
|
||||||
.get_plugin_configuration()
|
|
||||||
.expect("Arch config cannot be empty");
|
|
||||||
|
|
||||||
let config: Configuration = match serde_yaml::from_slice(&config_bytes) {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(err) => panic!("Invalid arch config \"{:?}\"", err),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.overrides = Rc::new(config.overrides);
|
|
||||||
|
|
||||||
let mut prompt_targets = HashMap::new();
|
|
||||||
for pt in config.prompt_targets {
|
|
||||||
prompt_targets.insert(pt.name.clone(), pt.clone());
|
|
||||||
}
|
|
||||||
self.system_prompt = Rc::new(config.system_prompt);
|
|
||||||
self.prompt_targets = Rc::new(prompt_targets);
|
|
||||||
self.mode = config.mode.unwrap_or_default();
|
|
||||||
|
|
||||||
ratelimit::ratelimits(Some(config.ratelimits.unwrap_or_default()));
|
|
||||||
|
|
||||||
if let Some(prompt_guards) = config.prompt_guards {
|
|
||||||
self.prompt_guards = Rc::new(prompt_guards)
|
|
||||||
}
|
|
||||||
|
|
||||||
match config.llm_providers.try_into() {
|
|
||||||
Ok(llm_providers) => self.llm_providers = Some(Rc::new(llm_providers)),
|
|
||||||
Err(err) => panic!("{err}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
|
|
||||||
debug!(
|
|
||||||
"||| create_http_context called with context_id: {:?} |||",
|
|
||||||
context_id
|
|
||||||
);
|
|
||||||
|
|
||||||
// No StreamContext can be created until the Embedding Store is fully initialized.
|
|
||||||
let embedding_store = match self.mode {
|
|
||||||
GatewayMode::Llm => None,
|
|
||||||
GatewayMode::Prompt => Some(Rc::clone(self.embeddings_store.as_ref().unwrap())),
|
|
||||||
};
|
|
||||||
Some(Box::new(StreamContext::new(
|
|
||||||
context_id,
|
|
||||||
Rc::clone(&self.metrics),
|
|
||||||
Rc::clone(&self.system_prompt),
|
|
||||||
Rc::clone(&self.prompt_targets),
|
|
||||||
Rc::clone(&self.prompt_guards),
|
|
||||||
Rc::clone(&self.overrides),
|
|
||||||
Rc::clone(
|
|
||||||
self.llm_providers
|
|
||||||
.as_ref()
|
|
||||||
.expect("LLM Providers must exist when Streams are being created"),
|
|
||||||
),
|
|
||||||
embedding_store,
|
|
||||||
self.mode.clone(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_type(&self) -> Option<ContextType> {
|
|
||||||
Some(ContextType::HttpContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_vm_start(&mut self, _: usize) -> bool {
|
|
||||||
self.set_tick_period(Duration::from_secs(1));
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_tick(&mut self) {
|
|
||||||
debug!("starting up arch filter in mode: {:?}", self.mode);
|
|
||||||
if self.mode == GatewayMode::Prompt {
|
|
||||||
self.process_prompt_targets();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_tick_period(Duration::from_secs(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use filter_context::FilterContext;
|
use prompt_filter_context::PromptGatewayFilterContext;
|
||||||
use proxy_wasm::traits::*;
|
use proxy_wasm::traits::*;
|
||||||
use proxy_wasm::types::*;
|
use proxy_wasm::types::*;
|
||||||
|
|
||||||
mod filter_context;
|
mod prompt_filter_context;
|
||||||
mod stream_context;
|
mod prompt_stream_context;
|
||||||
|
|
||||||
proxy_wasm::main! {{
|
proxy_wasm::main! {{
|
||||||
proxy_wasm::set_log_level(LogLevel::Trace);
|
proxy_wasm::set_log_level(LogLevel::Trace);
|
||||||
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
|
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
|
||||||
Box::new(FilterContext::new())
|
Box::new(PromptGatewayFilterContext::new())
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::stream_context::StreamContext;
|
use crate::prompt_stream_context::PromptStreamContext;
|
||||||
use common::common_types::EmbeddingType;
|
use common::common_types::EmbeddingType;
|
||||||
use common::configuration::{Configuration, GatewayMode, Overrides, PromptGuards, PromptTarget};
|
use common::configuration::{Configuration, GatewayMode, Overrides, PromptGuards, PromptTarget};
|
||||||
use common::consts::ARCH_INTERNAL_CLUSTER_NAME;
|
use common::consts::ARCH_INTERNAL_CLUSTER_NAME;
|
||||||
|
|
@ -11,8 +11,6 @@ use common::embeddings::{
|
||||||
use common::http::CallArgs;
|
use common::http::CallArgs;
|
||||||
use common::http::Client;
|
use common::http::Client;
|
||||||
use common::llm_providers::LlmProviders;
|
use common::llm_providers::LlmProviders;
|
||||||
use common::ratelimit;
|
|
||||||
use common::stats::Counter;
|
|
||||||
use common::stats::Gauge;
|
use common::stats::Gauge;
|
||||||
use common::stats::IncrementingMetric;
|
use common::stats::IncrementingMetric;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
@ -27,14 +25,12 @@ use std::time::Duration;
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct WasmMetrics {
|
pub struct WasmMetrics {
|
||||||
pub active_http_calls: Gauge,
|
pub active_http_calls: Gauge,
|
||||||
pub ratelimited_rq: Counter,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WasmMetrics {
|
impl WasmMetrics {
|
||||||
fn new() -> WasmMetrics {
|
fn new() -> WasmMetrics {
|
||||||
WasmMetrics {
|
WasmMetrics {
|
||||||
active_http_calls: Gauge::new(String::from("active_http_calls")),
|
active_http_calls: Gauge::new(String::from("active_http_calls")),
|
||||||
ratelimited_rq: Counter::new(String::from("ratelimited_rq")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +45,7 @@ pub struct FilterCallContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FilterContext {
|
pub struct PromptGatewayFilterContext {
|
||||||
metrics: Rc<WasmMetrics>,
|
metrics: Rc<WasmMetrics>,
|
||||||
// callouts stores token_id to request mapping that we use during #on_http_call_response to match the response to the request.
|
// callouts stores token_id to request mapping that we use during #on_http_call_response to match the response to the request.
|
||||||
callouts: RefCell<HashMap<u32, FilterCallContext>>,
|
callouts: RefCell<HashMap<u32, FilterCallContext>>,
|
||||||
|
|
@ -63,9 +59,9 @@ pub struct FilterContext {
|
||||||
temp_embeddings_store: EmbeddingsStore,
|
temp_embeddings_store: EmbeddingsStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FilterContext {
|
impl PromptGatewayFilterContext {
|
||||||
pub fn new() -> FilterContext {
|
pub fn new() -> PromptGatewayFilterContext {
|
||||||
FilterContext {
|
PromptGatewayFilterContext {
|
||||||
callouts: RefCell::new(HashMap::new()),
|
callouts: RefCell::new(HashMap::new()),
|
||||||
metrics: Rc::new(WasmMetrics::new()),
|
metrics: Rc::new(WasmMetrics::new()),
|
||||||
system_prompt: Rc::new(None),
|
system_prompt: Rc::new(None),
|
||||||
|
|
@ -121,7 +117,7 @@ impl FilterContext {
|
||||||
Duration::from_secs(60),
|
Duration::from_secs(60),
|
||||||
);
|
);
|
||||||
|
|
||||||
let call_context = crate::filter_context::FilterCallContext {
|
let call_context = crate::prompt_filter_context::FilterCallContext {
|
||||||
prompt_target_name: String::from(prompt_target_name),
|
prompt_target_name: String::from(prompt_target_name),
|
||||||
embedding_type,
|
embedding_type,
|
||||||
};
|
};
|
||||||
|
|
@ -198,7 +194,7 @@ impl FilterContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client for FilterContext {
|
impl Client for PromptGatewayFilterContext {
|
||||||
type CallContext = FilterCallContext;
|
type CallContext = FilterCallContext;
|
||||||
|
|
||||||
fn callouts(&self) -> &RefCell<HashMap<u32, Self::CallContext>> {
|
fn callouts(&self) -> &RefCell<HashMap<u32, Self::CallContext>> {
|
||||||
|
|
@ -210,7 +206,7 @@ impl Client for FilterContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for FilterContext {
|
impl Context for PromptGatewayFilterContext {
|
||||||
fn on_http_call_response(
|
fn on_http_call_response(
|
||||||
&mut self,
|
&mut self,
|
||||||
token_id: u32,
|
token_id: u32,
|
||||||
|
|
@ -239,7 +235,7 @@ impl Context for FilterContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RootContext allows the Rust code to reach into the Envoy Config
|
// RootContext allows the Rust code to reach into the Envoy Config
|
||||||
impl RootContext for FilterContext {
|
impl RootContext for PromptGatewayFilterContext {
|
||||||
fn on_configure(&mut self, _: usize) -> bool {
|
fn on_configure(&mut self, _: usize) -> bool {
|
||||||
let config_bytes = self
|
let config_bytes = self
|
||||||
.get_plugin_configuration()
|
.get_plugin_configuration()
|
||||||
|
|
@ -260,8 +256,6 @@ impl RootContext for FilterContext {
|
||||||
self.prompt_targets = Rc::new(prompt_targets);
|
self.prompt_targets = Rc::new(prompt_targets);
|
||||||
self.mode = config.mode.unwrap_or_default();
|
self.mode = config.mode.unwrap_or_default();
|
||||||
|
|
||||||
ratelimit::ratelimits(Some(config.ratelimits.unwrap_or_default()));
|
|
||||||
|
|
||||||
if let Some(prompt_guards) = config.prompt_guards {
|
if let Some(prompt_guards) = config.prompt_guards {
|
||||||
self.prompt_guards = Rc::new(prompt_guards)
|
self.prompt_guards = Rc::new(prompt_guards)
|
||||||
}
|
}
|
||||||
|
|
@ -285,20 +279,14 @@ impl RootContext for FilterContext {
|
||||||
GatewayMode::Llm => None,
|
GatewayMode::Llm => None,
|
||||||
GatewayMode::Prompt => Some(Rc::clone(self.embeddings_store.as_ref().unwrap())),
|
GatewayMode::Prompt => Some(Rc::clone(self.embeddings_store.as_ref().unwrap())),
|
||||||
};
|
};
|
||||||
Some(Box::new(StreamContext::new(
|
Some(Box::new(PromptStreamContext::new(
|
||||||
context_id,
|
context_id,
|
||||||
Rc::clone(&self.metrics),
|
Rc::clone(&self.metrics),
|
||||||
Rc::clone(&self.system_prompt),
|
Rc::clone(&self.system_prompt),
|
||||||
Rc::clone(&self.prompt_targets),
|
Rc::clone(&self.prompt_targets),
|
||||||
Rc::clone(&self.prompt_guards),
|
Rc::clone(&self.prompt_guards),
|
||||||
Rc::clone(&self.overrides),
|
Rc::clone(&self.overrides),
|
||||||
Rc::clone(
|
|
||||||
self.llm_providers
|
|
||||||
.as_ref()
|
|
||||||
.expect("LLM Providers must exist when Streams are being created"),
|
|
||||||
),
|
|
||||||
embedding_store,
|
embedding_store,
|
||||||
self.mode.clone(),
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,33 +1,28 @@
|
||||||
use crate::filter_context::{EmbeddingsStore, WasmMetrics};
|
use crate::prompt_filter_context::{EmbeddingsStore, WasmMetrics};
|
||||||
use acap::cos;
|
use acap::cos;
|
||||||
use common::common_types::open_ai::{
|
use common::common_types::open_ai::{
|
||||||
ArchState, ChatCompletionChunkResponse, ChatCompletionTool, ChatCompletionsRequest,
|
ArchState, ChatCompletionTool, ChatCompletionsRequest, ChatCompletionsResponse, Choice,
|
||||||
ChatCompletionsResponse, Choice, FunctionDefinition, FunctionParameter, FunctionParameters,
|
FunctionDefinition, FunctionParameter, FunctionParameters, Message, ParameterType,
|
||||||
Message, ParameterType, StreamOptions, ToolCall, ToolCallState, ToolType,
|
StreamOptions, ToolCall, ToolCallState, ToolType,
|
||||||
};
|
};
|
||||||
use common::common_types::{
|
use common::common_types::{
|
||||||
EmbeddingType, HallucinationClassificationRequest, HallucinationClassificationResponse,
|
EmbeddingType, HallucinationClassificationRequest, HallucinationClassificationResponse,
|
||||||
PromptGuardRequest, PromptGuardResponse, PromptGuardTask, ZeroShotClassificationRequest,
|
PromptGuardRequest, PromptGuardResponse, PromptGuardTask, ZeroShotClassificationRequest,
|
||||||
ZeroShotClassificationResponse,
|
ZeroShotClassificationResponse,
|
||||||
};
|
};
|
||||||
use common::configuration::{GatewayMode, LlmProvider};
|
|
||||||
use common::configuration::{Overrides, PromptGuards, PromptTarget};
|
use common::configuration::{Overrides, PromptGuards, PromptTarget};
|
||||||
use common::consts::{
|
use common::consts::{
|
||||||
ARCH_FC_MODEL_NAME, ARCH_FC_REQUEST_TIMEOUT_MS, ARCH_INTERNAL_CLUSTER_NAME,
|
ARCH_FC_MODEL_NAME, ARCH_FC_REQUEST_TIMEOUT_MS, ARCH_INTERNAL_CLUSTER_NAME, ARCH_MESSAGES_KEY,
|
||||||
ARCH_LLM_UPSTREAM_LISTENER, ARCH_MESSAGES_KEY, ARCH_MODEL_PREFIX, ARCH_PROVIDER_HINT_HEADER,
|
ARCH_MODEL_PREFIX, ARCH_STATE_HEADER, ARCH_UPSTREAM_HOST_HEADER, ARC_FC_CLUSTER,
|
||||||
ARCH_ROUTING_HEADER, ARCH_STATE_HEADER, ARCH_UPSTREAM_HOST_HEADER, ARC_FC_CLUSTER,
|
|
||||||
CHAT_COMPLETIONS_PATH, DEFAULT_EMBEDDING_MODEL, DEFAULT_HALLUCINATED_THRESHOLD,
|
CHAT_COMPLETIONS_PATH, DEFAULT_EMBEDDING_MODEL, DEFAULT_HALLUCINATED_THRESHOLD,
|
||||||
DEFAULT_INTENT_MODEL, DEFAULT_PROMPT_TARGET_THRESHOLD, GPT_35_TURBO, MODEL_SERVER_NAME,
|
DEFAULT_INTENT_MODEL, DEFAULT_PROMPT_TARGET_THRESHOLD, GPT_35_TURBO, MODEL_SERVER_NAME,
|
||||||
RATELIMIT_SELECTOR_HEADER_KEY, REQUEST_ID_HEADER, SYSTEM_ROLE, USER_ROLE,
|
REQUEST_ID_HEADER, SYSTEM_ROLE, USER_ROLE,
|
||||||
};
|
};
|
||||||
use common::embeddings::{
|
use common::embeddings::{
|
||||||
CreateEmbeddingRequest, CreateEmbeddingRequestInput, CreateEmbeddingResponse,
|
CreateEmbeddingRequest, CreateEmbeddingRequestInput, CreateEmbeddingResponse,
|
||||||
};
|
};
|
||||||
use common::http::{CallArgs, Client, ClientError};
|
use common::http::{CallArgs, Client, ClientError};
|
||||||
use common::llm_providers::LlmProviders;
|
|
||||||
use common::ratelimit::Header;
|
|
||||||
use common::stats::Gauge;
|
use common::stats::Gauge;
|
||||||
use common::{ratelimit, routing, tokenizer};
|
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use proxy_wasm::traits::*;
|
use proxy_wasm::traits::*;
|
||||||
|
|
@ -36,7 +31,6 @@ use serde_json::Value;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::num::NonZero;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
|
@ -81,17 +75,13 @@ pub enum ServerError {
|
||||||
path: String,
|
path: String,
|
||||||
status: String,
|
status: String,
|
||||||
},
|
},
|
||||||
#[error(transparent)]
|
|
||||||
ExceededRatelimit(ratelimit::Error),
|
|
||||||
#[error("jailbreak detected: {0}")]
|
#[error("jailbreak detected: {0}")]
|
||||||
Jailbreak(String),
|
Jailbreak(String),
|
||||||
#[error("{why}")]
|
#[error("{why}")]
|
||||||
BadRequest { why: String },
|
|
||||||
#[error("{why}")]
|
|
||||||
NoMessagesFound { why: String },
|
NoMessagesFound { why: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StreamContext {
|
pub struct PromptStreamContext {
|
||||||
context_id: u32,
|
context_id: u32,
|
||||||
metrics: Rc<WasmMetrics>,
|
metrics: Rc<WasmMetrics>,
|
||||||
system_prompt: Rc<Option<String>>,
|
system_prompt: Rc<Option<String>>,
|
||||||
|
|
@ -103,20 +93,16 @@ pub struct StreamContext {
|
||||||
tool_call_response: Option<String>,
|
tool_call_response: Option<String>,
|
||||||
arch_state: Option<Vec<ArchState>>,
|
arch_state: Option<Vec<ArchState>>,
|
||||||
request_body_size: usize,
|
request_body_size: usize,
|
||||||
ratelimit_selector: Option<Header>,
|
|
||||||
streaming_response: bool,
|
streaming_response: bool,
|
||||||
user_prompt: Option<Message>,
|
user_prompt: Option<Message>,
|
||||||
response_tokens: usize,
|
response_tokens: usize,
|
||||||
is_chat_completions_request: bool,
|
is_chat_completions_request: bool,
|
||||||
chat_completions_request: Option<ChatCompletionsRequest>,
|
chat_completions_request: Option<ChatCompletionsRequest>,
|
||||||
prompt_guards: Rc<PromptGuards>,
|
prompt_guards: Rc<PromptGuards>,
|
||||||
llm_providers: Rc<LlmProviders>,
|
|
||||||
llm_provider: Option<Rc<LlmProvider>>,
|
|
||||||
request_id: Option<String>,
|
request_id: Option<String>,
|
||||||
mode: GatewayMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamContext {
|
impl PromptStreamContext {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
context_id: u32,
|
context_id: u32,
|
||||||
|
|
@ -125,11 +111,9 @@ impl StreamContext {
|
||||||
prompt_targets: Rc<HashMap<String, PromptTarget>>,
|
prompt_targets: Rc<HashMap<String, PromptTarget>>,
|
||||||
prompt_guards: Rc<PromptGuards>,
|
prompt_guards: Rc<PromptGuards>,
|
||||||
overrides: Rc<Option<Overrides>>,
|
overrides: Rc<Option<Overrides>>,
|
||||||
llm_providers: Rc<LlmProviders>,
|
|
||||||
embeddings_store: Option<Rc<EmbeddingsStore>>,
|
embeddings_store: Option<Rc<EmbeddingsStore>>,
|
||||||
mode: GatewayMode,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
StreamContext {
|
PromptStreamContext {
|
||||||
context_id,
|
context_id,
|
||||||
metrics,
|
metrics,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
|
|
@ -141,75 +125,21 @@ impl StreamContext {
|
||||||
tool_call_response: None,
|
tool_call_response: None,
|
||||||
arch_state: None,
|
arch_state: None,
|
||||||
request_body_size: 0,
|
request_body_size: 0,
|
||||||
ratelimit_selector: None,
|
|
||||||
streaming_response: false,
|
streaming_response: false,
|
||||||
user_prompt: None,
|
user_prompt: None,
|
||||||
response_tokens: 0,
|
response_tokens: 0,
|
||||||
is_chat_completions_request: false,
|
is_chat_completions_request: false,
|
||||||
llm_providers,
|
|
||||||
llm_provider: None,
|
|
||||||
prompt_guards,
|
prompt_guards,
|
||||||
overrides,
|
overrides,
|
||||||
request_id: None,
|
request_id: None,
|
||||||
mode,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn llm_provider(&self) -> &LlmProvider {
|
|
||||||
self.llm_provider
|
|
||||||
.as_ref()
|
|
||||||
.expect("the provider should be set when asked for it")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn embeddings_store(&self) -> &EmbeddingsStore {
|
fn embeddings_store(&self) -> &EmbeddingsStore {
|
||||||
self.embeddings_store
|
self.embeddings_store
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("embeddings store is not set")
|
.expect("embeddings store is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_llm_provider(&mut self) {
|
|
||||||
let provider_hint = self
|
|
||||||
.get_http_request_header(ARCH_PROVIDER_HINT_HEADER)
|
|
||||||
.map(|provider_name| provider_name.into());
|
|
||||||
|
|
||||||
debug!("llm provider hint: {:?}", provider_hint);
|
|
||||||
self.llm_provider = Some(routing::get_llm_provider(
|
|
||||||
&self.llm_providers,
|
|
||||||
provider_hint,
|
|
||||||
));
|
|
||||||
debug!("selected llm: {}", self.llm_provider.as_ref().unwrap().name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_routing_header(&mut self) {
|
|
||||||
match self.mode {
|
|
||||||
GatewayMode::Prompt => {
|
|
||||||
// in prompt gateway mode, we need to route to llm upstream listener
|
|
||||||
self.add_http_request_header(ARCH_UPSTREAM_HOST_HEADER, ARCH_LLM_UPSTREAM_LISTENER);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.add_http_request_header(ARCH_ROUTING_HEADER, &self.llm_provider().name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn modify_auth_headers(&mut self) -> Result<(), ServerError> {
|
|
||||||
let llm_provider_api_key_value =
|
|
||||||
self.llm_provider()
|
|
||||||
.access_key
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(ServerError::BadRequest {
|
|
||||||
why: format!(
|
|
||||||
"No access key configured for selected LLM Provider \"{}\"",
|
|
||||||
self.llm_provider()
|
|
||||||
),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let authorization_header_value = format!("Bearer {}", llm_provider_api_key_value);
|
|
||||||
|
|
||||||
self.set_http_request_header("Authorization", Some(&authorization_header_value));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_content_length_header(&mut self) {
|
fn delete_content_length_header(&mut self) {
|
||||||
// Remove the Content-Length header because further body manipulations in the gateway logic will invalidate it.
|
// 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.
|
// Server's generally throw away requests whose body length do not match the Content-Length header.
|
||||||
|
|
@ -218,15 +148,6 @@ impl StreamContext {
|
||||||
self.set_http_request_header("content-length", None);
|
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>) {
|
fn send_server_error(&self, error: ServerError, override_status_code: Option<StatusCode>) {
|
||||||
debug!("server error occurred: {}", error);
|
debug!("server error occurred: {}", error);
|
||||||
self.send_http_response(
|
self.send_http_response(
|
||||||
|
|
@ -956,38 +877,10 @@ impl StreamContext {
|
||||||
};
|
};
|
||||||
debug!("arch => openai request body: {}", json_string);
|
debug!("arch => openai request body: {}", json_string);
|
||||||
|
|
||||||
// Tokenize and Ratelimit.
|
|
||||||
if let Err(e) = self.enforce_ratelimits(&chat_completions_request.model, &json_string) {
|
|
||||||
self.send_server_error(
|
|
||||||
ServerError::ExceededRatelimit(e),
|
|
||||||
Some(StatusCode::TOO_MANY_REQUESTS),
|
|
||||||
);
|
|
||||||
self.metrics.ratelimited_rq.increment(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_http_request_body(0, self.request_body_size, &json_string.into_bytes());
|
self.set_http_request_body(0, self.request_body_size, &json_string.into_bytes());
|
||||||
self.resume_http_request();
|
self.resume_http_request();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enforce_ratelimits(
|
|
||||||
&mut self,
|
|
||||||
model: &str,
|
|
||||||
json_string: &str,
|
|
||||||
) -> Result<(), ratelimit::Error> {
|
|
||||||
if let Some(selector) = self.ratelimit_selector.take() {
|
|
||||||
// Tokenize and Ratelimit.
|
|
||||||
if let Ok(token_count) = tokenizer::token_count(model, json_string) {
|
|
||||||
ratelimit::ratelimits(None).read().unwrap().check_limit(
|
|
||||||
model.to_owned(),
|
|
||||||
selector,
|
|
||||||
NonZero::new(token_count as u32).unwrap(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn arch_guard_handler(&mut self, body: Vec<u8>, callout_context: StreamCallContext) {
|
fn arch_guard_handler(&mut self, body: Vec<u8>, callout_context: StreamCallContext) {
|
||||||
debug!("response received for arch guard");
|
debug!("response received for arch guard");
|
||||||
let prompt_guard_resp: PromptGuardResponse = serde_json::from_slice(&body).unwrap();
|
let prompt_guard_resp: PromptGuardResponse = serde_json::from_slice(&body).unwrap();
|
||||||
|
|
@ -1137,23 +1030,17 @@ impl StreamContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HttpContext is the trait that allows the Rust code to interact with HTTP objects.
|
// HttpContext is the trait that allows the Rust code to interact with HTTP objects.
|
||||||
impl HttpContext for StreamContext {
|
impl HttpContext for PromptStreamContext {
|
||||||
// Envoy's HTTP model is event driven. The WASM ABI has given implementors events to hook onto
|
// 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.
|
// the lifecycle of the http request and response.
|
||||||
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
|
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
|
||||||
self.select_llm_provider();
|
|
||||||
self.add_routing_header();
|
|
||||||
if let Err(error) = self.modify_auth_headers() {
|
|
||||||
self.send_server_error(error, Some(StatusCode::BAD_REQUEST));
|
|
||||||
}
|
|
||||||
self.delete_content_length_header();
|
self.delete_content_length_header();
|
||||||
self.save_ratelimit_header();
|
|
||||||
|
|
||||||
self.is_chat_completions_request =
|
self.is_chat_completions_request =
|
||||||
self.get_http_request_header(":path").unwrap_or_default() == CHAT_COMPLETIONS_PATH;
|
self.get_http_request_header(":path").unwrap_or_default() == CHAT_COMPLETIONS_PATH;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"S[{}] req_headers={:?}",
|
"on_http_request_headers S[{}] req_headers={:?}",
|
||||||
self.context_id,
|
self.context_id,
|
||||||
self.get_http_request_headers()
|
self.get_http_request_headers()
|
||||||
);
|
);
|
||||||
|
|
@ -1176,6 +1063,8 @@ impl HttpContext for StreamContext {
|
||||||
|
|
||||||
self.request_body_size = body_size;
|
self.request_body_size = body_size;
|
||||||
|
|
||||||
|
debug!("on_http_request_body S[{}] body_size={}", self.context_id, body_size);
|
||||||
|
|
||||||
// Deserialize body into spec.
|
// Deserialize body into spec.
|
||||||
// Currently OpenAI API.
|
// Currently OpenAI API.
|
||||||
let mut deserialized_body: ChatCompletionsRequest =
|
let mut deserialized_body: ChatCompletionsRequest =
|
||||||
|
|
@ -1203,40 +1092,6 @@ impl HttpContext for StreamContext {
|
||||||
};
|
};
|
||||||
self.is_chat_completions_request = true;
|
self.is_chat_completions_request = true;
|
||||||
|
|
||||||
if self.mode == GatewayMode::Llm {
|
|
||||||
debug!("llm gateway mode, skipping over all prompt targets");
|
|
||||||
|
|
||||||
// remove metadata from the request body
|
|
||||||
deserialized_body.metadata = None;
|
|
||||||
// delete model key from message array
|
|
||||||
for message in deserialized_body.messages.iter_mut() {
|
|
||||||
message.model = None;
|
|
||||||
}
|
|
||||||
deserialized_body
|
|
||||||
.model
|
|
||||||
.clone_from(&self.llm_provider.as_ref().unwrap().model);
|
|
||||||
let chat_completion_request_str = serde_json::to_string(&deserialized_body).unwrap();
|
|
||||||
|
|
||||||
// enforce ratelimits
|
|
||||||
if let Err(e) =
|
|
||||||
self.enforce_ratelimits(&deserialized_body.model, &chat_completion_request_str)
|
|
||||||
{
|
|
||||||
self.send_server_error(
|
|
||||||
ServerError::ExceededRatelimit(e),
|
|
||||||
Some(StatusCode::TOO_MANY_REQUESTS),
|
|
||||||
);
|
|
||||||
self.metrics.ratelimited_rq.increment(1);
|
|
||||||
return Action::Continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"arch => {:?}, body: {}",
|
|
||||||
deserialized_body.model, chat_completion_request_str
|
|
||||||
);
|
|
||||||
self.set_http_request_body(0, body_size, chat_completion_request_str.as_bytes());
|
|
||||||
return Action::Continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.arch_state = match deserialized_body.metadata {
|
self.arch_state = match deserialized_body.metadata {
|
||||||
Some(ref metadata) => {
|
Some(ref metadata) => {
|
||||||
if metadata.contains_key(ARCH_STATE_HEADER) {
|
if metadata.contains_key(ARCH_STATE_HEADER) {
|
||||||
|
|
@ -1250,9 +1105,6 @@ impl HttpContext for StreamContext {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the model based on the chosen LLM Provider
|
|
||||||
deserialized_body.model = String::from(&self.llm_provider().model);
|
|
||||||
|
|
||||||
self.streaming_response = deserialized_body.stream;
|
self.streaming_response = deserialized_body.stream;
|
||||||
if deserialized_body.stream && deserialized_body.stream_options.is_none() {
|
if deserialized_body.stream && deserialized_body.stream_options.is_none() {
|
||||||
deserialized_body.stream_options = Some(StreamOptions {
|
deserialized_body.stream_options = Some(StreamOptions {
|
||||||
|
|
@ -1285,7 +1137,7 @@ impl HttpContext for StreamContext {
|
||||||
self.chat_completions_request = Some(deserialized_body);
|
self.chat_completions_request = Some(deserialized_body);
|
||||||
|
|
||||||
if !prompt_guard_jailbreak_task {
|
if !prompt_guard_jailbreak_task {
|
||||||
debug!("Missing input guard. Making inline call to retrieve");
|
debug!("Missing input guard. Making inline call to retrieve embeddings");
|
||||||
let callout_context = StreamCallContext {
|
let callout_context = StreamCallContext {
|
||||||
response_handler_type: ResponseHandlerType::ArchGuard,
|
response_handler_type: ResponseHandlerType::ArchGuard,
|
||||||
user_message: user_message_str.clone(),
|
user_message: user_message_str.clone(),
|
||||||
|
|
@ -1385,48 +1237,7 @@ impl HttpContext for StreamContext {
|
||||||
.expect("cant get response body");
|
.expect("cant get response body");
|
||||||
|
|
||||||
if self.streaming_response {
|
if self.streaming_response {
|
||||||
let body_str = String::from_utf8(body).expect("body is not utf-8");
|
|
||||||
debug!("streaming response");
|
debug!("streaming response");
|
||||||
let chat_completions_data = match body_str.split_once("data: ") {
|
|
||||||
Some((_, chat_completions_data)) => chat_completions_data,
|
|
||||||
None => {
|
|
||||||
self.send_server_error(
|
|
||||||
ServerError::LogicError(String::from("parsing error in streaming data")),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
return Action::Pause;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let chat_completions_chunk_response: ChatCompletionChunkResponse =
|
|
||||||
match serde_json::from_str(chat_completions_data) {
|
|
||||||
Ok(de) => de,
|
|
||||||
Err(_) => {
|
|
||||||
if chat_completions_data != "[NONE]" {
|
|
||||||
self.send_server_error(
|
|
||||||
ServerError::LogicError(String::from(
|
|
||||||
"error in streaming response",
|
|
||||||
)),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
return Action::Continue;
|
|
||||||
}
|
|
||||||
return Action::Continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(content) = chat_completions_chunk_response
|
|
||||||
.choices
|
|
||||||
.first()
|
|
||||||
.unwrap()
|
|
||||||
.delta
|
|
||||||
.content
|
|
||||||
.as_ref()
|
|
||||||
{
|
|
||||||
let model = &chat_completions_chunk_response.model;
|
|
||||||
let token_count = tokenizer::token_count(model, content).unwrap_or(0);
|
|
||||||
self.response_tokens += token_count;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
debug!("non streaming response");
|
debug!("non streaming response");
|
||||||
let chat_completions_response: ChatCompletionsResponse =
|
let chat_completions_response: ChatCompletionsResponse =
|
||||||
|
|
@ -1512,12 +1323,11 @@ impl HttpContext for StreamContext {
|
||||||
self.context_id, self.response_tokens, end_of_stream
|
self.context_id, self.response_tokens, end_of_stream
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO:: ratelimit based on response tokens.
|
|
||||||
Action::Continue
|
Action::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for StreamContext {
|
impl Context for PromptStreamContext {
|
||||||
fn on_http_call_response(
|
fn on_http_call_response(
|
||||||
&mut self,
|
&mut self,
|
||||||
token_id: u32,
|
token_id: u32,
|
||||||
|
|
@ -1563,7 +1373,7 @@ impl Context for StreamContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client for StreamContext {
|
impl Client for PromptStreamContext {
|
||||||
type CallContext = StreamCallContext;
|
type CallContext = StreamCallContext;
|
||||||
|
|
||||||
fn callouts(&self) -> &RefCell<HashMap<u32, Self::CallContext>> {
|
fn callouts(&self) -> &RefCell<HashMap<u32, Self::CallContext>> {
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "llm_gateway",
|
"name": "llm_gateway",
|
||||||
"path": "crates/prompt_gateway"
|
"path": "crates/llm_gateway"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "arch/tools",
|
"name": "arch/tools",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue