use crate::common::{ChatCompletionRequest, ChatCompletionResponse, chat_response_to_llm_response};
use crate::model_selection::ModelMetadata;
use crate::provider_api::{
CostClass, DataSovereignty, LlmError, LlmProvider, LlmRequest, LlmResponse,
};
use crate::secret::{EnvSecretProvider, SecretProvider, SecretString};
#[derive(Debug, Clone)]
pub struct KongRoute {
pub name: String,
pub upstream_provider: String,
pub upstream_model: String,
pub cost_class: CostClass,
pub typical_latency_ms: u32,
pub quality: f64,
pub has_reasoning: bool,
pub supports_tool_use: bool,
pub supports_vision: bool,
pub supports_structured_output: bool,
pub supports_code: bool,
pub supports_web_search: bool,
pub supports_multilingual: bool,
pub context_tokens: usize,
pub data_sovereignty: DataSovereignty,
}
impl KongRoute {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
upstream_provider: "unknown".into(),
upstream_model: "unknown".into(),
cost_class: CostClass::Medium,
typical_latency_ms: 5000,
quality: 0.80,
has_reasoning: false,
supports_tool_use: false,
supports_vision: false,
supports_structured_output: false,
supports_code: false,
supports_web_search: false,
supports_multilingual: false,
context_tokens: 128_000,
data_sovereignty: DataSovereignty::Any,
}
}
#[must_use]
pub fn upstream(mut self, provider: impl Into<String>, model: impl Into<String>) -> Self {
self.upstream_provider = provider.into();
self.upstream_model = model.into();
self
}
#[must_use]
pub fn cost(mut self, cost: CostClass) -> Self {
self.cost_class = cost;
self
}
#[must_use]
pub fn latency_ms(mut self, ms: u32) -> Self {
self.typical_latency_ms = ms;
self
}
#[must_use]
pub fn quality(mut self, q: f64) -> Self {
self.quality = q;
self
}
#[must_use]
pub fn reasoning(mut self, v: bool) -> Self {
self.has_reasoning = v;
self
}
#[must_use]
pub fn tool_use(mut self, v: bool) -> Self {
self.supports_tool_use = v;
self
}
#[must_use]
pub fn vision(mut self, v: bool) -> Self {
self.supports_vision = v;
self
}
#[must_use]
pub fn structured_output(mut self, v: bool) -> Self {
self.supports_structured_output = v;
self
}
#[must_use]
pub fn code(mut self, v: bool) -> Self {
self.supports_code = v;
self
}
#[must_use]
pub fn web_search(mut self, v: bool) -> Self {
self.supports_web_search = v;
self
}
#[must_use]
pub fn multilingual(mut self, v: bool) -> Self {
self.supports_multilingual = v;
self
}
#[must_use]
pub fn context_tokens(mut self, tokens: usize) -> Self {
self.context_tokens = tokens;
self
}
#[must_use]
pub fn sovereignty(mut self, ds: DataSovereignty) -> Self {
self.data_sovereignty = ds;
self
}
#[must_use]
pub fn to_model_metadata(&self) -> ModelMetadata {
ModelMetadata::new(
"kong",
&self.name,
self.cost_class,
self.typical_latency_ms,
self.quality,
)
.with_reasoning(self.has_reasoning)
.with_tool_use(self.supports_tool_use)
.with_vision(self.supports_vision)
.with_structured_output(self.supports_structured_output)
.with_code(self.supports_code)
.with_web_search(self.supports_web_search)
.with_multilingual(self.supports_multilingual)
.with_context_tokens(self.context_tokens)
.with_data_sovereignty(self.data_sovereignty)
}
}
pub struct KongProvider {
gateway_url: String,
api_key: SecretString,
route: KongRoute,
client: reqwest::blocking::Client,
}
impl KongProvider {
#[must_use]
pub fn new(
gateway_url: impl Into<String>,
api_key: impl Into<String>,
route: KongRoute,
) -> Self {
Self {
gateway_url: gateway_url.into(),
api_key: SecretString::new(api_key),
route,
client: reqwest::blocking::Client::new(),
}
}
pub fn from_env(route: KongRoute) -> Result<Self, LlmError> {
Self::from_secret_provider(&EnvSecretProvider, route)
}
pub fn from_secret_provider(
secrets: &dyn SecretProvider,
route: KongRoute,
) -> Result<Self, LlmError> {
let gateway_url = secrets
.get_secret("KONG_AI_GATEWAY_URL")
.map_err(|e| LlmError::auth(format!("KONG_AI_GATEWAY_URL: {e}")))?;
let api_key = secrets
.get_secret("KONG_API_KEY")
.map_err(|e| LlmError::auth(format!("KONG_API_KEY: {e}")))?;
Ok(Self {
gateway_url: gateway_url.expose().to_string(),
api_key,
route,
client: reqwest::blocking::Client::new(),
})
}
#[must_use]
pub fn with_client(mut self, client: reqwest::blocking::Client) -> Self {
self.client = client;
self
}
#[must_use]
pub fn route(&self) -> &KongRoute {
&self.route
}
#[must_use]
pub fn model_metadata(&self) -> ModelMetadata {
self.route.to_model_metadata()
}
}
impl LlmProvider for KongProvider {
fn name(&self) -> &'static str {
"kong"
}
fn model(&self) -> &str {
&self.route.name
}
fn complete(&self, request: &LlmRequest) -> Result<LlmResponse, LlmError> {
let url = format!("{}/ai/chat/completions", self.gateway_url);
let body = ChatCompletionRequest::from_llm_request(self.route.name.clone(), request);
let response = self
.client
.post(&url)
.header("x-api-key", self.api_key.expose())
.header("Content-Type", "application/json")
.json(&body)
.send()
.map_err(|e| LlmError::network(format!("Kong request failed: {e}")))?;
if !response.status().is_success() {
let code = response.status().as_u16();
let text = response.text().unwrap_or_default();
return Err(if code == 429 {
LlmError::rate_limit(text)
} else {
LlmError::provider(format!("Kong returned {code}: {text}"))
});
}
let chat_response: ChatCompletionResponse = response
.json()
.map_err(|e| LlmError::parse(format!("Failed to parse Kong response: {e}")))?;
chat_response_to_llm_response(chat_response)
}
fn provenance(&self, request_id: &str) -> String {
format!(
"kong:{}:{}:{}",
self.route.name, self.route.upstream_provider, request_id
)
}
}
pub struct KongGateway {
gateway_url: String,
api_key: SecretString,
client: reqwest::blocking::Client,
}
impl KongGateway {
#[must_use]
pub fn new(gateway_url: impl Into<String>, api_key: impl Into<String>) -> Self {
Self {
gateway_url: gateway_url.into(),
api_key: SecretString::new(api_key),
client: reqwest::blocking::Client::new(),
}
}
pub fn from_env() -> Result<Self, LlmError> {
Self::from_secret_provider(&EnvSecretProvider)
}
pub fn from_secret_provider(secrets: &dyn SecretProvider) -> Result<Self, LlmError> {
let gateway_url = secrets
.get_secret("KONG_AI_GATEWAY_URL")
.map_err(|e| LlmError::auth(format!("KONG_AI_GATEWAY_URL: {e}")))?;
let api_key = secrets
.get_secret("KONG_API_KEY")
.map_err(|e| LlmError::auth(format!("KONG_API_KEY: {e}")))?;
Ok(Self {
gateway_url: gateway_url.expose().to_string(),
api_key,
client: reqwest::blocking::Client::new(),
})
}
#[must_use]
pub fn llm_provider(&self, route: KongRoute) -> KongProvider {
KongProvider {
gateway_url: self.gateway_url.clone(),
api_key: self.api_key.clone(),
route,
client: self.client.clone(),
}
}
#[must_use]
pub fn mcp_url(&self, service_name: &str) -> String {
format!("{}/mcp/{service_name}", self.gateway_url)
}
#[must_use]
pub fn api_url(&self, path: &str) -> String {
format!("{}/{path}", self.gateway_url)
}
#[must_use]
pub fn auth_header(&self) -> (&'static str, String) {
("x-api-key", self.api_key.expose().to_string())
}
#[must_use]
pub fn gateway_url(&self) -> &str {
&self.gateway_url
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kong_provider_name_and_model() {
let route = KongRoute::new("gpt-4-route")
.upstream("openai", "gpt-4")
.cost(CostClass::Medium)
.quality(0.90)
.reasoning(true);
let provider = KongProvider::new("https://kong.example.com", "test-key", route);
assert_eq!(provider.name(), "kong");
assert_eq!(provider.model(), "gpt-4-route");
}
#[test]
fn gateway_builds_urls() {
let gw = KongGateway::new("https://kong.example.com", "key");
assert_eq!(
gw.mcp_url("vendor-registry"),
"https://kong.example.com/mcp/vendor-registry"
);
assert_eq!(
gw.api_url("vendors/v1/list"),
"https://kong.example.com/vendors/v1/list"
);
assert_eq!(gw.auth_header().0, "x-api-key");
}
#[test]
fn gateway_creates_llm_provider() {
let gw = KongGateway::new("https://kong.example.com", "key");
let route = KongRoute::new("test-route").cost(CostClass::Low);
let provider = gw.llm_provider(route);
assert_eq!(provider.name(), "kong");
assert_eq!(provider.model(), "test-route");
}
#[test]
fn route_produces_model_metadata() {
let route = KongRoute::new("claude-route")
.upstream("anthropic", "claude-sonnet-4-6")
.cost(CostClass::Low)
.latency_ms(3000)
.quality(0.93)
.reasoning(true)
.tool_use(true)
.vision(true)
.context_tokens(200_000);
let metadata = route.to_model_metadata();
assert_eq!(metadata.provider, "kong");
assert_eq!(metadata.model, "claude-route");
assert_eq!(metadata.cost_class, CostClass::Low);
assert!(metadata.has_reasoning);
assert!(metadata.supports_tool_use);
assert!(metadata.supports_vision);
assert_eq!(metadata.context_tokens, 200_000);
}
}