Skip to content

fix(discovery): set is_free from Bytez's real meterPrice signal - #948

Merged
seonghobae merged 4 commits into
mainfrom
fix/bytez-discovery-is-free
Aug 31, 2026
Merged

fix(discovery): set is_free from Bytez's real meterPrice signal#948
seonghobae merged 4 commits into
mainfrom
fix/bytez-discovery-is-free

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • _parse_bytez built every DiscoveredModel without ever passing is_free, so it silently
    took the dataclass default of False for every Bytez model regardless of real price. Bytez's
    own list-models response (https://docs.bytez.com/http-reference/list/models.md) does carry a
    real zero-cost signal — meterPrice, e.g. "0.0006478333 / sec" — that the parser read out of
    row for nothing (an existing comment explained why per-1k pricing is intentionally left unset,
    but that reasoning has no bearing on is_free). tests/test_model_discovery.py's existing
    Bytez fixture already proved this field is present and parseable
    ({"modelId": "0-hero/Matter-0.1-Slim-7B-C", "task": "chat", "meterPrice": "0.0006 / sec"}); no
    Bytez model could ever be classified free before this change, no matter its real price.
  • Adds _bytez_meter_price_is_free, which classifies only an exact-zero meterPrice rate as
    free (parsed via Decimal, matching _price_per_1k's underflow-safe precision handling), and
    treats a missing/malformed/non-numeric value as unknown, not free — the same fail-closed default
    every other pricing path in this module already uses. It deliberately does not use the
    sibling meter tier-name field (e.g. "sm-free"): live docs show a tier can be named "free"
    while its own meterPrice is nonzero, so tier naming is not a trustworthy zero-cost signal.
    prompt_price_per_1k/completion_price_per_1k are still left unset for GPU-second pricing —
    that existing, intentional design is untouched.
  • Adds a small non-fatal diagnostic (_log_zero_free_serving_contribution, called from
    general_free_serving_candidates) that logs one line per credential that discovered rows but
    seeded zero orchestrator/free candidates, naming the coarse reason
    (evidence_only / no_free_pricing_reported / free_rows_excluded_from_general_pool). Today a
    hard provider failure (e.g. Bytez's HTTP 500) already logs an explicit
    model discovery failed account=bytez ... line, but a provider that discovers fine and still
    contributes nothing — OpenRouter's deliberate evidence_only exclusion, OpenAI simply having no
    free-tier model today — left no comparable trace, making a single-family pool look identical
    whether every other provider was failing or just had nothing free to offer.

Context: broader orchestrator/free single-family investigation

This was found during a 5-agent investigation (today) into why orchestrator/free is served
100% by nvidia_nim (free_family_diversity: 1) with all 5 provider credentials
(BYTEZ_API_KEY, NVIDIA_NIM_API_KEY, NVIDIA_NIM_API_KEY_SUB, OPENROUTER_API_KEY,
OPENAI_API_KEY) present and registered ("5 of 5" in live logs). Two other, independent causes
(a stale vendored commit pin and a duplicate hardcoded provider-family mapping) are being fixed
separately in ContextualWisdomLab/.github — this PR is scoped to contextual-orchestrator only
and does not touch that repo.

This fix alone will not restore Bytez diversity today. Bytez's backend has been returning
HTTP 500 for this org's account/key for several days straight — an already-tracked, operator-side
issue documented in docs/product-technical-gap-baseline.md ("2026-08-30 provider-catalog-sync")
and confirmed independently via a live unauthenticated probe (fast, well-formed 401, not 500
the endpoint itself is reachable and enforcing auth normally). That outage is not resolvable from
this repo alone. This fix is still correct and necessary so the code behaves properly the moment
that external outage clears — and so a future non-Bytez cause of "provider discovers fine but
contributes nothing" is now visible instead of silent, via the new diagnostic line.

OpenRouter (evidence_only=True, deliberate ZDR-privacy hardening) and OpenAI (genuinely no
free-tier models today) both correctly contribute zero free models; neither was a bug.

What changed

  • contextual_orchestrator/model_discovery.py
    • _bytez_meter_price_is_free(meter_price) — new helper classifying an exact-zero Bytez
      meterPrice rate.
    • _parse_bytez — now passes is_free=_bytez_meter_price_is_free(row.get("meterPrice")) when
      constructing each DiscoveredModel.
    • _log_zero_free_serving_contribution(discovered, candidates) — new helper, called from
      general_free_serving_candidates, logging a per-credential zero-contribution diagnostic.
      Purely additive: no change to general_free_serving_candidates's return value.
  • tests/test_model_discovery.py
    • test_discover_bytez_parses_models_with_key_auth_scheme — now also asserts real nonzero
      GPU-second pricing stays is_free=False with both per-1k fields still None.
    • test_discover_bytez_marks_zero_meter_price_as_free — new regression test: a zero-rate
      meterPrice produces is_free=True with per-1k pricing still unset.
    • test_discover_bytez_missing_meter_price_stays_unknown_not_free — new: no meterPrice field
      at all stays is_free=False (unknown, not free).
    • test_bytez_meter_price_is_free_classifies_exact_zero_rates — new parametrized unit test over
      _bytez_meter_price_is_free (numeric/string zero and nonzero rates, missing/malformed/None
      values, bool, negative-zero, whitespace).
    • test_general_free_serving_candidates_logs_zero_contribution_reasons — new: asserts the
      diagnostic line for an evidence_only OpenRouter row and a real-but-nonzero-priced OpenAI row,
      and that a provider which did seed a candidate gets no such line.

Test plan

  • python -m pytest tests/test_model_discovery.py -q — 93 passed
  • python -m pytest tests -q (full suite) — 2834 passed, 1 skipped, 1 failed in 778.70s.
    The one failure (test_psychometric_routing.py::test_fast_mlsirm_fit_uses_judge_acceptance_item_for_context_score,
    ModuleNotFoundError: No module named 'fast_mlsirm') is a pre-existing, environment-only
    gap unrelated to this change: fast-mlsirm is only installed for
    python_full_version >= '3.12' per pyproject.toml, and this sandbox runs Python 3.11.15.
    Confirmed unrelated to model_discovery.py/test_model_discovery.py.
  • interrogate contextual_orchestrator/model_discovery.py — 100% docstring coverage on the
    changed file (module-level fail-under = 100; the two new functions are private and both
    carry docstrings anyway)
  • Manual robustness check: _bytez_meter_price_is_free never raises across ~20 adversarial
    inputs (None, NaN/inf floats, huge ints, empty/whitespace/garbage strings, dicts,
    lists, complex, negative zero) — matters because fuzz/targets.py
    (exercise_provider_model_payload) drives _parse_bytez over arbitrary decoded JSON.
  • git diff --stat scoped to exactly contextual_orchestrator/model_discovery.py and
    tests/test_model_discovery.py — no other files touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw


Generated by Claude Code


Devin Review

_parse_bytez built every DiscoveredModel without ever passing is_free, so
it silently took the dataclass default of False for every Bytez model
regardless of real price. Bytez's own list-models response does carry a
real zero-cost signal -- meterPrice, e.g. "0.0006478333 / sec" -- that the
parser read off `row` for nothing; the existing comment there explains why
per-1k pricing stays unset for GPU-second billing, but that reasoning has
no bearing on is_free, which the parser never set from any signal.

Adds _bytez_meter_price_is_free, which classifies only an exact-zero
meterPrice rate as free (via Decimal, matching _price_per_1k's
underflow-safe precision handling) and treats a missing/malformed value as
unknown, not free -- this module's existing fail-closed default. It
deliberately ignores the sibling `meter` tier-name field (e.g. "sm-free"):
live docs show a tier can be named "free" while its own meterPrice is
nonzero, so tier naming is not a trustworthy zero-cost signal.
prompt_price_per_1k/completion_price_per_1k stay unset for GPU-second
pricing, unchanged.

Also adds a small non-fatal diagnostic, logged from
general_free_serving_candidates, naming why a credential that discovered
rows still seeded zero orchestrator/free candidates (evidence_only /
no_free_pricing_reported / free_rows_excluded_from_general_pool) -- a hard
provider failure already logs an explicit reason, but a provider that
discovers fine and simply has nothing free to offer previously left no
comparable trace.

Found during an investigation into orchestrator/free being served 100% by
nvidia_nim with all 5 provider credentials registered. This fix alone will
not restore Bytez diversity today: Bytez's backend has been returning
HTTP 500 for this org's account for several days (docs/product-technical-gap-baseline.md,
"2026-08-30 provider-catalog-sync", an already-tracked operator-side
issue). The fix is still correct and necessary regardless.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 49 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c4c7404a-6e61-4632-b1ff-6d2b78821fff

📥 Commits

Reviewing files that changed from the base of the PR and between c107e3e and d1fe1c1.

📒 Files selected for processing (2)
  • contextual_orchestrator/model_discovery.py
  • tests/test_model_discovery.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 3 potential issues.

Devin Review

Comment thread contextual_orchestrator/model_discovery.py Outdated
Comment thread contextual_orchestrator/model_discovery.py
Comment thread contextual_orchestrator/model_discovery.py
Comment thread contextual_orchestrator/model_discovery.py Fixed
claude added 2 commits August 31, 2026 04:08
…_name

Devin's review on PR #948 found _bytez_meter_price_is_free trusted only the
text before the first "/", so a shape that does not match Bytez's
documented "<rate> / <unit>" grammar at all -- a missing unit ("0 /"), an
extra separator ("0 / sec / token"), or no separator whatsoever ("0") --
still read as "0" and got confidently classified free. An unexpected shape
is itself a signal something about the row is wrong, so this now requires
exactly one "/" with a non-empty rate and non-empty unit on both sides
before trusting the rate at all. The unit itself is deliberately not
required to be "sec": a zero rate is exactly as free regardless of its
time unit ("0 / hour" stays free), so this validates shape, not the unit
name. Adds the requested regression cases plus a few more malformed shapes
(missing rate, doubled separator, space-joined, both sides empty).

Also verified and documented, in response to a Semgrep/GHAS finding on the
same PR, that `credential_name` at _log_zero_free_serving_contribution's
log call is always the KV credential *name* (a literal string declared on
each ProviderModelSource, e.g. "BYTEZ_API_KEY"), never the secret value
resolved by get_credential() -- traced every DiscoveredModel construction
site and confirmed the actual secret stays in a distinct local (`api_key`)
that never reaches a DiscoveredModel or this log line. Verified empirically
with a local semgrep run: the exact rule fires on an unsuppressed copy of
this file and is silenced by the added nosemgrep annotation, matching this
repo's established suppression convention (docs/planning/adrs/0007) of a
one-line rule-specific nosemgrep plus justification at the exact call site,
never a blanket bypass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw
@seonghobae
seonghobae merged commit 045d17d into main Aug 31, 2026
30 of 31 checks passed
@seonghobae
seonghobae deleted the fix/bytez-discovery-is-free branch August 31, 2026 06:20
seonghobae pushed a commit that referenced this pull request Aug 31, 2026
…Router tests

PR #948/#949 (main) added three tests that mocked urllib.request.urlopen
directly. This branch's round-5 credential-leak fix already moved
_fetch_json's transport call behind _open_trusted_discovery_request (a
custom redirect-protected opener), so after merging origin/main the old
urlopen patch no longer intercepted the call and these tests hit the real
network (observed: a live 401 from api.bytez.com). Repoint the three tests
at _open_trusted_discovery_request, matching every other test in this file
post-refactor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX
seonghobae added a commit that referenced this pull request Sep 1, 2026
* feat(logging): add --log-level/--verbose CLI wiring and debug_logging module

New stdlib-only contextual_orchestrator/debug_logging.py owns level parsing,
configure_logging() (basicConfig with force=True so repeated in-process CLI
invocations actually re-apply), a handler-level redaction safety net, and
small pure log-formatting helpers. __main__.py wires --log-level/--verbose/
--debug and CONTEXTUAL_ORCHESTRATOR_LOG_LEVEL through a parse_known_args
pre-scan that runs before subcommand dispatch, covering register-credential,
discover-models, check-fast-mlsirm, one-shot completion, and --serve
uniformly. orchestrator.py gains a module logger for the next commit's
retry/circuit-breaker/ranking instrumentation.

Part of the verbose/debug logging feature (see forthcoming ADR).

* feat(logging): instrument retry/circuit-breaker/ranking/discovery/server

Adds DEBUG/WARNING/INFO instrumentation at the previously silent decision
points named in the task: ModelClient._send_with_retry /
_send_raw_with_retry (per-attempt, backoff, a WARNING provider_exhausted
line that fires without --verbose), TaskOrchestrator's circuit breaker
(_record_failure/_record_success/_circuit_open, with a WARNING
circuit_opened only on the edge transition), and evidence-based ranking
(_static_rank_key/_measured_member_order/_select_agent). Every call site
that logs caller- or provider-derived content wraps it in the existing
orchestrator.redact_text/redact_value first.

model_discovery.py gains per-provider discovery_attempt/discovery_result/
discovery_provider_failed DEBUG lines (credential *names*, never values)
and one discovery_complete INFO summary in discover_all_models.

server.py gains one body-free per-request INFO summary (method, path,
status, latency, ADR 0122 session correlation hash) via handle_one_request
and a DEBUG response-body summary that reuses the exact already-redacted
safe_payload object _response_payload computes, never a second copy.
telemetry.py exposes the shared session_id_hash() helper so both
_safe_attributes (OTLP) and server.py's summary use the identical hash.

Adds ADR 0007 (docs/adr/ series, matching the runtime-observability
precedent set by ADR 0122), the missing logging + OpenTelemetry rows in
docs/library_research.md, a CHANGELOG entry, and a light tech-stack.md
touch-up.

* fix(logging): close adversarial secret-leakage findings from PR #946 review

Fixes the three confirmed findings from the adversarial secret-leakage
review (github.com//pull/946#issuecomment-5473342909),
independently corroborated by Devin's automated review comments on the PR:

1. Critical: server.py's DEBUG response-summary log reused redact_value(),
   which only pattern-matches an in-string secret *value* shape and never
   inspects the JSON *key* a string is nested under, so a response field
   like {"private_key": "..."}, {"key": "..."}, {"auth": "..."}, or
   {"credential": "..."} leaked verbatim. Adds a new, additional,
   key-name-aware redaction pass, debug_logging.redact_credential_shaped_keys,
   applied on top of the existing redact_value output at that one call
   site. orchestrator.redact_text/redact_value are unmodified, per the
   review's explicit scope.

2. The INFO per-request summary (summarize_request_for_log) logged
   self.path verbatim, including any query string, contradicting its own
   body-free docstring. It now strips the query string before formatting.

3. A --log-level/--verbose/--debug flag placed before a subcommand (e.g.
   `--verbose discover-models`) bypassed subcommand dispatch, which only
   checked arguments[0]. Adds _subcommand_token_index to skip past
   recognized leading logging flags when locating the subcommand token,
   without removing them from the argument list each subcommand's own
   parser sees.

Each fix has a red/green regression test: the new tests reproduce the
leak/bug against the pre-fix code (verified failing) before the fix makes
them pass.

Also merges main into this branch (was reported dirty/mergeable_state) and
resolves a real conflict in model_discovery.py where this branch's new
discovery instrumentation collided with main's independently-landed,
privacy-hardened logging on the same call sites; reconciled by keeping the
richer instrumentation but adopting main's stricter contract (never log
source.credential_name, use account= naming), matching main's own
test_discovery_debug_log_identifies_account_without_secret. Separately, the
merge surfaced a non-conflicting but real regression: main independently
added a second, plain --verbose flag to the discover-models and one-shot/
serve parsers that collided with this branch's --add_log_level_arguments,
raising argparse.ArgumentError on every invocation of either path; removed
the redundant declarations (and their dead, non-redacted
logging.basicConfig follow-ups) now that _configure_logging_from_cli
already covers this centrally.

PR #942 (a separate, more conservative implementation of the same feature
on a different branch) still needs reconciliation by a human/reviewing
session -- out of scope here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(logging): address second-round Devin/CodeRabbit findings on PR #946

