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 }
|
argon2 = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||||
sea-orm = { workspace = true, features = [] }
|
sea-orm = { workspace = true, features = [] }
|
||||||
async-openai = { version = "0.34.0", features = ["chat-completion"] }
|
|
||||||
reqwest = { workspace = true, features = ["json", "native-tls"] }
|
reqwest = { workspace = true, features = ["json", "native-tls"] }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
rsa = { workspace = true }
|
rsa = { workspace = true }
|
||||||
|
|||||||
@ -1,218 +1,24 @@
|
|||||||
//! AI usage billing — records token costs against a project or workspace balance.
|
//! Billing — delegates to agent crate.
|
||||||
//!
|
|
||||||
//! 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.
|
|
||||||
|
|
||||||
use crate::AppService;
|
use crate::AppService;
|
||||||
use crate::error::AppError;
|
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;
|
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 {
|
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(
|
pub async fn record_ai_usage(
|
||||||
&self,
|
&self,
|
||||||
project_uid: Uuid,
|
project_uid: Uuid,
|
||||||
model_id: Uuid,
|
model_id: Uuid,
|
||||||
input_tokens: i64,
|
input_tokens: i64,
|
||||||
output_tokens: i64,
|
output_tokens: i64,
|
||||||
) -> Result<BillingRecord, AppError> {
|
) -> Result<agent::billing::BillingRecord, AppError> {
|
||||||
// 1. Look up the active price for this model.
|
Ok(agent::billing::record_ai_usage(
|
||||||
let pricing = model_pricing::Entity::find()
|
&self.db,
|
||||||
.filter(model_pricing::Column::ModelVersionId.eq(model_id))
|
project_uid,
|
||||||
.order_by_desc(model_pricing::Column::EffectiveFrom)
|
model_id,
|
||||||
.one(&self.db)
|
input_tokens,
|
||||||
.await?
|
output_tokens,
|
||||||
.ok_or_else(|| {
|
)
|
||||||
AppError::InternalServerError(
|
.await?)
|
||||||
"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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ use session::Session;
|
|||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::billing::BillingRecord;
|
use agent::billing::BillingRecord;
|
||||||
|
|
||||||
const AI_BOT_UUID: Uuid = Uuid::nil();
|
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 client_config = agent::AiClientConfig::new(api_key).with_base_url(base_url);
|
||||||
|
|
||||||
let messages = vec![
|
let messages = vec![agent::ChatRequestMessage::user(prompt.to_string())];
|
||||||
async_openai::types::chat::ChatCompletionRequestMessage::User(
|
|
||||||
async_openai::types::chat::ChatCompletionRequestUserMessage {
|
|
||||||
content: async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
|
|
||||||
prompt.to_string(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
agent::call_with_params(&messages, model_name, &client_config, 0.2, 8192, None, None, None)
|
agent::call_with_params(&messages, model_name, &client_config, 0.2, 8192, None, None, None)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -109,15 +109,7 @@ async fn call_ai_for_triage(
|
|||||||
let client_config =
|
let client_config =
|
||||||
::agent::AiClientConfig::new(api_key).with_base_url(base_url);
|
::agent::AiClientConfig::new(api_key).with_base_url(base_url);
|
||||||
|
|
||||||
let messages = vec![async_openai::types::chat::ChatCompletionRequestMessage::User(
|
let messages = vec![agent::ChatRequestMessage::user(prompt.to_string())];
|
||||||
async_openai::types::chat::ChatCompletionRequestUserMessage {
|
|
||||||
content:
|
|
||||||
async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
|
|
||||||
prompt.to_string(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)];
|
|
||||||
|
|
||||||
let response = ::agent::call_with_params(
|
let response = ::agent::call_with_params(
|
||||||
&messages,
|
&messages,
|
||||||
|
|||||||
@ -1,197 +1,54 @@
|
|||||||
|
//! Model management — delegates to agent crate.
|
||||||
|
|
||||||
use crate::AppService;
|
use crate::AppService;
|
||||||
use crate::error::AppError;
|
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 session::Session;
|
||||||
use utoipa::ToSchema;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::provider::require_system_caller;
|
pub use agent::model::model_entry::{CreateModelRequest, ModelResponse, UpdateModelRequest};
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppService {
|
impl AppService {
|
||||||
pub async fn agent_model_list(
|
pub async fn agent_model_list(
|
||||||
&self,
|
&self,
|
||||||
provider_id: Option<Uuid>,
|
provider_id: Option<Uuid>,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<Vec<ModelResponse>, AppError> {
|
) -> Result<Vec<agent::model::model_entry::ModelResponse>, AppError> {
|
||||||
let mut query = MEntity::find().order_by_asc(MColumn::Name);
|
Ok(agent::model::model_entry::list_models(&self.db, provider_id).await?)
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_get(
|
pub async fn agent_model_get(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<ModelResponse, AppError> {
|
) -> Result<agent::model::model_entry::ModelResponse, AppError> {
|
||||||
let model = MEntity::find_by_id(id)
|
Ok(agent::model::model_entry::get_model(&self.db, id).await?)
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound("Model not found".to_string()))?;
|
|
||||||
Ok(ModelResponse::from(model))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_create(
|
pub async fn agent_model_create(
|
||||||
&self,
|
&self,
|
||||||
request: CreateModelRequest,
|
request: agent::model::model_entry::CreateModelRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelResponse, AppError> {
|
) -> Result<agent::model::model_entry::ModelResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::model_entry::create_model(&self.db, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_update(
|
pub async fn agent_model_update(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
request: UpdateModelRequest,
|
request: agent::model::model_entry::UpdateModelRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelResponse, AppError> {
|
) -> Result<agent::model::model_entry::ModelResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::model_entry::update_model(&self.db, id, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_delete(&self, id: Uuid, ctx: &Session) -> Result<(), AppError> {
|
pub async fn agent_model_delete(
|
||||||
require_system_caller(ctx)?;
|
&self,
|
||||||
MEntity::delete_by_id(id).exec(&self.db).await?;
|
id: Uuid,
|
||||||
Ok(())
|
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::AppService;
|
||||||
use crate::error::AppError;
|
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 session::Session;
|
||||||
use utoipa::ToSchema;
|
|
||||||
|
|
||||||
use super::provider::require_system_caller;
|
pub use agent::model::capability::{CreateModelCapabilityRequest, ModelCapabilityResponse, UpdateModelCapabilityRequest};
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppService {
|
impl AppService {
|
||||||
pub async fn agent_model_capability_list(
|
pub async fn agent_model_capability_list(
|
||||||
&self,
|
&self,
|
||||||
model_version_id: i64,
|
model_version_id: i64,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<Vec<ModelCapabilityResponse>, AppError> {
|
) -> Result<Vec<agent::model::capability::ModelCapabilityResponse>, AppError> {
|
||||||
let caps = model_capability::Entity::find()
|
Ok(agent::model::capability::list_capabilities(&self.db, model_version_id).await?)
|
||||||
.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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_capability_get(
|
pub async fn agent_model_capability_get(
|
||||||
&self,
|
&self,
|
||||||
id: i64,
|
id: i64,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<ModelCapabilityResponse, AppError> {
|
) -> Result<agent::model::capability::ModelCapabilityResponse, AppError> {
|
||||||
let cap = model_capability::Entity::find_by_id(id)
|
Ok(agent::model::capability::get_capability(&self.db, id).await?)
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound(
|
|
||||||
"Capability record not found".to_string(),
|
|
||||||
))?;
|
|
||||||
Ok(ModelCapabilityResponse::from(cap))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_capability_create(
|
pub async fn agent_model_capability_create(
|
||||||
&self,
|
&self,
|
||||||
request: CreateModelCapabilityRequest,
|
request: agent::model::capability::CreateModelCapabilityRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelCapabilityResponse, AppError> {
|
) -> Result<agent::model::capability::ModelCapabilityResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::capability::create_capability(&self.db, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_capability_update(
|
pub async fn agent_model_capability_update(
|
||||||
&self,
|
&self,
|
||||||
id: i64,
|
id: i64,
|
||||||
request: UpdateModelCapabilityRequest,
|
request: agent::model::capability::UpdateModelCapabilityRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelCapabilityResponse, AppError> {
|
) -> Result<agent::model::capability::ModelCapabilityResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::capability::update_capability(&self.db, id, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_capability_delete(
|
pub async fn agent_model_capability_delete(
|
||||||
@ -128,10 +47,7 @@ impl AppService {
|
|||||||
id: i64,
|
id: i64,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
model_capability::Entity::delete_by_id(id)
|
Ok(agent::model::capability::delete_capability(&self.db, id).await?)
|
||||||
.exec(&self.db)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,152 +1,46 @@
|
|||||||
|
//! Parameter profile management — delegates to agent crate.
|
||||||
|
|
||||||
use crate::AppService;
|
use crate::AppService;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use models::agents::model_parameter_profile;
|
|
||||||
use sea_orm::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use session::Session;
|
use session::Session;
|
||||||
use utoipa::ToSchema;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::provider::require_system_caller;
|
pub use agent::model::parameter_profile::{CreateModelParameterProfileRequest, ModelParameterProfileResponse, UpdateModelParameterProfileRequest};
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppService {
|
impl AppService {
|
||||||
pub async fn agent_model_parameter_profile_list(
|
pub async fn agent_model_parameter_profile_list(
|
||||||
&self,
|
&self,
|
||||||
model_version_id: Uuid,
|
model_version_id: Uuid,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<Vec<ModelParameterProfileResponse>, AppError> {
|
) -> Result<Vec<agent::model::parameter_profile::ModelParameterProfileResponse>, AppError> {
|
||||||
let profiles = model_parameter_profile::Entity::find()
|
Ok(agent::model::parameter_profile::list_parameter_profiles(&self.db, model_version_id).await?)
|
||||||
.filter(model_parameter_profile::Column::ModelVersionId.eq(model_version_id))
|
|
||||||
.all(&self.db)
|
|
||||||
.await?;
|
|
||||||
Ok(profiles
|
|
||||||
.into_iter()
|
|
||||||
.map(ModelParameterProfileResponse::from)
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_parameter_profile_get(
|
pub async fn agent_model_parameter_profile_get(
|
||||||
&self,
|
&self,
|
||||||
id: i64,
|
id: i64,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<ModelParameterProfileResponse, AppError> {
|
) -> Result<agent::model::parameter_profile::ModelParameterProfileResponse, AppError> {
|
||||||
let profile = model_parameter_profile::Entity::find_by_id(id)
|
Ok(agent::model::parameter_profile::get_parameter_profile(&self.db, id).await?)
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound(
|
|
||||||
"Parameter profile not found".to_string(),
|
|
||||||
))?;
|
|
||||||
Ok(ModelParameterProfileResponse::from(profile))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_parameter_profile_create(
|
pub async fn agent_model_parameter_profile_create(
|
||||||
&self,
|
&self,
|
||||||
request: CreateModelParameterProfileRequest,
|
request: agent::model::parameter_profile::CreateModelParameterProfileRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelParameterProfileResponse, AppError> {
|
) -> Result<agent::model::parameter_profile::ModelParameterProfileResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::parameter_profile::create_parameter_profile(&self.db, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_parameter_profile_update(
|
pub async fn agent_model_parameter_profile_update(
|
||||||
&self,
|
&self,
|
||||||
id: i64,
|
id: i64,
|
||||||
request: UpdateModelParameterProfileRequest,
|
request: agent::model::parameter_profile::UpdateModelParameterProfileRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelParameterProfileResponse, AppError> {
|
) -> Result<agent::model::parameter_profile::ModelParameterProfileResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::parameter_profile::update_parameter_profile(&self.db, id, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_parameter_profile_delete(
|
pub async fn agent_model_parameter_profile_delete(
|
||||||
@ -154,10 +48,7 @@ impl AppService {
|
|||||||
id: i64,
|
id: i64,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
model_parameter_profile::Entity::delete_by_id(id)
|
Ok(agent::model::parameter_profile::delete_parameter_profile(&self.db, id).await?)
|
||||||
.exec(&self.db)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,148 +1,54 @@
|
|||||||
|
//! Pricing management — delegates to agent crate.
|
||||||
|
|
||||||
use crate::AppService;
|
use crate::AppService;
|
||||||
use crate::error::AppError;
|
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 session::Session;
|
||||||
use utoipa::ToSchema;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::provider::require_system_caller;
|
pub use agent::model::pricing::{CreateModelPricingRequest, ModelPricingResponse, UpdateModelPricingRequest};
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppService {
|
impl AppService {
|
||||||
pub async fn agent_model_pricing_list(
|
pub async fn agent_model_pricing_list(
|
||||||
&self,
|
&self,
|
||||||
model_version_id: Uuid,
|
model_version_id: Uuid,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<Vec<ModelPricingResponse>, AppError> {
|
) -> Result<Vec<agent::model::pricing::ModelPricingResponse>, AppError> {
|
||||||
let records = model_pricing::Entity::find()
|
Ok(agent::model::pricing::list_pricing(&self.db, model_version_id).await?)
|
||||||
.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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_pricing_get(
|
pub async fn agent_model_pricing_get(
|
||||||
&self,
|
&self,
|
||||||
id: i64,
|
id: i64,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<ModelPricingResponse, AppError> {
|
) -> Result<agent::model::pricing::ModelPricingResponse, AppError> {
|
||||||
let record = model_pricing::Entity::find_by_id(id)
|
Ok(agent::model::pricing::get_pricing(&self.db, id).await?)
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound("Pricing record not found".to_string()))?;
|
|
||||||
Ok(ModelPricingResponse::from(record))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_pricing_create(
|
pub async fn agent_model_pricing_create(
|
||||||
&self,
|
&self,
|
||||||
request: CreateModelPricingRequest,
|
request: agent::model::pricing::CreateModelPricingRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelPricingResponse, AppError> {
|
) -> Result<agent::model::pricing::ModelPricingResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::pricing::create_pricing(&self.db, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_pricing_update(
|
pub async fn agent_model_pricing_update(
|
||||||
&self,
|
&self,
|
||||||
id: i64,
|
id: i64,
|
||||||
request: UpdateModelPricingRequest,
|
request: agent::model::pricing::UpdateModelPricingRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelPricingResponse, AppError> {
|
) -> Result<agent::model::pricing::ModelPricingResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::pricing::update_pricing(&self.db, id, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_pricing_delete(&self, id: i64, ctx: &Session) -> Result<(), AppError> {
|
pub async fn agent_model_pricing_delete(
|
||||||
require_system_caller(ctx)?;
|
&self,
|
||||||
model_pricing::Entity::delete_by_id(id)
|
id: i64,
|
||||||
.exec(&self.db)
|
ctx: &Session,
|
||||||
.await?;
|
) -> Result<(), AppError> {
|
||||||
Ok(())
|
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::AppService;
|
||||||
use crate::error::AppError;
|
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 session::Session;
|
||||||
use utoipa::ToSchema;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::provider::require_system_caller;
|
pub use agent::model::version::{CreateModelVersionRequest, ModelVersionResponse, UpdateModelVersionRequest};
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppService {
|
impl AppService {
|
||||||
pub async fn agent_model_version_list(
|
pub async fn agent_model_version_list(
|
||||||
&self,
|
&self,
|
||||||
model_id: Option<Uuid>,
|
model_id: Option<Uuid>,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<Vec<ModelVersionResponse>, AppError> {
|
) -> Result<Vec<agent::model::version::ModelVersionResponse>, AppError> {
|
||||||
let mut query = MVEntity::find().order_by_asc(MVColumn::Version);
|
Ok(agent::model::version::list_versions(&self.db, model_id).await?)
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_version_get(
|
pub async fn agent_model_version_get(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<ModelVersionResponse, AppError> {
|
) -> Result<agent::model::version::ModelVersionResponse, AppError> {
|
||||||
let version = MVEntity::find_by_id(id)
|
Ok(agent::model::version::get_version(&self.db, id).await?)
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound("Model version not found".to_string()))?;
|
|
||||||
Ok(ModelVersionResponse::from(version))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_version_create(
|
pub async fn agent_model_version_create(
|
||||||
&self,
|
&self,
|
||||||
request: CreateModelVersionRequest,
|
request: agent::model::version::CreateModelVersionRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelVersionResponse, AppError> {
|
) -> Result<agent::model::version::ModelVersionResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::version::create_version(&self.db, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_version_update(
|
pub async fn agent_model_version_update(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
request: UpdateModelVersionRequest,
|
request: agent::model::version::UpdateModelVersionRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ModelVersionResponse, AppError> {
|
) -> Result<agent::model::version::ModelVersionResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::version::update_version(&self.db, id, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_model_version_delete(
|
pub async fn agent_model_version_delete(
|
||||||
@ -151,8 +48,7 @@ impl AppService {
|
|||||||
id: Uuid,
|
id: Uuid,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
require_system_caller(ctx)?;
|
super::provider::require_system_caller(ctx)?;
|
||||||
MVEntity::delete_by_id(id).exec(&self.db).await?;
|
Ok(agent::model::version::delete_version(&self.db, id).await?)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ use session::Session;
|
|||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::billing::BillingRecord;
|
use agent::billing::BillingRecord;
|
||||||
|
|
||||||
/// Structured PR description generated by AI.
|
/// Structured PR description generated by AI.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[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 client_config = agent::AiClientConfig::new(api_key).with_base_url(base_url);
|
||||||
|
|
||||||
let messages = vec![
|
let messages = vec![agent::ChatRequestMessage::user(prompt.to_string())];
|
||||||
async_openai::types::chat::ChatCompletionRequestMessage::User(
|
|
||||||
async_openai::types::chat::ChatCompletionRequestUserMessage {
|
|
||||||
content: async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
|
|
||||||
prompt.to_string(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
agent::call_with_params(&messages, model_name, &client_config, 0.3, 4096, None, None, None)
|
agent::call_with_params(&messages, model_name, &client_config, 0.3, 4096, None, None, None)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -1,52 +1,11 @@
|
|||||||
|
//! Provider management — delegates to agent crate.
|
||||||
|
|
||||||
use crate::AppService;
|
use crate::AppService;
|
||||||
use crate::error::AppError;
|
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 session::Session;
|
||||||
use utoipa::ToSchema;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
pub use agent::model::provider::{CreateProviderRequest, ProviderResponse, UpdateProviderRequest};
|
||||||
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(crate) fn require_system_caller(ctx: &Session) -> Result<(), AppError> {
|
pub(crate) fn require_system_caller(ctx: &Session) -> Result<(), AppError> {
|
||||||
if ctx.user() != Some(Uuid::nil()) {
|
if ctx.user() != Some(Uuid::nil()) {
|
||||||
@ -59,80 +18,43 @@ impl AppService {
|
|||||||
pub async fn agent_provider_list(
|
pub async fn agent_provider_list(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<Vec<ProviderResponse>, AppError> {
|
) -> Result<Vec<agent::model::provider::ProviderResponse>, AppError> {
|
||||||
let providers = ProviderEntity::find()
|
Ok(agent::model::provider::list_providers(&self.db).await?)
|
||||||
.order_by_asc(model_provider::Column::DisplayName)
|
|
||||||
.all(&self.db)
|
|
||||||
.await?;
|
|
||||||
Ok(providers.into_iter().map(ProviderResponse::from).collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_provider_get(
|
pub async fn agent_provider_get(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<ProviderResponse, AppError> {
|
) -> Result<agent::model::provider::ProviderResponse, AppError> {
|
||||||
let provider = ProviderEntity::find_by_id(id)
|
Ok(agent::model::provider::get_provider(&self.db, id).await?)
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound("Provider not found".to_string()))?;
|
|
||||||
Ok(ProviderResponse::from(provider))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_provider_create(
|
pub async fn agent_provider_create(
|
||||||
&self,
|
&self,
|
||||||
request: CreateProviderRequest,
|
request: agent::model::provider::CreateProviderRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ProviderResponse, AppError> {
|
) -> Result<agent::model::provider::ProviderResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::provider::create_provider(&self.db, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn agent_provider_update(
|
pub async fn agent_provider_update(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
request: UpdateProviderRequest,
|
request: agent::model::provider::UpdateProviderRequest,
|
||||||
ctx: &Session,
|
ctx: &Session,
|
||||||
) -> Result<ProviderResponse, AppError> {
|
) -> Result<agent::model::provider::ProviderResponse, AppError> {
|
||||||
require_system_caller(ctx)?;
|
require_system_caller(ctx)?;
|
||||||
|
Ok(agent::model::provider::update_provider(&self.db, id, request).await?)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)?;
|
require_system_caller(ctx)?;
|
||||||
ProviderEntity::delete_by_id(id).exec(&self.db).await?;
|
Ok(agent::model::provider::delete_provider(&self.db, id).await?)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
//! Synchronizes AI model metadata from OpenRouter into the local database.
|
//! Synchronizes AI model metadata from OpenRouter into the local database.
|
||||||
//!
|
//!
|
||||||
//! Flow:
|
//! Flow:
|
||||||
//! 1. Use the configured `async_openai` client (with the real API key) to call
|
//! 1. Call `GET /models` with the real API key to list accessible model IDs.
|
||||||
//! `GET /models` — this returns only the models the current key can access.
|
|
||||||
//! 2. Fetch full metadata (pricing, context_length, capabilities) for those
|
//! 2. Fetch full metadata (pricing, context_length, capabilities) for those
|
||||||
//! model IDs from OpenRouter's public `/api/v1/models` endpoint (no auth).
|
//! model IDs from OpenRouter's public `/api/v1/models` endpoint (no auth).
|
||||||
//! 3. Upsert provider / model / version / pricing / capability / profile
|
//! 3. Upsert provider / model / version / pricing / capability / profile
|
||||||
@ -12,9 +11,6 @@
|
|||||||
//! immediately and then every 10 minutes. On app startup, run it once
|
//! immediately and then every 10 minutes. On app startup, run it once
|
||||||
//! eagerly before accepting traffic.
|
//! 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 std::time::Duration;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
@ -690,8 +686,8 @@ async fn fetch_openrouter_models(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an async_openai Client from the AI config.
|
/// Build reqwest client and config from the AI config.
|
||||||
fn build_ai_client(config: &config::AppConfig) -> Result<Client<OpenAIConfig>, AppError> {
|
fn build_ai_client(config: &config::AppConfig) -> Result<(reqwest::Client, String, String), AppError> {
|
||||||
let api_key = config
|
let api_key = config
|
||||||
.ai_api_key()
|
.ai_api_key()
|
||||||
.map_err(|e| AppError::InternalServerError(format!("AI API key not configured: {}", e)))?;
|
.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()
|
.ai_basic_url()
|
||||||
.unwrap_or_else(|_| "https://api.openai.com".into());
|
.unwrap_or_else(|_| "https://api.openai.com".into());
|
||||||
|
|
||||||
let cfg = OpenAIConfig::new()
|
Ok((reqwest::Client::new(), base_url, api_key))
|
||||||
.with_api_key(&api_key)
|
}
|
||||||
.with_api_base(&base_url);
|
|
||||||
|
|
||||||
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 {
|
impl AppService {
|
||||||
@ -719,21 +744,8 @@ impl AppService {
|
|||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<SyncModelsResponse, AppError> {
|
) -> Result<SyncModelsResponse, AppError> {
|
||||||
// Step 1: list models the AI client can access.
|
// Step 1: list models the AI client can access.
|
||||||
let ai_client = build_ai_client(&self.config)?;
|
let (http_client, base_url, api_key) = build_ai_client(&self.config)?;
|
||||||
let available_ids: std::collections::HashSet<String> = ai_client
|
let available_ids = list_accessible_models(&http_client, &base_url, &api_key).await?;
|
||||||
.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();
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
model_count = available_ids.len(),
|
model_count = available_ids.len(),
|
||||||
@ -896,7 +908,7 @@ impl AppService {
|
|||||||
ai_base_url: Option<String>,
|
ai_base_url: Option<String>,
|
||||||
) {
|
) {
|
||||||
// Build AI client to list accessible models.
|
// 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,
|
Ok(c) => c,
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
tracing::warn!(error = %msg, "OpenRouter model sync");
|
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 {
|
let available_ids = match list_accessible_models(&http_client, &base_url, &api_key).await {
|
||||||
Ok(resp) => resp.data.into_iter().map(|m: OpenAiModel| m.id).collect(),
|
Ok(ids) => ids,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = ?e, "OpenRouter model sync: failed to list available models");
|
tracing::warn!(error = ?e, "OpenRouter model sync: failed to list available models");
|
||||||
return;
|
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(
|
fn build_ai_client_from_parts(
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
base_url: 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 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 base_url = base_url.unwrap_or_else(|| "https://api.openai.com".into());
|
||||||
|
Ok((reqwest::Client::new(), base_url, api_key))
|
||||||
let cfg = OpenAIConfig::new()
|
|
||||||
.with_api_key(&api_key)
|
|
||||||
.with_api_base(&base_url);
|
|
||||||
|
|
||||||
Ok(Client::with_config(cfg))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
let result = match (&base_oid, &head_oid) {
|
||||||
(None, None) => {
|
(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())?;
|
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
|
// Bare repos have no working tree — use tree-to-tree diff instead
|
||||||
if domain.repo().is_bare() {
|
if domain.repo().is_bare() {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use ::agent::chat::ChatService;
|
|||||||
use ::agent::client::AiClientConfig;
|
use ::agent::client::AiClientConfig;
|
||||||
use ::agent::task::service::TaskService;
|
use ::agent::task::service::TaskService;
|
||||||
use ::agent::tool::ToolRegistry;
|
use ::agent::tool::ToolRegistry;
|
||||||
use async_openai::config::OpenAIConfig;
|
use ::agent::{EmbedService, new_embed_client};
|
||||||
use avatar::AppAvatar;
|
use avatar::AppAvatar;
|
||||||
use config::AppConfig;
|
use config::AppConfig;
|
||||||
use db::cache::AppCache;
|
use db::cache::AppCache;
|
||||||
@ -163,25 +163,49 @@ impl AppService {
|
|||||||
.and_then(|urls| urls.first().cloned())
|
.and_then(|urls| urls.first().cloned())
|
||||||
.unwrap_or_else(|| "redis://127.0.0.1:6379".to_string());
|
.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)
|
// Build ChatService if AI is configured; otherwise AI chat is disabled (graceful degradation)
|
||||||
let chat_service: Option<Arc<ChatService>> =
|
let chat_service: Option<Arc<ChatService>> =
|
||||||
match (config.ai_api_key(), config.ai_basic_url()) {
|
match (config.ai_api_key(), config.ai_basic_url()) {
|
||||||
(Ok(api_key), Ok(base_url)) => {
|
(Ok(api_key), Ok(base_url)) => {
|
||||||
tracing::info!(url = %base_url, "AI chat enabled");
|
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 ai_client_config = AiClientConfig::new(api_key).with_base_url(&base_url);
|
||||||
let mut registry = ToolRegistry::new();
|
let mut registry = ToolRegistry::new();
|
||||||
git_tools::register_all(&mut registry);
|
git_tools::register_all(&mut registry);
|
||||||
file_tools::register_all(&mut registry);
|
file_tools::register_all(&mut registry);
|
||||||
project_tools::register_all(&mut registry);
|
project_tools::register_all(&mut registry);
|
||||||
Some(Arc::new(
|
let mut chat_svc = ChatService::new()
|
||||||
ChatService::new(client)
|
.with_ai_client_config(ai_client_config)
|
||||||
.with_ai_client_config(ai_client_config)
|
.with_tool_registry(registry);
|
||||||
.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), _) => {
|
(Err(e), _) => {
|
||||||
tracing::warn!(error = %e, "AI chat disabled");
|
tracing::warn!(error = %e, "AI chat disabled");
|
||||||
@ -243,6 +267,7 @@ impl AppService {
|
|||||||
Some(task_service.clone()),
|
Some(task_service.clone()),
|
||||||
None,
|
None,
|
||||||
push_fn,
|
push_fn,
|
||||||
|
embed_service,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build WsTokenService
|
// Build WsTokenService
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user