use reqwest::Client;
use url::Url;
use crate::agent::{Agent, AgentMode};
use crate::recipe::Recipe;
#[derive(Debug, Clone)]
pub struct AgentOvenClient {
base_url: Url,
http: Client,
api_key: Option<String>,
kitchen_id: Option<String>,
}
impl AgentOvenClient {
pub fn new(base_url: &str) -> anyhow::Result<Self> {
Ok(Self {
base_url: Url::parse(base_url)?,
http: Client::new(),
api_key: None,
kitchen_id: None,
})
}
pub fn from_env() -> anyhow::Result<Self> {
let cfg = crate::config::AgentOvenConfig::load();
Self::from_config(&cfg)
}
pub fn from_config(cfg: &crate::config::AgentOvenConfig) -> anyhow::Result<Self> {
let mut client = Self::new(&cfg.url)?;
client.api_key = cfg.auth_credential().map(|s| s.to_string());
client.kitchen_id = cfg.kitchen.clone();
Ok(client)
}
pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
pub fn with_kitchen(mut self, kitchen_id: impl Into<String>) -> Self {
self.kitchen_id = Some(kitchen_id.into());
self
}
pub async fn register(&self, agent: &Agent) -> anyhow::Result<Agent> {
let url = self.url("/api/v1/agents");
let mut body = serde_json::json!({
"name": agent.name,
"description": agent.description,
"framework": agent.framework,
});
let obj = body.as_object_mut().unwrap();
if !agent.version.is_empty() {
obj.insert("version".into(), agent.version.clone().into());
}
if !agent.model_provider.is_empty() {
obj.insert("model_provider".into(), agent.model_provider.clone().into());
}
if !agent.model_name.is_empty() {
obj.insert("model_name".into(), agent.model_name.clone().into());
}
if let Some(ref bp) = agent.backup_provider {
obj.insert("backup_provider".into(), bp.clone().into());
}
if let Some(ref bm) = agent.backup_model {
obj.insert("backup_model".into(), bm.clone().into());
}
if let Some(ref sp) = agent.system_prompt {
obj.insert("system_prompt".into(), sp.clone().into());
}
if let Some(mt) = agent.max_turns {
obj.insert("max_turns".into(), mt.into());
}
if !agent.skills.is_empty() {
obj.insert("skills".into(), serde_json::to_value(&agent.skills)?);
}
if !agent.ingredients.is_empty() {
obj.insert(
"ingredients".into(),
serde_json::to_value(&agent.ingredients)?,
);
}
if !agent.tags.is_empty() {
obj.insert("tags".into(), serde_json::to_value(&agent.tags)?);
}
if !agent.guardrails.is_empty() {
obj.insert(
"guardrails".into(),
serde_json::to_value(&agent.guardrails)?,
);
}
if agent.mode != AgentMode::default() {
obj.insert("mode".into(), serde_json::to_value(&agent.mode)?);
}
let resp = self
.authed_request(self.http.post(url))
.json(&body)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_agent(&self, name: &str, version: Option<&str>) -> anyhow::Result<Agent> {
let path = match version {
Some(v) => format!("/api/v1/agents/{name}/versions/{v}"),
None => format!("/api/v1/agents/{name}"),
};
let url = self.url(&path);
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_agents(&self) -> anyhow::Result<Vec<Agent>> {
let url = self.url("/api/v1/agents");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn bake(&self, agent: &Agent, environment: &str) -> anyhow::Result<Agent> {
let url = self.url(&format!("/api/v1/agents/{}/bake", agent.name));
let resp = self
.authed_request(self.http.post(url))
.json(&serde_json::json!({
"version": agent.version,
"environment": environment,
}))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn rewarm(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/rewarm"));
let resp = self
.authed_request(self.http.post(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn create_recipe(&self, recipe: &Recipe) -> anyhow::Result<Recipe> {
let url = self.url("/api/v1/recipes");
let resp = self
.authed_request(self.http.post(url))
.json(recipe)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn bake_recipe(
&self,
recipe_name: &str,
input: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/recipes/{recipe_name}/bake"));
let resp = self
.authed_request(self.http.post(url))
.json(&input)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn update_agent(
&self,
name: &str,
updates: serde_json::Value,
) -> anyhow::Result<Agent> {
let url = self.url(&format!("/api/v1/agents/{name}"));
let resp = self
.authed_request(self.http.put(url))
.json(&updates)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn delete_agent(&self, name: &str) -> anyhow::Result<()> {
let url = self.url(&format!("/api/v1/agents/{name}"));
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn recook_agent(
&self,
name: &str,
edits: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/recook"));
let resp = self
.authed_request(self.http.post(url))
.json(&edits)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn cool_agent(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/cool"));
let resp = self
.authed_request(self.http.post(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn retire_agent(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/retire"));
let resp = self
.authed_request(self.http.post(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn test_agent(
&self,
name: &str,
message: &str,
thinking: bool,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/test"));
let resp = self
.authed_request(self.http.post(url))
.json(&serde_json::json!({
"message": message,
"thinking_enabled": thinking,
}))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn invoke_agent(
&self,
name: &str,
message: &str,
variables: Option<serde_json::Value>,
thinking: bool,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/invoke"));
let mut body = serde_json::json!({
"message": message,
"thinking_enabled": thinking,
});
if let Some(vars) = variables {
body["variables"] = vars;
}
let resp = self
.authed_request(self.http.post(url))
.json(&body)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn agent_config(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/config"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn agent_card(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{name}/card"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn agent_versions(&self, name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url(&format!("/api/v1/agents/{name}/versions"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_providers(&self) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url("/api/v1/models/providers");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn add_provider(
&self,
provider: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/models/providers");
let resp = self
.authed_request(self.http.post(url))
.json(&provider)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_provider(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/models/providers/{name}"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn update_provider(
&self,
name: &str,
provider: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/models/providers/{name}"));
let resp = self
.authed_request(self.http.put(url))
.json(&provider)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn delete_provider(&self, name: &str) -> anyhow::Result<()> {
let url = self.url(&format!("/api/v1/models/providers/{name}"));
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn test_provider(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/models/providers/{name}/test"));
let resp = self
.authed_request(self.http.post(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn discover_provider(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/models/providers/{name}/discover"));
let resp = self
.authed_request(self.http.post(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_tools(&self) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url("/api/v1/tools");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn add_tool(&self, tool: serde_json::Value) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/tools");
let resp = self
.authed_request(self.http.post(url))
.json(&tool)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn bulk_add_tools(
&self,
payload: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/tools/bulk");
let resp = self
.authed_request(self.http.post(url))
.json(&payload)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_tool(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/tools/{name}"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn update_tool(
&self,
name: &str,
tool: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/tools/{name}"));
let resp = self
.authed_request(self.http.put(url))
.json(&tool)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn delete_tool(&self, name: &str) -> anyhow::Result<()> {
let url = self.url(&format!("/api/v1/tools/{name}"));
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn list_prompts(&self) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url("/api/v1/prompts");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn add_prompt(&self, prompt: serde_json::Value) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/prompts");
let resp = self
.authed_request(self.http.post(url))
.json(&prompt)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_prompt(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/prompts/{name}"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn update_prompt(
&self,
name: &str,
prompt: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/prompts/{name}"));
let resp = self
.authed_request(self.http.put(url))
.json(&prompt)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn delete_prompt(&self, name: &str) -> anyhow::Result<()> {
let url = self.url(&format!("/api/v1/prompts/{name}"));
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn validate_prompt(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/prompts/{name}/validate"));
let resp = self
.authed_request(self.http.post(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn prompt_versions(&self, name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url(&format!("/api/v1/prompts/{name}/versions"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_kitchens(&self) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url("/api/v1/kitchens");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_kitchen(&self, id: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/kitchens/{id}"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn create_kitchen(
&self,
kitchen: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/kitchens");
let resp = self
.authed_request(self.http.post(url))
.json(&kitchen)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn delete_kitchen(&self, id: &str) -> anyhow::Result<()> {
let url = self.url(&format!("/api/v1/kitchens/{id}"));
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn server_info(&self) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/info");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_settings(&self) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/settings");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn update_settings(
&self,
settings: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/settings");
let resp = self
.authed_request(self.http.put(url))
.json(&settings)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_sessions(&self, agent_name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url(&format!("/api/v1/agents/{agent_name}/sessions"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn create_session(&self, agent_name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/agents/{agent_name}/sessions"));
let resp = self
.authed_request(self.http.post(url))
.json(&serde_json::json!({}))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_session(
&self,
agent_name: &str,
session_id: &str,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!(
"/api/v1/agents/{agent_name}/sessions/{session_id}"
));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn delete_session(&self, agent_name: &str, session_id: &str) -> anyhow::Result<()> {
let url = self.url(&format!(
"/api/v1/agents/{agent_name}/sessions/{session_id}"
));
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn send_session_message(
&self,
agent_name: &str,
session_id: &str,
message: &str,
thinking: bool,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!(
"/api/v1/agents/{agent_name}/sessions/{session_id}/messages"
));
let resp = self
.authed_request(self.http.post(url))
.json(&serde_json::json!({
"message": message,
"thinking_enabled": thinking,
}))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn rag_query(&self, query: serde_json::Value) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/rag/query");
let resp = self
.authed_request(self.http.post(url))
.json(&query)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn rag_ingest(
&self,
request: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/rag/ingest");
let resp = self
.authed_request(self.http.post(url))
.json(&request)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_traces(
&self,
agent: Option<&str>,
limit: u32,
) -> anyhow::Result<Vec<serde_json::Value>> {
let mut url = self.url("/api/v1/traces");
{
let mut query = url.query_pairs_mut();
query.append_pair("limit", &limit.to_string());
if let Some(a) = agent {
query.append_pair("agent", a);
}
}
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_trace(&self, trace_id: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/traces/{trace_id}"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn model_catalog(&self) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url("/api/v1/models/catalog");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn catalog_refresh(&self) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/models/catalog/refresh");
let resp = self
.authed_request(self.http.post(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn model_cost(&self) -> anyhow::Result<serde_json::Value> {
let url = self.url("/api/v1/models/cost");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn get_recipe(&self, name: &str) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!("/api/v1/recipes/{name}"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_recipes(&self) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url("/api/v1/recipes");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn delete_recipe(&self, name: &str) -> anyhow::Result<()> {
let url = self.url(&format!("/api/v1/recipes/{name}"));
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn recipe_runs(&self, name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url(&format!("/api/v1/recipes/{name}/runs"));
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn approve_gate(
&self,
recipe: &str,
run_id: &str,
step_name: &str,
approved: bool,
comment: Option<&str>,
) -> anyhow::Result<serde_json::Value> {
let url = self.url(&format!(
"/api/v1/recipes/{recipe}/runs/{run_id}/gates/{step_name}/approve"
));
let mut body = serde_json::json!({ "approved": approved });
if let Some(c) = comment {
body["comment"] = serde_json::json!(c);
}
let resp = self
.authed_request(self.http.post(url))
.json(&body)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn list_audit(&self, limit: u32) -> anyhow::Result<Vec<serde_json::Value>> {
let mut url = self.url("/api/v1/audit");
url.query_pairs_mut()
.append_pair("limit", &limit.to_string());
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn guardrail_kinds(&self) -> anyhow::Result<Vec<serde_json::Value>> {
let url = self.url("/api/v1/guardrails/kinds");
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn raw_get<T: serde::de::DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
let url = self.url(path);
let resp = self
.authed_request(self.http.get(url))
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn raw_post<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &serde_json::Value,
) -> anyhow::Result<T> {
let url = self.url(path);
let resp = self
.authed_request(self.http.post(url))
.json(body)
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}
pub async fn raw_delete(&self, path: &str) -> anyhow::Result<()> {
let url = self.url(path);
self.authed_request(self.http.delete(url))
.send()
.await?
.error_for_status()?;
Ok(())
}
fn url(&self, path: &str) -> Url {
self.base_url.join(path).expect("Invalid URL path")
}
fn authed_request(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
let mut b = builder;
if let Some(ref key) = self.api_key {
b = b.bearer_auth(key);
}
if let Some(ref kitchen) = self.kitchen_id {
b = b.header("X-Kitchen-Id", kitchen.as_str());
}
b
}
}