Fixes three further confirmed real findings from the re-review after the
first push, plus verifies (and closes with a regression test rather than a
code change) a fourth finding that turned out to be a false positive:

1. BUG (Devin, server.py): keep-alive phantom requests. On a persistent
   HTTP connection, handle_one_request never reset self.command/self.path
   before each call; stdlib's own handle_one_request only assigns them when
   it actually parses a request line, so when a keep-alive client closed
   the connection (nothing read), the *previous* request's command/path
   were still in place. _log_request_summary's existing "nothing to
   report" guard therefore never fired, logging the prior request a second
   time with a statusless "phantom" entry. Fixed by resetting
   self.command/self.path to None at the top of handle_one_request,
   alongside the existing per-request state resets, so the guard correctly
   recognizes when no new request was parsed.

2. BUG (Devin, orchestrator.py): permanent failures misreported as
   exhausted retries. _send_with_retry/_send_raw_with_retry unconditionally
   logged provider_exhausted whenever any failure occurred, even when a
   non-transient error on the very first attempt broke the loop immediately
   without spending any retry budget. Added a distinctly named
   provider_rejected_permanent WARNING event, chosen by whether the loop
   broke from reaching retry_limit (provider_exhausted) or from an early
   non-transient break (provider_rejected_permanent), applied identically
   in both duplicated retry loops.

