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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 16:54:08 +01:00
1 parent f3a2fe3816
commit 49262c612c
70 files changed
+8495 -714

No files matched your search

+38 -1
View File
@@ -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