docs: document llm.toml and llama-swap metadata discovery
README, CLAUDE.md and .env.example now point at llm.toml for LLM settings (OPENROUTER_API_KEY stays in env) and describe the legacy env override window, the discovered picker and the startup cross-check. ADR-0003 records the move and amends ADR-0002's deployment-config home; CONTEXT.md's Deployment config / Hard default entries follow. Source comments and the "set LLAMA_SWAP_URL" error strings name the llm.toml keys. Also fixes README's AGENTIC_MAX_ITERATIONS default (10 -> 12, what the code uses). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d68d9b09f3
commit
09b9e87396
21 files changed
+209
-244
No files matched your search
+1
-1
@@ -1089,7 +1089,7 @@ pub struct OpenRouterModelsResponse {
|
||||
|
||||
/// GET /insights/openrouter/models - Curated OpenRouter model ids exposed
|
||||
/// to clients for the hybrid backend. Returned verbatim from
|
||||
/// `OPENROUTER_ALLOWED_MODELS`; no live call to OpenRouter.
|
||||
/// `openrouter.allowed`; no live call to OpenRouter.
|
||||
#[get("/insights/openrouter/models")]
|
||||
pub async fn get_openrouter_models_handler(
|
||||
_claims: Claims,
|
||||
|
||||
@@ -2164,7 +2164,7 @@ fn resolve_date_taken_for_context(
|
||||
/// symmetry and future rules.
|
||||
///
|
||||
/// Whether "local" routes through Ollama or llama-swap is decided at
|
||||
/// startup by `LLM_BACKEND`; both share the same transcript shape from
|
||||
/// startup by llm.toml's `backend`; both share the same transcript shape from
|
||||
/// the chat-replay perspective.
|
||||
fn validate_cross_replay(_stored: &str, effective: &str) -> Result<()> {
|
||||
if !matches!(effective, "local" | "hybrid") {
|
||||
@@ -3829,7 +3829,7 @@ mod tests {
|
||||
#[test]
|
||||
fn bootstrap_backend_rejects_unknown_label() {
|
||||
// `llamacpp` is no longer a per-request backend value — it's chosen
|
||||
// at deploy time via `LLM_BACKEND`.
|
||||
// at deploy time via llm.toml's `backend`.
|
||||
for label in &["openrouter", "llamacpp", "ollama"] {
|
||||
let err = resolve_bootstrap_backend(Some(label)).unwrap_err();
|
||||
let msg = format!("{}", err);
|
||||
|
||||
@@ -234,7 +234,7 @@ pub struct InsightGenerator {
|
||||
/// `None` when `OPENROUTER_API_KEY` is not configured.
|
||||
openrouter: Option<Arc<OpenRouterClient>>,
|
||||
/// Optional llama-swap client, used when `backend=llamacpp` is requested.
|
||||
/// `None` when `LLAMA_SWAP_URL` is not configured.
|
||||
/// `None` when `llamacpp.url` is not configured.
|
||||
llamacpp: Option<Arc<LlamaCppClient>>,
|
||||
sms_client: SmsApiClient,
|
||||
/// Optional integration with Apollo's user-defined Places. When the
|
||||
@@ -735,7 +735,7 @@ impl InsightGenerator {
|
||||
log::info!("========================================");
|
||||
|
||||
// Generate embedding for the query via the configured local backend
|
||||
// (`LLM_BACKEND` switch). Must match the backend that populated the
|
||||
// (llm.toml `backend` switch). Must match the backend that populated the
|
||||
// daily-summary embeddings or similarity search will be garbage.
|
||||
let query_embedding =
|
||||
crate::ai::embed_query(&self.ollama, self.llamacpp.as_deref(), &query).await?;
|
||||
@@ -3331,7 +3331,7 @@ Return ONLY the summary, nothing else."#,
|
||||
}
|
||||
|
||||
/// Tool: store_entity — upsert an entity into the knowledge memory.
|
||||
/// Embeddings go through the configured local backend (`LLM_BACKEND`),
|
||||
/// Embeddings go through the configured local backend (llm.toml `backend`),
|
||||
/// independent of the per-request chat backend in the caller.
|
||||
async fn tool_store_entity(&self, args: &serde_json::Value) -> String {
|
||||
use crate::database::models::InsertEntity;
|
||||
|
||||
+3
-3
@@ -69,11 +69,11 @@ pub struct LlamaCppClient {
|
||||
pub embedding_model: String,
|
||||
/// Vision model slot id. Used for `describe_image` routing. Defaults
|
||||
/// to `primary_model` so describe_image works out of the box; override
|
||||
/// via `LLAMA_SWAP_VISION_MODEL` for a dedicated vision slot.
|
||||
/// via `llamacpp.vision` for a dedicated vision slot.
|
||||
pub vision_model: String,
|
||||
/// TTS model slot id (e.g. `"chatterbox"`). Routes `text_to_speech` and
|
||||
/// is the `/upstream/<id>/voices` path segment for the voice library.
|
||||
/// Override via `LLAMA_SWAP_TTS_MODEL`.
|
||||
/// Set via `tts.model`.
|
||||
pub tts_model: String,
|
||||
num_ctx: Option<i32>,
|
||||
temperature: Option<f32>,
|
||||
@@ -1778,7 +1778,7 @@ mod tests {
|
||||
// In hybrid mode, overrides.model is an OpenRouter model id
|
||||
// (e.g. "google/gemini-3-flash-preview"). The local llamacpp
|
||||
// client must NOT adopt that as its vision_model — it should
|
||||
// keep the configured LLAMA_SWAP_VISION_MODEL so describe_image
|
||||
// keep the configured `llamacpp.vision` so describe_image
|
||||
// hits the correct local slot instead of sending an unknown
|
||||
// model name to llama-swap.
|
||||
let mut base = LlamaCppClient::new(None, Some("chat".into()));
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
//! Bundle of the local LLM pair (Ollama + optional llama-swap) with the
|
||||
//! `LLM_BACKEND` dispatch baked in.
|
||||
//! `backend` (llm.toml) dispatch baked in.
|
||||
//!
|
||||
//! Exists because passing the pair around as loose values invited the same
|
||||
//! bug three times: import/backfill tooling embedded corpora via
|
||||
//! `OllamaClient` directly while the query side dispatched through
|
||||
//! `embed_one`, so flipping `LLM_BACKEND=llamacpp` silently split queries
|
||||
//! `embed_one`, so flipping `backend = "llamacpp"` silently split queries
|
||||
//! and corpus into different vector spaces. Anything that writes or reads
|
||||
//! embeddings should go through this type (or `embed_one`/`embed_many`),
|
||||
//! never a concrete client.
|
||||
|
||||
+1
-1
@@ -1163,7 +1163,7 @@ Analyze the image and use specific details from both the visual content and the
|
||||
}
|
||||
};
|
||||
|
||||
// Validate embedding dimensions (EMBEDDING_DIM; 768 for nomic-embed-text:v1.5)
|
||||
// Validate embedding dimensions (`embedding.dim`; 768 for nomic-embed-text:v1.5)
|
||||
for (i, embedding) in embeddings.iter().enumerate() {
|
||||
if embedding.len() != crate::ai::embedding_dim() {
|
||||
log::warn!(
|
||||
|
||||
@@ -26,7 +26,7 @@ const CACHE_DURATION_SECS: u64 = 15 * 60;
|
||||
/// Default completion cap sent as `max_tokens`. OpenRouter pre-authorizes
|
||||
/// credit against the requested max (defaulting to the model's full output
|
||||
/// window when omitted), so expensive models fail with credit-limit errors
|
||||
/// even when the actual reply is short. Override via `OPENROUTER_MAX_TOKENS`.
|
||||
/// even when the actual reply is short. Override via `openrouter.max_tokens`.
|
||||
pub const DEFAULT_MAX_TOKENS: u32 = 16384;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
+5
-5
@@ -391,7 +391,7 @@ pub async fn tts_speech_handler(
|
||||
let Some(client) = app_state.llamacpp.as_ref() else {
|
||||
span.set_status(Status::error("tts backend not configured"));
|
||||
return HttpResponse::ServiceUnavailable()
|
||||
.json(json!({ "error": "TTS backend not configured (set LLAMA_SWAP_URL)" }));
|
||||
.json(json!({ "error": "TTS backend not configured (set llamacpp.url in llm.toml)" }));
|
||||
};
|
||||
|
||||
let format = req
|
||||
@@ -521,7 +521,7 @@ pub async fn create_speech_job_handler(
|
||||
if app_state.llamacpp.is_none() {
|
||||
span.set_status(Status::error("tts backend not configured"));
|
||||
return HttpResponse::ServiceUnavailable()
|
||||
.json(json!({ "error": "TTS backend not configured (set LLAMA_SWAP_URL)" }));
|
||||
.json(json!({ "error": "TTS backend not configured (set llamacpp.url in llm.toml)" }));
|
||||
}
|
||||
|
||||
let format = req
|
||||
@@ -958,7 +958,7 @@ pub struct CreateVoiceFromLibraryRequest {
|
||||
/// lets the client pick the clean-speech section of a long recording.
|
||||
#[serde(default)]
|
||||
pub start_seconds: Option<f64>,
|
||||
/// Reference window length; clamped to LLAMA_SWAP_TTS_REF_SECONDS.
|
||||
/// Reference window length; clamped to `tts.ref_seconds`.
|
||||
#[serde(default)]
|
||||
pub duration_seconds: Option<f64>,
|
||||
}
|
||||
@@ -966,7 +966,7 @@ pub struct CreateVoiceFromLibraryRequest {
|
||||
/// POST /tts/voices/from-library — register a cloned voice from a file already
|
||||
/// in a library. Audio and video alike are ffmpeg-normalized to a mono 24 kHz
|
||||
/// WAV reference clip (window selected by start/duration_seconds, length
|
||||
/// capped by LLAMA_SWAP_TTS_REF_SECONDS).
|
||||
/// capped by `tts.ref_seconds`).
|
||||
#[post("/tts/voices/from-library")]
|
||||
#[tracing::instrument(name = "http.tts.voices.from_library", skip_all)]
|
||||
pub async fn create_voice_from_library_handler(
|
||||
@@ -1153,7 +1153,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolve_ref_window_defaults_to_start_of_clip_at_cap_length() {
|
||||
// Reads the live cap rather than mutating LLAMA_SWAP_TTS_REF_SECONDS:
|
||||
// Reads the live cap rather than mutating `tts.ref_seconds`:
|
||||
// env mutation flakes under the parallel suite (see env_dispatch).
|
||||
let cap = f64::from(tts_ref_seconds());
|
||||
assert_eq!(resolve_ref_window(None, None), Ok((0.0, cap)));
|
||||
|
||||
@@ -42,7 +42,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
info!("Found {} calendar events", events.len());
|
||||
|
||||
// LocalLlm dispatches per LLM_BACKEND, so embeddings written here land
|
||||
// LocalLlm dispatches per llm.toml's backend, so embeddings written here land
|
||||
// in the same vector space the query side searches.
|
||||
let llm = if args.generate_embeddings {
|
||||
Some(LocalLlm::from_config())
|
||||
|
||||
@@ -38,7 +38,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
info!("Found {} search records", searches.len());
|
||||
|
||||
// LocalLlm dispatches per LLM_BACKEND, so embeddings written here land
|
||||
// LocalLlm dispatches per llm.toml's backend, so embeddings written here land
|
||||
// in the same vector space the query side searches.
|
||||
let llm = LocalLlm::from_config();
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ struct Args {
|
||||
#[arg(long)]
|
||||
path: Option<String>,
|
||||
|
||||
/// Ollama model override. Defaults to OLLAMA_PRIMARY_MODEL from .env
|
||||
/// Ollama model override. Defaults to ollama.primary_model from llm.toml
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Re-embed stored corpora through `LocalLlm`, i.e. the same
|
||||
//! `LLM_BACKEND` dispatch the query side uses. The original import /
|
||||
//! `backend` (llm.toml) dispatch the query side uses. The original import /
|
||||
//! backfill tools always embedded via Ollama, so a deploy running
|
||||
//! `LLM_BACKEND=llamacpp` queries vector spaces the corpora may not live
|
||||
//! `backend = "llamacpp"` queries vector spaces the corpora may not live
|
||||
//! in. Three tables share the problem and are all covered here:
|
||||
//!
|
||||
//! - `daily_conversation_summaries` — re-embeds
|
||||
@@ -30,7 +30,7 @@ use image_api::bin_progress;
|
||||
use std::env;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about = "Re-embed stored corpora via the configured LLM_BACKEND", long_about = None)]
|
||||
#[command(author, version, about = "Re-embed stored corpora via the configured local backend", long_about = None)]
|
||||
struct Args {
|
||||
/// Comma-separated tables to process: summaries, calendar, search, entities
|
||||
#[arg(long, default_value = "summaries,calendar,search,entities")]
|
||||
@@ -195,12 +195,12 @@ async fn reembed_table(
|
||||
};
|
||||
|
||||
// The whole pipeline (DAO checks, stored corpora) assumes
|
||||
// EMBEDDING_DIM dims. A mismatch means the active embed slot is not
|
||||
// embedding.dim dims. A mismatch means the active embed slot is not
|
||||
// serving the configured model — stop rather than corrupt the table.
|
||||
anyhow::ensure!(
|
||||
new_emb.len() == image_api::ai::embedding_dim(),
|
||||
"backend returned {}-dim embedding (expected {}) — '{}' does not \
|
||||
match the configured EMBEDDING_DIM",
|
||||
match the configured embedding.dim",
|
||||
new_emb.len(),
|
||||
image_api::ai::embedding_dim(),
|
||||
llm.embedding_model_version()
|
||||
|
||||
+2
-2
@@ -157,7 +157,7 @@ pub enum ScripterMode {
|
||||
/// Fast path: single LLM call via the direct client.
|
||||
Fast,
|
||||
/// Agentic path: resolves the backend through the InsightGenerator
|
||||
/// (honouring LLM_BACKEND, model overrides, etc.). Falls back to
|
||||
/// (honouring llm.toml's backend, model overrides, etc.). Falls back to
|
||||
/// Fast on error so a scripting failure never sinks a reel.
|
||||
Agentic,
|
||||
}
|
||||
@@ -450,7 +450,7 @@ pub async fn create_reel_handler(
|
||||
) -> impl Responder {
|
||||
if app_state.llamacpp.is_none() {
|
||||
return HttpResponse::ServiceUnavailable().json(json!({
|
||||
"error": "Reel narration needs the LLM/TTS backend (set LLAMA_SWAP_URL)"
|
||||
"error": "Reel narration needs the LLM/TTS backend (set llamacpp.url in llm.toml)"
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -289,7 +289,7 @@ pub async fn generate_script(
|
||||
}
|
||||
|
||||
/// Agentic version of script generation: resolves the backend via the
|
||||
/// InsightGenerator (honouring LLM_BACKEND, model overrides, etc.), builds
|
||||
/// InsightGenerator (honouring llm.toml's backend, model overrides, etc.), builds
|
||||
/// a read-only tool set, runs the tool loop, then parses the JSON response.
|
||||
/// Returns the same ReelScript shape. On failure the caller may fall back to
|
||||
/// `generate_script`.
|
||||
|
||||
+2
-2
@@ -70,9 +70,9 @@ pub struct AppState {
|
||||
#[allow(dead_code)]
|
||||
pub openrouter: Option<Arc<OpenRouterClient>>,
|
||||
/// Curated list of OpenRouter model ids exposed to clients. Sourced from
|
||||
/// `OPENROUTER_ALLOWED_MODELS` (comma-separated). Empty when unset.
|
||||
/// `openrouter.allowed`. Empty when unset.
|
||||
pub openrouter_allowed_models: Vec<String>,
|
||||
/// `None` when `LLAMA_SWAP_URL` is not configured. Consulted only when a
|
||||
/// `None` when `llamacpp.url` is not configured. Consulted only when a
|
||||
/// request explicitly opts into `backend=llamacpp`. Same shape as the
|
||||
/// `openrouter` slot — present here so handlers can route to it without
|
||||
/// threading through the generator.
|
||||
|
||||
@@ -635,7 +635,7 @@ async fn translate<TagD: TagDao>(
|
||||
}
|
||||
};
|
||||
|
||||
// Respect env/config for the LLM backend (LLM_BACKEND → ollama or
|
||||
// Respect env/config for the LLM backend (llm.toml `backend` → ollama or
|
||||
// llama-swap); local only, no hybrid, per the feature's design.
|
||||
//
|
||||
// Translation-model precedence:
|
||||
|
||||
Reference in new issue
Block a user