3. Security/Privacy (CodeRabbit, CWE-532, server.py): the DEBUG
   response-body summary serialized the entire redacted payload, including
   ordinary response text (choices[].message.content, tool-call arguments,
   an error.message that can reflect caller-supplied input) -- none of
   which is credential-shaped, so neither redact_value nor
   redact_credential_shaped_keys ever masked it, and it could carry PII or
   business-sensitive content straight into DEBUG output. Added
   debug_logging.response_metadata_for_log, which extracts only an
   allowlisted metadata shape (has_error, model, choice_count, and
   numeric-only usage counts) for this log line to serialize instead of the
   payload; redact_credential_shaped_keys is still applied on top,
   defense-in-depth, in case a future allowlist field ever collides with a
   credential-shaped key name. Updated the two pre-existing tests whose
   assertions depended on the old "redact-then-log-the-payload" mechanism to
   match the new (strictly stronger) "never log the payload" contract.

4. Verified, NOT fixed (Devin, __main__.py, false positive): "the
   parse_known_args logging pre-scan doesn't respect -- as an option
   terminator." Directly tested against the real
   _configure_logging_from_cli pre-scan: a literal -- already correctly
   stops it from treating what follows as --log-level/--verbose/--debug,
   since parse_known_args honors stdlib argparse's -- semantics with no
   special-casing needed here. Documented this in the function's docstring
   and added a regression test locking in the already-correct behavior,
   rather than changing working code.

