refactor(service): clean up agent modules, use agent crate types
- service now delegates model/provider/pricing logic to agent crate - ChatService built at startup with EmbedService (graceful degradation) - RoomService wired with EmbedService for Qdrant embedding - Add error types for embedding service
This commit is contained in:
parent
10c0cc007b
commit
881fbdb6ea
@ -38,7 +38,6 @@ session = { workspace = true }
|
||||
argon2 = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
sea-orm = { workspace = true, features = [] }
|
||||
async-openai = { version = "0.34.0", features = ["chat-completion"] }
|
||||
reqwest = { workspace = true, features = ["json", "native-tls"] }
|
||||
base64 = { workspace = true }
|
||||
rsa = { workspace = true }
|
||||
|
||||
@ -1,218 +1,24 @@
|
||||
//! AI usage billing — records token costs against a project or workspace balance.
|
||||
//!
|
||||
//! Called by service-layer code after each successful AI call. If the project
|
||||
//! belongs to a workspace, the cost is deducted from the workspace's shared quota
|
||||
//! (workspace_billing). Otherwise it is deducted from the project's own quota.
|
||||
//!
|
||||
//! 1. Queries the most recent active price for `model_id`.
|
||||
//! 2. Computes `cost = (input/1000)*input_price + (output/1000)*output_price`.
|
||||
//! 3. Determines whether to bill the project or its workspace.
|
||||
//! 4. Writes a billing_history entry and decrements the appropriate balance.
|
||||
//! Billing — delegates to agent crate.
|
||||
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use models::agents::model_pricing;
|
||||
use models::projects::project;
|
||||
use models::projects::project_billing;
|
||||
use models::projects::project_billing_history;
|
||||
use models::workspaces::workspace_billing;
|
||||
use models::workspaces::workspace_billing_history;
|
||||
use rust_decimal::Decimal;
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Breakdown of a billing record.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BillingRecord {
|
||||
/// Total cost in the billing currency.
|
||||
pub cost: f64,
|
||||
pub currency: String,
|
||||
pub input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
/// Record AI usage for a project.
|
||||
///
|
||||
/// If the project belongs to a workspace, the cost is deducted from the
|
||||
/// workspace's shared quota. Otherwise it is deducted from the project's own
|
||||
/// billing balance.
|
||||
///
|
||||
/// Returns an error if there is insufficient balance.
|
||||
pub async fn record_ai_usage(
|
||||
&self,
|
||||
project_uid: Uuid,
|
||||
model_id: Uuid,
|
||||
input_tokens: i64,
|
||||
output_tokens: i64,
|
||||
) -> Result<BillingRecord, AppError> {
|
||||
// 1. Look up the active price for this model.
|
||||
let pricing = model_pricing::Entity::find()
|
||||
.filter(model_pricing::Column::ModelVersionId.eq(model_id))
|
||||
.order_by_desc(model_pricing::Column::EffectiveFrom)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
AppError::InternalServerError(
|
||||
"No pricing record found for this model. Please configure AI model pricing first."
|
||||
.into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 2. Compute cost using Decimal arithmetic.
|
||||
let input_price: Decimal = pricing
|
||||
.input_price_per_1k_tokens
|
||||
.parse()
|
||||
.unwrap_or(Decimal::ZERO);
|
||||
let output_price: Decimal = pricing
|
||||
.output_price_per_1k_tokens
|
||||
.parse()
|
||||
.unwrap_or(Decimal::ZERO);
|
||||
let tokens_i = Decimal::from(input_tokens);
|
||||
let tokens_o = Decimal::from(output_tokens);
|
||||
let thousand = Decimal::from(1000);
|
||||
|
||||
let total_cost: f64 = ((tokens_i / thousand) * input_price
|
||||
+ (tokens_o / thousand) * output_price)
|
||||
.to_string()
|
||||
.parse()
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let currency = pricing.currency.clone();
|
||||
|
||||
// 3. Determine whether to bill the project or its workspace.
|
||||
let proj = project::Entity::find_by_id(project_uid)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Project not found".to_string()))?;
|
||||
|
||||
if let Some(workspace_id) = proj.workspace_id {
|
||||
// ── Workspace-shared quota ──────────────────────────────────
|
||||
let current = workspace_billing::Entity::find_by_id(workspace_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
AppError::NotFound("Workspace billing account not found".to_string())
|
||||
})?;
|
||||
|
||||
let current_balance: f64 = current.balance.to_string().parse().unwrap_or(0.0);
|
||||
|
||||
if current_balance < total_cost {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Insufficient workspace billing balance. Required: {:.4} {}, Available: {:.4} {}",
|
||||
total_cost, currency, current_balance, currency
|
||||
)));
|
||||
}
|
||||
|
||||
let amount_dec = Decimal::from_f64_retain(-total_cost).unwrap_or(Decimal::ZERO);
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// Insert workspace billing history.
|
||||
let _ = workspace_billing_history::ActiveModel {
|
||||
uid: Set(Uuid::new_v4()),
|
||||
workspace_id: Set(workspace_id),
|
||||
user_id: Set(Some(proj.created_by)),
|
||||
amount: Set(amount_dec),
|
||||
currency: Set(currency.clone()),
|
||||
reason: Set(format!("ai_usage:{}", project_uid)),
|
||||
extra: Set(Some(serde_json::json!({
|
||||
"project_id": project_uid.to_string(),
|
||||
"model_id": model_id.to_string(),
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
}))),
|
||||
created_at: Set(now),
|
||||
}
|
||||
.insert(&self.db)
|
||||
.await;
|
||||
|
||||
// Deduct from workspace balance.
|
||||
let new_balance =
|
||||
Decimal::from_f64_retain(current_balance - total_cost).unwrap_or(Decimal::ZERO);
|
||||
let mut updated: workspace_billing::ActiveModel = current.into();
|
||||
updated.balance = Set(new_balance);
|
||||
updated.updated_at = Set(now);
|
||||
updated.update(&self.db).await?;
|
||||
|
||||
tracing::info!(
|
||||
project_id = %project_uid,
|
||||
model_id = %model_id,
|
||||
input_tokens = input_tokens,
|
||||
output_tokens = output_tokens,
|
||||
cost = %total_cost,
|
||||
currency = %currency,
|
||||
workspace_id = %workspace_id.to_string(),
|
||||
"ai_usage_recorded"
|
||||
);
|
||||
|
||||
Ok(BillingRecord {
|
||||
cost: total_cost,
|
||||
currency,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
})
|
||||
} else {
|
||||
// ── Project-owned quota ─────────────────────────────────────
|
||||
let amount_dec = Decimal::from_f64_retain(-total_cost).unwrap_or(Decimal::ZERO);
|
||||
|
||||
let _ = project_billing_history::ActiveModel {
|
||||
uid: Set(Uuid::new_v4()),
|
||||
project: Set(project_uid),
|
||||
user: Set(None),
|
||||
amount: Set(amount_dec),
|
||||
currency: Set(currency.clone()),
|
||||
reason: Set("ai_usage".to_string()),
|
||||
extra: Set(Some(serde_json::json!({
|
||||
"model_id": model_id.to_string(),
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
}))),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&self.db)
|
||||
.await;
|
||||
|
||||
let current = project_billing::Entity::find_by_id(project_uid)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
AppError::NotFound("Project billing account not found".to_string())
|
||||
})?;
|
||||
|
||||
let current_balance: f64 = current.balance.to_string().parse().unwrap_or(0.0);
|
||||
|
||||
if current_balance < total_cost {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Insufficient billing balance. Required: {:.4} {}, Available: {:.4} {}",
|
||||
total_cost, currency, current_balance, currency
|
||||
)));
|
||||
}
|
||||
|
||||
let new_balance =
|
||||
Decimal::from_f64_retain(current_balance - total_cost).unwrap_or(Decimal::ZERO);
|
||||
let mut updated: project_billing::ActiveModel = current.into();
|
||||
updated.balance = Set(new_balance);
|
||||
updated.update(&self.db).await?;
|
||||
|
||||
tracing::info!(
|
||||
project_id = %project_uid,
|
||||
model_id = %model_id,
|
||||
input_tokens = input_tokens,
|
||||
output_tokens = output_tokens,
|
||||
cost = %total_cost,
|
||||
currency = %currency,
|
||||
"ai_usage_recorded"
|
||||
);
|
||||
|
||||
Ok(BillingRecord {
|
||||
cost: total_cost,
|
||||
currency,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
})
|
||||
}
|
||||
) -> Result<agent::billing::BillingRecord, AppError> {
|
||||
Ok(agent::billing::record_ai_usage(
|
||||
&self.db,
|
||||
project_uid,
|
||||
model_id,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::billing::BillingRecord;
|
||||
use agent::billing::BillingRecord;
|
||||
|
||||
const AI_BOT_UUID: Uuid = Uuid::nil();
|
||||
|
||||
@ -401,16 +401,7 @@ async fn call_ai_model(
|
||||
|
||||
let client_config = agent::AiClientConfig::new(api_key).with_base_url(base_url);
|
||||
|
||||
let messages = vec![
|
||||
async_openai::types::chat::ChatCompletionRequestMessage::User(
|
||||
async_openai::types::chat::ChatCompletionRequestUserMessage {
|
||||
content: async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
|
||||
prompt.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
let messages = vec![agent::ChatRequestMessage::user(prompt.to_string())];
|
||||
|
||||
agent::call_with_params(&messages, model_name, &client_config, 0.2, 8192, None, None, None)
|
||||
.await
|
||||
|
||||
@ -109,15 +109,7 @@ async fn call_ai_for_triage(
|
||||
let client_config =
|
||||
::agent::AiClientConfig::new(api_key).with_base_url(base_url);
|
||||
|
||||
let messages = vec![async_openai::types::chat::ChatCompletionRequestMessage::User(
|
||||
async_openai::types::chat::ChatCompletionRequestUserMessage {
|
||||
content:
|
||||
async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
|
||||
prompt.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
)];
|
||||
let messages = vec![agent::ChatRequestMessage::user(prompt.to_string())];
|
||||
|
||||
let response = ::agent::call_with_params(
|
||||
&messages,
|
||||
|
||||
@ -1,197 +1,54 @@
|
||||
//! Model management — delegates to agent crate.
|
||||
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::Utc;
|
||||
use models::agents::model;
|
||||
use models::agents::{
|
||||
ModelCapability, ModelModality, ModelStatus,
|
||||
model::{Column as MColumn, Entity as MEntity},
|
||||
model_provider::Entity as ProviderEntity,
|
||||
};
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::provider::require_system_caller;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct CreateModelRequest {
|
||||
pub provider_id: Uuid,
|
||||
pub name: String,
|
||||
pub modality: String,
|
||||
pub capability: String,
|
||||
pub context_length: i64,
|
||||
pub max_output_tokens: Option<i64>,
|
||||
pub training_cutoff: Option<chrono::DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub is_open_source: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct UpdateModelRequest {
|
||||
pub display_name: Option<String>,
|
||||
pub modality: Option<String>,
|
||||
pub capability: Option<String>,
|
||||
pub context_length: Option<i64>,
|
||||
pub max_output_tokens: Option<i64>,
|
||||
pub training_cutoff: Option<chrono::DateTime<Utc>>,
|
||||
pub is_open_source: Option<bool>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ModelResponse {
|
||||
pub id: Uuid,
|
||||
pub provider_id: Uuid,
|
||||
pub name: String,
|
||||
pub modality: String,
|
||||
pub capability: String,
|
||||
pub context_length: i64,
|
||||
pub max_output_tokens: Option<i64>,
|
||||
pub training_cutoff: Option<chrono::DateTime<Utc>>,
|
||||
pub is_open_source: bool,
|
||||
pub status: String,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<model::Model> for ModelResponse {
|
||||
fn from(m: model::Model) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
provider_id: m.provider_id,
|
||||
name: m.name,
|
||||
modality: m.modality,
|
||||
capability: m.capability,
|
||||
context_length: m.context_length,
|
||||
max_output_tokens: m.max_output_tokens,
|
||||
training_cutoff: m.training_cutoff,
|
||||
is_open_source: m.is_open_source,
|
||||
status: m.status,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use agent::model::model_entry::{CreateModelRequest, ModelResponse, UpdateModelRequest};
|
||||
|
||||
impl AppService {
|
||||
pub async fn agent_model_list(
|
||||
&self,
|
||||
provider_id: Option<Uuid>,
|
||||
_ctx: &Session,
|
||||
) -> Result<Vec<ModelResponse>, AppError> {
|
||||
let mut query = MEntity::find().order_by_asc(MColumn::Name);
|
||||
if let Some(pid) = provider_id {
|
||||
query = query.filter(MColumn::ProviderId.eq(pid));
|
||||
}
|
||||
let models = query.all(&self.db).await?;
|
||||
Ok(models.into_iter().map(ModelResponse::from).collect())
|
||||
) -> Result<Vec<agent::model::model_entry::ModelResponse>, AppError> {
|
||||
Ok(agent::model::model_entry::list_models(&self.db, provider_id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_get(
|
||||
&self,
|
||||
id: Uuid,
|
||||
_ctx: &Session,
|
||||
) -> Result<ModelResponse, AppError> {
|
||||
let model = MEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Model not found".to_string()))?;
|
||||
Ok(ModelResponse::from(model))
|
||||
) -> Result<agent::model::model_entry::ModelResponse, AppError> {
|
||||
Ok(agent::model::model_entry::get_model(&self.db, id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_create(
|
||||
&self,
|
||||
request: CreateModelRequest,
|
||||
request: agent::model::model_entry::CreateModelRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
ProviderEntity::find_by_id(request.provider_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Provider not found".to_string()))?;
|
||||
|
||||
let _ = request
|
||||
.modality
|
||||
.parse::<ModelModality>()
|
||||
.map_err(|_| AppError::BadRequest("Invalid modality".to_string()))?;
|
||||
let _ = request
|
||||
.capability
|
||||
.parse::<ModelCapability>()
|
||||
.map_err(|_| AppError::BadRequest("Invalid capability".to_string()))?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = model::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
provider_id: Set(request.provider_id),
|
||||
name: Set(request.name),
|
||||
modality: Set(request.modality),
|
||||
capability: Set(request.capability),
|
||||
context_length: Set(request.context_length),
|
||||
max_output_tokens: Set(request.max_output_tokens),
|
||||
training_cutoff: Set(request.training_cutoff),
|
||||
is_open_source: Set(request.is_open_source),
|
||||
status: Set(ModelStatus::Active.to_string()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
let model = active.insert(&self.db).await?;
|
||||
Ok(ModelResponse::from(model))
|
||||
) -> Result<agent::model::model_entry::ModelResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::model_entry::create_model(&self.db, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
request: UpdateModelRequest,
|
||||
request: agent::model::model_entry::UpdateModelRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let model = MEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Model not found".to_string()))?;
|
||||
|
||||
let mut active: model::ActiveModel = model.into();
|
||||
if let Some(modality) = request.modality {
|
||||
let _ = modality
|
||||
.parse::<ModelModality>()
|
||||
.map_err(|_| AppError::BadRequest("Invalid modality".to_string()))?;
|
||||
active.modality = Set(modality);
|
||||
}
|
||||
if let Some(capability) = request.capability {
|
||||
let _ = capability
|
||||
.parse::<ModelCapability>()
|
||||
.map_err(|_| AppError::BadRequest("Invalid capability".to_string()))?;
|
||||
active.capability = Set(capability);
|
||||
}
|
||||
if let Some(context_length) = request.context_length {
|
||||
active.context_length = Set(context_length);
|
||||
}
|
||||
if let Some(max_output_tokens) = request.max_output_tokens {
|
||||
active.max_output_tokens = Set(Some(max_output_tokens));
|
||||
}
|
||||
if let Some(training_cutoff) = request.training_cutoff {
|
||||
active.training_cutoff = Set(Some(training_cutoff));
|
||||
}
|
||||
if let Some(is_open_source) = request.is_open_source {
|
||||
active.is_open_source = Set(is_open_source);
|
||||
}
|
||||
if let Some(status) = request.status {
|
||||
active.status = Set(status);
|
||||
}
|
||||
active.updated_at = Set(Utc::now());
|
||||
|
||||
let model = active.update(&self.db).await?;
|
||||
Ok(ModelResponse::from(model))
|
||||
) -> Result<agent::model::model_entry::ModelResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::model_entry::update_model(&self.db, id, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_delete(&self, id: Uuid, ctx: &Session) -> Result<(), AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
MEntity::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(())
|
||||
pub async fn agent_model_delete(
|
||||
&self,
|
||||
id: Uuid,
|
||||
ctx: &Session,
|
||||
) -> Result<(), AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::model_entry::delete_model(&self.db, id).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,126 +1,45 @@
|
||||
//! Capability management — delegates to agent crate.
|
||||
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::Utc;
|
||||
use models::agents::CapabilityType;
|
||||
use models::agents::model_capability;
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use super::provider::require_system_caller;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct CreateModelCapabilityRequest {
|
||||
pub model_version_id: i64,
|
||||
pub capability: String,
|
||||
#[serde(default)]
|
||||
pub is_supported: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct UpdateModelCapabilityRequest {
|
||||
pub is_supported: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ModelCapabilityResponse {
|
||||
pub id: i64,
|
||||
pub model_version_id: i64,
|
||||
pub capability: String,
|
||||
pub is_supported: bool,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<model_capability::Model> for ModelCapabilityResponse {
|
||||
fn from(mc: model_capability::Model) -> Self {
|
||||
Self {
|
||||
id: mc.id,
|
||||
model_version_id: mc.model_version_id,
|
||||
capability: mc.capability,
|
||||
is_supported: mc.is_supported,
|
||||
created_at: mc.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use agent::model::capability::{CreateModelCapabilityRequest, ModelCapabilityResponse, UpdateModelCapabilityRequest};
|
||||
|
||||
impl AppService {
|
||||
pub async fn agent_model_capability_list(
|
||||
&self,
|
||||
model_version_id: i64,
|
||||
_ctx: &Session,
|
||||
) -> Result<Vec<ModelCapabilityResponse>, AppError> {
|
||||
let caps = model_capability::Entity::find()
|
||||
.filter(model_capability::Column::ModelVersionId.eq(model_version_id))
|
||||
.order_by_asc(model_capability::Column::Capability)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(caps
|
||||
.into_iter()
|
||||
.map(ModelCapabilityResponse::from)
|
||||
.collect())
|
||||
) -> Result<Vec<agent::model::capability::ModelCapabilityResponse>, AppError> {
|
||||
Ok(agent::model::capability::list_capabilities(&self.db, model_version_id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_capability_get(
|
||||
&self,
|
||||
id: i64,
|
||||
_ctx: &Session,
|
||||
) -> Result<ModelCapabilityResponse, AppError> {
|
||||
let cap = model_capability::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound(
|
||||
"Capability record not found".to_string(),
|
||||
))?;
|
||||
Ok(ModelCapabilityResponse::from(cap))
|
||||
) -> Result<agent::model::capability::ModelCapabilityResponse, AppError> {
|
||||
Ok(agent::model::capability::get_capability(&self.db, id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_capability_create(
|
||||
&self,
|
||||
request: CreateModelCapabilityRequest,
|
||||
request: agent::model::capability::CreateModelCapabilityRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelCapabilityResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let _ = request
|
||||
.capability
|
||||
.parse::<CapabilityType>()
|
||||
.map_err(|_| AppError::BadRequest("Invalid capability type".to_string()))?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = model_capability::ActiveModel {
|
||||
model_version_id: Set(request.model_version_id),
|
||||
capability: Set(request.capability),
|
||||
is_supported: Set(request.is_supported),
|
||||
created_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
let cap = active.insert(&self.db).await?;
|
||||
Ok(ModelCapabilityResponse::from(cap))
|
||||
) -> Result<agent::model::capability::ModelCapabilityResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::capability::create_capability(&self.db, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_capability_update(
|
||||
&self,
|
||||
id: i64,
|
||||
request: UpdateModelCapabilityRequest,
|
||||
request: agent::model::capability::UpdateModelCapabilityRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelCapabilityResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let cap = model_capability::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound(
|
||||
"Capability record not found".to_string(),
|
||||
))?;
|
||||
|
||||
let mut active: model_capability::ActiveModel = cap.into();
|
||||
if let Some(is_supported) = request.is_supported {
|
||||
active.is_supported = Set(is_supported);
|
||||
}
|
||||
|
||||
let cap = active.update(&self.db).await?;
|
||||
Ok(ModelCapabilityResponse::from(cap))
|
||||
) -> Result<agent::model::capability::ModelCapabilityResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::capability::update_capability(&self.db, id, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_capability_delete(
|
||||
@ -128,10 +47,7 @@ impl AppService {
|
||||
id: i64,
|
||||
ctx: &Session,
|
||||
) -> Result<(), AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
model_capability::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(())
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::capability::delete_capability(&self.db, id).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,152 +1,46 @@
|
||||
//! Parameter profile management — delegates to agent crate.
|
||||
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use models::agents::model_parameter_profile;
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::provider::require_system_caller;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct CreateModelParameterProfileRequest {
|
||||
pub model_version_id: Uuid,
|
||||
pub temperature_min: f64,
|
||||
pub temperature_max: f64,
|
||||
pub top_p_min: f64,
|
||||
pub top_p_max: f64,
|
||||
#[serde(default)]
|
||||
pub frequency_penalty_supported: bool,
|
||||
#[serde(default)]
|
||||
pub presence_penalty_supported: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct UpdateModelParameterProfileRequest {
|
||||
pub temperature_min: Option<f64>,
|
||||
pub temperature_max: Option<f64>,
|
||||
pub top_p_min: Option<f64>,
|
||||
pub top_p_max: Option<f64>,
|
||||
pub frequency_penalty_supported: Option<bool>,
|
||||
pub presence_penalty_supported: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ModelParameterProfileResponse {
|
||||
pub id: i64,
|
||||
pub model_version_id: Uuid,
|
||||
pub temperature_min: f64,
|
||||
pub temperature_max: f64,
|
||||
pub top_p_min: f64,
|
||||
pub top_p_max: f64,
|
||||
pub frequency_penalty_supported: bool,
|
||||
pub presence_penalty_supported: bool,
|
||||
}
|
||||
|
||||
impl From<model_parameter_profile::Model> for ModelParameterProfileResponse {
|
||||
fn from(p: model_parameter_profile::Model) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
model_version_id: p.model_version_id,
|
||||
temperature_min: p.temperature_min,
|
||||
temperature_max: p.temperature_max,
|
||||
top_p_min: p.top_p_min,
|
||||
top_p_max: p.top_p_max,
|
||||
frequency_penalty_supported: p.frequency_penalty_supported,
|
||||
presence_penalty_supported: p.presence_penalty_supported,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use agent::model::parameter_profile::{CreateModelParameterProfileRequest, ModelParameterProfileResponse, UpdateModelParameterProfileRequest};
|
||||
|
||||
impl AppService {
|
||||
pub async fn agent_model_parameter_profile_list(
|
||||
&self,
|
||||
model_version_id: Uuid,
|
||||
_ctx: &Session,
|
||||
) -> Result<Vec<ModelParameterProfileResponse>, AppError> {
|
||||
let profiles = model_parameter_profile::Entity::find()
|
||||
.filter(model_parameter_profile::Column::ModelVersionId.eq(model_version_id))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(profiles
|
||||
.into_iter()
|
||||
.map(ModelParameterProfileResponse::from)
|
||||
.collect())
|
||||
) -> Result<Vec<agent::model::parameter_profile::ModelParameterProfileResponse>, AppError> {
|
||||
Ok(agent::model::parameter_profile::list_parameter_profiles(&self.db, model_version_id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_parameter_profile_get(
|
||||
&self,
|
||||
id: i64,
|
||||
_ctx: &Session,
|
||||
) -> Result<ModelParameterProfileResponse, AppError> {
|
||||
let profile = model_parameter_profile::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound(
|
||||
"Parameter profile not found".to_string(),
|
||||
))?;
|
||||
Ok(ModelParameterProfileResponse::from(profile))
|
||||
) -> Result<agent::model::parameter_profile::ModelParameterProfileResponse, AppError> {
|
||||
Ok(agent::model::parameter_profile::get_parameter_profile(&self.db, id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_parameter_profile_create(
|
||||
&self,
|
||||
request: CreateModelParameterProfileRequest,
|
||||
request: agent::model::parameter_profile::CreateModelParameterProfileRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelParameterProfileResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let active = model_parameter_profile::ActiveModel {
|
||||
model_version_id: Set(request.model_version_id),
|
||||
temperature_min: Set(request.temperature_min),
|
||||
temperature_max: Set(request.temperature_max),
|
||||
top_p_min: Set(request.top_p_min),
|
||||
top_p_max: Set(request.top_p_max),
|
||||
frequency_penalty_supported: Set(request.frequency_penalty_supported),
|
||||
presence_penalty_supported: Set(request.presence_penalty_supported),
|
||||
..Default::default()
|
||||
};
|
||||
let profile = active.insert(&self.db).await?;
|
||||
Ok(ModelParameterProfileResponse::from(profile))
|
||||
) -> Result<agent::model::parameter_profile::ModelParameterProfileResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::parameter_profile::create_parameter_profile(&self.db, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_parameter_profile_update(
|
||||
&self,
|
||||
id: i64,
|
||||
request: UpdateModelParameterProfileRequest,
|
||||
request: agent::model::parameter_profile::UpdateModelParameterProfileRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelParameterProfileResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let profile = model_parameter_profile::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound(
|
||||
"Parameter profile not found".to_string(),
|
||||
))?;
|
||||
|
||||
let mut active: model_parameter_profile::ActiveModel = profile.into();
|
||||
if let Some(v) = request.temperature_min {
|
||||
active.temperature_min = Set(v);
|
||||
}
|
||||
if let Some(v) = request.temperature_max {
|
||||
active.temperature_max = Set(v);
|
||||
}
|
||||
if let Some(v) = request.top_p_min {
|
||||
active.top_p_min = Set(v);
|
||||
}
|
||||
if let Some(v) = request.top_p_max {
|
||||
active.top_p_max = Set(v);
|
||||
}
|
||||
if let Some(v) = request.frequency_penalty_supported {
|
||||
active.frequency_penalty_supported = Set(v);
|
||||
}
|
||||
if let Some(v) = request.presence_penalty_supported {
|
||||
active.presence_penalty_supported = Set(v);
|
||||
}
|
||||
|
||||
let profile = active.update(&self.db).await?;
|
||||
Ok(ModelParameterProfileResponse::from(profile))
|
||||
) -> Result<agent::model::parameter_profile::ModelParameterProfileResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::parameter_profile::update_parameter_profile(&self.db, id, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_parameter_profile_delete(
|
||||
@ -154,10 +48,7 @@ impl AppService {
|
||||
id: i64,
|
||||
ctx: &Session,
|
||||
) -> Result<(), AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
model_parameter_profile::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(())
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::parameter_profile::delete_parameter_profile(&self.db, id).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,148 +1,54 @@
|
||||
//! Pricing management — delegates to agent crate.
|
||||
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::Utc;
|
||||
use models::agents::PricingCurrency;
|
||||
use models::agents::model_pricing;
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::provider::require_system_caller;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct CreateModelPricingRequest {
|
||||
pub model_version_id: Uuid,
|
||||
pub input_price_per_1k_tokens: String,
|
||||
pub output_price_per_1k_tokens: String,
|
||||
pub currency: String,
|
||||
pub effective_from: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct UpdateModelPricingRequest {
|
||||
pub input_price_per_1k_tokens: Option<String>,
|
||||
pub output_price_per_1k_tokens: Option<String>,
|
||||
pub currency: Option<String>,
|
||||
pub effective_from: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ModelPricingResponse {
|
||||
pub id: i64,
|
||||
pub model_version_id: Uuid,
|
||||
pub input_price_per_1k_tokens: String,
|
||||
pub output_price_per_1k_tokens: String,
|
||||
pub currency: String,
|
||||
pub effective_from: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<model_pricing::Model> for ModelPricingResponse {
|
||||
fn from(p: model_pricing::Model) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
model_version_id: p.model_version_id,
|
||||
input_price_per_1k_tokens: p.input_price_per_1k_tokens,
|
||||
output_price_per_1k_tokens: p.output_price_per_1k_tokens,
|
||||
currency: p.currency,
|
||||
effective_from: p.effective_from,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use agent::model::pricing::{CreateModelPricingRequest, ModelPricingResponse, UpdateModelPricingRequest};
|
||||
|
||||
impl AppService {
|
||||
pub async fn agent_model_pricing_list(
|
||||
&self,
|
||||
model_version_id: Uuid,
|
||||
_ctx: &Session,
|
||||
) -> Result<Vec<ModelPricingResponse>, AppError> {
|
||||
let records = model_pricing::Entity::find()
|
||||
.filter(model_pricing::Column::ModelVersionId.eq(model_version_id))
|
||||
.order_by_desc(model_pricing::Column::EffectiveFrom)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(records
|
||||
.into_iter()
|
||||
.map(ModelPricingResponse::from)
|
||||
.collect())
|
||||
) -> Result<Vec<agent::model::pricing::ModelPricingResponse>, AppError> {
|
||||
Ok(agent::model::pricing::list_pricing(&self.db, model_version_id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_pricing_get(
|
||||
&self,
|
||||
id: i64,
|
||||
_ctx: &Session,
|
||||
) -> Result<ModelPricingResponse, AppError> {
|
||||
let record = model_pricing::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Pricing record not found".to_string()))?;
|
||||
Ok(ModelPricingResponse::from(record))
|
||||
) -> Result<agent::model::pricing::ModelPricingResponse, AppError> {
|
||||
Ok(agent::model::pricing::get_pricing(&self.db, id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_pricing_create(
|
||||
&self,
|
||||
request: CreateModelPricingRequest,
|
||||
request: agent::model::pricing::CreateModelPricingRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelPricingResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let _ = request
|
||||
.currency
|
||||
.parse::<PricingCurrency>()
|
||||
.map_err(|_| AppError::BadRequest("Invalid pricing currency".to_string()))?;
|
||||
|
||||
let active = model_pricing::ActiveModel {
|
||||
model_version_id: Set(request.model_version_id),
|
||||
input_price_per_1k_tokens: Set(request.input_price_per_1k_tokens),
|
||||
output_price_per_1k_tokens: Set(request.output_price_per_1k_tokens),
|
||||
currency: Set(request.currency),
|
||||
effective_from: Set(request.effective_from),
|
||||
..Default::default()
|
||||
};
|
||||
let record = active.insert(&self.db).await?;
|
||||
Ok(ModelPricingResponse::from(record))
|
||||
) -> Result<agent::model::pricing::ModelPricingResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::pricing::create_pricing(&self.db, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_pricing_update(
|
||||
&self,
|
||||
id: i64,
|
||||
request: UpdateModelPricingRequest,
|
||||
request: agent::model::pricing::UpdateModelPricingRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelPricingResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let record = model_pricing::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Pricing record not found".to_string()))?;
|
||||
|
||||
let mut active: model_pricing::ActiveModel = record.into();
|
||||
if let Some(v) = request.input_price_per_1k_tokens {
|
||||
active.input_price_per_1k_tokens = Set(v);
|
||||
}
|
||||
if let Some(v) = request.output_price_per_1k_tokens {
|
||||
active.output_price_per_1k_tokens = Set(v);
|
||||
}
|
||||
if let Some(v) = request.currency {
|
||||
let _ = v
|
||||
.parse::<PricingCurrency>()
|
||||
.map_err(|_| AppError::BadRequest("Invalid pricing currency".to_string()))?;
|
||||
active.currency = Set(v);
|
||||
}
|
||||
if let Some(v) = request.effective_from {
|
||||
active.effective_from = Set(v);
|
||||
}
|
||||
|
||||
let record = active.update(&self.db).await?;
|
||||
Ok(ModelPricingResponse::from(record))
|
||||
) -> Result<agent::model::pricing::ModelPricingResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::pricing::update_pricing(&self.db, id, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_pricing_delete(&self, id: i64, ctx: &Session) -> Result<(), AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
model_pricing::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(())
|
||||
pub async fn agent_model_pricing_delete(
|
||||
&self,
|
||||
id: i64,
|
||||
ctx: &Session,
|
||||
) -> Result<(), AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::pricing::delete_pricing(&self.db, id).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,149 +1,46 @@
|
||||
//! Model version management — delegates to agent crate.
|
||||
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::Utc;
|
||||
use models::agents::model_version;
|
||||
use models::agents::{
|
||||
ModelStatus,
|
||||
model_version::{Column as MVColumn, Entity as MVEntity, Model as ModelVersionModel},
|
||||
};
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::provider::require_system_caller;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct CreateModelVersionRequest {
|
||||
pub model_id: Uuid,
|
||||
pub version: String,
|
||||
pub release_date: Option<chrono::DateTime<Utc>>,
|
||||
pub change_log: Option<String>,
|
||||
#[serde(default)]
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct UpdateModelVersionRequest {
|
||||
pub version: Option<String>,
|
||||
pub release_date: Option<chrono::DateTime<Utc>>,
|
||||
pub change_log: Option<String>,
|
||||
pub is_default: Option<bool>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ModelVersionResponse {
|
||||
pub id: Uuid,
|
||||
pub model_id: Uuid,
|
||||
pub version: String,
|
||||
pub release_date: Option<chrono::DateTime<Utc>>,
|
||||
pub change_log: Option<String>,
|
||||
pub is_default: bool,
|
||||
pub status: String,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<ModelVersionModel> for ModelVersionResponse {
|
||||
fn from(mv: ModelVersionModel) -> Self {
|
||||
Self {
|
||||
id: mv.id,
|
||||
model_id: mv.model_id,
|
||||
version: mv.version,
|
||||
release_date: mv.release_date,
|
||||
change_log: mv.change_log,
|
||||
is_default: mv.is_default,
|
||||
status: mv.status,
|
||||
created_at: mv.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use agent::model::version::{CreateModelVersionRequest, ModelVersionResponse, UpdateModelVersionRequest};
|
||||
|
||||
impl AppService {
|
||||
pub async fn agent_model_version_list(
|
||||
&self,
|
||||
model_id: Option<Uuid>,
|
||||
_ctx: &Session,
|
||||
) -> Result<Vec<ModelVersionResponse>, AppError> {
|
||||
let mut query = MVEntity::find().order_by_asc(MVColumn::Version);
|
||||
if let Some(mid) = model_id {
|
||||
query = query.filter(MVColumn::ModelId.eq(mid));
|
||||
}
|
||||
let versions = query.all(&self.db).await?;
|
||||
Ok(versions
|
||||
.into_iter()
|
||||
.map(ModelVersionResponse::from)
|
||||
.collect())
|
||||
) -> Result<Vec<agent::model::version::ModelVersionResponse>, AppError> {
|
||||
Ok(agent::model::version::list_versions(&self.db, model_id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_version_get(
|
||||
&self,
|
||||
id: Uuid,
|
||||
_ctx: &Session,
|
||||
) -> Result<ModelVersionResponse, AppError> {
|
||||
let version = MVEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Model version not found".to_string()))?;
|
||||
Ok(ModelVersionResponse::from(version))
|
||||
) -> Result<agent::model::version::ModelVersionResponse, AppError> {
|
||||
Ok(agent::model::version::get_version(&self.db, id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_version_create(
|
||||
&self,
|
||||
request: CreateModelVersionRequest,
|
||||
request: agent::model::version::CreateModelVersionRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelVersionResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = model_version::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
model_id: Set(request.model_id),
|
||||
version: Set(request.version),
|
||||
release_date: Set(request.release_date),
|
||||
change_log: Set(request.change_log),
|
||||
is_default: Set(request.is_default),
|
||||
status: Set(ModelStatus::Active.to_string()),
|
||||
created_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
let version = active.insert(&self.db).await?;
|
||||
Ok(ModelVersionResponse::from(version))
|
||||
) -> Result<agent::model::version::ModelVersionResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::version::create_version(&self.db, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_version_update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
request: UpdateModelVersionRequest,
|
||||
request: agent::model::version::UpdateModelVersionRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ModelVersionResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let version = MVEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Model version not found".to_string()))?;
|
||||
|
||||
let mut active: model_version::ActiveModel = version.into();
|
||||
if let Some(version) = request.version {
|
||||
active.version = Set(version);
|
||||
}
|
||||
if let Some(release_date) = request.release_date {
|
||||
active.release_date = Set(Some(release_date));
|
||||
}
|
||||
if let Some(change_log) = request.change_log {
|
||||
active.change_log = Set(Some(change_log));
|
||||
}
|
||||
if let Some(is_default) = request.is_default {
|
||||
active.is_default = Set(is_default);
|
||||
}
|
||||
if let Some(status) = request.status {
|
||||
active.status = Set(status);
|
||||
}
|
||||
|
||||
let version = active.update(&self.db).await?;
|
||||
Ok(ModelVersionResponse::from(version))
|
||||
) -> Result<agent::model::version::ModelVersionResponse, AppError> {
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::version::update_version(&self.db, id, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_model_version_delete(
|
||||
@ -151,8 +48,7 @@ impl AppService {
|
||||
id: Uuid,
|
||||
ctx: &Session,
|
||||
) -> Result<(), AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
MVEntity::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(())
|
||||
super::provider::require_system_caller(ctx)?;
|
||||
Ok(agent::model::version::delete_version(&self.db, id).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::billing::BillingRecord;
|
||||
use agent::billing::BillingRecord;
|
||||
|
||||
/// Structured PR description generated by AI.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
@ -136,16 +136,7 @@ async fn call_ai_model_for_description(
|
||||
|
||||
let client_config = agent::AiClientConfig::new(api_key).with_base_url(base_url);
|
||||
|
||||
let messages = vec![
|
||||
async_openai::types::chat::ChatCompletionRequestMessage::User(
|
||||
async_openai::types::chat::ChatCompletionRequestUserMessage {
|
||||
content: async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
|
||||
prompt.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
let messages = vec![agent::ChatRequestMessage::user(prompt.to_string())];
|
||||
|
||||
agent::call_with_params(&messages, model_name, &client_config, 0.3, 4096, None, None, None)
|
||||
.await
|
||||
|
||||
@ -1,52 +1,11 @@
|
||||
//! Provider management — delegates to agent crate.
|
||||
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::Utc;
|
||||
use models::agents::model_provider;
|
||||
use models::agents::{ModelStatus, model_provider::Entity as ProviderEntity};
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct CreateProviderRequest {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct UpdateProviderRequest {
|
||||
pub display_name: Option<String>,
|
||||
pub website: Option<String>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ProviderResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub website: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<model_provider::Model> for ProviderResponse {
|
||||
fn from(p: model_provider::Model) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
display_name: p.display_name,
|
||||
website: p.website,
|
||||
status: p.status,
|
||||
created_at: p.created_at,
|
||||
updated_at: p.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use agent::model::provider::{CreateProviderRequest, ProviderResponse, UpdateProviderRequest};
|
||||
|
||||
pub(crate) fn require_system_caller(ctx: &Session) -> Result<(), AppError> {
|
||||
if ctx.user() != Some(Uuid::nil()) {
|
||||
@ -59,80 +18,43 @@ impl AppService {
|
||||
pub async fn agent_provider_list(
|
||||
&self,
|
||||
_ctx: &Session,
|
||||
) -> Result<Vec<ProviderResponse>, AppError> {
|
||||
let providers = ProviderEntity::find()
|
||||
.order_by_asc(model_provider::Column::DisplayName)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(providers.into_iter().map(ProviderResponse::from).collect())
|
||||
) -> Result<Vec<agent::model::provider::ProviderResponse>, AppError> {
|
||||
Ok(agent::model::provider::list_providers(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_provider_get(
|
||||
&self,
|
||||
id: Uuid,
|
||||
_ctx: &Session,
|
||||
) -> Result<ProviderResponse, AppError> {
|
||||
let provider = ProviderEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Provider not found".to_string()))?;
|
||||
Ok(ProviderResponse::from(provider))
|
||||
) -> Result<agent::model::provider::ProviderResponse, AppError> {
|
||||
Ok(agent::model::provider::get_provider(&self.db, id).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_provider_create(
|
||||
&self,
|
||||
request: CreateProviderRequest,
|
||||
request: agent::model::provider::CreateProviderRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ProviderResponse, AppError> {
|
||||
) -> Result<agent::model::provider::ProviderResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = model_provider::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
name: Set(request.name),
|
||||
display_name: Set(request.display_name),
|
||||
website: Set(request.website),
|
||||
status: Set(ModelStatus::Active.to_string()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
let model = active.insert(&self.db).await?;
|
||||
Ok(ProviderResponse::from(model))
|
||||
Ok(agent::model::provider::create_provider(&self.db, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_provider_update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
request: UpdateProviderRequest,
|
||||
request: agent::model::provider::UpdateProviderRequest,
|
||||
ctx: &Session,
|
||||
) -> Result<ProviderResponse, AppError> {
|
||||
) -> Result<agent::model::provider::ProviderResponse, AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
|
||||
let provider = ProviderEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("Provider not found".to_string()))?;
|
||||
|
||||
let mut active: model_provider::ActiveModel = provider.into();
|
||||
if let Some(display_name) = request.display_name {
|
||||
active.display_name = Set(display_name);
|
||||
}
|
||||
if let Some(website) = request.website {
|
||||
active.website = Set(Some(website));
|
||||
}
|
||||
if let Some(status) = request.status {
|
||||
active.status = Set(status);
|
||||
}
|
||||
active.updated_at = Set(Utc::now());
|
||||
|
||||
let model = active.update(&self.db).await?;
|
||||
Ok(ProviderResponse::from(model))
|
||||
Ok(agent::model::provider::update_provider(&self.db, id, request).await?)
|
||||
}
|
||||
|
||||
pub async fn agent_provider_delete(&self, id: Uuid, ctx: &Session) -> Result<(), AppError> {
|
||||
pub async fn agent_provider_delete(
|
||||
&self,
|
||||
id: Uuid,
|
||||
ctx: &Session,
|
||||
) -> Result<(), AppError> {
|
||||
require_system_caller(ctx)?;
|
||||
ProviderEntity::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(())
|
||||
Ok(agent::model::provider::delete_provider(&self.db, id).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
//! Synchronizes AI model metadata from OpenRouter into the local database.
|
||||
//!
|
||||
//! Flow:
|
||||
//! 1. Use the configured `async_openai` client (with the real API key) to call
|
||||
//! `GET /models` — this returns only the models the current key can access.
|
||||
//! 1. Call `GET /models` with the real API key to list accessible model IDs.
|
||||
//! 2. Fetch full metadata (pricing, context_length, capabilities) for those
|
||||
//! model IDs from OpenRouter's public `/api/v1/models` endpoint (no auth).
|
||||
//! 3. Upsert provider / model / version / pricing / capability / profile
|
||||
@ -12,9 +11,6 @@
|
||||
//! immediately and then every 10 minutes. On app startup, run it once
|
||||
//! eagerly before accepting traffic.
|
||||
|
||||
use async_openai::Client;
|
||||
use async_openai::config::OpenAIConfig;
|
||||
use async_openai::types::models::Model as OpenAiModel;
|
||||
use std::time::Duration;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::interval;
|
||||
@ -690,8 +686,8 @@ async fn fetch_openrouter_models(
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an async_openai Client from the AI config.
|
||||
fn build_ai_client(config: &config::AppConfig) -> Result<Client<OpenAIConfig>, AppError> {
|
||||
/// Build reqwest client and config from the AI config.
|
||||
fn build_ai_client(config: &config::AppConfig) -> Result<(reqwest::Client, String, String), AppError> {
|
||||
let api_key = config
|
||||
.ai_api_key()
|
||||
.map_err(|e| AppError::InternalServerError(format!("AI API key not configured: {}", e)))?;
|
||||
@ -700,11 +696,40 @@ fn build_ai_client(config: &config::AppConfig) -> Result<Client<OpenAIConfig>, A
|
||||
.ai_basic_url()
|
||||
.unwrap_or_else(|_| "https://api.openai.com".into());
|
||||
|
||||
let cfg = OpenAIConfig::new()
|
||||
.with_api_key(&api_key)
|
||||
.with_api_base(&base_url);
|
||||
Ok((reqwest::Client::new(), base_url, api_key))
|
||||
}
|
||||
|
||||
Ok(Client::with_config(cfg))
|
||||
/// Response from `GET /v1/models`.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ModelsListResponse {
|
||||
data: Vec<ModelEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ModelEntry {
|
||||
id: String,
|
||||
}
|
||||
|
||||
/// List accessible model IDs from the AI endpoint.
|
||||
async fn list_accessible_models(
|
||||
client: &reqwest::Client,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
) -> Result<std::collections::HashSet<String>, AppError> {
|
||||
let url = format!("{}/v1/models", base_url.trim_end_matches('/'));
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(format!("failed to list models: {}", e)))?;
|
||||
|
||||
let body: ModelsListResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(format!("failed to parse models response: {}", e)))?;
|
||||
|
||||
Ok(body.data.into_iter().map(|m| m.id).collect())
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
@ -719,21 +744,8 @@ impl AppService {
|
||||
_ctx: &Session,
|
||||
) -> Result<SyncModelsResponse, AppError> {
|
||||
// Step 1: list models the AI client can access.
|
||||
let ai_client = build_ai_client(&self.config)?;
|
||||
let available_ids: std::collections::HashSet<String> = ai_client
|
||||
.models()
|
||||
.list()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::InternalServerError(format!(
|
||||
"failed to list available models from AI endpoint: {}",
|
||||
e
|
||||
))
|
||||
})?
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|m: OpenAiModel| m.id)
|
||||
.collect();
|
||||
let (http_client, base_url, api_key) = build_ai_client(&self.config)?;
|
||||
let available_ids = list_accessible_models(&http_client, &base_url, &api_key).await?;
|
||||
|
||||
tracing::info!(
|
||||
model_count = available_ids.len(),
|
||||
@ -896,7 +908,7 @@ impl AppService {
|
||||
ai_base_url: Option<String>,
|
||||
) {
|
||||
// Build AI client to list accessible models.
|
||||
let ai_client = match build_ai_client_from_parts(ai_api_key, ai_base_url) {
|
||||
let (http_client, base_url, api_key) = match build_ai_client_from_parts(ai_api_key, ai_base_url) {
|
||||
Ok(c) => c,
|
||||
Err(msg) => {
|
||||
tracing::warn!(error = %msg, "OpenRouter model sync");
|
||||
@ -904,8 +916,8 @@ impl AppService {
|
||||
}
|
||||
};
|
||||
|
||||
let available_ids: std::collections::HashSet<String> = match ai_client.models().list().await {
|
||||
Ok(resp) => resp.data.into_iter().map(|m: OpenAiModel| m.id).collect(),
|
||||
let available_ids = match list_accessible_models(&http_client, &base_url, &api_key).await {
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "OpenRouter model sync: failed to list available models");
|
||||
return;
|
||||
@ -1031,18 +1043,12 @@ impl AppService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an async_openai Client from raw API key and base URL (for background task).
|
||||
/// Build a reqwest client and config parts for background sync task.
|
||||
fn build_ai_client_from_parts(
|
||||
api_key: Option<String>,
|
||||
base_url: Option<String>,
|
||||
) -> Result<Client<OpenAIConfig>, String> {
|
||||
) -> Result<(reqwest::Client, String, String), String> {
|
||||
let api_key = api_key.ok_or_else(|| "AI API key not configured".to_string())?;
|
||||
|
||||
let base_url = base_url.unwrap_or_else(|| "https://api.openai.com".into());
|
||||
|
||||
let cfg = OpenAIConfig::new()
|
||||
.with_api_key(&api_key)
|
||||
.with_api_base(&base_url);
|
||||
|
||||
Ok(Client::with_config(cfg))
|
||||
Ok((reqwest::Client::new(), base_url, api_key))
|
||||
}
|
||||
|
||||
@ -298,3 +298,15 @@ impl From<room::RoomError> for AppError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<agent::AgentError> for AppError {
|
||||
fn from(e: agent::AgentError) -> Self {
|
||||
match e {
|
||||
agent::AgentError::NotFound(s) => AppError::NotFound(s),
|
||||
agent::AgentError::InvalidInput { field, reason } => {
|
||||
AppError::BadRequest(format!("invalid {}: {}", field, reason))
|
||||
}
|
||||
_ => AppError::InternalServerError(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,10 @@ async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde
|
||||
|
||||
let result = match (&base_oid, &head_oid) {
|
||||
(None, None) => {
|
||||
// Check if repo has any commits before attempting to diff
|
||||
if domain.repo().head().is_err() {
|
||||
return Err("No commits found in repository".into());
|
||||
}
|
||||
let head_meta = domain.commit_get_prefix("HEAD").map_err(|e| e.to_string())?;
|
||||
// Bare repos have no working tree — use tree-to-tree diff instead
|
||||
if domain.repo().is_bare() {
|
||||
|
||||
@ -4,7 +4,7 @@ use ::agent::chat::ChatService;
|
||||
use ::agent::client::AiClientConfig;
|
||||
use ::agent::task::service::TaskService;
|
||||
use ::agent::tool::ToolRegistry;
|
||||
use async_openai::config::OpenAIConfig;
|
||||
use ::agent::{EmbedService, new_embed_client};
|
||||
use avatar::AppAvatar;
|
||||
use config::AppConfig;
|
||||
use db::cache::AppCache;
|
||||
@ -163,25 +163,49 @@ impl AppService {
|
||||
.and_then(|urls| urls.first().cloned())
|
||||
.unwrap_or_else(|| "redis://127.0.0.1:6379".to_string());
|
||||
|
||||
// Build EmbedService if Qdrant and embedding model are configured (graceful degradation)
|
||||
let embed_service: Option<Arc<EmbedService>> =
|
||||
match new_embed_client(&config).await {
|
||||
Ok(client) => {
|
||||
let model_name = config
|
||||
.get_embed_model_name()
|
||||
.unwrap_or_else(|_| "text-embedding-3-small".into());
|
||||
let dimensions = config
|
||||
.get_embed_model_dimensions()
|
||||
.unwrap_or(1536);
|
||||
let svc = EmbedService::new(
|
||||
client,
|
||||
db.writer().clone(),
|
||||
model_name,
|
||||
dimensions,
|
||||
);
|
||||
let _ = svc.ensure_collections().await;
|
||||
tracing::info!("EmbedService initialized (Qdrant + embeddings)");
|
||||
Some(Arc::new(svc))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "EmbedService not available — vector search disabled");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Build ChatService if AI is configured; otherwise AI chat is disabled (graceful degradation)
|
||||
let chat_service: Option<Arc<ChatService>> =
|
||||
match (config.ai_api_key(), config.ai_basic_url()) {
|
||||
(Ok(api_key), Ok(base_url)) => {
|
||||
tracing::info!(url = %base_url, "AI chat enabled");
|
||||
let cfg = OpenAIConfig::new()
|
||||
.with_api_key(&api_key)
|
||||
.with_api_base(&base_url);
|
||||
let client = async_openai::Client::with_config(cfg);
|
||||
let ai_client_config = AiClientConfig::new(api_key).with_base_url(&base_url);
|
||||
let mut registry = ToolRegistry::new();
|
||||
git_tools::register_all(&mut registry);
|
||||
file_tools::register_all(&mut registry);
|
||||
project_tools::register_all(&mut registry);
|
||||
Some(Arc::new(
|
||||
ChatService::new(client)
|
||||
.with_ai_client_config(ai_client_config)
|
||||
.with_tool_registry(registry),
|
||||
))
|
||||
let mut chat_svc = ChatService::new()
|
||||
.with_ai_client_config(ai_client_config)
|
||||
.with_tool_registry(registry);
|
||||
if let Some(ref es) = embed_service {
|
||||
chat_svc = chat_svc.with_embed_service((**es).clone());
|
||||
}
|
||||
Some(Arc::new(chat_svc))
|
||||
}
|
||||
(Err(e), _) => {
|
||||
tracing::warn!(error = %e, "AI chat disabled");
|
||||
@ -243,6 +267,7 @@ impl AppService {
|
||||
Some(task_service.clone()),
|
||||
None,
|
||||
push_fn,
|
||||
embed_service,
|
||||
);
|
||||
|
||||
// Build WsTokenService
|
||||
|
||||
Loading…
Reference in New Issue
Block a user