docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹 페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요 type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec 의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring 다이어그램 + 진입 가이드 보유. ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로 교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입 링크 추가. 이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md 의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md, per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.0 KiB
LLM
텍스트 + vision 생성 모델 인터페이스.
LanguageModeltrait + Ollama HTTP 어댑터. streaming 결과 + 항상Done종료 보장.
구성 crate
| Crate | 역할 |
|---|---|
kebab-llm |
LanguageModel trait re-export + MockLanguageModel (feature mock, default OFF). 새 type 추가 금지 — 순수 facade. |
kebab-llm-local |
OllamaLanguageModel — reqwest::blocking 기반 Ollama POST /api/generate 어댑터. line-delimited JSON streaming 디코드. |
구조
classDiagram
class LanguageModel {
<<trait kebab-core>>
model_ref() ModelRef
context_tokens() usize
generate_stream(req) Iterator~Result~TokenChunk~~
}
class GenerateRequest {
system: String
user: String
stop: Vec~String~
max_tokens: usize
temperature: f32
seed: Option~u64~
images: Vec~String~ [base64]
}
class TokenChunk {
<<enum>>
Token(String)
Done {finish_reason, usage}
}
class FinishReason {
<<enum>>
Stop
Length
Aborted
Error(String)
}
class OllamaLanguageModel {
+new(cfg) Result~Self~
-client: reqwest::blocking::Client
-endpoint: String
-model: String
-context_tokens: usize
}
class MockLanguageModel {
feature = "mock"
deterministic test double
}
class LlmError {
<<error>>
ConnectionRefused
HttpStatus
Decode
...
}
LanguageModel <|.. OllamaLanguageModel
LanguageModel <|.. MockLanguageModel
LanguageModel ..> GenerateRequest
LanguageModel ..> TokenChunk
TokenChunk ..> FinishReason
OllamaLanguageModel ..> LlmError
Data flow
flowchart LR
Req["GenerateRequest<br/>{system, user, stop,<br/>max_tokens, temp, seed, images}"]
Wire["JSON wire<br/>POST /api/generate<br/>stream: true"]
Lines["line-delimited JSON<br/>frames"]
Token["TokenChunk::Token(...)"]
DoneOk["TokenChunk::Done<br/>{finish_reason: Stop|Length, usage}"]
DoneErr["TokenChunk::Done<br/>{finish_reason: Error/Aborted, usage}"]
Iter["Iterator~Result~TokenChunk~~<br/>(lazy, Send)"]
Req --> Wire --> Lines
Lines -->|frame| Token --> Iter
Lines -->|done frame| DoneOk --> Iter
Lines -->|error/abort| DoneErr --> Iter
Caller["caller (kebab-rag)<br/>+ assert_finish_chunk"] --> Iter
주요 type / trait / 함수
Trait (kebab-core, re-export kebab-llm):
LanguageModel::model_ref() -> ModelRef— provider/model/version 식별.Answer.model_ref으로 흘려서 wire payload 가 자가 식별.LanguageModel::context_tokens() -> usize— 모델 별 max prompt+completion 합. RAG 가 budget 계산에 사용.LanguageModel::generate_stream(req: GenerateRequest) -> Result<Box<dyn Iterator<Item = Result<TokenChunk>> + Send>>— async 안 됨, 매 next() 가 blocking. 모든 stream 이 마지막에TokenChunk::Done으로 끝남 (error 케이스 포함, §0 Q5).
GenerateRequest (kebab-core::traits):
images: Vec<String>(base64) — 빈 vec = text-only path. 비어있지 않으면 vision-capable adapter 가images: [...]로 wire 에 포함 (Ollama). 다른 backend 는 다르게 라우팅.#[serde(default)]— older snapshot 호환.
TokenChunk:
Token(String)— partial text. 누적은 caller 책임.Done { finish_reason: FinishReason, usage: TokenUsage }— 항상 마지막.finish_reason::Aborted가 cancel signal,Error(s)가 mid-stream 실패.
OllamaLanguageModel (kebab-llm-local::ollama):
OllamaLanguageModel::new(&kebab_config::Config) -> anyhow::Result<Self>—config.models.llm.endpoint+model+context_tokens+temperature+seed읽음. lazy connect — network 안 침, 첫 generate_stream 에서 실패 surface.- 내부:
reqwest::blocking::Client— top-level async 표면 없음. (참고: reqwest 0.12 의 blocking 이 private current-thread tokio runtime wrap 해서cargo tree에 tokio 보임. invariant = "top-level tokio dep 없음 + async surface 노출 안 함".) - streaming decode:
BufReader::lines()위에서serde_json::from_str로 frame 별 lazy parse →TokenChunk::Tokenyield.
LlmError (kebab-llm-local::error):
- ConnectionRefused / HttpStatus(code) / Decode(json error) / Timeout / Aborted / 그 외 —
Err로 first chunk 전 surface 가능.
테스트 도구 (kebab-llm):
assert_finish_chunk(chunks: &[TokenChunk])— 마지막이Done이어야 — 모든 stream contract pin.MockLanguageModel(featuremock, default OFF) — deterministic test double. 실 adapter 만Err가능, mock 은 항상 stream 시작 후 yield.
외부 의존
kebab-llm→kebab-core만 (re-export crate).kebab-llm-local→kebab-llm+kebab-config,reqwest(blockingfeature, JSON),serde+serde_json,thiserror,anyhow.- 외부 서비스: Ollama HTTP (default
http://127.0.0.1:11434). default 모델gemma4:e4b(OCR / caption / RAG 모두 같은 family — 단일 모델 다운로드면 전 시스템 동작).
핵심 결정
-
kebab-llm= trait re-export only, 새 type 금지. 왜:kebab-rag등 downstream 이use kebab_llm::LanguageModel안정 surface 의존. 어댑터 (Ollama/llama.cpp/candle) 는 별 crate. swap config-only. -
synchronous + blocking + stream iterator. 왜: §0 Q5 가 streaming 명시.
async가 trait object 와 잘 안 맞음 (Rust async-in-trait 안정성 + Send bound 복잡).reqwest::blocking+ line-delimited frame 의Iterator가 caller 코드 단순. RAG 가 동기 소비 + UI thread 가 별도 worker 로 spawn. -
모든 stream 이
Done으로 끝남 (error 포함). 왜: caller 가 partial accumulation 한 텍스트 + finish reason 함께 받음.Done(Error)vsIterator::next() = None차이가 contract 명확.assert_finish_chunk가 invariant pin. -
lazy connect (생성자에서 network 안 침). 왜:
kebab init/kebab doctor가 Ollama 안 떠도 동작해야 함. 첫generate_stream에서Err가 나는 게 사용자 기대 — startup 이 죽으면 진단 어려움. -
Ollama 가 default backend. 왜: macOS / Linux 모두 single-binary install, GGUF 모델 다운로드 한 줄. local-first 의 핵심. llama.cpp / candle 어댑터는 future P+ —
LanguageModeltrait 그대로라 swap 가능. -
default 모델
gemma4:e4b(OCR / caption / RAG 통일). 왜: OCR (P6-2) + caption (P6-3) + RAG 가 같은 family 사용 → 사용자가 모델 1개만 ollama pull. variant (gemma4:26b 등) 으로 override 가능. (HOTFIXES P6-2.) -
GenerateRequest.images: Vec<String>추가 (P6-3). 왜: 기존 trait 가 text-only 였는데 caption 이 vision 필요. base64 image vec 으로 wire 형식 통일 — Ollama 의images필드와 1:1. text-only caller 모두images: Vec::new()마이그레이션 +#[serde(default)]로 snapshot 호환. (HOTFIXES P6-3.) -
MockLanguageModel가Err안 던짐. 왜: mock 은 deterministic — first chunk 전 connection-refused 같은 케이스 시뮬레이션 안 함. 실 adapter (OllamaLanguageModel) 가 그 분기 책임. RAG 의 RefusalReason::LlmStreamAborted 분기 (p9-fb-15) 는 실 adapter 만 trigger.
관련 spec / HOTFIXES
- frozen 설계 §7.1 (
GenerateRequest/TokenChunk/FinishReason), §7.2 (LanguageModeltrait), §0 Q5 (streaming), §3.8 (ModelRef/TokenUsage), §6.4 (models.llm), §10 (errors), §11.2 (Ollama protocol notes):docs/superpowers/specs/2026-04-27-kebab-final-form-design.md - task spec:
- trait crate:
tasks/p4/p4-1-llm-trait.md - Ollama adapter:
tasks/p4/p4-2-llm-ollama.md
- trait crate:
- HOTFIXES (P6-3
GenerateRequest.images추가, P6-2 OCR 기본을 Ollama vision 으로 통일):tasks/HOTFIXES.md