Skip to content

Post-review fixes: proxy input hardening, non-blocking Ollama stop, client shutdown, loud arg decode - #86

Merged
antoinezambelli merged 5 commits into
mainfrom
az/pr-cleanup
May 26, 2026
Merged

Post-review fixes: proxy input hardening, non-blocking Ollama stop, client shutdown, loud arg decode#86
antoinezambelli merged 5 commits into
mainfrom
az/pr-cleanup

Conversation

@antoinezambelli

Copy link
Copy Markdown
Owner

Follow-ups from reviewing the AI-generated PR batch (#65#73). Reimplements the keepers cleanly and backfills missing tests.

No version bump / CHANGELOG entry — changelog tracks formal releases only; this rides into the next one. 1022 unit tests pass.

Supersedes #66 (still open — close on merge). Ideas from #67/#69 credited via co-author trailers. Thanks @hobostay.

antoinezambelli and others added 5 commits May 26, 2026 00:33
Backfills tests for the input validation merged in #71, which landed
without coverage for its new 400 responses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
subprocess.run() blocked the event loop for the duration of
"ollama stop". Switch to asyncio.create_subprocess_exec + await so
concurrent coroutines stay responsive. Reimplements #66; lets the
command's stderr surface rather than suppressing it.

Co-authored-by: hobostay <110803307+hobostay@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… bad tool-call args

- Add aclose() to the LLMClient protocol and all clients; ProxyServer
  closes the active client's httpx pool in _async_stop, eliminating the
  unclosed-pool ResourceWarning. Completes #67, which added the method
  but wired no caller.
- AnthropicClient._convert_messages now raises a clear ValueError naming
  the tool and offending payload on malformed tool-call argument JSON,
  instead of an opaque JSONDecodeError. Addresses #69 (kept loud rather
  than swallowing to an empty dict).

Co-authored-by: hobostay <110803307+hobostay@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The facade covers validation, retry nudges, and step enforcement;
prerequisites are a granular-API feature (StepEnforcer.check_prerequisites).
Makes the existing "caller wires" intent explicit in the Mode 3 section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@antoinezambelli
antoinezambelli merged commit e8567f4 into main May 26, 2026
2 checks passed
@antoinezambelli
antoinezambelli deleted the az/pr-cleanup branch May 26, 2026 05:39
antoinezambelli added a commit that referenced this pull request Jun 1, 2026
* Proxy: native-only + transparent OpenAI passthrough

Make the OpenAI-compatible proxy native-tool-call-only and forward the
client's tools/messages verbatim, bypassing the lossy ToolSpec round-trip
that dropped schema detail and leaked empty tool names.

- Remove the proxy's --mode surface; the proxy always drives the backend
  client native. LlamafileClient's prompt-injection machinery is retained
  for non-proxy WorkflowRunner / direct-client use (it still wins for some
  models in full-guardrail workflow evals).
- Add raw_openai_tools to the LLMClient protocol; LlamafileClient's native
  path sends it verbatim. Other clients accept-and-ignore (vLLM also gains
  the previously-missing passthrough/inbound_anthropic_body kwargs).
- run_inference forwards raw OpenAI messages/tools only on the clean first
  attempt (use_raw_messages gate); any mutation falls back to fold+serialize.
- respond tool is now opt-in (--inject-respond-tool, default off).
- No instrumentation (proxy_trace/guardrail_stats deliberately not ported).
- Tests: drop removed mode-guard tests; respond tests opt in explicitly; add
  native-passthrough, detachment, respond-default, and first-attempt-gate
  coverage. Docs: ADR-012 revision + BACKEND_SETUP proxy note.

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

* Add prompt-injection as opt-in proxy capability (--backend-capability)

The proxy serves tool-call-capable backends natively (verbatim tool/message
passthrough). This adds prompt-injection back as an explicit opt-in for
non-function-calling backends (llama.cpp / llamafile without a tool template).

- New --backend-capability {native,prompt} (default native), declared once at
  construction and frozen — no runtime probing or mid-request mode mutation.
- prompt capability reuses LlamafileClient's existing prompt path (build the
  tool prompt, downgrade tool/assistant-tool_call history to text, parse the
  JSON tool call back into native tool_calls). No client changes.
- Handler suppresses verbatim raw passthrough when in prompt mode so inference
  folds normally and the client injects the tool prompt.
- Rejected for backends that are native-only (vLLM, Ollama, anthropic protocol).
- Docs: BACKEND_SETUP + ADR-012 updated to native-first + prompt opt-in.

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

* Proxy: log effective backend_timeout at startup

