Files
codecraft/server/src/provider/mod.rs
T
Richard ZinkandClaude Sonnet 5.5 62fe5bf467 Claude Code engine, image viewer, colour tags, checks per turn and more
- Claude Code as an engine: chats on a "claude-code/<alias>" model run through
  the claude CLI (stream-json); tool approvals go through our permission
  system. Sign in from the web UI (URL + pasted code), CLI version and update
  from Settings; the Docker image contains the CLI.
- Small models (8k context) for side tasks: input is cut to the model's
  context window; one-line chat status, "What does this do?" on approvals,
  "Plan as automation" (job draft from a chat).
- Mobile: top bar no longer widens the page, image viewer (zoom, swipe, mark
  up, use in message), jump to previous own message, message menu, time under
  every message.
- Approvals: push actions (allow/reject), "always allow in this project"
  (never for dangerous commands) with revoke.
- Per turn: changed files with diffs and rollback; project check command with
  a green/red badge and "let the agent fix it".
- Colour tags for chats in the colours of the active theme.
- Project defaults for new chats, project page (timeline), offline queue.

Tested in a throwaway container with a mock model (8k small model): 26 API
checks passed (permissions, diffs, checks, defaults, job draft, side-task
budget, Claude Code status/login URL, unsigned-in chat).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-14 22:30:12 +01:00

153 lines
6.0 KiB
Rust

//! Model providers (implementations in codecraft-core) and the registry of
//! configured providers with their concurrency limits.
pub use codecraft_core::provider::*;
use crate::config::{Config, ModelConfig};
use anyhow::{Result, anyhow};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
pub struct ProviderEntry {
pub name: String,
pub provider: Arc<dyn Provider>,
pub models: Vec<ModelConfig>,
}
#[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>,
}
impl Registry {
/// Build providers from config. Providers with an empty model list are queried
/// for their models; failures are logged and leave the list empty.
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;
}
let provider: Arc<dyn Provider> = build(client.clone(), pc);
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,
Ok(Err(e)) => tracing::warn!("provider {name}: model discovery failed: {e:#}"),
Err(_) => tracing::warn!("provider {name}: model discovery timed out"),
}
}
providers.insert(
name.clone(),
ProviderEntry { name: name.clone(), provider, models },
);
}
// Claude Code (the CLI's own agent) appears as a model list when it is installed.
if !providers.contains_key(crate::claude_code::PROVIDER) && crate::claude_code::binary().is_some() {
providers.insert(
crate::claude_code::PROVIDER.to_string(),
ProviderEntry { name: crate::claude_code::PROVIDER.to_string(), provider: Arc::new(crate::claude_code::Marker), models: crate::claude_code::models() },
);
}
Self { providers, limits }
}
/// First model of any provider but `skip`.
pub fn first_model_excluding(&self, skip: &str) -> Option<String> {
self.providers.values().filter(|e| e.name != skip).find_map(|e| e.models.first().map(|m| format!("{}/{}", e.name, m.id)))
}
/// 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
/// with default settings so users can type any model id.
pub fn resolve(&self, spec: &str) -> Result<(Arc<dyn Provider>, ModelConfig)> {
let (pname, mid) = spec
.split_once('/')
.ok_or_else(|| anyhow!("model must be 'provider/model', got '{spec}'"))?;
let entry = self
.providers
.get(pname)
.ok_or_else(|| anyhow!("unknown or disabled provider '{pname}'"))?;
let model = entry
.models
.iter()
.find(|m| m.id == mid)
.cloned()
.unwrap_or_else(|| ModelConfig { id: mid.to_string(), ..Default::default() });
Ok((entry.provider.clone(), model))
}
pub fn first_model(&self) -> Option<String> {
self.providers
.values()
.find_map(|e| e.models.first().map(|m| format!("{}/{}", e.name, m.id)))
}
pub fn models_json(&self) -> Value {
let list: Vec<Value> = self
.providers
.values()
.map(|e| {
serde_json::json!({
"provider": e.name,
"models": e.models.iter().map(|m| serde_json::json!({
"id": m.id,
"name": m.name.clone().unwrap_or_else(|| m.id.clone()),
"context": m.context,
"max_output": m.max_output,
"reasoning": m.reasoning,
"vision": m.vision,
"effort": m.effort,
"input_price": m.input_price,
"output_price": m.output_price,
"cache_read_price": m.cache_read_price,
"cache_write_price": m.cache_write_price,
"max_concurrency": m.max_concurrency,
})).collect::<Vec<_>>()
})
})
.collect();
Value::Array(list)
}
}