diff --git a/README.md b/README.md index 2f3c009..bdac886 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ - **Rust toolchain** ≥ 1.85 (workspace 가 edition 2024 + resolver 3 사용). [rustup](https://rustup.rs) 권장. - **Ollama** — `kebab ask` 와 이미지 OCR/caption 가 사용. `https://ollama.com/download` 에서 설치 후 `ollama serve` 실행. 기본 LLM 은 gemma4 계열 (`ollama pull gemma4:e4b`) — OCR / caption 도 같은 family 라 모델 하나만 pull 하면 됨. 더 큰 variant 원하면 `gemma4:26b` 등으로 config override. config 의 `[models.llm].endpoint` 에 host:port 명시. + - **CPU only / RAM ≤ 16 GB 환경 권장 모델**: gemma4:e4b (8B) 는 CPU 추론에 무거워 RAG 한 답변이 5분을 넘기기 쉽다 — `[models.llm] request_timeout_secs` 의 기본 300 s 한도에 걸려 `error: kb-rag: llm.generate_stream` 으로 떨어진다 (HOTFIXES 2026-05-25). `gemma3:4b` / `qwen2.5:3b` / `phi3:mini` 같은 ≤ 4B Q4 모델로 바꾸면 답변 1-3 분에 안정 동작 (확장 도그푸딩에서 검증). 모델 storage 가 부담이면 `OLLAMA_MODELS=/path` env 로 위치 분리 가능. + - **`request_timeout_secs` 노브 (v0.17.0)**: `[models.llm] request_timeout_secs = 1200` (또는 `KEBAB_MODELS_LLM_REQUEST_TIMEOUT_SECS=1200`) 로 한도를 늘려 큰 모델도 시도 가능. 단 응답 동안 RAM 점유가 길어진다. - **빌드 디스크** — 첫 빌드 시 `target/` 가 6–10 GB (Lance + DataFusion + fastembed). 여유 확인. - **fastembed 모델** — 첫 `kebab ingest` 시 `multilingual-e5-large` (~1.3 GB, fb-39b) 자동 다운로드. `config.toml` 에서 `model = "multilingual-e5-small"` 로 명시하면 이전 모델 사용. diff --git a/crates/kebab-config/src/lib.rs b/crates/kebab-config/src/lib.rs index 1117713..a91ec43 100644 --- a/crates/kebab-config/src/lib.rs +++ b/crates/kebab-config/src/lib.rs @@ -122,6 +122,16 @@ pub struct LlmCfg { pub endpoint: String, pub temperature: f32, pub seed: u64, + /// v0.17.0 post-dogfood: Hard ceiling on a single HTTP exchange to + /// the LLM endpoint (Ollama, etc.). Cold-loading an 8B+ model on + /// CPU-only hosts can spend 60-90s on model load + several minutes + /// on a first inference, blowing past the old hard-coded 300s cap + /// and surfacing as `error: kb-rag: llm.generate_stream` to the + /// user. Config-driven so 16-GB / CPU-only deployments using small + /// (≤4B) models can keep the original 300s and large-model dogfood + /// can dial it up (e.g. 1200s) without rebuilding. + #[serde(default = "default_llm_request_timeout_secs")] + pub request_timeout_secs: u64, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -147,6 +157,13 @@ fn default_cache_capacity() -> usize { 256 } +/// v0.17.0 post-dogfood: matches the legacy hard-coded ceiling so +/// existing configs that omit the field keep behaving identically. +/// Overridable per config / `KEBAB_MODELS_LLM_REQUEST_TIMEOUT_SECS`. +fn default_llm_request_timeout_secs() -> u64 { + 300 +} + fn default_stale_threshold_days() -> u32 { 30 } @@ -363,12 +380,14 @@ impl Config { // gemma4 계열 통일 — OCR (P6-2) + caption (P6-3) // 어댑터가 같은 family 사용. 사용자가 더 큰 // variant (gemma4:26b 등) 원하면 자기 config.toml - // 에서 override. + // 에서 override. CPU-only / ≤16 GB RAM 환경이면 + // gemma3:4b 같은 ≤4B Q4 모델 권장 (README 참조). model: "gemma4:e4b".to_string(), context_tokens: 32768, endpoint: "http://127.0.0.1:11434".to_string(), temperature: 0.0, seed: 0, + request_timeout_secs: default_llm_request_timeout_secs(), }, }, search: SearchCfg { @@ -621,6 +640,11 @@ impl Config { self.models.llm.seed = n; } } + "KEBAB_MODELS_LLM_REQUEST_TIMEOUT_SECS" => { + if let Ok(n) = v.parse::() { + self.models.llm.request_timeout_secs = n; + } + } // search "KEBAB_SEARCH_DEFAULT_K" => { @@ -873,6 +897,103 @@ mod tests { assert!((c.models.llm.temperature - 0.7).abs() < 1e-6); } + /// v0.17.0 post-dogfood: matches the legacy hard-coded 300s cap so + /// existing configs that omit the new field are not affected. + #[test] + fn default_llm_request_timeout_secs_is_300() { + assert_eq!(Config::defaults().models.llm.request_timeout_secs, 300); + } + + #[test] + fn env_overrides_models_llm_request_timeout_secs() { + let mut env = HashMap::new(); + env.insert( + "KEBAB_MODELS_LLM_REQUEST_TIMEOUT_SECS".to_string(), + "1200".to_string(), + ); + let c = Config::defaults().apply_env(&env); + assert_eq!(c.models.llm.request_timeout_secs, 1200); + } + + /// v0.17.0 post-dogfood: a config file written before the field + /// existed (no `request_timeout_secs` key) must still parse and fall + /// back to the 300s default — backwards-compat invariant. + #[test] + fn legacy_config_without_request_timeout_secs_uses_default() { + let toml_src = r#" +schema_version = 1 + +[workspace] +root = "/tmp/x" +exclude = [] + +[storage] +data_dir = "/tmp/x" +sqlite = "/tmp/x/kebab.sqlite" +vector_dir = "/tmp/x/lancedb" +asset_dir = "/tmp/x/assets" +artifact_dir = "/tmp/x/artifacts" +model_dir = "/tmp/x/models" +runs_dir = "/tmp/x/runs" +copy_threshold_mb = 100 + +[indexing] +max_parallel_extractors = 2 +max_parallel_embeddings = 1 +watch_filesystem = false + +[chunking] +target_tokens = 500 +overlap_tokens = 80 +respect_markdown_headings = true +chunker_version = "md-heading-v1" + +[models.embedding] +provider = "fastembed" +model = "multilingual-e5-large" +version = "v1" +dimensions = 1024 +batch_size = 64 + +[models.llm] +provider = "ollama" +model = "gemma3:4b" +context_tokens = 4096 +endpoint = "http://127.0.0.1:11434" +temperature = 0.0 +seed = 0 + +[search] +default_k = 10 +hybrid_fusion = "rrf" +rrf_k = 60 +snippet_chars = 220 + +[rag] +prompt_template_version = "rag-v2" +score_gate = 0.3 +explain_default = false +max_context_tokens = 8000 + +[image.ocr] +enabled = false +engine = "ollama-vision" +model = "gemma3:4b" +languages = ["eng"] +max_pixels = 1600 + +[image.caption] +enabled = false +max_pixels = 768 +prompt_template_version = "caption-v1" + +[ui] +theme = "dark" +"#; + let c: Config = toml::from_str(toml_src).expect("parse legacy config"); + assert_eq!(c.models.llm.request_timeout_secs, 300); + } + #[test] fn env_overrides_indexing_watch_filesystem_bool() { let mut env = HashMap::new(); diff --git a/crates/kebab-llm-local/src/ollama.rs b/crates/kebab-llm-local/src/ollama.rs index 42a9b13..a3205a0 100644 --- a/crates/kebab-llm-local/src/ollama.rs +++ b/crates/kebab-llm-local/src/ollama.rs @@ -48,10 +48,12 @@ use serde::{Deserialize, Serialize}; use crate::error::LlmError; -/// Hard ceiling on a single HTTP exchange. Cold-loading a 14B model on -/// first call can take ~30s; 5 minutes is generous without being -/// open-ended. -const REQUEST_TIMEOUT: Duration = Duration::from_secs(300); +// v0.17.0 post-dogfood: the per-request ceiling now lives in +// `kebab_config::LlmCfg::request_timeout_secs` (default 300s) so users +// running larger models on CPU-only hosts can extend it without a +// rebuild. Cold-loading an 8B+ model on first call routinely takes +// 60-90 s plus multi-minute inference; 300s was the legacy hard +// ceiling and remains the default for back-compat. /// `reqwest::blocking` adapter implementing [`LanguageModel`] over Ollama's /// local HTTP API. Construction is cheap and offline; the first network @@ -79,7 +81,7 @@ impl OllamaLanguageModel { pub fn new(config: &kebab_config::Config) -> anyhow::Result { let llm = &config.models.llm; let client = reqwest::blocking::Client::builder() - .timeout(REQUEST_TIMEOUT) + .timeout(Duration::from_secs(llm.request_timeout_secs)) .build()?; Ok(Self { client, @@ -262,9 +264,11 @@ struct OllamaLine { /// /// Timeout invariant: the iterator has no inherent stop condition for an /// indefinitely-stalled server — only the underlying -/// `reqwest::blocking::Client`'s read timeout (`REQUEST_TIMEOUT`, 300s) -/// breaks the hang. Callers needing tighter cancellation should adjust -/// the client timeout in [`OllamaLanguageModel::new`]. +/// `reqwest::blocking::Client`'s read timeout (configured via +/// `kebab_config::LlmCfg::request_timeout_secs`, default 300 s) breaks +/// the hang. Callers needing tighter / looser bounds should set +/// `[models.llm] request_timeout_secs = N` (or +/// `KEBAB_MODELS_LLM_REQUEST_TIMEOUT_SECS=N`) before building. struct OllamaStream { reader: BufReader, line_buf: Vec, diff --git a/docs/SMOKE.md b/docs/SMOKE.md index c191cea..60f9582 100644 --- a/docs/SMOKE.md +++ b/docs/SMOKE.md @@ -21,6 +21,11 @@ cargo build --release -p kebab-cli # debug 도 무방. 디버그가 더 빠르 # Mac 등 별도 호스트에서 OLLAMA_HOST=0.0.0.0:11434 ollama serve ollama pull gemma4:e4b # 기본 default. 더 큰 variant 원하면 gemma4:26b +# CPU only / RAM ≤ 16 GB 환경이면 ≤ 4B Q4 모델 권장 (gemma3:4b / qwen2.5:3b 등) — +# 8B+ 모델은 첫 RAG 답변이 5분 (기본 [models.llm] request_timeout_secs) +# 한도를 넘기 쉬워 `error: kb-rag: llm.generate_stream` 으로 떨어짐. +# 노브 늘리려면 config 에 request_timeout_secs = 1200 추가 +# 또는 KEBAB_MODELS_LLM_REQUEST_TIMEOUT_SECS=1200 env. HOTFIXES 2026-05-25 참조. ``` 본 머신에서 reachability 검증: diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index 944cb8d..044db27 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -14,6 +14,25 @@ historical contract that was implemented; this file accumulates the deltas so phase 5+ readers can find the live behavior without diffing git history. +## 2026-05-25 — v0.17.0 post-dogfood: `[models.llm] request_timeout_secs` 노브 + 권장 모델 가이드 + +v0.17.0 확장 도그푸딩에서 발견: 사용자가 default `gemma4:e4b` (8B Q4, 9.6 GB) 를 CPU only / 16 GB RAM 환경에서 시도 시 첫 RAG 답변이 인 5 분 (hard-coded 300 s) 한도를 항상 넘겨 `error: kb-rag: llm.generate_stream` 으로 떨어졌다. 메모리도 ollama RSS 10.7 GB / free 2 GB 까지 압박. 확장 도그푸딩 32 분 / 199 mem-monitor sample 결과는 `tasks/HOTFIXES.md` 의 본 entry 와 conversation 의 도그푸딩 보고 참조. + +**변경**: +- `crates/kebab-config/src/lib.rs::LlmCfg` 에 `request_timeout_secs: u64` additive 필드 (`#[serde(default = "default_llm_request_timeout_secs")]`, default `300`). 옛 config 가 필드 누락해도 그대로 파싱 + 동일 동작 (3 신규 unit test 가 default / env override / legacy parse 핀). +- env override `KEBAB_MODELS_LLM_REQUEST_TIMEOUT_SECS`. +- `crates/kebab-llm-local/src/ollama.rs` 의 `REQUEST_TIMEOUT` 상수 제거. `OllamaLanguageModel::new` 가 `Duration::from_secs(llm.request_timeout_secs)` 로 reqwest blocking client 빌드. doc comment 도 동일하게 갱신. +- `README.md` 사전 요구 절 + `docs/SMOKE.md` 의 ollama 안내에 권장 모델 (≤ 4B Q4 — `gemma3:4b` / `qwen2.5:3b` / `phi3:mini`) + timeout 노브 anchor 한 줄. 8B+ 시도 시 timeout 패턴 사전 안내. +- `crates/kebab-config/src/lib.rs::Config::defaults` 의 LlmCfg literal 에 `request_timeout_secs: default_llm_request_timeout_secs()` + comment 한 줄로 CPU only 권장 안내. + +**미진행 (scope 밖)**: +- `crates/kebab-parse-image/src/ocr.rs::REQUEST_TIMEOUT` 도 동일한 hard-coded 300 s — OCR 이 보통 짧아 LLM 만큼 부담 안 되지만, 일관성 측면에서 다음 round 에 같은 노브 (또는 별 노브) 로 재검토. +- `kebab ask --stream` (fb-33) 권장 강조: 5분 cold-start 동안 첫 token 빠르게 surface — UX 개선. README/SKILL.md 추가 한 줄 후속. + +**확장 도그푸딩 baseline 보존**: `/build/cache/dogfood-v017/` (466 MB workspace + DB + memory.log), `/build/cache/ollama/` (21 GB binary + gemma3:4b/gemma4:e4b 모델). 다음 round 회귀 비교용. + +Cross-link: `crates/kebab-config/src/lib.rs::LlmCfg::request_timeout_secs`, `crates/kebab-llm-local/src/ollama.rs::OllamaLanguageModel::new`. + ## 2026-05-24 — v0.17.0: 한국어 trigram FTS5 tokenizer 채택 (closure of 2026-05-22 한국어 lexical) V007 migration 으로 `chunks_fts` 의 tokenizer 를 `unicode61` → `trigram` 으로 교체. `chunks` 원본 + embedding + vector index 는 그대로, FTS shadow 만 재구축 + 자동 backfill — 사용자는 `kebab ingest` 재실행 불필요 (binary 만 교체하면 다음 open 시 V007 가 즉시 적용). 같은 라운드의 다른 두 follow-up (`code_lang_chunk_breakdown`, C typedef) 은 별 PR (PR-C / PR-B).