The configurable backend_timeout (#91) was validated, stored, and threaded
into every client request, but never surfaced at launch. Extend the
"Proxy ready" line to report the effective value so the operative timeout
is visible/diagnosable from the startup log.

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

* vLLM: single source of truth for model identity (#75)

VLLMClient kept two identity fields with distinct roles — model_path (the
verbatim wire "model" field, which vLLM validates against its
--served-model-name) and model (the derived registry-lookup key). The proxy's
external-mode served-name adoption set both by hand (model_path = served;
model = served), duplicating the derivation logic and storing the full served
name where the constructor's rule stores the stem.

Extract the path->key derivation into _derive_model_field and wrap both
assignments in _set_model_identity, then call it from __init__ and from the
proxy. External adoption now upholds the same (model_path, model) invariant as
construction: an HF-repo-id served name reaches the wire verbatim while the
registry key is the derived stem.

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

* Clients: consistent malformed-tool-call + response-shape handling

Audited malformed-tool-call and unexpected-payload handling across the
OpenAI-shape clients against the reference set by OpenAICompatClient (#89) and
LlamafileClient. Standardize on one principle, applied uniformly:

- Malformed argument JSON (a model mistake) -> TextResponse, routing the raw
  output back through the inference loop so the rescue/retry path can recover.
- A broken provider envelope (missing choices/message) or unexpected args type
  (a contract violation, not the model's fault) -> BackendError: fail loud and
  consistent, never a stray KeyError/IndexError.

Changes:
- vLLM: replace the bare-json.loads _parse_tool_args (which *raised* on
  malformed args, unlike llamafile's retry-driving TextResponse) with a
  _parse_tool_calls mirroring the reference. Route both send() and send_stream()
  through it so streaming and non-streaming agree: a fully accumulated but
  unparseable arguments string finalizes as a TextResponse, not an exception.
- llamafile / openai_compat: guard the bare data["choices"][0]["message"]
  subscripts -> BackendError on a broken envelope (matching what vLLM already
  did for choices). llamafile also hardens function/name access.
- ollama: defensive .get on function/name (both paths); document that Ollama
  emits dict args by contract, so no json.loads is needed there.

Tests: vLLM _parse_tool_calls (string/dict/empty/malformed/unexpected/missing-
function/reasoning) + streaming malformed-fragment parity; envelope-guard tests
for llamafile and openai_compat. 1092 unit tests green.

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

* LlamafileClient: remove runtime auto mode; native-first, frozen capability

Drop mode="auto" and its runtime probe-and-mutate (_resolve_and_send: try
native, fall back to prompt on HTTP error, recording resolved_mode). This was
the last vestige of the mid-request capability mutation the proxy rewrite
excised everywhere else; the proxy already declares its capability up front via
--backend-capability. With auto gone, resolved_mode is always == self.mode, so
the whole tri-state indirection collapses to a direct dispatch on self.mode.

The default is now native. This is both hardening and a deliberate posture
shift: local-model function-calling support has matured into the more reliable
path, so native-first is the right default. Prompt-injection is preserved as an
explicit opt-in (mode="prompt") and is the theoretically correct fallback for
non-FC backends — but it is honestly flagged, in the docstring and docs, that
models tend to struggle to drive the prompt-injected protocol reliably on more
complex, multi-step interactions. Capability is declared-and-frozen: an invalid
mode (including the old "auto") now raises ValueError rather than silently
degrading.

- llamafile.py: validate mode in __init__; default native; delete
  _resolve_and_send and the resolved_mode attribute/branches; dispatch send /
  send_stream on self.mode; rewrite the class docstring (native-first rationale
  + prompt caveat).
- eval_runner.py: --llamafile-mode choices [native, prompt], default native.
- docs (BACKEND_SETUP, EVAL_GUIDE): native-first wording + the prompt caveat.
- tests: drop the auto-mode suite; assert native default + ValueError on "auto".

Consumers verified unaffected: the proxy (both sites), batch_eval, and the
integration script all pass mode explicitly. 1086 unit tests green.

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

* fix(eval): honor manual context budget in batch_eval via start_with_budget

batch_eval brought servers up with a bare server.start() (no ctx_override)
and resolved the budget separately via server.resolve_budget(), so
--budget-mode manual --num-ctx N was a no-op for llama-server: the server
booted at the model's full native context (no -c), and resolve_budget(MANUAL)
just read that full value back from /props. (Ollama was unaffected — its
context is per-request via set_num_ctx.)

Route both the initial bring-up and _recover_server through the prod
start_with_budget() path, which threads manual_tokens -> ctx_override -> -c
at launch and returns the resolved budget. _recover_server gains
budget_mode/manual_tokens params so a restarted server reuses the same
budget. Drops the now-redundant standalone resolve_budget() on the happy
path (still used on the recovery branch to read back the resolved value).

This also fixes FORGE_FAST mode, which the old bare-start() path never
supported.

Smoke-tested live (Ministral-3 14B-Reasoning, native, --num-ctx 20000):
server boots with -c, rows record budget_tokens=20224 (server-clamped)
instead of the previous 262144 full-native read-back.

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

* chore(release): 0.7.3 — native-first proxy

Bump version 0.7.2 -> 0.7.3 and add the CHANGELOG entry covering this
branch plus the commits that landed on main since 0.7.2 (OpenAICompatClient
#89, --backend-timeout #91, and fixes #71/#72/#73/#86/#94).

Headline: native-first proxy. BREAKING — the proxy --mode flag is renamed
to --backend-capability (no alias; --mode was only introduced in 0.7.1).
Native is the default and only auto-selected protocol; prompt-injection is
an explicit opt-in for non-FC llama.cpp/llamafile backends.

USER_GUIDE: --mode -> --backend-capability, with the caveat that prompt mode
tends to degrade on more complex multi-step interactions. BACKEND_SETUP,
EVAL_GUIDE, and ADR-012 were already updated earlier on this branch.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
antoinezambelli added a commit that referenced this pull request Jun 3, 2026
* Args-validation: route malformed args through tool-error channel

Models occasionally emit a structurally valid tool call with malformed
args content (e.g. arguments="" instead of arguments="{}"). Pydantic
rejected at ToolCall construction, crashing the stage with
ValidationError. Observed at 86% of error rows on Qwen3-Next prompt
mode (77/89), same family on Qwen3.6 (rig-02).

This is conceptually "tool called with bad args" — the call exists,
the inputs are wrong — same as FileNotFoundError at runtime. Should
ride the tool-error channel with max_tool_errors=2 budget, not crash.

- ToolCall / TextResponse: BaseModel → @DataClass. args is no longer
  validated at construction; ResponseValidator enforces dict-shape.
  Audit: no .model_* API on ToolCall anywhere in forge.
- ResponseValidator: new args-shape branch after unknown-tool check.
  Unknown-tool runs first (cheap; no point validating args on a
  hallucinated tool name).
- nudges.tool_arg_validation_nudge: schema-derived message naming the
  tool, the received args type, and the required JSON-object shape.
- inference: parse-error nudges drain max_tool_errors (record_result)
  not max_retries (record_retry). Message prefix
  [ToolArgValidationError] vs [UnknownTool].
- Exhaustion message simplified: includes which budget and nudge kind.

Smoke (Ministral-3-14B-Reasoning, 26 scenarios × 25 runs prompt-mode):
score 78.77% (vs v0.7.0 baseline 79.5% at n=50). Delta within
±1.6% noise band — no regression. Patch never tripped on this model;
this is a regression check, full bake on a model that hits the path
to follow.

884 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* batch_eval/sampling: 32GB-eval lineup — Qwen3.6-27B, Qwen3.6-35B-A3B-UD, Nemotron-3-Nano

GGUF + server-flag + sampling-default entries for the three rig-02
32GB-tier models added for the v0.7.1 run. Launch config of record
for eval_results_rig-02_v0.7.1.jsonl (31,200 rows).

* clients: route malformed tool-call args through the tool-error channel

Unify all OpenAI-shape clients on one decode_tool_args helper
(clients/base.py): JSON-string args are parsed; malformed or non-dict
payloads ride through on the ToolCall as raw (non-dict) args instead of
collapsing to a TextResponse (openai_compat, vllm, llamafile) or raising
(anthropic streaming). ResponseValidator's args-shape check then routes
them to the tool-error channel + max_tool_errors budget — the same lane
as a runtime tool error — rather than a retry nudge.

Completes the client normalization #86 started (one decoder, all
clients) and keeps fail-loud (never coerced to {}). Also closes an
unguarded json.loads crash in the anthropic streaming finalize.

Behavior change: structural malformed-args now drains max_tool_errors
(2), not max_retries (3), in proxy mode. Wire-invisible; no public
signature changes.

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

* review fixes: honest ToolCall.args type + de-stale comments + llamafile malformed test

From the helper-vs-inline structural review (verdict: keep the shared
decode_tool_args helper). Two correctness/honesty caveats actioned:

- ToolCall.args annotated dict[str, Any] while the runtime contract now
  intentionally allows non-dicts (the docstring already says so). Widen
  to Any so the type stops lying.
- Stale comments in openai_compat/vllm streaming finalize still claimed
  malformed args yield a retry-driving TextResponse; corrected to the
  raw-args → tool-error-channel routing (they invited the exact drift
  the helper prevents).
- Add an explicit llamafile malformed-args test (non-stream native path).

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

* guardrails: make tool-call faults consistent across all three modes + expose proxy max_tool_errors

From the H1/H2 design review. Two consistency gaps closed:

1. The Guardrails middleware facade recorded EVERY validator failure as a
   retry (max_retries) and returned action='retry' — diverging from
   run_inference/proxy, where malformed args drain the tool-error budget
   and tool-call faults ride the tool channel. The facade now:
     - routes malformed args (tool_arg_validation) to max_tool_errors,
     - returns a new action='tool_error' for tool-call faults (unknown
       tool name OR malformed args), with nudge.role='tool' so callers
       emit the correction on the tool-result channel.
   Channel vs budget are now two explicit kind-sets in nudge.py
   (TOOL_CHANNEL_KINDS ⊃ TOOL_ERROR_KINDS); unknown-tool rides the tool
   channel but still drains the retry budget, matching run_inference.
   _TOOL_ERROR_KINDS moved from inference.py to the shared nudge module.

2. Proxy exposed max_retries but not max_tool_errors, hiding the budget
   exactly where malformed-arg recovery now matters. Added --max-tool-errors
   (default 2) threaded ProxyServer → HTTPServer → handler → ErrorTracker.

Nobody depends on the middleware facade yet, so the CheckResult.action
addition is free. Channel parity in run_inference unchanged (it emits
role=tool for list-branch corrections regardless of nudge.role).

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

* steps: guard arg-match prereqs against non-dict args + ADR-016

Closes the last crash vector from the design review (hole 4):
StepTracker.check_prerequisites did args.get(match_arg), which raises on
a non-dict args. ResponseValidator fences this before dispatch in the
runner/proxy, but a granular caller that bypasses check() could reach it
directly. Treat non-dict args as unsatisfied (block, don't crash).

ADR-016 records the malformed-args → tool-error-channel decision in its
honest framing: a native-mode conditioning bet, not an ontology claim;
prompt mode degrades to the prior retry shape; the tool-error budget
coupling is deliberate but revisitable. CHANGELOG held for release time.

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

* eval data: tag generations + add 32GB tier (v0.7.4)

Inject a per-row `gen` field so one dashboard can fold eval waves run
against different code states. gen is a comparability epoch, not a
release version: v0.6.0 -> gen 1 (carries the Anthropic ablation +
Retired-tier models, neither re-run since), and v0.7.0 plus the new
32GB tier -> gen 2.

Rename the 32GB wave to eval_results_v0.7.4.jsonl (its landing release)
and keep it as a separate file beside v0.7.0 — same gen, distinct wave,
so each wave keeps its own landing commit for reproducibility.

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

* eval dashboard: eval-generation badges + retired toggle

report.py now accepts multiple result files and keeps the newest gen per
config (dedup_latest_gen), so the board folds all generations into one
view. Lagging rows (gen < newest) get a superscript badge backed by a
commit/date legend; Retired-tier models are carried forward but hidden
by default (--include-retired, or a sidebar checkbox in the HTML). Adds
MODEL_FAMILIES entries for the 6 32GB models so they render clean family
names and cross-backend keys instead of raw GGUF stems.

React dashboard: Show-retired checkbox (dimmed rows + a "retired" pill),
superscript gen badges with provenance tooltips from the data blob.

Regenerated docs/results/ from the three gen-tagged datasets.

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

* release: 0.7.4 — version bump, CHANGELOG, registry 32GB tier

Bump 0.7.3 -> 0.7.4 and add the 0.7.4 CHANGELOG entry (malformed args ->
tool-error channel; 32GB eval tier + dashboard eval-generations). Move
the 6 32GB models (Mistral-Small-3.2, Qwen3.5/3.6 27-35B, Nemotron-3
Nano) from Unpublished to Current now that they're in the published eval,
and reword the tier definitions for the dashboard's eval generations.

Also scrub a stale bring-up note from the Qwen3.5-122B footnote (it leaked
operator smoke-probe process into a public doc) and exclude the built
dashboard dist/ from the hatchling sdist sweep.

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

* changelog: de-escalate dataclass entry from BREAKING to Changed

The ToolCall/TextResponse pydantic->dataclass move only breaks callers
who serialized these via the pydantic .model_* API or relied on
construction-time validation; attribute reads and keyword construction
are unchanged. Reserving BREAKING for forced-migration changes (cf.
0.7.3 --mode rename) keeps the badge meaningful.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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