This repository was archived by the owner on Aug 11, 2026. It is now read-only.
fix(inference): forward request fields by default, pin only what protects the grant - #554
Merged
Merged
Conversation
Peyton-Spencer
force-pushed
the
peyton/inference-schema-normalize
branch
2 times, most recently
from
July 28, 2026 17:12
2e42255 to
5b14869
Compare
Peyton-Spencer
changed the base branch from
main
to
peyton/merge-divergent-alembic-heads
July 28, 2026 17:12
Contributor
Author
…he run
The v7 chat gate refused any top-level field outside a flat allowlist, and
did so 43 lines before begin_inference_request -- so no inference_requests
row was written and the rejections were invisible in telemetry. The error
body did not name the offending key either.
`Cooking` (rank 15 on v1) hit this by adding one line that sent
`reasoning: {"effort": "medium"}`. The platform already stamps
`{"effort": "medium", "exclude": True}` on every v7 request, so the miner
was refused for redundantly requesting exactly what it was being given.
~81 of ~480 chat calls died per run; v3 shipped byte-identical and still
failed.
Split the allowlist into three sets by what a field can actually do:
- pinned (model, max_tokens/max_completion_tokens, n, reasoning):
accepted, then overwritten with the ticket's own value.
- dropped (best_of, reasoning_effort, service_tier, user, metadata,
store, stream_options, response_format, logprobs,
top_logprobs, logit_bias): accepted, then stripped before the
provider call, each for a stated reason.
- forwarded (messages, temperature, top_p, seed, stop, tools, tool_choice,
parallel_tool_calls, frequency_penalty, presence_penalty).
Unknown keys stay fail-closed -- OpenRouter's surface is additive and a
field invented later is exactly the one that might buy compute -- but the
refusal now names them. max_tokens over-asks and disagreeing aliases now
clamp downward rather than kill the run.
Anti-cheat is unchanged and now testable as a partition: nothing is
accepted without being pinned, dropped, or deliberately forwarded, so no
request can obtain compute, a model, or a reasoning effort its ticket did
not grant. `effort: "high"` is served as medium; so is `effort: "none"`,
which would otherwise let an agent opt out of v7's mandatory reasoning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Flips the posture: forwarding needs no justification, every pin and drop carries a concrete reason. This is a retrieval-agent competition, not a prompt-shape competition -- a harness's sampling and observability choices are its own. The rule for a drop: stripping is acceptable only when it changes neither what the model produces nor what the harness can observe. Silently discarding a knob a miner set is the Cooking bug with extra steps -- they ask for a JSON schema, get prose, their parser fails, nothing names the cause. Provider support was established empirically against the pinned v7 model rather than from docs. Two findings drove the placements: - OpenRouter EXCLUDES an endpoint lacking a requested parameter rather than routing to it and failing. Pinning order:[amazon-bedrock] (no response_format) returns 404 with the field and succeeds without it, and under the v7 aggregate route it lands on one of eight supporting providers. So forwarding response_format cannot manufacture a 400. - reasoning_effort HARD-FAILS: OpenRouter 400s when it disagrees with the pinned reasoning.effort. It moves from drop to pin for that reason. Moved to forwarded: response_format (+structured_outputs), logprobs, logit_bias, top_k, min_p, top_a, repetition_penalty, prediction, verbosity. json_schema was verified to conform on all five supporting providers WITH the pinned reasoning block, so the feared reasoning/ structured-output interaction does not exist for this model. logprobs is now plumbed through _public_provider_response; forwarding a request field whose response is stripped would be a silent no-op. top_logprobs is refused as a measured capability limit: ~80 bytes per token per alternative puts top_logprobs=3 at ~2.65 MiB and =20 at ~13.3 MiB against the 2 MiB response cap, and a breach raises a 502 that is attributed to the PROVIDER and cools the shared route -- so a caller's own choice would be charged to infrastructure. No non-zero value is safe at the 8192-token ceiling. logprobs alone is ~0.81 MiB and fits. Pin now also covers include_reasoning, service_tier, usage and prompt_cache_key. Drop is down to user/metadata/safety_identifier/store/ stream_options -- all zero-effect on the completion. Refusals are named with reasons, including the two capability limits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Peyton-Spencer
force-pushed
the
peyton/inference-schema-normalize
branch
from
July 28, 2026 17:34
5b14869 to
efe28a8
Compare
Peyton-Spencer
marked this pull request as ready for review
July 28, 2026 17:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The principle
The default is FORWARD. Forwarding needs no justification; every pin and every drop below carries a concrete, stated reason. This is a retrieval-agent competition, not a prompt-shape competition — a harness's own sampling and observability choices are its business.
The rule for a drop: stripping a field is acceptable only when it changes neither what the model produces nor what the harness can observe. Silently discarding a knob a miner deliberately set is the original bug with extra steps — they ask for a JSON schema, receive prose, their parser fails, and nothing anywhere names the cause.
Why
The v7 gate refused any field outside a flat allowlist, at
inference.py:902— 43 lines beforebegin_inference_request. Noinference_requestsrow was written, so the rejections were invisible in telemetry, and the error named no key.Cooking(rank 15 on v1) added one line sendingreasoning: {"effort": "medium"}— the valuebenchmark_reasoningalready stamps on every v7 request. ~81 of ~480 chat calls died per run. v3 is byte-identical on that line and still failing._locked_upstream_payloadalready overwrotereasoningunconditionally; the only thing standing between Cooking and a working run was one allowlist entry.Empirical provider verification
Established against the pinned v7 model (
openai/gpt-oss-20b) through the exactaggregate_throughputpreferences this lane pins, not from documentation. Three probe passes; the first two were confounded (throughput routing picked a different provider per call;max_tokens=40was consumed by reasoning tokens, sinceexclude: truewithholds reasoning from the response but still generates and bills it) and were re-run provider-pinned with room to generate.Two findings drove the design:
order:["amazon-bedrock"](the only live provider advertising noresponse_format) returns404 No endpoints foundwith the field and succeeds without it — same order, same fallbacks, one variable. With fallbacks on it served from CoreWeave and conformed. Under the v7 aggregate route (allow_fallbacks: true) the request simply lands on one of the eight supporting providers, so forwardingresponse_formatcannot manufacture a 400. Norequire_parametersneeded.reasoning_efforthard-fails. OpenRouter answers400 "reasoning_effort" and "reasoning.effort" are both provided with conflicting valueswhenever it disagrees with the pinned block. Forwarding it would break every harness setting it to anything butmedium. It moves from drop to pin for that reason.response_formatjson_schema+ pinned reasoningresponse_formatjson_schemawithout reasoningresponse_formatjson_schema, aggregate route ×4response_formatjson_schemaon Bedrock (unsupporting)response_formatjson_objectjson_schema)structured_outputslogprobstop_logprobslogprobsis — but see size limitlogit_bias,frequency_penalty,presence_penalty,seed,stop,top_k,repetition_penalty,min_pprediction,verbosity,parallel_tool_callsservice_tier: priorityuser,metadata,safety_identifier,store,prompt_cache_keyreasoning_effort(agrees with pinned)reasoning_effort(conflicts)Probes were throwaway (single-digit
max_tokens, one call per case), read the key from gcloud in-process, and are not landed in either repo. If you want this as a permanent contract test, say so and I'll add it — it would need a live key in CI, which is why I didn't land it unilaterally.The partition
Four fates, disjoint and covering, asserted by
test_every_field_has_exactly_one_decided_fate. Under a forward-by-default posture the risk inverts — no longer "a normal field is refused" but "a lever that should have been pinned is forwarded by omission" — so the test asserts the anti-cheat set by name.Pin — the anti-cheat boundary (does not loosen)
model_locked_grant_model); disagreement logged as an evasion signalmax_tokens,max_completion_tokensmin(), so neither can raise the othernbest_ofreasoningreasoning_effortinclude_reasoningexclude: trueservice_tierprioritybuys faster compute the ticket did not grantusageprompt_cache_keyDrop — zero effect on the completion and on observation
user,metadata,safety_identifier— caller-controlled strings that travel to a third party under the pinneddata_collection: "deny"/zdr: trueposture and identify the agent behind the request. None affects the completion, so dropping cannot change a result — which is exactly what makes stripping inert here and not elsewhere.storecontradicts that same pinned retention posture.stream_optionsis inert without streaming.Forward — everything else
messages,tools,tool_choice,parallel_tool_calls,temperature,top_p,top_k,min_p,top_a,seed,stop,frequency_penalty,presence_penalty,repetition_penalty,logit_bias,logprobs,response_format,structured_outputs,prediction,verbosity.logit_biasmoved to forward: the earlier tokenizer-mismatch worry doesn't survive contact, since the grant pins gpt-oss-20b and the exchange response tells the harness which model it is talking to. It biases selection and buys nothing.logprobsis the pattern case: it does not alter generation at all, so there was never a determinism or comparability argument against it — only the allowlist's silence. It is now plumbed through_public_provider_response, because forwarding a request field whose response is stripped is a silent no-op: the caller sets the flag, pays for the larger provider response, and receives nothing. Providers that don't support it returnnull, which is the honest answer.Refuse — named, with the reason
Route identity and egress (
models,provider,route,preset,transforms,plugins,web_search_options); deprecated tool spellings (functions,function_call— no provider advertises them, so forwarding would leave a harness silently toolless; the message namestools/tool_choice); non-text modalities (audio,modalities— this lane's response contract is text-only and a non-text modality would surface as a 502 blamed on the provider); unknown keys (fail-closed on purpose, since OpenRouter's surface is additive).Plus two capability limits, which the message says are limits rather than policy:
stream— this lane genuinely cannot stream. It is buffered end to end: the proxy readsupstream.contentwhole,_public_provider_responserebuilds a singlechat.completion, accounting settles once against the final usage block, and the broker reads and rewrites the whole body. Streaming would need changes in the platform proxy, the broker, and the accounting. Not normalised tofalse— a caller that asked for SSE and silently got a single JSON body would fail parsing it. Worth reporting as a possible future ask.top_logprobs— measured, not assumed: ~80 bytes per token per alternative. At the 8192-token ceiling,logprobsalone extrapolates to 0.81 MiB (fits the 2 MiBresponse_body_bytescap),top_logprobs=3to 2.65 MiB,top_logprobs=20to 13.3 MiB. A breach raises502 provider response is too large, which isroute_observable— so it cools the shared provider route and is attributed to the provider, meaning a caller's own choice would be charged to infrastructure and mint a retry grant. No non-zero value is safe at the ceiling, so clamping cannot rescue it. Caps untouched as instructed; raisingresponse_body_bytes(embeddings already run at 16 MiB) would unlock it — operator's call.Anti-cheat
Preserved by construction. Every pinned field is overwritten or removed in
_locked_upstream_payload; the partition test asserts each lever is in_PINNED_REQUEST_FIELDSand not in_FORWARDED_REQUEST_FIELDS, by name. Structured outputs specifically:response_formatselects a decoding constraint, not a model, a provider, or an effort level. It cannot raisemax_tokens(clamped), cannot add generations (npinned to 1), and cannot alter reasoning — verified empirically, the pinned{"effort":"medium","exclude":true}block travels alongside it unchanged and the schema still conforms. Constrained decoding does change latency, which the efficiency bonus observes; that is a miner's own trade-off to make, not a grant escape.exclude: Trueverified against OpenRouter docs: the model still reasons and is still billed; only the content is withheld. Valideffortincludesmax/xhighabove medium (pinned down) and"none", which disables reasoning entirely (pinned up) — so pinning also stops an agent opting out of v7's mandatory-reasoning contract, a direction the old rejection never covered.Legibility
Refusals name every offending key, sorted for stability, each with its reason. Paired with ditto-assistant/dittobench-api#125, which stops the broker overwriting this string with
"inference request denied", so it reaches the harness's stderr.Does this fix
Cookingv3 with no miner action?Yes. Verified: v2/v3
tool_agent.rsare byte-identical; the entirevendor/ditto-harness/tree is byte-identical between v1 (rank 15, working) and v3; the only request-shape delta isreasoningandtool_choice, andtool_choicewas already allowlisted with both emitted shapes already passing. Every v3 branch — plus the vendoredmodels/mod.rs:250path gated onDITTOBENCH_REASONING_EFFORT— was run through the new gate: all accepted, all pinned to medium. No resubmission needed.Tests
reasoning: {"effort":"high"}is accepted (200), reaches the provider pinned tomedium, and is charged as that call (settled row + grant counters). The old gate never wrote a row, which is why this was invisible.response_formatjson_schemareaches the provider byte-identical alongsidelogprobs/logit_bias/frequency_penalty/seed, the pinned reasoning block rides along unchanged, and the conforming answer returns unmangled with logprobs intact.nullwhen absent); both capability limits explain themselves; over-ask clamping and aliasmin().make lint,make lint-copy,make typecheckclean; full suite green (2489 passed).Open for the operator
user/metadata/safety_identifier— dropped on a privacy argument that is genuinely product-shaped. Easy to move to forward.top_logprobs— blocked only by the 2 MiBresponse_body_bytescap, which I left untouched as instructed.Related
validator_lease_audit, the fleet-health dashboard, or the scores read path. No overlap with in-flightwiden-failure-detail(validator models/tests only). Size caps unchanged.🤖 Generated with Claude Code