Tags: antoinezambelli/forge
Tags
Fix Proxy command ownership and release 0.9.3 (#149) * test: support Anthropic 1.0 wire capture * fix(proxy): enforce exclusive command ownership * test(proxy): serialize Windows uninstall retry * fix(proxy): make Windows ownership cleanup self-contained
feat(proxy): ship standalone developer sidecar (#145) * feat(proxy): add standalone installer * docs(proxy): document standalone installation * release: prepare 0.9.1 * ci(proxy): make cross-platform installer gates portable * ci(proxy): scope release gates to pointer bumps * fix(proxy): make release gates portable
refactor(proxy): consolidate the Forge 0.9 sidecar contract (#139) * Refactor proxy contracts for Forge 0.9.0 * test(proxy): ignore volatile vLLM catalog metadata * test(proxy): add generic OpenAI live battery * docs: add common Forge 0.9 migrations * docs: finalize Forge 0.9 release notes
release: v0.8.2 — expanded model evals and dashboard refresh (#133) * feat(eval): add Gemma-4 large (26B-A4B, 31B) to batch_eval roster + serving recipe Wire the two 32GB-tier Gemma-4 models into batch_eval for the rig-02 forge-native eval sweep (26 non-compaction scenarios): - _GGUF_FILES: gemma-4-26B-A4B-it-UD-Q4_K_M, gemma-4-31B-it-Q4_K_M (auto-generates their llamaserver native+prompt configs). - _SERVER_EXTRA_FLAGS: per-model serving recipe — --reasoning-format auto, --ctx-checkpoints 1 (SWA checkpoint RAM, llama.cpp #21690), q8 KV, -fa, --samplers temp;top_p;top_k. No --reasoning-budget (absent as every other config here); no -c (context from --budget-mode forge-full). Sampling values resolve client-side from recommended_sampling -> sampling_defaults (already registers both stems at temp1.0/top_p0.95/top_k64). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BmJdi2BHK1LZRLen6HF4 * eval: wire 120B tier (gpt-oss/Qwen3.5/Nemotron) for rig-03 Baseline provenance point for the rig-03 120B campaign. Adds the three multi-shard first-shard GGUFs to _GGUF_FILES, strips the -NNNNN-of-NNNNN suffix in the config loop so the model keys on its clean stem, and adds the UMA/Vulkan serving recipes (reasoning-format auto, q8 KV + -fa, --no-prefill-assistant, --no-mmap) to _SERVER_EXTRA_FLAGS. This is the code state that produced the medium/low-effort baseline rows (gpt-oss reforged 74.2 / bare 47.8; nemotron reforged 74.5 / bare 55.0; qwen reforged 91.3 / bare 66.5) in eval_results_rig-03.jsonl. The reasoning-effort axis wiring lands as a separate commit on top. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01UWVT8JXuhSxfhaeA2upHZc * eval: add reasoning-effort axis (gpt-oss@high, nemotron@high) Adds a reasoning_level dimension so effort variants of the same GGUF stem run and render as parallel configs instead of clobbering the baseline (silent-skip). Effort is applied via bespoke configs — NOT by mutating the sampling registry: a new BatchConfig.sampling_override carries an explicit param set built DRY from get_sampling_defaults(stem) with chat_template_kwargs swapped to high; _build_client passes it with recommended_sampling=False. LlamafileClient is untouched (its constructor already accepts these kwargs). reasoning_level ("default"/"high") threads through _run_key, _count_completed_runs and the JSONL row exactly like reasoning_replay, and joins report's _config_tuple dedup identity (UNLIKE reasoning_replay — parallel effort levels must coexist, not supersede). report renders the model's native vendor term via a stem-keyed _REASONING_VOCAB (gpt-oss default->medium, nemotron default->low, both +high; qwen/instruct models untagged). The @high configs live in a dedicated "reasoning-high" CONFIG_SET, kept out of "all"/llamaserver-native. Storage stays generic; pre-axis rows (no field) read as "default" — no backfill. Verified: dry-run shows both @high configs 0/50 -> RUN 50 (no silent-skip); the existing 7800 baseline rows aggregate identically through the report pipeline (6 ConfigKeys, per-arm scores unchanged, 1300 rows each). Effort dicts resolved from the GGUF chat templates: gpt-oss {reasoning_effort:high}; nemotron {enable_thinking:True, low_effort:False, force_nonempty_content:True} (template defaults undefined low_effort to False, so omit==False). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01UWVT8JXuhSxfhaeA2upHZc * eval: add rig-03 120B-tier results (15,600 runs, 12 arms) Native llamaserver eval on the 120B tier, 26 non-compaction scenarios x 50 runs per arm. Tracked via LFS (eval_results*.jsonl). Disjoint from the 32B-tier dump — merge by concatenation; row identity carries model/ablation/ reasoning_level/reasoning_replay so arms never collide. Arms (1300 rows each): gpt-oss-120b bare/medium, bare/high, reforged/medium, reforged/high Nemotron-3-Super bare/low, reforged/low Qwen3.5-122B {reforged, bare} x {none, keep-last, full} replay Notes for downstream analysis: - Nemotron@high is absent by design: the backend could not serve that model's high-effort mode with stable liveness (requests wedged at 0 iterations / 300s). Those rows were infra artifacts, not model results, and were purged rather than scored. - gpt-oss ran at 128k, Nemotron and Qwen at 256k (forge-full auto-fit). - Score semantics: correct = accuracy truthy, non-None, no validate_error. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QKtkSFbMGRKPzFvTTa2ZFE * eval: add full reasoning-replay arms (19,500 runs, 15 arms) Adds three arms to the rig-03 120B-tier results: gpt-oss-120b reforged medium full-replay gpt-oss-120b reforged high full-replay Nemotron-3-Super-120B reforged low full-replay Nemotron ran at manual 128k; its none-replay baseline is 256k. Also purges one Timeout row (nemotron full, grounded_synthesis run 41) and refills it via resume. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QKtkSFbMGRKPzFvTTa2ZFE * eval: consolidate rig-01/03/04 into eval_results_v0.8.2.jsonl (74,100 runs, 57 arms) Folds the three rig campaigns into one gen-3 result set named for the release that will publish it, replacing the per-rig files. 74,100 rows / 57 arms, all stamped gen: 3, every arm 26 scenarios x 50 runs with no gaps or duplicates. Contents beyond what v0.7.5 published: - Qwen3.6-35B-A3B + 27B: full reasoning-replay arms (rig-04), restoring the arms the gen-2 pre-knob rows could no longer carry. 35B reforged/native reproduces at 95.2% vs the 94.8% measured under v0.7.4. - gemma-4-26B-A4B + 31B: none/keep-last/full ladders, native (rig-04). - LFM2.5-8B-A1B + Mellum2-12B-A2.5B (Instruct, Thinking): keep-last/full (rig-01), pairing with the none arms already in v0.7.5. Note these three ladders span forge versions -- none was measured under 0.7.5, the replay arms under 0.8.x. - gpt-oss-120b at default and high effort, NVIDIA-Nemotron-3-Super-120B-A12B, Qwen3.5-122B-A10B (rig-03). The gen-3 stamp is unchanged on purpose. The epoch boundary is the reasoning_replay default flip in v0.7.5; nothing since alters the default policy, the scenario set, the scoring, or the harness loop. The one behavioral change on the eval path (v0.8.1 tool-call rescue from malformed-500 bodies) never fired -- no rescue warnings in any run log, and tool_errors/run is identical to four decimals across both eras for the models measured in each. report.py: scope dedup supersession by whether a row carries an explicit reasoning_replay policy. Legacy pre-knob rows (no field) are still superseded by any newer-gen sweep of the config, so phantom ':full' twins stay cleaned up; explicit policy rows are now superseded only by a newer gen of the SAME policy, so an arm that only ever ran in one gen carries forward with a badge instead of being destroyed by a sweep that ran different policies. eval_rigs.json: add rig-04 (RTX 5090, 32GB). batch_eval.py: correct two comments that credited the Gemma-4 large campaign to rig-02; it ran on rig-04. One data fix carried in: a Nemotron-3-Super grounded_synthesis re-run had been appended as a second run 50 while run 41 was absent, because _count_completed_runs derives run indices from row counts rather than tracking which index is missing. Renumbered to 41; the arm is contiguous 1..50 again. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BmJdi2BHK1LZRLen6HF4 * release: prepare v0.8.2 * docs(eval): update dashboard regeneration examples --------- Co-authored-by: Claude Opus 4.8 <[email protected]>
fix(llamafile): rescue tool calls from malformed tool-call 500s inste… …ad of leaking error JSON (#127) * fix(llamafile): retry malformed tool-call 500s instead of leaking error JSON llama.cpp returns a 500 when its tool-call parser rejects malformed or incomplete model output (e.g. a call missing a required parameter, or a duplicated/incomplete call). That is a transient sampling artifact, not a backend fault, and is recoverable by re-sampling. Detect this specific 500 (body contains "Failed to parse input" plus a tool-call marker) and surface it as a retryable text response carrying a clean nudge to re-emit a single well-formed call, so the inference retry loop recovers instead of echoing the raw 500 JSON into the conversation. Every other 500 still cascades as a BackendError. Applies to both the streaming and non-streaming send paths. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * feat(llamafile): rescue complete tool calls from malformed-500 bodies The 27B write-stutter: the model emits a skeletal path-only tool_call block, then (often) the same call complete with content. llama.cpp 500s the whole generation on the first block, discarding the valid call, and blind nudge-retries exhaust (observed 0/13 runs, sticky). Flow on a malformed-tool-call 500 (all other 500s still cascade): - parse all <tool_call> blocks out of the error body (llama.cpp embeds the raw rejected generation in the message) - rescue: return blocks that strictly parse, name a tool in the request's tools array, and carry every required param (schema-coerced); dedupe exact repeats; count on client.rescued_tool_calls; log [rescue] - else nudge: stutter-naming retry text only when blocks were actually seen; bare parse-500s keep the generic text verbatim Verified against a captured production 500 body: salvages the complete 17k-char write behind two skeleton blocks. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019ZUd3AHScRUcgCJn9owysD * refactor(llamafile): return re-parsed 500 calls to the tool channel Supersedes the salvage-vs-nudge split: on a malformed-tool-call 500, leniently re-parse ALL blocks naming a known tool and return them as real ToolCalls — skeletons included. Downstream machinery decides: complete calls execute; a skeleton is rejected by the runner's fn(**args) dispatch as [ToolError] TypeError naming the missing param, anchored to the call on the tool channel (the corrective signal the model was trained on), under the tool-error budget instead of the 5-strike retry wall. The stutter retry text now fires only for blocks that are visible but unparseable/unknown; bare parse-500s keep the generic text. Verified on both captured production bodies: mixed stutter -> skeleton + complete 17k-char write returned; skeleton-only -> one path-only call for dispatch to reject. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019ZUd3AHScRUcgCJn9owysD * chore(release): bump to 0.8.1 with changelog Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
v0.8.0: first-class authentication (#120) * feat(clients): per-client auth credential mechanism (v0.8.0 Phase A) Add the one-credential-per-request mechanism at the client layer: forge carries exactly one credential to the backend in its native auth header, and two credentials present anywhere is a hard error (Design Principle #1, fail loud, no silent merge or precedence). - base.py: AUTH_HEADER_NAMES, has_auth_header, static_auth_present (construction-time two-source guard), resolve_request_headers (per-call one-credential rule), redact_auth_headers; extra_headers added to the send/send_stream protocol. New MultipleCredentialsError. - openai_compat/ollama/llamafile/vllm: construction api_key+extra_headers (new for the latter three) and per-call extra_headers on every outbound path (incl. ollama think-retry re-issues and llamafile's three bodies), via a _request_headers helper. Never mutates shared construction headers, so a per-request credential can't leak across the proxy's reused client. - anthropic: construction default_headers + per-call extra_headers via the SDK's per-call extra_headers=; re-cases auth headers to the SDK's case-sensitive X-Api-Key/Authorization slots; drops SDK-pinned anthropic-version/anthropic-beta; strips SDK control kwargs (extra_headers/extra_body/extra_query/timeout) a verbatim/passthrough body could smuggle past the credential gate. - Removes the old silent "extra_headers overrides api_key Authorization" behavior (ambiguous two-source config) in favor of a fail-loud guard. Tests: tests/unit/test_client_auth.py asserts real wire headers via httpx.MockTransport (and the Anthropic SDK pipeline), covering construction and per-call credentials, the two-source raises, the no-leak property across serialized requests, cross-protocol re-casing, and the smuggle guards. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * feat(inference): thread per-call credential through run_inference (v0.8.0 Phase B) run_inference and _send_streaming gain an extra_headers param, forwarded to client.send / send_stream using the same splat-only-when-set idiom as raw_openai_tools — the kwarg is passed only when set, so clients and test doubles that don't declare it keep their original signature. This is the seam the proxy uses to forward a relocated inbound credential to the backend. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * feat(proxy): inbound credential relocation + --backend-api-key (v0.8.0 Phase C) Wire the proxy to forward exactly one credential to the backend in its native auth header. An inbound auth header is relocated to the target protocol's canonical slot; a static --backend-api-key is the alternative; two credentials anywhere is a hard error (Design Principle #1). - proxy/auth.py (new): extract_inbound_credential (refuses two distinct auth headers, and — via a marker the header reader sets — the same auth header name repeated; treats a blank value as absent), relocate_credential (same-protocol verbatim; cross-protocol normalizes the token and writes the target's canonical slot: anthropic x-api-key, openai Authorization: Bearer), resolve_inbound_credential (extract → refuse inbound+static → relocate). forge forwards ONLY the one relocated credential; no other inbound header is forwarded, so httpx recomputes transport headers and no hop-by-hop stripping is needed. - server.py: thread inbound headers (per-request _QueueItem.headers) through both the direct and serialized-queue dispatch paths; HTTPServer learns backend_protocol + backend_api_key_present (never the raw secret). A credential conflict surfaces as 400 (client error), not 502. - handler.py: resolve the one credential once; thread into run_inference and the no-tools direct send. - proxy.py / __main__.py: --backend-api-key flag (FORGE_BACKEND_API_KEY env), baked into all backend clients at construction. - anthropic.py: when forge owns the credential (api_key is not None, incl. ""), suppress ambient ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN during construction and map ""→None, so no env credential and no spurious empty X-Api-Key can become a hidden second credential. api_key=None still defers to env (WR). Tests: proxy/auth full relocation matrix + hard errors; server-level threading across both serialize paths; duplicate-header and inbound+static both refused 400 with no secret in the body; Anthropic env-suppression regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * docs(auth): redacted credential debug log + BACKEND_SETUP auth section (v0.8.0 Phase D) - handler.py: at the proxy's credential-resolve point, emit a DEBUG log of the forwarded auth header NAME with the value redacted wholesale (x-api-key: ***) via redact_auth_headers. Never logs a raw secret. No-ops without handlers (library use); the proxy CLI enables it under -v. - docs/BACKEND_SETUP.md: new Authentication section — the one-credential principle, WorkflowRunner (construction vs per-call) and proxy (inbound passthrough vs --backend-api-key) usage, the cross-protocol relocation table, the documented OAuth-via-OpenAI-endpoint limitation, and the ambient-env / keyless-passthrough / redacted-logging notes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * fix(proxy): zero-credential request to Anthropic backend fails loud as 401 Live testing surfaced that a request reaching an Anthropic backend with NO credential at all (proxy pure-passthrough: api_key="" -> None, no ambient env, no inbound auth header) was refused by the Anthropic SDK with an opaque client-side "Could not resolve authentication method" error, surfaced to the caller as HTTP 502 + raw SDK text. The refusal is correct (an Anthropic backend always needs a credential) but the status/message were poor. Detect the zero-credential precondition before dispatch and fail loud with a clear forge error mapped to HTTP 401: - errors.py: new MissingCredentialError (counterpart to MultipleCredentialsError; carries no secret). - clients/anthropic.py: _ensure_credential() guards send()/send_stream() — a credential is present iff the SDK resolved a construction key (api_key/ auth_token, incl. ambient env at build time) or this call carries a per-call auth header. WR direct use (api_key=None reading ANTHROPIC_API_KEY) and static --backend-api-key are unaffected; only true zero-credential dispatch raises. - proxy/server.py: map MissingCredentialError -> 401 (Unauthorized). Verified live: pure-passthrough Anthropic proxy with no inbound auth now returns a clean 401 with the forge message, no SDK gibberish. +3 unit tests; full suite 1270 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * feat(proxy): defer external-mode backend discovery to first request (v0.8.0 finding #2) External passthrough mode (no --backend-api-key) against a gated OpenAI-shape backend crashed at startup: the context-length probe (llama.cpp /props, vLLM /v1/models) and vLLM's served-name probe ran unauthenticated at boot and got a 401. Defer all external-mode startup backend probes to the first request, where they authenticate with that request's inbound credential — preserving zero-config auto-discovery for the gated-passthrough cohort instead of forcing --budget-tokens. - New LLMClient.discover_backend_metadata(extra_headers): one credentialed probe that returns the context budget and adopts any backend-owned wire identity (vLLM served-model-name) into the client. vLLM collapses its two /v1/models round-trips into one; anthropic/openai_compat/ollama are no-network stubs. - proxy._setup_external defers when passthrough (no static key) and there is a probe to run; a static --backend-api-key keeps eager startup discovery and boot fail-fast. Managed mode and the Anthropic external path are untouched. - A LazyDiscovery latch carries the deferral state proxy -> HTTPServer -> handler; the handler runs the probe once, before BOTH dispatch paths (vLLM needs its identity on every request), fails loud on a missing budget (no silent default), and latches on success only (a failed probe retries on the next request). - New BackendDiscoveryError -> 401 on a backend auth rejection (401/403), else 502. Validation: - 1302 unit tests (32 new: deferral wiring, lazy probe run/latch/failure, client discover_backend_metadata, concurrent first-requests, error->status mapping). - New self-contained gated-backend smoke test (scripts/smoke_test_proxy.py) proving deferred startup, first-request credentialed discovery, clean 401 on a missing credential, and no-latch-on-failure (retry succeeds). Also unstales harness scripts surfaced while live-validating (pre-existing drift): integration_test_proxy.py used a removed `mode=` kwarg (now backend_capability); smoke_test_proxy.py's path-1 test asserted pre-finding-#1 behaviour (now sends an inbound credential, as the one-credential rule requires) and its mocks send Connection: close for the two-hit deferred-probe flow. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * fix(proxy): address Codex review — secret hygiene, auth-error status, empty-bearer, CORS Independent review (Codex) of the v0.8.0 auth branch. This commit addresses the findings that don't change request behaviour; streaming-error status (#2) is held for discussion and the discovery race (#3) is documented, not locked. - #1 (secret hygiene): a backend error body or traceback can echo an inbound auth header. Add redact_secrets() and apply it at the proxy boundary — error responses, the error log, and the handler-error traceback — scrubbing Bearer tokens, x-api-key values, and sk- key prefixes. Best-effort net; forge still never authors a secret into a message. - #4 (empty credential): "Authorization: Bearer " (scheme, no token) was treated as present and relocated to an empty x-api-key / "Bearer ". Presence now checks the token after the scheme, matching relocate_credential — token-less auth headers are absent and fail loud. - #5 (status): a backend 401/403 during normal dispatch was mapped to 502. Map BackendError(401/403) -> 401 (caller's auth problem), keeping other faults 502. - #6 (CORS): allow X-Api-Key (+ anthropic-version/anthropic-beta) in preflight so browser clients can send the Anthropic credential slot. - #3 (documented): comment the deferred-discovery concurrency model — no lock by design (idempotent probe + await-free commit, no torn state) and the single-backend/credential-independent-metadata assumption it relies on. +12 tests (empty/scheme-only bearer, redact_secrets, backend 401/403->401 vs 500->502, no-secret-leak through the proxy response, CORS x-api-key). 1314 unit pass; gated-passthrough smoke suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * fix(proxy): resolve credential + run discovery before flushing streaming headers (Codex #2) A streaming request (stream=true) flushed the 200 OK / SSE headers before the handler ran, so a bad/duplicate credential or a failed first-request discovery came back as 200 + an SSE error event instead of a real 400/401. Run those checks in a pre-dispatch pass before the header flush: on failure return the real HTTP status; on success flush as before (success path unchanged, including the managed-mode "alive while queued" early header). - Extract run_lazy_discovery() from the handler so the pre-check and the handler share one copy (the handler call is a no-op once latched). - server._predispatch resolves the credential + runs discovery; failures map via a shared _send_exception (also dedups the existing error-status mapping). - Only the streaming-error path changes; non-streaming already carried a real status, and successful streaming is unchanged. - Update the BACKEND_SETUP SSE caveat accordingly. Scope note: this catches the proxy-level streaming errors — MultipleCredentials (two creds) and deferred-discovery 401/502. MissingCredentialError raised by the Anthropic client at send time (external passthrough, zero credentials) is a client-send decision the proxy can't pre-detect without a per-backend opinion, so that narrow streaming case still surfaces as an SSE event. +3 tests (streaming dup-auth -> 400, streaming discovery 401, streaming success unchanged). 1317 unit pass; smoke suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * fix(auth): cut backend bodies from error messages; enforce one-credential + reject blank creds at the client layer (Codex pass 2) Second Codex pass on the v0.8.0 auth branch. #3 (secret hygiene, the right way): replace the best-effort redact_secrets() denylist with safe-by-construction errors. BackendError keeps a forge-authored `detail` in its message (safe — forge never writes a secret) and takes the raw backend body via `raw_body=`, which rides exc.body and never enters the message, logs, or traceback. All raw-response sites (resp.text / streaming error bodies) route through raw_body; forge's own diagnostics ("no choices", "missing max_model_len", ...) stay in the message. redact_secrets and the proxy redaction plumbing are deleted. #2 (one credential at the client layer): static_auth_present and resolve_request_headers now COUNT auth credentials and refuse more than one in any single bag — two construction auth headers, or two per-call extra_headers — not just the static-vs-per-call collision. The proxy already enforced this on inbound; this brings the direct library path in line. #4 (no blank/garbage credentials): a blank/whitespace/scheme-only credential is not counted or forwarded. Client constructors guard the Bearer header on api_key.strip(); ProxyServer normalizes a blank --backend-api-key to None so it neither rides the wire as "Bearer " nor wrongly disables lazy discovery. #1 (streaming): kept the pre-dispatch checks before the SSE header. The residual — a backend rejecting the credential mid-dispatch on a streaming request arrives as an SSE error event because the proxy buffers — is documented in BACKEND_SETUP as a known limitation (the real fix is incremental streaming; WorkflowRunner already streams). #5 (multi-tenant race) is out of scope: it requires multiple tenants sharing one forge proxy in front of a per-key model router; the one-backend-one- model assumption is already noted in code. +10 auth tests (two-header refusal at construction & per-call, blank-credential absence, blank static key still defers); secret-not-leaked updated to the cut. 1323 unit pass; smoke suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu * chore(release): bump to v0.8.0 + CHANGELOG entry Version 0.7.6 -> 0.8.0 and the v0.8.0 changelog section (first-class auth; one BREAKING change scoped to auth-required backends, ungated local backends unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01NTCB4kdRSVnaw7JrGzJ6Xu --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
PreviousNext