Skip to content

feat(guardian): an OpenRouter provider, so #246 has a third vendor to compare - #412

Merged
zaebee merged 4 commits into
mainfrom
openrouter-arm-246
Aug 17, 2026
Merged

feat(guardian): an OpenRouter provider, so #246 has a third vendor to compare#412
zaebee merged 4 commits into
mainfrom
openrouter-arm-246

Conversation

@zaebee

@zaebee zaebee commented Aug 17, 2026

Copy link
Copy Markdown
Owner

The comparison is still not run. What this adds is the third vendor #246 was blocked on, and two guards that stopped two wrong answers from being published.

Why a third provider

The two providers this repository speaks to are the two #246 is comparing, and the local mistral key returns 402. Ollama would answer it for nothing but needs a server this project does not have. OpenRouter reaches a third vendor over an OpenAI-shaped API.

Three things in the provider are measurements, not choices

max_tokens is generous because a first probe capped output at 600 and read three capable reasoning models as incapable — they spend 709 to 2,243 tokens reasoning before the answer, so truncated reasoning came back where JSON was expected. Hitting the budget now raises rather than returning that prefix: the limit was this file's choice, and attributing it to the model was the error.

A 429 raises and carries OpenRouter's body. The free tier gives no warning — /api/v1/auth/key reports no limit and completions carry no rate-limit headers — and the body is what separates an account's daily allowance from one upstream's shared pool. That distinction is why z-ai/glm-5.2:free was rejected as an arm: limit_source: upstream_provider_shared_pool.

An error object arriving with HTTP 200 is reported rather than KeyError'd. That is how the account's allowed-providers setting surfaces, and the remedy is in the message.

reasoning is sent explicitly, off by default. qwen3.7-plus puts its thought in a separate field and fills content only at the end; on the larger prompts it spent the whole budget thinking and returned content: null. Raising the budget would have fixed the symptom and broken the experiment — gemini-2.5-flash is not doing extended thinking, so the arms would differ in a dimension nobody chose.

The validity gate, which is the real deliverable

judge_finding contains provider errors per finding and returns None, so a spent quota, a 429, a 402 and unparseable JSON all arrive as the same row shape as a skeptic that refused to refute anything — which is exactly what #246 predicts for a same-vendor skeptic. A broken arm would have confirmed the hypothesis.

MAX_UNRULED_RATE refuses to print a comparison above 5% unruled, keeps the rows, and exits 2. It fired on both attempts:

attempt model unruled cause
free nemotron-3-super-120b:free 96/135 quota — 50 requests/day, measured
paid qwen/qwen3.7-plus 118/135 thinking, then HTTP 402 (0 credits)

Neither produced a number. Without the gate, both would have read as "the cross-vendor skeptic refutes nothing".

And a diagnosis that cost half an hour

