From 7ce6c16361fad5efe492a07d299e98ef3a8912c5 Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:37:45 -0500 Subject: [PATCH 1/7] feat: update Gemini model support with latest Google models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade routing defaults: use gemini-2.5-pro for channel/branch (quality tasks) and gemini-2.5-flash for worker/compactor/cortex (speed tasks) - Add fallback chain: gemini-2.5-pro → gemini-2.5-flash → gemini-2.5-flash-lite - Add native Gemini API voice transcription models alongside existing OpenRouter paths Closes spacedriveapp/spacebot#133 --- src/api/models.rs | 9 +++++++++ src/llm/routing.rs | 9 +++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/api/models.rs b/src/api/models.rs index 5c1c8e008..ffe71253f 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -81,6 +81,15 @@ const MODELS_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(360 /// Models known to work with Spacebot's current voice transcription path /// (OpenAI-compatible `/v1/chat/completions` with `input_audio`). const KNOWN_VOICE_TRANSCRIPTION_MODELS: &[&str] = &[ + // Native Gemini API + "gemini/gemini-2.0-flash", + "gemini/gemini-2.5-flash", + "gemini/gemini-2.5-flash-lite", + "gemini/gemini-2.5-pro", + "gemini/gemini-3-flash-preview", + "gemini/gemini-3-pro-preview", + "gemini/gemini-3.1-pro-preview", + // Via OpenRouter "openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemini-2.5-flash", "openrouter/google/gemini-2.5-flash-lite", diff --git a/src/llm/routing.rs b/src/llm/routing.rs index ed49f4323..995a1cf34 100644 --- a/src/llm/routing.rs +++ b/src/llm/routing.rs @@ -305,16 +305,21 @@ pub fn defaults_for_provider(provider: &str) -> RoutingConfig { } } "gemini" => { - let channel: String = "gemini/gemini-2.5-flash".into(); + let channel: String = "gemini/gemini-2.5-pro".into(); let worker: String = "gemini/gemini-2.5-flash".into(); + let lite: String = "gemini/gemini-2.5-flash-lite".into(); RoutingConfig { channel: channel.clone(), branch: channel.clone(), worker: worker.clone(), compactor: worker.clone(), cortex: worker.clone(), + voice: String::new(), task_overrides: HashMap::from([("coding".into(), channel.clone())]), - fallbacks: HashMap::new(), + fallbacks: HashMap::from([ + (channel, vec![worker.clone()]), + (worker, vec![lite]), + ]), rate_limit_cooldown_secs: 60, ..RoutingConfig::default() } From cc4c52ab95619adaa459a6f39a192b4700f0800a Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:46:58 -0500 Subject: [PATCH 2/7] fix: resolve compilation errors in browser tool and API server - browser.rs: replace `url::Url` with `reqwest::Url` since the `url` crate is not a direct dependency (reqwest re-exports it) - server.rs: use `State>` extractor for the auth middleware, required by axum 0.8's `from_fn_with_state` --- src/api/server.rs | 8 ++++++-- src/tools/browser.rs | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/api/server.rs b/src/api/server.rs index 39dcc9d64..17c82d48f 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -7,7 +7,7 @@ use super::{ }; use axum::Json; -use axum::extract::Request; +use axum::extract::{Request, State}; use axum::Router; use axum::http::{StatusCode, Uri, header}; use axum::middleware::{self, Next}; @@ -177,7 +177,11 @@ pub async fn start_http_server( Ok(handle) } -async fn api_auth_middleware(state: Arc, request: Request, next: Next) -> Response { +async fn api_auth_middleware( + State(state): State>, + request: Request, + next: Next, +) -> Response { let Some(expected_token) = state.auth_token.as_deref() else { return next.run(request).await; }; diff --git a/src/tools/browser.rs b/src/tools/browser.rs index e650a5ac3..7b4b7a999 100644 --- a/src/tools/browser.rs +++ b/src/tools/browser.rs @@ -5,6 +5,7 @@ //! ref system for LLM-friendly element addressing. use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + use crate::config::BrowserConfig; use chromiumoxide::browser::{Browser, BrowserConfig as ChromeConfig}; @@ -31,7 +32,7 @@ use tokio::task::JoinHandle; /// Blocks private/loopback IPs, link-local addresses, and cloud metadata endpoints /// to prevent server-side request forgery. fn validate_url(url: &str) -> Result<(), BrowserError> { - let parsed = url::Url::parse(url).map_err(|error| { + let parsed = reqwest::Url::parse(url).map_err(|error| { BrowserError::new(format!("invalid URL '{url}': {error}")) })?; From 33e83ac3e06e589fe498c8fac7e722851fbe5d35 Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:23:28 -0500 Subject: [PATCH 3/7] feat: support ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, and SPACEBOT_MODEL env vars Add environment variable support for custom Anthropic API endpoints and authentication tokens, enabling use with API proxies (LiteLLM, Azure AI Gateway, corporate proxies). New environment variables: - ANTHROPIC_BASE_URL: override the Anthropic API endpoint - ANTHROPIC_AUTH_TOKEN: alternative to ANTHROPIC_API_KEY for proxy auth - SPACEBOT_MODEL: override all process types with a single model Also fixes a bug where build_anthropic_request() hardcoded the API URL instead of using the provider's configured base_url, making custom base URLs via TOML config ineffective. Closes spacedriveapp/spacebot#132 --- src/config.rs | 25 ++++++++++++++++++++----- src/llm/anthropic/params.rs | 23 ++++++++++++++++++++--- src/llm/model.rs | 1 + 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index 6e1c1586a..abb6f8288 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1803,7 +1803,9 @@ impl Config { /// Load from environment variables only (no config file). pub fn load_from_env(instance_dir: &Path) -> Result { let mut llm = LlmConfig { - anthropic_key: std::env::var("ANTHROPIC_API_KEY").ok(), + anthropic_key: std::env::var("ANTHROPIC_API_KEY") + .ok() + .or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok()), openai_key: std::env::var("OPENAI_API_KEY").ok(), openrouter_key: std::env::var("OPENROUTER_API_KEY").ok(), zhipu_key: std::env::var("ZHIPU_API_KEY").ok(), @@ -1826,11 +1828,13 @@ impl Config { // Populate providers from env vars (same as from_toml does) if let Some(anthropic_key) = llm.anthropic_key.clone() { + let base_url = std::env::var("ANTHROPIC_BASE_URL") + .unwrap_or_else(|_| ANTHROPIC_PROVIDER_BASE_URL.to_string()); llm.providers .entry("anthropic".to_string()) .or_insert_with(|| ProviderConfig { api_type: ApiType::Anthropic, - base_url: ANTHROPIC_PROVIDER_BASE_URL.to_string(), + base_url, api_key: anthropic_key, name: None, }); @@ -1938,8 +1942,16 @@ impl Config { // Note: We allow boot without provider keys now. System starts in setup mode. // Agents are initialized later when keys are added via API. - // Env-only routing: check for env overrides on channel/worker models + // Env-only routing: check for env overrides on channel/worker models. + // SPACEBOT_MODEL overrides all process types at once; specific vars take precedence. let mut routing = RoutingConfig::default(); + if let Ok(model) = std::env::var("SPACEBOT_MODEL") { + routing.channel = model.clone(); + routing.branch = model.clone(); + routing.worker = model.clone(); + routing.compactor = model.clone(); + routing.cortex = model; + } if let Ok(channel_model) = std::env::var("SPACEBOT_CHANNEL_MODEL") { routing.channel = channel_model; } @@ -2039,7 +2051,8 @@ impl Config { .anthropic_key .as_deref() .and_then(resolve_env_value) - .or_else(|| std::env::var("ANTHROPIC_API_KEY").ok()), + .or_else(|| std::env::var("ANTHROPIC_API_KEY").ok()) + .or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok()), openai_key: toml .llm .openai_key @@ -2162,11 +2175,13 @@ impl Config { }; if let Some(anthropic_key) = llm.anthropic_key.clone() { + let base_url = std::env::var("ANTHROPIC_BASE_URL") + .unwrap_or_else(|_| ANTHROPIC_PROVIDER_BASE_URL.to_string()); llm.providers .entry("anthropic".to_string()) .or_insert_with(|| ProviderConfig { api_type: ApiType::Anthropic, - base_url: ANTHROPIC_PROVIDER_BASE_URL.to_string(), + base_url, api_key: anthropic_key, name: None, }); diff --git a/src/llm/anthropic/params.rs b/src/llm/anthropic/params.rs index 7bc59497e..2f45462af 100644 --- a/src/llm/anthropic/params.rs +++ b/src/llm/anthropic/params.rs @@ -7,7 +7,6 @@ use super::tools; use reqwest::RequestBuilder; use rig::completion::CompletionRequest; -const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages"; const CLAUDE_CODE_SYSTEM_PREAMBLE: &str = "You are Claude Code, Anthropic's official CLI for Claude."; @@ -32,13 +31,30 @@ fn is_opus(model_id: &str) -> bool { model_id.contains("opus") } +/// Construct the full messages endpoint URL from a base URL. +/// +/// If the base URL already ends with a path segment (e.g. `/v1/messages`), +/// use it as-is. Otherwise append `/v1/messages`. +fn messages_url(base_url: &str) -> String { + let trimmed = base_url.trim_end_matches('/'); + if trimmed.ends_with("/v1/messages") { + trimmed.to_string() + } else { + format!("{trimmed}/v1/messages") + } +} + /// Build a fully configured Anthropic API request from a CompletionRequest. /// +/// `base_url` is the provider's configured base URL (e.g. `https://api.anthropic.com` +/// or a custom proxy). The `/v1/messages` path is appended automatically. +/// /// `thinking_effort` controls adaptive thinking: "auto" picks max for Opus / /// high for others, or pass "max", "high", "medium", "low" explicitly. pub fn build_anthropic_request( http_client: &reqwest::Client, api_key: &str, + base_url: &str, model_name: &str, request: &CompletionRequest, thinking_effort: &str, @@ -46,7 +62,8 @@ pub fn build_anthropic_request( let is_oauth = auth::detect_auth_path(api_key) == AnthropicAuthPath::OAuthToken; let adaptive_thinking = supports_adaptive_thinking(model_name); let retention = cache::resolve_cache_retention(None); - let cache_control = cache::get_cache_control(ANTHROPIC_API_URL, retention); + let url = messages_url(base_url); + let cache_control = cache::get_cache_control(&url, retention); let mut body = serde_json::json!({ "model": model_name, @@ -80,7 +97,7 @@ pub fn build_anthropic_request( } let builder = http_client - .post(ANTHROPIC_API_URL) + .post(&url) .header("anthropic-version", "2023-06-01") .header("content-type", "application/json"); diff --git a/src/llm/model.rs b/src/llm/model.rs index fdb8d69de..95d81a914 100644 --- a/src/llm/model.rs +++ b/src/llm/model.rs @@ -352,6 +352,7 @@ impl SpacebotModel { let anthropic_request = crate::llm::anthropic::build_anthropic_request( self.llm_manager.http_client(), api_key, + &provider_config.base_url, &self.model_name, &request, effort, From a358a2b8eb0a1cef3028c9f0e7cab29174b794f6 Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:27:12 -0500 Subject: [PATCH 4/7] fix: remove VOLUME keyword from Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VOLUME keyword is banned on Railway deployments. The /data directory is still configured via SPACEBOT_DIR env var — volumes should be mounted via the hosting platform's configuration instead. --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 84124c3c1..5f757f9ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,8 +71,6 @@ ENV SPACEBOT_DIR=/data ENV SPACEBOT_DEPLOYMENT=docker EXPOSE 19898 18789 -VOLUME /data - HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:19898/api/health || exit 1 From c4f2d5b12afc0515f7ec3b0823ad4923a9799bc1 Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:32:44 -0500 Subject: [PATCH 5/7] fix: add cache mount IDs to Dockerfile for Railway compatibility Railway requires --mount=type=cache,id= format. --- Dockerfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5f757f9ca..0c9b5dde4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,19 +22,19 @@ WORKDIR /build # 1. Fetch and cache Rust dependencies. # cargo fetch needs a valid target, so we create stubs that get replaced later. COPY Cargo.toml Cargo.lock ./ -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/build/target \ +RUN --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry \ + --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ + --mount=type=cache,id=cargo-target,target=/build/target \ mkdir src && echo "fn main() {}" > src/main.rs && touch src/lib.rs \ && cargo build --release \ && rm -rf src # 2. Build the frontend. COPY interface/package.json interface/ -RUN --mount=type=cache,target=/root/.bun/install/cache \ +RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \ cd interface && bun install COPY interface/ interface/ -RUN --mount=type=cache,target=/root/.bun/install/cache \ +RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \ cd interface && bun run build # 3. Copy source and compile the real binary. @@ -45,9 +45,9 @@ COPY build.rs ./ COPY prompts/ prompts/ COPY migrations/ migrations/ COPY src/ src/ -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/build/target \ +RUN --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry \ + --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ + --mount=type=cache,id=cargo-target,target=/build/target \ SPACEBOT_SKIP_FRONTEND_BUILD=1 cargo build --release \ && mv /build/target/release/spacebot /usr/local/bin/spacebot \ && cargo clean -p spacebot --release --target-dir /build/target From 3cba979c5e1c46dfc72728c9cb0c641b36b70810 Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:34:54 -0500 Subject: [PATCH 6/7] fix: remove BuildKit cache mounts for Railway compatibility Railway requires cache mount IDs prefixed with a hardcoded service UUID (--mount=type=cache,id=s/-), which is incompatible with a generic open-source Dockerfile. Remove all cache mounts since they are a build-speed optimization, not a correctness requirement. --- Dockerfile | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0c9b5dde4..7388f3dc7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,3 @@ -# syntax=docker/dockerfile:1.6 # ---- Builder stage ---- # Compiles the React frontend and the Rust binary with the frontend embedded. FROM rust:bookworm AS builder @@ -22,20 +21,15 @@ WORKDIR /build # 1. Fetch and cache Rust dependencies. # cargo fetch needs a valid target, so we create stubs that get replaced later. COPY Cargo.toml Cargo.lock ./ -RUN --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry \ - --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ - --mount=type=cache,id=cargo-target,target=/build/target \ - mkdir src && echo "fn main() {}" > src/main.rs && touch src/lib.rs \ +RUN mkdir src && echo "fn main() {}" > src/main.rs && touch src/lib.rs \ && cargo build --release \ && rm -rf src # 2. Build the frontend. COPY interface/package.json interface/ -RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \ - cd interface && bun install +RUN cd interface && bun install COPY interface/ interface/ -RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \ - cd interface && bun run build +RUN cd interface && bun run build # 3. Copy source and compile the real binary. # build.rs runs the frontend build (already done above, node_modules present). @@ -45,10 +39,7 @@ COPY build.rs ./ COPY prompts/ prompts/ COPY migrations/ migrations/ COPY src/ src/ -RUN --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry \ - --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ - --mount=type=cache,id=cargo-target,target=/build/target \ - SPACEBOT_SKIP_FRONTEND_BUILD=1 cargo build --release \ +RUN SPACEBOT_SKIP_FRONTEND_BUILD=1 cargo build --release \ && mv /build/target/release/spacebot /usr/local/bin/spacebot \ && cargo clean -p spacebot --release --target-dir /build/target From a8fc4bf8497d75edfb971157df2d9a4a29d03ab0 Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:52:41 -0500 Subject: [PATCH 7/7] Update .gitignore --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 86cb5bf24..d650bb21a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,13 @@ interface/dist/ .idea list/ agents/ + +# Claude Code - Fichiers propriétaires (auto-généré) +.claude/ +CLAUDE.md +.claude-context +.claude-memory +.claude-decisions +docs/phases/ +docs/specs/ +PROJECT-STATUS.md