From 41c5edc517f1524d4609ad35e8843a1ac34a86c7 Mon Sep 17 00:00:00 2001 From: altair823 Date: Mon, 25 May 2026 05:06:53 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(image.ocr):=20request=5Ftimeout=5Fsecs?= =?UTF-8?q?=20config=20knob=20+=20closure=20of=20v0.17.1=20=EB=AF=B8?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.17.1 (PR #162) 가 LLM 쪽 hard-coded 300s 를 [models.llm] request_timeout_secs 로 풀어준 것과 같은 패턴을 OCR 어댑터에 적용. 사용자 결정으로 별 노브 분리 ([image.ocr] request_timeout_secs) — OCR 는 LLM 대비 cold start 패턴이 달라 독립 조절이 편함. - OcrCfg.request_timeout_secs: u64 (serde default 300) - KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS env override - OllamaVisionOcr::build / from_parts 시그니처에 timeout 인자 추가 - REQUEST_TIMEOUT 상수 제거 - 3 신규 unit test (default / env / legacy parse) — LlmCfg 패턴 그대로 - HOTFIXES 2026-05-25 v0.17.1 entry 의 두 미진행 항목 모두 closure (OCR timeout = 본 PR, --stream docs = PR #163 에서 이미 완료) 기존 config / 옛 KB 영향 없음 — 새 필드는 default 로 채워지고 동작도 동일 (300s). vision 모델 cold start 가 길면 env 또는 config 로 늘릴 수 있음. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/kebab-config/src/lib.rs | 127 ++++++++++++++++++++++++++ crates/kebab-parse-image/src/ocr.rs | 34 ++++--- crates/kebab-parse-image/tests/ocr.rs | 8 +- tasks/HOTFIXES.md | 22 ++++- 4 files changed, 174 insertions(+), 17 deletions(-) diff --git a/crates/kebab-config/src/lib.rs b/crates/kebab-config/src/lib.rs index c7ed521..7310d96 100644 --- a/crates/kebab-config/src/lib.rs +++ b/crates/kebab-config/src/lib.rs @@ -228,6 +228,19 @@ pub struct OcrCfg { /// Cap the long edge of the image (in pixels) before sending. Larger /// images bloat prompt cost. Default `1600`. pub max_pixels: u32, + /// v0.17.2 post-dogfood: Hard ceiling on a single HTTP exchange to + /// the OCR endpoint. Sister knob to [`LlmCfg::request_timeout_secs`] + /// — kept separate because OCR latency is typically shorter than + /// chat-LLM cold start, and large vision models on CPU-only hosts + /// occasionally need a different budget. See HOTFIXES 2026-05-25 + /// for the rationale. + /// + /// **Edge case — `0` is NOT a disable sentinel.** Same semantics as + /// `LlmCfg::request_timeout_secs`: `Duration::from_secs(0)` means + /// "every request fails immediately", not "no timeout". Use a + /// large finite value for an effectively-uncapped budget. + #[serde(default = "default_ocr_request_timeout_secs")] + pub request_timeout_secs: u64, } impl OcrCfg { @@ -239,10 +252,18 @@ impl OcrCfg { endpoint: None, languages: vec!["eng".to_string(), "kor".to_string()], max_pixels: 1600, + request_timeout_secs: default_ocr_request_timeout_secs(), } } } +/// v0.17.2 post-dogfood: matches the legacy hard-coded ceiling so +/// existing configs that omit the field keep behaving identically. +/// Overridable per config / `KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS`. +fn default_ocr_request_timeout_secs() -> u64 { + 300 +} + /// Caption settings (P6-3). Caption uses the same Ollama-vision / /// `LanguageModel` pipeline as the rest of the workspace; the trait /// abstraction is the part the spec demands. `enabled` defaults to @@ -722,6 +743,11 @@ impl Config { self.image.ocr.max_pixels = n; } } + "KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS" => { + if let Ok(n) = v.parse::() { + self.image.ocr.request_timeout_secs = n; + } + } // image.caption (P6-3) "KEBAB_IMAGE_CAPTION_ENABLED" => { @@ -1022,6 +1048,107 @@ theme = "dark" assert_eq!(c.image.ocr.max_pixels, 1600); } + /// v0.17.2 post-dogfood: matches the legacy hard-coded 300s cap so + /// existing configs that omit the new field keep behaving identically. + #[test] + fn default_ocr_request_timeout_secs_is_300() { + assert_eq!( + Config::defaults().image.ocr.request_timeout_secs, + 300 + ); + } + + #[test] + fn env_overrides_image_ocr_request_timeout_secs() { + let mut env = HashMap::new(); + env.insert( + "KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS".to_string(), + "900".to_string(), + ); + let c = Config::defaults().apply_env(&env); + assert_eq!(c.image.ocr.request_timeout_secs, 900); + } + + /// v0.17.2 post-dogfood: a config file written before the OCR + /// timeout field existed must still parse and fall back to the + /// 300s default — backwards-compat invariant. Reuses the same + /// minimal legacy TOML fixture as the LLM-side test. + #[test] + fn legacy_config_without_ocr_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.image.ocr.request_timeout_secs, 300); + } + #[test] fn image_ocr_env_overrides() { let mut env = HashMap::new(); diff --git a/crates/kebab-parse-image/src/ocr.rs b/crates/kebab-parse-image/src/ocr.rs index 77b6bef..e764a17 100644 --- a/crates/kebab-parse-image/src/ocr.rs +++ b/crates/kebab-parse-image/src/ocr.rs @@ -39,10 +39,6 @@ use crate::image_prep; /// Engine name written into `OcrText.engine` for the Ollama-vision adapter. pub const OLLAMA_VISION_ENGINE: &str = "ollama-vision"; -/// Hard ceiling on the OCR HTTP exchange. Cold-loading a vision model on -/// first call can take ~30s; 5 minutes is generous without being open-ended. -const REQUEST_TIMEOUT: Duration = Duration::from_secs(300); - /// Lower bound on `config.image.ocr.max_pixels`. Anything below this is /// silently bumped to keep the model from receiving an unreadable thumbnail. const MIN_LONG_EDGE: u32 = 256; @@ -139,7 +135,13 @@ impl OllamaVisionOcr { Some(s) if !s.is_empty() => s.to_string(), _ => config.models.llm.endpoint.clone(), }; - Self::build(endpoint, ocr.model.clone(), ocr.languages.clone(), ocr.max_pixels) + Self::build( + endpoint, + ocr.model.clone(), + ocr.languages.clone(), + ocr.max_pixels, + ocr.request_timeout_secs, + ) } /// Build directly from explicit fields. Useful for tests that need @@ -153,8 +155,15 @@ impl OllamaVisionOcr { model: impl Into, languages: Vec, max_pixels: u32, + request_timeout_secs: u64, ) -> Result { - Self::build(endpoint.into(), model.into(), languages, max_pixels) + Self::build( + endpoint.into(), + model.into(), + languages, + max_pixels, + request_timeout_secs, + ) } /// Shared validation + construction. Centralised so `new` and @@ -164,6 +173,7 @@ impl OllamaVisionOcr { model: String, languages: Vec, requested_max_pixels: u32, + request_timeout_secs: u64, ) -> Result { if endpoint.is_empty() { anyhow::bail!( @@ -183,7 +193,7 @@ impl OllamaVisionOcr { ); } let client = reqwest::blocking::Client::builder() - .timeout(REQUEST_TIMEOUT) + .timeout(Duration::from_secs(request_timeout_secs)) .build() .context("building OCR HTTP client")?; Ok(Self { @@ -375,6 +385,7 @@ mod tests { "m", vec!["eng".into(), "kor".into()], 1024, + 300, ) .unwrap(); let p = engine.build_prompt(Some(&Lang("ko".into()))); @@ -389,6 +400,7 @@ mod tests { "m", vec!["eng".into()], 1024, + 300, ) .unwrap(); let p = engine.build_prompt(Some(&Lang("und".into()))); @@ -400,7 +412,7 @@ mod tests { /// the constructor cannot drift to "silently accept a bad config". #[test] fn build_rejects_empty_endpoint() { - let r = OllamaVisionOcr::from_parts("", "m", vec![], 1024); + let r = OllamaVisionOcr::from_parts("", "m", vec![], 1024, 300); let err = r.expect_err("empty endpoint must bail").to_string(); assert!( err.contains("endpoint is empty"), @@ -413,7 +425,7 @@ mod tests { /// so testing `from_parts` covers both. #[test] fn build_rejects_empty_model_after_trim() { - let r = OllamaVisionOcr::from_parts("http://x", " ", vec![], 1024); + let r = OllamaVisionOcr::from_parts("http://x", " ", vec![], 1024, 300); let err = r.expect_err("empty model must bail").to_string(); assert!( err.contains("model is empty"), @@ -428,10 +440,10 @@ mod tests { #[test] fn build_clamps_max_pixels_outside_legal_range() { let too_small = - OllamaVisionOcr::from_parts("http://x", "m", vec![], 1).unwrap(); + OllamaVisionOcr::from_parts("http://x", "m", vec![], 1, 300).unwrap(); assert_eq!(too_small.max_pixels(), MIN_LONG_EDGE); let too_big = - OllamaVisionOcr::from_parts("http://x", "m", vec![], u32::MAX).unwrap(); + OllamaVisionOcr::from_parts("http://x", "m", vec![], u32::MAX, 300).unwrap(); assert_eq!(too_big.max_pixels(), MAX_LONG_EDGE); } } diff --git a/crates/kebab-parse-image/tests/ocr.rs b/crates/kebab-parse-image/tests/ocr.rs index f6c0d9b..62fae92 100644 --- a/crates/kebab-parse-image/tests/ocr.rs +++ b/crates/kebab-parse-image/tests/ocr.rs @@ -322,7 +322,8 @@ async fn ocr_downscales_large_image_before_sending() { #[test] fn from_parts_clamps_max_pixels_into_legal_range() { // Below MIN_LONG_EDGE — bumped up to the floor. - let too_small = OllamaVisionOcr::from_parts("http://x", "m", vec![], 10).unwrap(); + let too_small = + OllamaVisionOcr::from_parts("http://x", "m", vec![], 10, 300).unwrap(); assert_eq!( too_small.max_pixels(), 256, @@ -331,7 +332,7 @@ fn from_parts_clamps_max_pixels_into_legal_range() { // Above MAX_LONG_EDGE — capped at the ceiling. let too_big = - OllamaVisionOcr::from_parts("http://x", "m", vec![], 99_999).unwrap(); + OllamaVisionOcr::from_parts("http://x", "m", vec![], 99_999, 300).unwrap(); assert_eq!( too_big.max_pixels(), 4096, @@ -339,7 +340,8 @@ fn from_parts_clamps_max_pixels_into_legal_range() { ); // Inside the legal range — pass through untouched. - let in_range = OllamaVisionOcr::from_parts("http://x", "m", vec![], 1024).unwrap(); + let in_range = + OllamaVisionOcr::from_parts("http://x", "m", vec![], 1024, 300).unwrap(); assert_eq!(in_range.max_pixels(), 1024); } diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index f0f572a..466efb0 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -25,14 +25,30 @@ v0.17.0 후속 도그푸딩에서 발견: 사용자가 default `gemma4:e4b` (8B - `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 추가 한 줄 후속. +**미진행 (scope 밖) — closure 갱신**: +- ~~`crates/kebab-parse-image/src/ocr.rs::REQUEST_TIMEOUT` 도 동일한 hard-coded 300 s — OCR 이 보통 짧아 LLM 만큼 부담 안 되지만, 일관성 측면에서 다음 round 에 같은 노브 (또는 별 노브) 로 재검토.~~ → **closure**: 아래 2026-05-25 v0.17.2 OCR timeout entry 참조 (별 노브 `[image.ocr] request_timeout_secs` 신설). +- ~~`kebab ask --stream` (fb-33) 권장 강조: 5분 cold-start 동안 첫 token 빠르게 surface — UX 개선. README/SKILL.md 추가 한 줄 후속.~~ → **closure**: PR #163 (v0.17.1 cut) 에서 이미 README + SMOKE + SKILL.md 세 곳 모두 추가됨 (`README.md:22` cold start 권장 단락, `docs/SMOKE.md:45/209` 예제, `SKILL.md:114/119` 사용 가이드). 본 entry 의 미진행 표기가 outdated 였음. **후속 도그푸딩 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-25 — v0.17.2: `[image.ocr] request_timeout_secs` 노브 (closure of 2026-05-25 v0.17.1 미진행) + +v0.17.1 entry 의 첫 번째 미진행 항목 closure. LLM 쪽이 v0.17.1 에서 `[models.llm] request_timeout_secs` 로 풀려난 패턴을 OCR 어댑터에 동일 적용. 별 노브로 분리한 이유 (사용자 결정): OCR 은 통상 LLM 대비 짧고 cold start 패턴도 다름 — 두 노브를 독립 조절할 수 있어야 16 GB / CPU only 환경에서 vision 모델만 다른 timeout 을 쓰기 편함. + +**변경**: +- `crates/kebab-config/src/lib.rs::OcrCfg` 에 `request_timeout_secs: u64` additive 필드 (`#[serde(default = "default_ocr_request_timeout_secs")]`, default `300`). 옛 config 가 필드 누락해도 그대로 파싱 + 동일 동작 (3 신규 unit test 가 default / env override / legacy parse 핀). +- env override `KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS`. +- `crates/kebab-parse-image/src/ocr.rs` 의 `REQUEST_TIMEOUT` 상수 제거. `OllamaVisionOcr::build` 시그니처가 `request_timeout_secs: u64` 추가, `new(&Config)` 는 `config.image.ocr.request_timeout_secs` 전달. `from_parts` (테스트 전용 surface) 도 동일하게 시그니처 확장 — caller 6 곳 (`crates/kebab-parse-image/src/ocr.rs::tests` 5, `crates/kebab-parse-image/tests/ocr.rs::from_parts_clamps_max_pixels_into_legal_range` 3 site) 모두 `300` 명시 갱신. +- `OcrCfg::defaults()` 에 `request_timeout_secs: default_ocr_request_timeout_secs()` 추가. `Config::defaults()` 는 `ImageCfg::defaults()` 경유라 cascade. + +**Edge case 동일**: `0` 은 disable 아닌 "즉시 timeout" (`Duration::from_secs(0)` 의 reqwest 의미). LlmCfg 의 doc comment 와 같은 안내가 OcrCfg field doc 에 명시. + +**사용자 영향**: 기존 v0.17.x KB / config 는 변경 불필요 — 새 필드는 serde default 로 채워지고 동작도 동일 (300s). vision 모델 cold start 가 길면 `KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS=600` 또는 config 에서 `[image.ocr] request_timeout_secs = 600` 설정. + +Cross-link: `crates/kebab-config/src/lib.rs::OcrCfg::request_timeout_secs`, `crates/kebab-parse-image/src/ocr.rs::OllamaVisionOcr::build`. + ## 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). -- 2.49.1 From e1188442560e178a4493c5feb51d16a7042854fb Mon Sep 17 00:00:00 2001 From: altair823 Date: Mon, 25 May 2026 05:13:09 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(ocr):=20PR=20#164=20=ED=9A=8C?= =?UTF-8?q?=EC=B0=A8=201=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HOTFIXES 헤더 `v0.17.2` (vaporware) → `post-v0.17.1 dogfood` 로 변경, release tag 결정과 무관하게 정확한 anchor. - HOTFIXES caller 수 `6 (5+3)` → `9 call site (6+3)` 으로 정정. - OcrCfg.request_timeout_secs doc 의 edge case 가 LlmCfg sister doc 과 동일한 구체 예제 (`u64::MAX`, `86400`) + reqwest 0.12.x 명시 주석으로 강화. - LLM + OCR 양쪽의 legacy TOML fixture (78 줄 거의 동일) 를 module-level `LEGACY_PRE_TIMEOUT_TOML` const 로 추출. 두 test 가 동일 source 공유 → 옛 schema 가 또 변하면 한 곳만 수정. reqwest::Duration::ZERO fact-check (회차 1 점 5) 는 회차 2 reply 에서 검증 결과 보고. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/kebab-config/src/lib.rs | 241 +++++++++++++-------------------- tasks/HOTFIXES.md | 6 +- 2 files changed, 95 insertions(+), 152 deletions(-) diff --git a/crates/kebab-config/src/lib.rs b/crates/kebab-config/src/lib.rs index 7310d96..77455fd 100644 --- a/crates/kebab-config/src/lib.rs +++ b/crates/kebab-config/src/lib.rs @@ -236,9 +236,12 @@ pub struct OcrCfg { /// for the rationale. /// /// **Edge case — `0` is NOT a disable sentinel.** Same semantics as - /// `LlmCfg::request_timeout_secs`: `Duration::from_secs(0)` means - /// "every request fails immediately", not "no timeout". Use a - /// large finite value for an effectively-uncapped budget. + /// [`LlmCfg::request_timeout_secs`]: `Duration::from_secs(0)` means + /// "every request fails immediately" (reqwest 0.12.x — the read + /// timeout is applied as a 0-second deadline), not "no timeout". + /// To approximate "no cap", use a large finite value (e.g. + /// `u64::MAX` ≈ 5.8 × 10¹¹ years, or just a generous number like + /// `86400`). #[serde(default = "default_ocr_request_timeout_secs")] pub request_timeout_secs: u64, } @@ -860,6 +863,83 @@ fn parse_bool(s: &str) -> bool { mod tests { use super::*; + /// Legacy TOML fixture written before the `request_timeout_secs` + /// knobs (LLM in v0.17.1, OCR follow-up) existed. Shared by + /// `legacy_config_without_request_timeout_secs_uses_default` + /// (LLM-side) and `legacy_config_without_ocr_request_timeout_secs_uses_default` + /// (OCR-side) so both invariants pin against the same on-disk + /// shape — schema drift in the legacy form only needs one edit. + const LEGACY_PRE_TIMEOUT_TOML: &str = 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" +"#; + #[test] fn defaults_are_serde_roundtrip_stable() { let c = Config::defaults(); @@ -950,80 +1030,12 @@ mod tests { /// 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. + /// back to the 300s default — backwards-compat invariant. Fixture + /// shared with the OCR-side invariant via [`LEGACY_PRE_TIMEOUT_TOML`]. #[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"); + let c: Config = toml::from_str(LEGACY_PRE_TIMEOUT_TOML) + .expect("parse legacy config"); assert_eq!(c.models.llm.request_timeout_secs, 300); } @@ -1069,83 +1081,14 @@ theme = "dark" assert_eq!(c.image.ocr.request_timeout_secs, 900); } - /// v0.17.2 post-dogfood: a config file written before the OCR + /// post-v0.17.1 dogfood: a config file written before the OCR /// timeout field existed must still parse and fall back to the - /// 300s default — backwards-compat invariant. Reuses the same - /// minimal legacy TOML fixture as the LLM-side test. + /// 300s default — backwards-compat invariant. Fixture shared + /// with the LLM-side invariant via [`LEGACY_PRE_TIMEOUT_TOML`]. #[test] fn legacy_config_without_ocr_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"); + let c: Config = toml::from_str(LEGACY_PRE_TIMEOUT_TOML) + .expect("parse legacy config"); assert_eq!(c.image.ocr.request_timeout_secs, 300); } diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index 466efb0..0f4b07f 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -33,14 +33,14 @@ v0.17.0 후속 도그푸딩에서 발견: 사용자가 default `gemma4:e4b` (8B Cross-link: `crates/kebab-config/src/lib.rs::LlmCfg::request_timeout_secs`, `crates/kebab-llm-local/src/ollama.rs::OllamaLanguageModel::new`. -## 2026-05-25 — v0.17.2: `[image.ocr] request_timeout_secs` 노브 (closure of 2026-05-25 v0.17.1 미진행) +## 2026-05-25 — post-v0.17.1 dogfood: `[image.ocr] request_timeout_secs` 노브 (closure of v0.17.1 미진행) -v0.17.1 entry 의 첫 번째 미진행 항목 closure. LLM 쪽이 v0.17.1 에서 `[models.llm] request_timeout_secs` 로 풀려난 패턴을 OCR 어댑터에 동일 적용. 별 노브로 분리한 이유 (사용자 결정): OCR 은 통상 LLM 대비 짧고 cold start 패턴도 다름 — 두 노브를 독립 조절할 수 있어야 16 GB / CPU only 환경에서 vision 모델만 다른 timeout 을 쓰기 편함. +v0.17.1 entry 의 첫 번째 미진행 항목 closure. LLM 쪽이 v0.17.1 에서 `[models.llm] request_timeout_secs` 로 풀려난 패턴을 OCR 어댑터에 동일 적용. 별 노브로 분리한 이유 (사용자 결정): OCR 은 통상 LLM 대비 짧고 cold start 패턴도 다름 — 두 노브를 독립 조절할 수 있어야 16 GB / CPU only 환경에서 vision 모델만 다른 timeout 을 쓰기 편함. release tag 는 본 entry 시점 미결정 — cut 합의 시점에 동일 entry 가 v0.17.2 / v0.18.0 등으로 anchor 갱신. **변경**: - `crates/kebab-config/src/lib.rs::OcrCfg` 에 `request_timeout_secs: u64` additive 필드 (`#[serde(default = "default_ocr_request_timeout_secs")]`, default `300`). 옛 config 가 필드 누락해도 그대로 파싱 + 동일 동작 (3 신규 unit test 가 default / env override / legacy parse 핀). - env override `KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS`. -- `crates/kebab-parse-image/src/ocr.rs` 의 `REQUEST_TIMEOUT` 상수 제거. `OllamaVisionOcr::build` 시그니처가 `request_timeout_secs: u64` 추가, `new(&Config)` 는 `config.image.ocr.request_timeout_secs` 전달. `from_parts` (테스트 전용 surface) 도 동일하게 시그니처 확장 — caller 6 곳 (`crates/kebab-parse-image/src/ocr.rs::tests` 5, `crates/kebab-parse-image/tests/ocr.rs::from_parts_clamps_max_pixels_into_legal_range` 3 site) 모두 `300` 명시 갱신. +- `crates/kebab-parse-image/src/ocr.rs` 의 `REQUEST_TIMEOUT` 상수 제거. `OllamaVisionOcr::build` 시그니처가 `request_timeout_secs: u64` 추가, `new(&Config)` 는 `config.image.ocr.request_timeout_secs` 전달. `from_parts` (테스트 전용 surface) 도 동일하게 시그니처 확장 — caller 9 call site (`crates/kebab-parse-image/src/ocr.rs::tests` 5 test / 6 call site, `crates/kebab-parse-image/tests/ocr.rs::from_parts_clamps_max_pixels_into_legal_range` 1 test / 3 call site) 모두 `300` 명시 갱신. - `OcrCfg::defaults()` 에 `request_timeout_secs: default_ocr_request_timeout_secs()` 추가. `Config::defaults()` 는 `ImageCfg::defaults()` 경유라 cascade. **Edge case 동일**: `0` 은 disable 아닌 "즉시 timeout" (`Duration::from_secs(0)` 의 reqwest 의미). LlmCfg 의 doc comment 와 같은 안내가 OcrCfg field doc 에 명시. -- 2.49.1