style: cargo fmt --all (round 4 ingest log feature follow-up)
Phase C4 executor 의 마지막 `fix(test): clippy + fmt fixes` commit 이 test file 부분만 fmt 적용. workspace 전체 fmt 누락 발견 → cargo fmt --all 적용. 모든 import alphabetical reorder + line wrapping 정합. 추가 untracked artifact 동시 commit: - docs/superpowers/specs/2026-05-28-v0.20-ingest-log-spec.md (491 line, ACCEPT) - docs/superpowers/plans/2026-05-28-v0.20-ingest-log-plan.md (616 line, ACCEPT) workspace test: 1370 passed / 0 failed / 50 ignored, ingest_log_smoke green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,9 +41,7 @@
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::time::Duration;
|
||||
|
||||
use kebab_core::{
|
||||
FinishReason, GenerateRequest, LanguageModel, ModelRef, TokenChunk, TokenUsage,
|
||||
};
|
||||
use kebab_core::{FinishReason, GenerateRequest, LanguageModel, ModelRef, TokenChunk, TokenUsage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::LlmError;
|
||||
@@ -346,9 +344,9 @@ impl Iterator for OllamaStream {
|
||||
// misrouted reverse proxy returning 200). Per §10
|
||||
// error taxonomy this is `Stream`, not
|
||||
// `Malformed`.
|
||||
return Some(Err(anyhow::Error::from(LlmError::Stream(
|
||||
truncate_body(&preview, 512),
|
||||
))));
|
||||
return Some(Err(anyhow::Error::from(LlmError::Stream(truncate_body(
|
||||
&preview, 512,
|
||||
)))));
|
||||
}
|
||||
// Mid-stream corruption — earlier lines parsed, this
|
||||
// one didn't. That's `Malformed`.
|
||||
@@ -364,9 +362,9 @@ impl Iterator for OllamaStream {
|
||||
// Server-side error envelope on a 200 stream.
|
||||
if let Some(err) = line.error {
|
||||
self.done = true;
|
||||
return Some(Err(anyhow::Error::from(LlmError::Stream(
|
||||
truncate_body(&err, 512),
|
||||
))));
|
||||
return Some(Err(anyhow::Error::from(LlmError::Stream(truncate_body(
|
||||
&err, 512,
|
||||
)))));
|
||||
}
|
||||
|
||||
if line.done {
|
||||
@@ -451,11 +449,7 @@ fn map_send_error(err: reqwest::Error, endpoint: &str) -> LlmError {
|
||||
/// Map a non-2xx HTTP response to an [`LlmError`]. Pattern-matches on the
|
||||
/// 404 + "model" / "not found" body envelope to surface the actionable
|
||||
/// `ollama pull <model>` hint.
|
||||
fn map_status_error(
|
||||
status: reqwest::StatusCode,
|
||||
body: &str,
|
||||
model_id: &str,
|
||||
) -> LlmError {
|
||||
fn map_status_error(status: reqwest::StatusCode, body: &str, model_id: &str) -> LlmError {
|
||||
if status == reqwest::StatusCode::NOT_FOUND {
|
||||
let lower = body.to_ascii_lowercase();
|
||||
// Heuristic: Ollama's "model not pulled" envelope is roughly
|
||||
@@ -473,10 +467,7 @@ fn map_status_error(
|
||||
return LlmError::ModelNotPulled(model_id.to_string());
|
||||
}
|
||||
}
|
||||
LlmError::Stream(truncate_body(
|
||||
&format!("status={status} body={body}"),
|
||||
512,
|
||||
))
|
||||
LlmError::Stream(truncate_body(&format!("status={status} body={body}"), 512))
|
||||
}
|
||||
|
||||
/// Truncate a body / error string to `n` characters, appending an
|
||||
@@ -491,7 +482,10 @@ fn truncate_body(s: &str, n: usize) -> String {
|
||||
return s.to_string();
|
||||
}
|
||||
let mut out: String = s.chars().take(n).collect();
|
||||
out.push_str(&format!("... (truncated, original {} chars)", s.chars().count()));
|
||||
out.push_str(&format!(
|
||||
"... (truncated, original {} chars)",
|
||||
s.chars().count()
|
||||
));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -512,11 +506,7 @@ mod tests {
|
||||
#[test]
|
||||
fn map_status_error_404_with_model_not_found_returns_not_pulled() {
|
||||
let body = r#"{"error":"model 'qwen2.5:7b-instruct' not found, try pulling it first"}"#;
|
||||
let err = map_status_error(
|
||||
reqwest::StatusCode::NOT_FOUND,
|
||||
body,
|
||||
"qwen2.5:7b-instruct",
|
||||
);
|
||||
let err = map_status_error(reqwest::StatusCode::NOT_FOUND, body, "qwen2.5:7b-instruct");
|
||||
match err {
|
||||
LlmError::ModelNotPulled(m) => assert_eq!(m, "qwen2.5:7b-instruct"),
|
||||
other => panic!("expected ModelNotPulled, got {other:?}"),
|
||||
@@ -540,11 +530,7 @@ mod tests {
|
||||
// The English "not found" substring is absent, but the model id
|
||||
// is echoed — heuristic should still route to ModelNotPulled.
|
||||
let body = r#"{"error":"모델 'qwen2.5:7b-instruct' 을(를) 찾을 수 없습니다"}"#;
|
||||
let err = map_status_error(
|
||||
reqwest::StatusCode::NOT_FOUND,
|
||||
body,
|
||||
"qwen2.5:7b-instruct",
|
||||
);
|
||||
let err = map_status_error(reqwest::StatusCode::NOT_FOUND, body, "qwen2.5:7b-instruct");
|
||||
assert!(
|
||||
matches!(err, LlmError::ModelNotPulled(ref m) if m == "qwen2.5:7b-instruct"),
|
||||
"expected ModelNotPulled for localized 404 body, got {err:?}",
|
||||
|
||||
@@ -41,10 +41,7 @@ fn sample_request() -> GenerateRequest {
|
||||
|
||||
/// Helper: drive `generate_stream` to completion on a blocking thread so
|
||||
/// the sync `OllamaLanguageModel` stays off the async runtime.
|
||||
async fn collect_chunks(
|
||||
cfg: Config,
|
||||
req: GenerateRequest,
|
||||
) -> anyhow::Result<Vec<TokenChunk>> {
|
||||
async fn collect_chunks(cfg: Config, req: GenerateRequest) -> anyhow::Result<Vec<TokenChunk>> {
|
||||
tokio::task::spawn_blocking(move || -> anyhow::Result<Vec<TokenChunk>> {
|
||||
let llm = OllamaLanguageModel::new(&cfg)?;
|
||||
let stream = llm.generate_stream(req)?;
|
||||
@@ -58,10 +55,7 @@ async fn collect_chunks(
|
||||
/// `generate_stream` itself (rather than a stream-mid error). Used by the
|
||||
/// "unreachable endpoint" / "model not pulled" tests where the error
|
||||
/// surfaces on `.send()` before any chunks flow.
|
||||
async fn run_expecting_request_error(
|
||||
cfg: Config,
|
||||
req: GenerateRequest,
|
||||
) -> anyhow::Error {
|
||||
async fn run_expecting_request_error(cfg: Config, req: GenerateRequest) -> anyhow::Error {
|
||||
tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let llm = OllamaLanguageModel::new(&cfg)?;
|
||||
let _stream = llm.generate_stream(req)?;
|
||||
@@ -78,9 +72,12 @@ async fn run_expecting_request_error(
|
||||
async fn streamed_response_produces_tokens_then_done() {
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
r#"{"response":"hi","done":false}"#, "\n",
|
||||
r#"{"response":" there","done":false}"#, "\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":3,"eval_count":2,"total_duration":1500000}"#, "\n",
|
||||
r#"{"response":"hi","done":false}"#,
|
||||
"\n",
|
||||
r#"{"response":" there","done":false}"#,
|
||||
"\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":3,"eval_count":2,"total_duration":1500000}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
@@ -96,7 +93,10 @@ async fn streamed_response_produces_tokens_then_done() {
|
||||
assert!(matches!(&chunks[0], TokenChunk::Token(t) if t == "hi"));
|
||||
assert!(matches!(&chunks[1], TokenChunk::Token(t) if t == " there"));
|
||||
match &chunks[2] {
|
||||
TokenChunk::Done { finish_reason, usage } => {
|
||||
TokenChunk::Done {
|
||||
finish_reason,
|
||||
usage,
|
||||
} => {
|
||||
assert!(matches!(finish_reason, FinishReason::Stop));
|
||||
assert_eq!(usage.prompt_tokens, 3);
|
||||
assert_eq!(usage.completion_tokens, 2);
|
||||
@@ -155,10 +155,13 @@ async fn multibyte_chars_within_a_line_round_trip() {
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
// "한국어" (Korean) — each char is 3 bytes in UTF-8.
|
||||
r#"{"response":"한국어","done":false}"#, "\n",
|
||||
r#"{"response":"한국어","done":false}"#,
|
||||
"\n",
|
||||
// Followed by an emoji ZWJ sequence (4 bytes per scalar).
|
||||
r#"{"response":"🦀","done":false}"#, "\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":1,"eval_count":4,"total_duration":0}"#, "\n",
|
||||
r#"{"response":"🦀","done":false}"#,
|
||||
"\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":1,"eval_count":4,"total_duration":0}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
@@ -257,8 +260,10 @@ async fn other_4xx_maps_to_stream_error() {
|
||||
async fn done_reason_length_maps_to_finish_reason_length() {
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
r#"{"response":"a","done":false}"#, "\n",
|
||||
r#"{"response":"","done":true,"done_reason":"length","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#, "\n",
|
||||
r#"{"response":"a","done":false}"#,
|
||||
"\n",
|
||||
r#"{"response":"","done":true,"done_reason":"length","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
@@ -281,8 +286,10 @@ async fn done_reason_length_maps_to_finish_reason_length() {
|
||||
async fn done_reason_abort_maps_to_finish_reason_aborted() {
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
r#"{"response":"a","done":false}"#, "\n",
|
||||
r#"{"response":"","done":true,"done_reason":"abort","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#, "\n",
|
||||
r#"{"response":"a","done":false}"#,
|
||||
"\n",
|
||||
r#"{"response":"","done":true,"done_reason":"abort","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
@@ -312,9 +319,11 @@ async fn missing_eval_counts_default_to_zero() {
|
||||
// here — the comment documents the intent.
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
r#"{"response":"hi","done":false}"#, "\n",
|
||||
r#"{"response":"hi","done":false}"#,
|
||||
"\n",
|
||||
// No prompt_eval_count / eval_count / total_duration.
|
||||
r#"{"response":"","done":true,"done_reason":"stop"}"#, "\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop"}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
@@ -339,9 +348,11 @@ async fn missing_eval_counts_default_to_zero() {
|
||||
async fn missing_done_reason_defaults_to_stop() {
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
r#"{"response":"hi","done":false}"#, "\n",
|
||||
r#"{"response":"hi","done":false}"#,
|
||||
"\n",
|
||||
// Final frame omits done_reason entirely.
|
||||
r#"{"response":"","done":true,"prompt_eval_count":1,"eval_count":1,"total_duration":0}"#, "\n",
|
||||
r#"{"response":"","done":true,"prompt_eval_count":1,"eval_count":1,"total_duration":0}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
@@ -406,8 +417,10 @@ async fn endpoint_with_trailing_slash_does_not_double_slash() {
|
||||
// fail the assertion.
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
r#"{"response":"ok","done":false}"#, "\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#, "\n",
|
||||
r#"{"response":"ok","done":false}"#,
|
||||
"\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
@@ -451,8 +464,10 @@ async fn determinism_seed_zero_temp_zero_two_runs_identical() {
|
||||
// (#[ignore]) where reproducibility is modulo model-internal nondet.
|
||||
let server = MockServer::start().await;
|
||||
let body = concat!(
|
||||
r#"{"response":"deterministic","done":false}"#, "\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#, "\n",
|
||||
r#"{"response":"deterministic","done":false}"#,
|
||||
"\n",
|
||||
r#"{"response":"","done":true,"done_reason":"stop","prompt_eval_count":1,"eval_count":1,"total_duration":0}"#,
|
||||
"\n",
|
||||
);
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/generate"))
|
||||
|
||||
Reference in New Issue
Block a user