Each of the three real fixes has a red/green regression test (verified
failing against the pre-fix code, per this repo's TDD convention). Also
added deterministic, non-threaded unit tests for the query-string-stripping
and keep-alive fixes as reliable counterparts to their real-server
integration tests, which can occasionally flake on unrelated
ThreadingHTTPServer teardown timing (pre-existing in this test file, not
introduced by this change -- reproduces identically on an untouched,
pre-existing test in the same file).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* docs+test: update ADR/CHANGELOG for the second-round fixes; de-flake real-server tests

Documents the response-body allowlist redesign, keep-alive phantom-request
fix, provider_rejected_permanent split, and the confirmed-false-positive
-- option-terminator check in docs/adr/0007-verbose-debug-logging.md and
CHANGELOG.md's Unreleased/Added entry.

Also de-flakes the four real-ThreadingHTTPServer tests in
tests/test_telemetry.py that assert on the per-request INFO summary
(present pre-existing in this file, not introduced by this PR -- it
reproduced identically on an untouched test during verification). Root
cause: the per-request summary logs in handle_one_request's `finally`
block, which runs strictly after the response has already been flushed to
the client, so a test asserting immediately after its client call returns
has no guarantee the server thread reached that `finally` block yet.
Fixed with a small bounded caplog-polling helper (_wait_for_caplog) used
while the relevant caplog.at_level scope is still open, rather than a
fixed sleep that would be either too short (still flaky) or wastefully
long; kept a brief post-teardown settle too, since ThreadingHTTPServer's
per-connection handler threads are daemons server_close() does not wait
for, and a lingering straggler could otherwise log into a later test's
capture window. Verified stable across 15 repeated runs (10 solo, 5 with
the rest of this PR's touched test files) with zero failures, versus
roughly 1-in-3 to 1-in-5 before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(logging): address third-round Devin findings on PR #946

Fixes four further confirmed real findings from the second re-review
(after push cd57aa1), which also independently confirmed the keep-alive
fix and the -- option-terminator false-positive verdict from round 2:

1. BUG, orchestrator.py: zero-retry failures mislabeled as exhausted.
   With max_retries=0, attempt >= retry_limit is trivially true after the
   single allowed attempt regardless of whether it was transient, so
   provider_exhausted fired even though no retry budget ever existed to
   exhaust. Both _send_with_retry and _send_raw_with_retry now also
   require retry_limit > 0 before calling provider_exhausted;
   provider_rejected_permanent covers both the pre-existing "non-transient
   break" case and this "no retry budget configured at all" case.

2. BUG, server.py: framework-generated responses lost their status in the
   per-request log. _last_status was only set by this module's own
   _send/_send_text/_send_bytes/_send_sse writers; a response
   BaseHTTPRequestHandler generates itself (e.g. its built-in 501 for an
   HTTP method with no matching do_* handler) bypasses all of those,
   logging status=- for a response the client actually received. Fixed by
   overriding send_response -- stdlib's own single choke point every
   response path (including its own send_error) already goes through --
   so every current and future response path is captured uniformly.

3. BUG, __main__.py: abbreviated logging flags (--log-l, --ver) bypassed
   subcommand dispatch the same way full un-recognized flags used to,
   since argparse's own prefix-abbreviation matching and
   _subcommand_token_index's plain string comparison disagreed about
   what counts as a recognized flag. Set allow_abbrev=False on every CLI
   parser (the pre-scan and each subcommand's own parser), so an
   abbreviated flag is now rejected everywhere with a clear argparse
   error instead of silently accepted by one parser and not the other.

4. SEC, debug_logging.py: exception tracebacks bypassed log redaction.
   _RedactingLogFilter only rewrote record.msg; logging.Formatter.format()
   renders record.exc_info into the traceback text strictly after every
   filter has already run, so a call site using exc_info=True or
   logger.exception(...) could leak a secret embedded in the exception's
   own str() (e.g. an upstream error reflecting api_key=sk-...) straight
   into the unredacted traceback -- a real hole in the core safety-net
   mechanism itself, not just a call site. The filter now renders
   exc_info into text, redacts it, caches it as record.exc_text, and
   clears exc_info so the handler's formatter uses the already-redacted
   text instead of re-deriving an unredacted one.

Each fix has a red/green regression test (verified failing against the
pre-fix code): tests/test_orchestrator_debug_logging.py (two new zero-retry
tests, one per retry loop), tests/test_telemetry.py
(test_framework_generated_error_status_is_captured_in_log),
tests/test_cli_logging.py (abbreviated value-flag and boolean-flag cases),
tests/test_debug_logging.py
(test_configure_logging_redactor_masks_exception_traceback_in_captured_output).

Full suite (python -m pytest tests -q --ignore=tests/test_psychometric_routing.py)
-> 2886 passed, 1 skipped, zero regressions. interrogate -> 100%.
test_conventions.py -> all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(logging): distinguish no-retry-budget from permanent rejection (round 4)

Fixes a real regression from the round-3 provider_exhausted/
provider_rejected_permanent split, found by Devin's re-review of 68291d4:
with a configured retry limit of 0, EVERY failure -- including a genuinely
transient one (HTTP 503, timeout) that simply never got a chance to retry
-- was collapsed into provider_rejected_permanent. "No retry budget was
configured" and "this error is non-retryable by nature" are two
independent facts; conflating them mislabels a transient failure as
permanent.

Adds a third, distinctly named WARNING event, provider_no_retry_budget,
for the retry_limit == 0 case specifically, carrying the error's own
transient/non-transient classification explicitly via a `transient=%s`
field rather than discarding it. provider_rejected_permanent now covers
only its original, narrower case: a real non-zero budget existed, but the
loop broke early because the failure was classified non-transient.
provider_exhausted is unchanged (a real, non-zero budget was actually used
up). Fixed identically in both duplicated retry loops
(_send_with_retry/_send_raw_with_retry).

Regression tests now cover all 4 (retries>0 vs =0) x (transient vs
non-transient) combinations per retry loop, as requested: the two existing
tests already covered retries>0 x transient (provider_exhausted) and
retries>0 x non-transient (provider_rejected_permanent); this adds
retries=0 x transient and retries=0 x non-transient (both now
provider_no_retry_budget, differing only in transient=True/False) for both
_send_with_retry and _send_raw_with_retry. Verified each new/changed
assertion fails against the pre-fix (round-3) code before the fix, per this
repo's TDD convention.

Investigated, not fixed: Devin also flagged CONTEXTUAL_ORCHESTRATOR_LOG_LEVEL
reading directly from os.environ as bypassing this repo's KV config
registry (AGENTS.md/CLAUDE.md: "runtime config... never os.getenv"). Per
the coordinator's request, checked whether this repo's existing
CONTEXTUAL_ORCHESTRATOR_STATE_DB/_AGENTS_DB/_CLEARFOLIO_URL/
_PROVIDER_CA_BUNDLE env vars (contextual_orchestrator/__main__.py) follow
the same direct-os.environ pattern: they do, all four, predating this PR.
This is a pre-existing architectural pattern in this file, not a new or
different violation this PR introduces -- reported as such in the PR
comment rather than fixed unilaterally inside a logging-feature PR, per
the coordinator's explicit guidance for that outcome.

Full suite (python -m pytest tests -q --ignore=tests/test_psychometric_routing.py)
-> 2888 passed, 1 skipped, zero regressions. interrogate -> 100%.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(discovery): close credential leak via cross-host redirect (round 5)

Real, pre-existing security finding from CodeRabbit's automated review of
PR #946: `_fetch_json` in model_discovery.py -- the function every standard
provider's authenticated "list models" call goes through (openai, openrouter,
nvidia_nim, nvidia_nim_sub, bytez) -- called plain `urllib.request.urlopen`.
Python's default `HTTPRedirectHandler` copies the `Authorization` header onto
a redirected request even when the redirect target is a completely different
host (unlike some other HTTP clients, urllib never strips sensitive headers
on cross-origin redirects). A malicious or compromised provider endpoint
issuing a 3xx redirect could exfiltrate the credential -- and #923's retry
(already on main, inherited into this branch) means up to twice per attempt,
since it calls _fetch_json with the same api_key on both tries.

This codebase already had the correct fix for this exact risk class:
_TrustedDiscoveryRedirectHandler (raises on any redirect leaving the original
host) already protected _fetch_json_same_host_https. It just never got
applied to _fetch_json, the function actually used for the real per-provider
calls. Fixed by routing both functions through one new shared
_open_trusted_discovery_request helper, so there is a single implementation
of the redirect guard instead of two copies that could drift apart (as they
already had). _fetch_json_same_host_https's own external behavior (size cap,
TimeoutError conversion) is unchanged. A legitimate same-host redirect (e.g.
/v1/models -> /v2/models) still succeeds.

New regression tests (tests/test_model_discovery.py): a cross-host-redirect
test (mirrors the existing test_openrouter_zdr_evidence_rejects_cross_host_
redirects pattern) proving exactly one request is ever issued and the
credential never reaches the attacker host, verified failing against the
pre-fix _fetch_json before the fix; and a same-host negative control proving
the legitimate case still works. ~38 pre-existing tests across
test_model_discovery.py, test_model_discovery_boundaries.py,
test_discover_models_cli.py, and test_chat_model_capability_isolation.py
that mocked bare urllib.request.urlopen for _fetch_json's behavior now mock
the new shared seam instead (mechanical -- _fetch_json no longer calls
urlopen directly). Two local test _Response fixtures gained an optional
read(amt) argument (mirroring http.client.HTTPResponse.read(amt)) since
_fetch_json_same_host_https's always-invoked OpenRouter ZDR fetch inside
discover_all_models now shares the same seam and caps its read.

Also folded into this round, four further confirmed findings relayed by the
coordinator from the same review pass:

1. model_discovery.py: the configured_gateway provider's /model/info
   metadata fetch now also catches RuntimeError, matching the primary
   list-request retry loop's except tuple. Previously a raw RuntimeError
   from ModelClient's DNS/address-validation transport escaped
   discover_provider_models uncaught and aborted the entire discovery pass
   instead of just that one provider's metadata.

2. server.py: per-request latency_ms no longer counts a keep-alive
   connection's idle time between requests. request_started is now
   timestamped inside an overridden parse_request(), right after
   BaseHTTPRequestHandler.handle_one_request()'s blocking readline() has
   already returned real request bytes, instead of before that blocking read.

3. orchestrator.py: the retry-outcome classification (no budget at all /
   exhausted / stopped early on non-transient) duplicated verbatim between
   _send_with_retry and _send_raw_with_retry -- duplication that already
   caused a real regression once, fixed in one copy and missed in the other
   (round 4, provider_no_retry_budget) -- is now one shared
   _log_retry_outcome helper both call.

4. debug_logging.py: response_metadata_for_log's usage summary now keeps
   only a fixed allowlist of known counter names (prompt_tokens,
   completion_tokens, total_tokens, input_tokens, output_tokens), not any
   string key with a numeric value -- closes a path where a key shaped like
   "customer_note=<secret>" with a throwaway numeric value would have
   reached DEBUG output verbatim (CWE-532). New regression test with a
   customer_note-shaped key proving exclusion, plus a positive control.

Full suite (python -m pytest tests -q --ignore=tests/test_psychometric_routing.py)
-> 2892 passed, 1 skipped, zero regressions (test_psychometric_routing.py
pre-existingly fails on ModuleNotFoundError: No module named 'numpy' in this
sandbox -- environment-only, unrelated). interrogate (repo-root invocation,
pyproject.toml fail-under=100) -> 100%. ruff check on all changed files ->
clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(test): patch _open_trusted_discovery_request in merged Bytez/OpenRouter tests

PR #948/#949 (main) added three tests that mocked urllib.request.urlopen
directly. This branch's round-5 credential-leak fix already moved
_fetch_json's transport call behind _open_trusted_discovery_request (a
custom redirect-protected opener), so after merging origin/main the old
urlopen patch no longer intercepted the call and these tests hit the real
network (observed: a live 401 from api.bytez.com). Repoint the three tests
at _open_trusted_discovery_request, matching every other test in this file
post-refactor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(discovery,logging): bound _fetch_json's read; stop misclassifying one-shot calls (round 6)