judge_finding logged only "Skeptic judgement failed; finding stays unruled" — 118 identical lines, no type, no message. Four different faults land in that one except and they have four different remedies. It now carries both. Containing the error per finding stays deliberate (#246 §3.4); saying nothing about it was an omission, not a decision.

To unblock

Top up OpenRouter (~$0.15 for the run with reasoning off), or split the free tier across three days (the input is a frozen recording, so it is deterministic — but the tool has no resume), or a local ollama.

Data and the full write-up: benchmarks/guardian/experiments/246-cross-vendor/.

2031 tests.

🤖 Generated with Claude Code

zaebee and others added 2 commits August 17, 2026 13:37
… compare

The two providers this repository speaks to are the two #246 is comparing, and
the local mistral key returns 402. Ollama would answer it for nothing but needs
a server this project does not have. OpenRouter reaches a third vendor over an
OpenAI-shaped API, which is what the comparison was blocked on.

Three things in the provider are measurements rather than choices.

max_tokens is generous because a first probe capped output at 600 and read
three capable reasoning models as incapable: they spend 709 to 2,243 tokens
reasoning before the answer, so the truncated reasoning came back where JSON
was expected. Hitting the budget now raises instead of returning that prefix,
because the limit is this file's choice and attributing it to the model was
the error.

A 429 raises and carries OpenRouter's body. The free tier gives no warning —
/api/v1/auth/key reports no limit and completions carry no rate-limit headers
— and the body is what distinguishes an account's daily allowance from one
upstream's shared pool, which is why z-ai/glm-5.2:free was rejected as an arm.

An error object arriving with HTTP 200 is reported rather than KeyError'd; that
is how the account's allowed-providers setting surfaces, and the remedy is in
the message.

skeptic_arms gains a per-arm model (--arms gemini,openrouter=vendor/model) and
a validity gate. The gate is not a tolerance: judge_finding contains provider
errors per finding and returns None, so a spent quota, a 429 and unparseable
JSON all arrive as 'unruled' — the same row shape as a skeptic that refuted
nothing, which is what #246 predicts for a same-vendor skeptic. A broken arm
would have confirmed the hypothesis.

It fired on the first real run: the free nemotron arm left 96 of 135 findings
unruled, all after the first 39, which with today's probes puts the free tier
at 50 requests per day. Measured, not recalled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t failed (#246)

Two faults, found by running the arm rather than by reading about it.

qwen3.7-plus puts its chain of thought in a separate reasoning field and
fills content only at the end. On the larger prompts it spent the entire
8,000-token budget thinking and returned content: null — 118 of 135 findings
unruled, caught by the validity gate. Raising the budget would have fixed the
symptom and broken the experiment: gemini-2.5-flash, the arm it is compared
against, is not doing extended thinking, so the two arms would have differed
in a dimension nobody chose. The provider now sends reasoning explicitly in
both directions, off by default, so a run can state what it did rather than
inherit it and be unable to describe itself afterwards.

The second fault cost half an hour because judge_finding logged only
"Skeptic judgement failed; finding stays unruled" — 118 identical lines with
no type and no message. A spent free quota, an HTTP 429, a 402 and
unparseable JSON all land in that one except, and they have four different
remedies. It now carries the type and the message. Containing the error per
finding stays deliberate (#246 3.4); saying nothing about it was not a
decision, it was an omission.

What the two attempts measured, in benchmarks/guardian/experiments/
246-cross-vendor/: the free tier allows 50 requests a day (39 answered in the
run plus 10 in the day's probes, then a clean wall), and the paid account has
$0 credits against $0.159 used, so 402. The comparison itself is still not
run, and the gate is why no number was published for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an OpenRouter provider to enable skeptic evaluations across a third vendor, addressing issue #246. It also implements a validity gate that refuses to publish results if an arm leaves more than 5% of findings unruled, preventing provider errors from being misinterpreted as leniency. The skeptic runner, fingerprinting, and command-line scripts are updated to support the new provider and model-per-arm specifications, accompanied by extensive unit tests. Feedback from the review highlights two improvement opportunities: first, checking that the choices list returned by OpenRouter is non-empty to prevent an IndexError; second, making the markdown code fence parser in parse_or_raise more robust to handle single-line fences without newlines.

Comment thread src/cgis/guardian/providers/openrouter.py Outdated
Comment thread src/cgis/guardian/providers/openrouter.py Outdated
zaebee and others added 2 commits August 17, 2026 17:38
An empty choices list was an IndexError. "choices" not in body passes when
the key is present and holds [], which is what an upstream content filter
returns, and body["choices"][0] then raised IndexError — an exception whose
message says nothing about which provider declined. Now falsy-checked.

A single-line code fence was unparseable. Splitting on a newline returned the
whole string when there was none, so the backticks survived and json.loads
failed — counting a good answer as unruled, which is the reading this whole
file exists to prevent. Regexes now match the fence and its optional language
tag. The suggested fix used lstrip with a character class; lstrip("json")
removes any of j, o, s, n and would eat into the payload, so the shape of the
fix is different from the one proposed while the finding is the same.

SonarCloud separately put build_skeptic_provider at cognitive complexity 18
against a limit of 15, which openrouter pushed it over. Split into one builder
per provider behind a table, matching build_provider. Each arm's requirements
— which key, which default model, whether one exists at all — are per-provider
facts and read better beside that provider than interleaved with the others.

Coverage on the new provider is 100%; the openrouter paths through
build_provider and build_skeptic_provider now have tests of their own rather
than being exercised only through the experiment script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SonarCloud on #412: an unanchored `\s*```$` is quadratic on a long run of
whitespace that never reaches a fence, because every start position retries
the \s*. Model output is not input this module chooses, so unbounded cost on
hostile shapes is a real property rather than a theoretical one — 200k spaces
now parse in under a millisecond.

str.endswith answers the same question in one comparison. The opening fence
keeps its regex: anchored at ^, it is tried from a single position and cannot
backtrack across the input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@zaebee
zaebee merged commit d744896 into main Aug 17, 2026
3 checks passed
@zaebee
zaebee deleted the openrouter-arm-246 branch August 17, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant