# Findings Running log of things that came up building and testing the Gemma → Shieldstral pipeline, sorted by how much they actually mattered in practice. ## Actual obstacles Things that were real problems and required a fix. - **Trailing colon typo in `LLAMA_SERVER_URL`** produced an "invalid authority" error from Rig's URI parser — `llamafile::Client::from_url` needs a bare `http://host:port`, no trailing punctuation, no `/v1` suffix (the client appends that itself). - **`std::fs::read_to_string("prompts.toml")` used a path relative to the process's runtime working directory**, which differs between `cargo run`, RustRover's run config, and any future install location — file-not-found in practice. Fixed by switching to `include_str!`, which resolves relative to the source file at compile time instead. - **Ollama has no logprobs support at all**, in either its native `/api/chat` or its OpenAI-compatible `/v1/chat/completions` endpoint. This was a hard blocker for the whole scoring approach — had to serve Shieldstral through `llama-server` directly instead of Ollama. - **`raw_completion()` doesn't exist in the last published `rig-core` crate (0.41.0)** — it's only on git `main`, ahead of any release. Had to pin a git dependency to get it, accepting the instability that comes with tracking an unreleased branch. - **Missing `//` scaffolding + system preamble** produced meaningless, unreliable scores when testing against raw unscaffolded text — the model has no policy to judge against without it. - **`temperature: 1.0` vs `0.0`** silently distorted reported scores. llama-server only bypasses the full sampler chain (top_k/top_p/min_p/ repetition penalties) for logprobs reporting at greedy decoding (`temperature` effectively 0); at `1.0` the reported probabilities reflect the post-sampler-chain distribution, not raw logits. - **Gemma's "thinking" mode was on by default** for the Ollama model tag. A tight `max_tokens` budget meant it sometimes got cut off mid-thought before ever emitting `content`, crashing the naive `AssistantContent::Text` match on an unhandled `Reasoning` block. Fixed with `"think": false`. - **`uv` dependency resolution failures on `transformers==4.57.6`** — the version genuinely exists on PyPI, but was shadowed by a same-named package on PyTorch's own wheel index under uv's default `first-index` strategy. Needed `--index-strategy unsafe-best-match` (safe here since both indexes are reputable) or a per-package `--override`. - **Gemma refusing the "generate hostile text" seed prompt** depending on exact wording — explicit "hate speech" / "insulting people" phrasing triggered refusals noticeably more than softer framing. Needed a few iterations on the prompt to land on wording that reliably produces scoreable content without tripping Gemma's own alignment training every time. ## Overblown, but theoretically impactful under the right conditions Concerns that turned out not to matter in the cases tested, but aren't nothing — worth revisiting if circumstances change. - **Qualifying/meta text ("Here is a short text:") diluting the score** — negligible on a seed document already saturated with hostile content (a ~4-word neutral preamble on a ~50-word hostile block didn't move a 0.9999 score). Could plausibly matter more on *revision*-step output sitting near the 0.1 threshold, where a few tokens of padding might tip an not-actually-safer revision under the line. Worth watching iteration logs for revision scores landing suspiciously close to threshold right when padding shows up — not worth defending against pre-emptively without evidence it's happening. - **`ServerConfig` single-field wrapper struct**, flagged during a cleanliness review as unnecessary indirection — true in isolation, but it deliberately mirrors the existing `Prompts`/`prompts.toml` pattern for consistency, so the real cost is close to nil in context. ## Genuinely was irrelevant Things that looked like they might be a problem and just weren't. - **Whether `GenericCompletionModel`/`ollama::CompletionModel` needed to be `Clone`** to support a loop calling them repeatedly — turned out both already derive `Clone`, and more to the point, didn't even need cloning since both can just be borrowed across iterations. A non-issue once actually checked against source instead of assumed. - **Whether the literal sampled token from Shieldstral's single forced token matters** — irrelevant, since scoring always reads the full `top_logprobs` list regardless of which single token happened to get emitted as `content`. - **Rig's `.completion()` normalizing away provider-specific `logprobs`** initially looked like a dead end for using Rig at all for scoring — turned out to have a clean, intended escape hatch (`raw_completion()`) once the source was actually checked, so the abstraction gap was real but never a blocker.