Two findings deferred during round 5 to avoid colliding with the
in-flight merge-conflict fix:

- model_discovery.py: _fetch_json read a discovery response body with an
  unbounded response.read(), unlike _fetch_json_same_host_https and
  _fetch_configured_gateway_json, which already cap at
  MAX_DISCOVERY_RESPONSE_BYTES and fail closed on an overage. A large or
  malicious/misbehaving provider response could exhaust worker memory
  before JSON parsing ever ran. _fetch_json now shares the identical
  bounded-read-then-check pattern.

- orchestrator.py: _log_retry_outcome logged provider_no_retry_budget
  whenever retry_limit was 0, without distinguishing an agent with a
  genuinely zero configured retry budget from ModelClient.proxy_send_once's
  deliberate allow_transient_retries=False one-shot call (which forces
  retry_limit to 0 regardless of the agent's real budget, so an
  already-failing-over passthrough request cannot itself amplify load).
  _log_retry_outcome now takes allow_transient_retries explicitly and logs
  a distinctly named provider_one_shot_call_failed WARNING for the
  caller-forced case instead of misreporting it as no budget configured.

New regression tests cover both: an oversized fake response for the
size bound, and a mocked proxy_send_once one-shot failure (plus direct
_send_raw_with_retry coverage) for the logging fix.

