feat(ocr): request_timeout_secs config knob #164
Reference in New Issue
Block a user
Delete Branch "feat/ocr-timeout-config"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
v0.17.1 (PR #162) 가 LLM 쪽 hard-coded 300s timeout 을
[models.llm] request_timeout_secs노브로 풀어준 것과 동일 패턴을 OCR 어댑터에 적용. 사용자 결정으로 별 노브 분리 ([image.ocr] request_timeout_secs) — OCR 와 LLM 의 cold start / latency 패턴이 달라 독립 조절이 편함. 16 GB / CPU only 환경에서 vision 모델만 다른 timeout 을 쓰는 케이스 cover.HOTFIXES 2026-05-25 v0.17.1 entry 의 두 미진행 항목 모두 closure:
kebab ask --streamREADME/SKILL.md 안내 → PR #163 에서 이미 완료 (HOTFIXES 본 PR 에서 표기 정정)변경
crates/kebab-config/src/lib.rs::OcrCfg에request_timeout_secs: u64additive 필드 (#[serde(default = "default_ocr_request_timeout_secs")], default300). 옛 config 가 필드 누락해도 그대로 파싱.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(test only) 도 시그니처 확장.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 의미). LLM 쪽 doc 과 동일한 안내가OcrCfg.request_timeout_secsfield doc 에 명시.검증
cargo test -p kebab-config -p kebab-parse-image— 전부 녹색 (3 신규 config unit test + 기존 8 OCR integration test).cargo clippy -p kebab-config -p kebab-parse-image -p kebab-app --all-targets -- -D warnings— clean.OllamaVisionOcr::new(&app.config)caller (kebab-app) 영향 없음 — config 가 자동으로 새 default 채움.시험 항목 (Test Plan)
default_ocr_request_timeout_secs_is_300— defaults() 300 확인env_overrides_image_ocr_request_timeout_secs— env 값 900 적용 확인legacy_config_without_ocr_request_timeout_secs_uses_default— 옛 TOML 파싱 시 default 300 적용 확인from_parts_clamps_max_pixels_into_legal_range— 시그니처 확장 후도 통과비범위
crates/kebab-llm-local/src/ollama.rs— v0.17.1 에서 이미 처리됨.Assisted-by: Claude Code
회차 1 — LlmCfg 패턴 mirror 가 깔끔하고 test invariant 핀이 좋음. actionable 5건:
v0.17.2가 vaporware (release cut 미결정) — 트리거 기반 헤더로 변경 권장caller 6 곳 (5 + 3)산수 오류 — 실제 9 call siteOcrCfg.request_timeout_secsdoc 의 edge case 가LlmCfg의 sister doc 보다 약함 (구체 예제 누락) — sister 답게 doc 일관 권장Duration::from_secs(0)의 reqwest 실제 동작 fact-check 의문 — LLM 쪽 source of truth 검증 여부 확인 필요칭찬 1건:
enabled=false경로의 안전한 default 처리.@@ -231,0 +232,4 @@/// 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-25Question — fact-check 필요: doc 의
Duration::from_secs(0)가 "every request fails immediately" 라는 주장은 LLM 쪽 doc 에서 그대로 복사된 것 같은데, reqwest 0.12.x 의 실제 동작은 명세 없음. 검색해보니 reqwest 의ClientBuilder::timeout(Duration::ZERO)동작이 버전에 따라 다른 사례 보고 있음 (어떤 버전은 panic, 어떤 버전은 disable, 어떤 버전은 즉시 fail).LLM 쪽 (v0.17.1) doc 의 주장이 실제 검증된 거라면 OK — 그렇지 않다면 LLM + OCR 양쪽 doc 을 함께 정정해야 함. 본 PR 에서 검증 부담은 부당하지만, 검증된 기반이 있는지 사용자 확인 요청 (HOTFIXES 2026-05-25 LLM entry 도 동일 주장을 source of truth 로 가짐).
@@ -231,0 +237,4 @@////// **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 aedge case docstring 의 "Use a large finite value for an effectively-uncapped budget" 가 LLM 쪽의 동일 doc 보다 약함 —
LlmCfg::request_timeout_secs는 구체적 예제 (u64::MAX ≈ 5.8 × 10¹¹ years, or just a generous number like 86400) 까지 적어둠. 사용자가 이 doc 만 보고 LLM 쪽 도 anchor 로 cross-link 하기 좋게 동일한 구체 예제 추가 권장 — "sister knob to LlmCfg" 라고 한 만큼 doc 도 sister 처럼 두는 게 maintenance 비용 ↓.@@ -1025,0 +1146,4 @@theme = "dark""#;let c: Config = toml::from_str(toml_src).expect("parse legacy config");assert_eq!(c.image.ocr.request_timeout_secs, 300);legacy_config_without_ocr_request_timeout_secs_uses_default의 TOML fixture (78 줄) 가legacy_config_without_request_timeout_secs_uses_default(LLM 쪽, 942-999 줄) 와 거의 동일. module-levelconst LEGACY_PRE_TIMEOUT_TOML: &str = ...로 추출하면 두 test 가 같은 fixture 를 공유 — 옛 config schema 가 또 변하면 한 곳만 손대면 됨.Suggestion 만 — fixture 의도된 중복 (test isolation, 한 test 가 빠지면 다른 test 도 안전) 도 valid trade-off. 사용자 판단.
@@ -185,3 +195,3 @@let client = reqwest::blocking::Client::builder().timeout(REQUEST_TIMEOUT).timeout(Duration::from_secs(request_timeout_secs)).build()칭찬 —
OllamaVisionOcr::build가enabled와 무관하게 timeout 을 적용해 client 를 빌드하는 구조가 깔끔.enabled=false케이스에서는new()자체가 호출 안 되므로 (호출자가if config.image.ocr.enabled { ... }gate 책임) timeout 필드의 dead-code 우려 없음. config 의 모든 필드가 비활성 케이스에서도 안전하게 default 채워지는 패턴이 일관.헤더의
v0.17.2는 vaporware — 본 PR 머지 시점에 사용자가 release cut 결정한 상태가 아니므로 머지 후 cut 까지의 간격 동안 HOTFIXES 가 "이미 v0.17.2 가 나왔다" 고 거짓말함. 이전 entry 들 (2026-05-25 — v0.17.0 post-dogfood) 의 패턴을 따라 헤더를 release tag 가 아닌 트리거로 작성하면 안전:Cross-link 도 같은 톤으로 정리. release cut 이 합의되면 그때 헤더에 v0.17.2 추가 가능 — 단방향 변경이 항상 더 안전함.
caller 6 곳 (5 + 3)의 산수가 맞지 않음. 실제 diff:crates/kebab-parse-image/src/ocr.rs::tests— 5 test 안에 6 call site (build_clamps_max_pixels_outside_legal_range가too_small+too_big두 번 호출).crates/kebab-parse-image/tests/ocr.rs::from_parts_clamps_max_pixels_into_legal_range— 1 test 안에 3 call site.따라서 정확히는
caller 9 곳 (6 + 3)또는6 test (5 + 1) 에 걸쳐 9 call site. 작은 디테일이지만 HOTFIXES 가 후속 archeology 의 source of truth 라 수치는 정확해야 함.회차 2 — 회차 1 의 4건 모두 잘 반영. 잔여 actionable 0건. APPROVE.
회차 1 점 5 (reqwest::Duration::ZERO fact-check) 결과 보고:
reqwest = "0.12.28"(Cargo.lock 6954) 의ClientBuilder::timeoutdoc: "Set a timeout for connect, read and write operations of a Client. Default is no timeout."Duration::ZERO가Some으로 들어가면 hyper 의 read/write deadline 이 0초로 설정 → 모든 in-flight future 가 즉시error::Timeout으로 fail. "no timeout" 은.timeout()호출 안 함 (builder의timeout: Option<Duration>가None) 또는u64::MAX같은 큰 값 —LlmCfg/OcrCfgdoc 의 주장이 reqwest 0.12.x 에서 정확.if request_timeout_secs == 0 { bail!(...) }) 는 over-engineering, 사용자가 명시적으로 0 을 설정한 의도를 추정해 막을 근거 약함.회차 1 점 2 (legacy fixture 추출) 도 의도된 중복 vs single-source 사이 합리적 trade-off 였는데 추출 선택. 산출도 단순 (const 한 개 + 두 줄 참조).
다음 PR (heading_path text column filter, 사용자 작업 3) 도 같은 cadence 로 진행 권장.
@@ -837,0 +867,4 @@/// 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칭찬 —
LEGACY_PRE_TIMEOUT_TOML추출이 깔끔. doc comment 가 두 invariant 의 share 의도를 명시 (shared with the LLM-side invariant via [LEGACY_PRE_TIMEOUT_TOML]) + 두 test 각각의 doc 에서 cross-reference. 옛 schema 가 또 변하면 한 곳만 손대면 되는 single-source 구조. 152 줄 삭제 / 95 줄 추가 net 57 줄 감소도 부산물.@@ -36,0 +38,4 @@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 핀).칭찬 —
## 2026-05-25 — post-v0.17.1 dogfood:패턴이 release tag 결정과 분리된 entry anchor 로 정확.release tag 는 본 entry 시점 미결정 — cut 합의 시점에 동일 entry 가 v0.17.2 / v0.18.0 등으로 anchor 갱신.한 줄도 후속 archeology 가 entry 의 release 추적 의도를 즉시 파악 가능. caller 수9 call site (6 + 3)정정도 정확.