Users, file manager, image generation, budgets and agent workflow
- User management: local accounts (Argon2) and OpenID Connect, admin/user roles, per-user folders plus admin-shared folders, read-only session sharing, event filtering per user - File manager with CodeMirror editor and git client (status, diff, stage, commit, branches, fetch/pull/push, log, clone/init) - Image generation via fal.ai: generate_image tool and image page - Budgets: monthly limits per provider, provider balances/costs (OpenRouter, Anthropic admin cost report, fal.ai), price discovery - Subagents on other models, background tasks, concurrency limits per host/model/subagent - MCP tools loaded on demand (mcp_search/mcp_call), read-only hints - Permission mode "auto", safe read-only commands always allowed - Persistent message queue with attachments, deletable - Live reasoning incl. <think> tags, activity animations, CSS themes, mobile layout, statistics per user, upload size handling - Docker: named volume for state, localhost-bound port Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3a2fe3816
commit
49262c612c
70 files changed
+8495
-714
No files matched your search
@@ -105,6 +105,13 @@ pub struct ProviderEntry {
|
||||
#[derive(Default)]
|
||||
pub struct Registry {
|
||||
pub providers: BTreeMap<String, ProviderEntry>,
|
||||
/// Concurrency limits keyed by "provider" and "provider/model".
|
||||
limits: BTreeMap<String, Arc<tokio::sync::Semaphore>>,
|
||||
}
|
||||
|
||||
/// Held while a request runs; dropping it frees the slot(s).
|
||||
pub struct Slot {
|
||||
_permits: Vec<tokio::sync::OwnedSemaphorePermit>,
|
||||
}
|
||||
|
||||
pub fn http_client() -> reqwest::Client {
|
||||
@@ -121,6 +128,7 @@ impl Registry {
|
||||
pub async fn build(cfg: &Config) -> Self {
|
||||
let client = http_client();
|
||||
let mut providers = BTreeMap::new();
|
||||
let mut limits = BTreeMap::new();
|
||||
for (name, pc) in &cfg.providers {
|
||||
if !pc.enabled {
|
||||
continue;
|
||||
@@ -131,6 +139,14 @@ impl Registry {
|
||||
ProviderKind::Openai => Arc::new(openai::OpenAi::new(client.clone(), ep)),
|
||||
};
|
||||
let mut models: Vec<ModelConfig> = pc.models.iter().map(|m| m.resolved()).collect();
|
||||
if let Some(n) = pc.max_concurrency.filter(|n| *n > 0) {
|
||||
limits.insert(name.clone(), Arc::new(tokio::sync::Semaphore::new(n)));
|
||||
}
|
||||
for m in &models {
|
||||
if let Some(n) = m.max_concurrency.filter(|n| *n > 0) {
|
||||
limits.insert(format!("{name}/{}", m.id), Arc::new(tokio::sync::Semaphore::new(n)));
|
||||
}
|
||||
}
|
||||
if models.is_empty() {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(15), provider.list_models()).await {
|
||||
Ok(Ok(m)) => models = m,
|
||||
@@ -143,7 +159,28 @@ impl Registry {
|
||||
ProviderEntry { name: name.clone(), provider, models },
|
||||
);
|
||||
}
|
||||
Self { providers }
|
||||
Self { providers, limits }
|
||||
}
|
||||
|
||||
/// Wait for a free slot on the model and its provider. Always acquired in the
|
||||
/// same order (model, then provider), so concurrent requests cannot deadlock.
|
||||
pub async fn acquire(&self, spec: &str) -> Slot {
|
||||
let provider = spec.split_once('/').map(|(p, _)| p).unwrap_or(spec);
|
||||
let mut permits = Vec::new();
|
||||
for key in [spec, provider] {
|
||||
if let Some(sem) = self.limits.get(key) {
|
||||
if let Ok(p) = sem.clone().acquire_owned().await {
|
||||
permits.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
Slot { _permits: permits }
|
||||
}
|
||||
|
||||
/// True if a request to this model would have to wait.
|
||||
pub fn is_saturated(&self, spec: &str) -> bool {
|
||||
let provider = spec.split_once('/').map(|(p, _)| p).unwrap_or(spec);
|
||||
[spec, provider].iter().any(|k| self.limits.get(*k).is_some_and(|s| s.available_permits() == 0))
|
||||
}
|
||||
|
||||
/// Resolve "provider/model". Unknown models of a known provider are allowed
|
||||
|
||||
Reference in new issue
Block a user