Full suite: 2929 passed, 1 skipped (baseline 2925 + 4 new tests, zero
regressions). interrogate: 100%. git diff --check: clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(logging,server): suppress CodeQL FP, fix disconnect status honesty, renumber ADR (round 7)

Three findings from the latest automated review round on PR #946:

- tests/test_debug_logging.py: two CodeQL py/clear-text-logging-sensitive-data
  HIGH alerts (lines 144, 167) are a deliberate redaction positive/negative
  control pair -- a hardcoded, non-functional fake secret is logged once WITH
  a redactor (proving it's masked) and once WITHOUT one (the negative control,
  proving the first test isn't a tautology). Verified this repo's CodeQL is
  advanced setup (github/codeql-action/init+analyze, no config-file) with
  security-events: write and default upload:true, and that `# codeql[rule-id]`
  inline suppression comments on the line before a flagged call are GitHub's
  documented, currently-supported mechanism, honored regardless of setup
  style once the analysis flows through upload-sarif. Added precise, two-line
  suppressions with an explanatory comment on exactly the two flagged lines.

- contextual_orchestrator/server.py: `_send`/`_send_text`/`_send_bytes`/
  `_send_sse`/`_begin_sse` all recorded their *intended* status into
  `_last_status` before handing off to `_write_response`, then ignored its
  boolean return value. `_write_response` deliberately swallows a dead peer's
  BrokenPipeError/ConnectionError/OSError, but the pre-set `_last_status`
  survived that failure untouched, so the per-request INFO summary reported a
  false "delivered" status for a write that never completed. Fixed at the one
  shared choke point (`_write_response`'s except block) instead of patching
  each writer, so it covers every current and future writer uniformly.

  While tracing that consumer, also found `_log_request_summary`'s "nothing
  to report" guard checked only method/path, so it silently dropped a request
  that *did* deliver bytes and *did* get a real 400/414 response but whose
  malformed/oversized request line left command/path unset (stdlib's own
  parse_request resets `self.command` to None "in case of error on the first
  line"). The guard now also logs when a status was actually recorded, while
  still skipping a true byte-free keep-alive close.

  Regression tests: test_http_response_write_disconnect_safety.py (dead-peer
  write no longer reports the intended status) and test_telemetry.py (a
  malformed request line's real 400 is no longer skipped).

- docs/adr/0007-verbose-debug-logging.md renamed to 0005 -- the indexed
  series in docs/adr/README.md ends at 0004, and ADR 0122 (checked
  separately) is a deliberate cross-repo-numbered exception explicitly not
  listed in that series table, not evidence the sequential convention was
  already broken. Updated the file's own title, the README index row,
  conductor/tech-stack.md, and CHANGELOG.md's remaining "ADR 0007" mentions.

Also replied on PR #946 to two review threads that investigation showed were
false positives, with no code change: the CLI log-level env-var read (a
bootstrap-time argparse pre-scan before any KV store exists, matching five
other pre-existing bootstrap-time os.environ reads in the same file) and the
"research artifact" ADR requirement (this is a pure observability/engineering
feature with no algorithmic claim to ground, unlike ADR 0002/0003).

Full suite: `pytest tests -q --ignore=tests/test_psychometric_routing.py` ->
2931 passed, 1 skipped, 0 failed. `interrogate .` -> 100%. `git diff --check`
clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(server): preserve delivered status on a post-header disconnect (round 8)

Follow-up to round 7's _write_response fix, per this round's Devin review:

- contextual_orchestrator/server.py: clearing `_last_status` on *every*
  caught disconnect (BrokenPipeError/ConnectionError/OSError) was too broad.
  A write failure can strike in two different places: before the status
  line/headers were ever flushed (the client received nothing -- clearing
  is correct), or after `end_headers()` already completed (a later body
  write in the same call, or a later `_write_sse` frame on an SSE stream
  `_begin_sse` already opened -- the client genuinely received the real
  status, so clearing it would falsely report "no status" for a request
  that was, in fact, answered -- the exact overcorrection direction of the
  original bug).

  Every `_send`/`_send_text`/`_send_bytes`/`_send_sse`/`_begin_sse` writer
  now sets `self._response_headers_sent = True` immediately after its own
  `end_headers()` call returns without raising; `handle_one_request` resets
  that marker to `False` once per request. `_write_response`'s disconnect
  handler now clears `_last_status` only when the marker is still unset.
  `_write_sse` does not touch the marker itself -- it is only ever called
  after a prior successful `_begin_sse`, so the marker it already set
  correctly covers a later frame's failure too.

  Regression tests added to tests/test_http_response_write_disconnect_safety.py:
  - test_disconnected_write_does_not_report_intended_status_as_delivered
    (rewritten to fail inside end_headers() itself -- genuinely "before
    headers" -- so it still represents the original bug's case correctly;
    it previously failed in the body write, which is now the preserved case)
  - test_disconnected_body_write_after_headers_preserves_delivered_status
    (new: body write fails after end_headers() succeeds -> status preserved)
  - test_sse_frame_disconnect_after_headers_preserves_delivered_status
    (new: a later _write_sse frame fails after _begin_sse succeeded ->
    status preserved)

Also investigated a second finding relayed this round ("transient failures
labeled permanent" in _send_with_retry/_send_raw_with_retry) and found no
reproducible bug: GitHub's own review data for this commit's parent shows
Devin's re-review found exactly one new issue (the fix above), and an
existing, currently-passing test
(test_send_raw_with_retry_one_shot_call_does_not_log_no_retry_budget)
already proves a transient error under allow_transient_retries=False with a
real non-zero configured retry budget logs
`provider_one_shot_call_failed ... transient=True`, never
`provider_rejected_permanent` -- structurally, `_log_retry_outcome` can only
reach `_log_provider_rejected_permanent` when `retry_limit != 0`, which
`allow_transient_retries=False` always forces to 0. No code change made for
that finding; likely a stale reference to the already-resolved round-5/6
thread on the same topic.

Full suite: `pytest tests -q --ignore=tests/test_psychometric_routing.py` ->
2933 passed, 1 skipped, 0 failed. `interrogate .` -> 100%. `git diff --check`
clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(cli): give check-fast-mlsirm its own argparse parser (round 9)

Devin's review of PR #946 found: `check-fast-mlsirm --help` bypassed
argument parsing and ran the diagnostic instead of showing help. The
subcommand's dispatch function took no arguments and ignored everything
after its own name in argv, so `--help` never reached an argparse parser
that would have handled it -- it was silently swallowed and the real
diagnostic ran anyway (and its exit code, not argparse's, decided the
process exit status).

Give _check_fast_mlsirm_command its own argparse.ArgumentParser, matching
the pattern every other subcommand (register-credential, discover-models)
already uses: declare the shared --log-level/--verbose/--debug flags via
_add_log_level_arguments so --help documents them, call parse_args(argv)
so --help exits before running anything and an unrecognized trailing
option is rejected instead of silently ignored, then run the diagnostic
as before. Updated the one call site to pass arguments_after_subcommand.

Regression tests: test_check_fast_mlsirm_help_shows_help_without_running_diagnostic
and test_check_fast_mlsirm_rejects_unknown_option.

Full suite: 2935 passed, 1 skipped (baseline 2933 + 2 new tests, zero
regressions). interrogate: 100%. git diff --check: clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

* fix(logging): keep runtime config out of environment

* test(logging): avoid clear-text credential fixtures

* fix(logging): distinguish ranking evidence from throughput

---------

Co-authored-by: Claude <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.

3 participants