Skip to content

fix(discovery): exclude vision-input-only models from orchestrator/free - #933

Merged
seonghobae merged 4 commits into
mainfrom
fix/nim-vision-tool-calling-pool
Aug 31, 2026
Merged

fix(discovery): exclude vision-input-only models from orchestrator/free#933
seonghobae merged 4 commits into
mainfrom
fix/nim-vision-tool-calling-pool

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Update (commit efd44f6, second review round): Devin's first round (5648567) found three real gaps in the original fix; a second automated pass on that fix then found one more real regression it introduced plus two informational notes, now also fixed (efd44f6). See the review comments below for the full writeup on both rounds.

Round 1 (5648567): (1) _auto_discover_runtime_agents and provider_bootstrap/provider_catalog_bootstrap could still activate the vision model as cost:free through a second and third path -- closed at a single choke point, TaskOrchestrator._is_free_agent; (2) narrowing the modality exclusion to spare "also declares text" models was evaluated and not adopted, because the incident model itself declares text and image, so that narrowing would silently reopen this exact incident; (3) free_discovered_models() is pure price-based inventory again, with a new general_free_serving_candidates() selector (also surfaced as general_free_serving_count in the discover-models CLI report).

Round 2 (efd44f6): the round-1 choke point overshot -- _is_free_agent also backed every capability-scoped free route (/v1/audio/transcriptions, /v1/videos, image, speech, rerank) and server.py's _require_pool_model, so a genuinely free transcription/video/image agent's own non-text input:<modality> tag made it wrongly unreachable through its own free route. Split into a plain, modality-blind _is_free_agent (capability-scoped routes) and a stricter _is_general_free_agent (blind general-chat routes only). Also extracted the shared "what counts as non-text input" classification into chat_capability.requires_non_text_input, used by both the discovery-time and selection-time predicates so they can't drift independently, and documented why general_free_serving_count and free_tier_count deliberately share one population regardless of --free-only.

Full re-verification after round 2: pytest 2775 passed/1 skipped, interrogate 100.0%. The original PR description (fix for the initial Strix incident) follows unchanged below.


Reproduction evidence

ContextualWisdomLab/.github PR #1198's required Strix Security Scan check failed at head b051f5da34998fcd0ed42990d9f5e29b128a59ab (run 33325907333, job 99295892400). Strix calls orchestrator/free through the vendored gateway sidecar (scripts/ci/contextual_orchestrator_review_sidecar.sh in .github). The job log shows, verbatim, across 3 independent scan attempts (each a fresh sidecar process, ~17:47/17:49/17:52/17:56 UTC):

openai.BadRequestError: Error code: 400 - {'error': {'code': 'invalid_request_error', 'message': "Model 'meta/llama-3.2-90b-vision-instruct' via agent 'nvidia_nim_meta_llama_3_2_90b_vision_instruct': provider rejected the request with HTTP 400. Adjust the request parameters and retry.
...
Strix run failed for model 'orchestrator/free' after 129s (exit code 1).
STRIX_PROVIDER_UNAVAILABLE: contextual-orchestrator/orchestrator/free exhausted; the gateway owns provider discovery and failover.

Every one of the 3 attempts hit the exact same model/agent and got the exact same 400.

Root cause

meta/llama-3.2-90b-vision-instruct is zero-priced on NVIDIA NIM (Models.dev reports cost: {"input": 0, "output": 0}), and it passes every existing chat-capability check (text output modality, no disqualifying model-id token). So free_discovered_models() admitted it into the general-purpose orchestrator/free pool that Strix's tool-calling (tools=[...]) requests route through (confirmed: server.py's tool-loop branch calls orchestrator.proxy_completion(..., single_agent=True), which fails over across free candidates on transient/size errors — but this agent alone occupies the top-ranked "free" slot, so nothing was actually protecting the pool from it).

I verified live against models.dev/api.json that its tool_call field claims true for this exact model — so that field cannot be used to gate this; NVIDIA NIM's own /v1/models listing carries no capability metadata at all. The model's declared input modality (text + image) is the only honest catalog evidence that distinguishes it from an ordinary text-only free worker, and it's evidence the discovery layer already records.

Two fixes considered and rejected

  • Loosen ModelClient's cross-provider failover to retry a plain HTTP 400 on the next free candidate. Explicitly foreclosed by this repo's own tested contracts in tests/test_passthrough_provider_failover.py: test_non_transient_error_is_not_replayed ("Caller errors fail closed instead of duplicating a request across providers") and test_virtual_passthrough_keeps_non_size_tool_errors_sticky ("A generic provider invalid_tools response must not hide a bad request"). Changing this would weaken a deliberate, tested safety gate.
  • Make the circuit breaker record sticky (non-failover-eligible) rejections. This is a real, separate gap (_record_failure is never called on the immediately-raised sticky-error branch), but it would not have prevented this incident: each Strix attempt is a fresh sidecar process (in-memory circuit-breaker state), and the observed failure is each attempt's very first request — never enough repetitions within one process to trip a threshold-3 breaker.

Fix

free_discovered_models() (contextual_orchestrator/model_discovery.py) now excludes a free model that declares a non-text input modality from the general-purpose free pool. This is a pool-composition fix, not a per-request retry change: the model stays fully discovered and price-evidenced (available to a pool that explicitly wants a vision/multimodal capability); it is only withheld from the capability-blind orchestrator/free default that Strix and other tool-calling callers route through.

Scoped specifically to the free selector (not general chat-candidate eligibility everywhere), so a genuinely tool-capable paid multimodal model on another provider (e.g. OpenAI) is unaffected — only NVIDIA NIM's free catalog is realistically in scope today (OpenRouter discovery is evidence_only, Bytez never gets is_free=True).

.github's own sidecar/launcher script calls contextual_orchestrator.model_discovery.discover_all_models/free_discovered_models directly and derives its orchestrator/free route identities from the latter's output — so this fix requires no change to .github and takes effect automatically the next time its required-workflow pin picks up this repo.

Known limitation (not silently declaring victory)

NVIDIA NIM's own model-listing API carries no tool/function-calling capability metadata at all, and Models.dev's tool_call field is unreliable at the per-deployment granularity (proven wrong for this exact model). If NIM's remaining free text-only catalog also turns out to lack genuine tool-calling support for some models, Strix could still hit a similar wall — that would need broader free-tier discovery (OpenRouter/Bytez/OpenAI free-tier offerings, all currently evidence_only or never free in this repo's provider list) as a follow-up. This PR does not fabricate evidence that such a follow-up is unnecessary.

TDD / test evidence

  • tests/test_model_discovery.py::test_free_discovered_models_excludes_a_free_vision_only_input_model reproduces the bug against a DiscoveredModel fixture shaped like the broken NIM agent (fails pre-fix, passes post-fix); a text-only free model and a free model with no modality evidence at all remain eligible in the same assertion.
  • python -m pytest tests -q2770 passed, 1 skipped (full suite, no regressions).
  • interrogate (this repo's pyproject.toml sets fail-under = 100) → 100.0%.
  • python tests/test_self_check.py, test_conventions.py, test_chat_capability.py, test_chat_capability_unknown_identifiers.py, test_provider_bootstrap.py, test_discovery_bootstrap_selection.py, test_chat_model_capability_isolation.py, test_chat_passthrough_capability_isolation.py all pass individually.
  • Installed via python -m pip install --require-hashes -r requirements.lock && python -m pip install --no-deps -e . (Python 3.12, per this repo's own pin) before running the above.

Not merging this myself — leaving it for the normal OpenCode review → merge-scheduler pipeline.

Co-Authored-By: Claude Sonnet 5


Generated by Claude Code


``&lt;img src="https://static.devin.ai/assets/gh-devin-review-light.svg?v=3" alt="Devin Review"&gt;``

ContextualWisdomLab/.github PR #1198's required Strix Security Scan check
failed (run 33325907333, job 99295892400): 3 independent scan attempts each
hit NVIDIA NIM's meta/llama-3.2-90b-vision-instruct via
orchestrator/free and got an identical HTTP 400 invalid_request_error
("Adjust the request parameters and retry"), exhausting the whole free pool
against this one agent.

Root cause: this model is zero-priced (Models.dev reports cost 0/0) and
passes every existing chat-capability check (text output modality, no
disqualifying model-id token), so free_discovered_models() admitted it into
the general-purpose free pool that Strix's tool-calling requests route
through. Models.dev's own tool_call field claims true for this exact model
(verified live against models.dev/api.json), so that field cannot gate this;
its declared input modality (text + image) is the only honest catalog
evidence that distinguishes it from an ordinary text-only free worker.

Two alternate fixes were considered and rejected against this repo's own
tested contracts:
- Loosening ModelClient's cross-provider failover to retry a plain HTTP 400
  on a different free candidate is explicitly foreclosed by
  test_non_transient_error_is_not_replayed and
  test_virtual_passthrough_keeps_non_size_tool_errors_sticky
  (tests/test_passthrough_provider_failover.py): "caller errors fail closed
  instead of duplicating a request across providers."
- The circuit breaker never records a sticky (non-failover-eligible)
  rejection at all, but recording it would not have prevented this incident
  either: each Strix attempt is a fresh sidecar process/gateway instance
  (in-memory circuit state), and the observed failure is the very first
  request each attempt makes.

Fix: free_discovered_models() now excludes a free model that declares a
non-text input modality from the general-purpose free pool. This is
pool-composition, not per-request retry: the model stays fully discovered
and price-evidenced (available to a pool that explicitly wants a
vision/multimodal capability) and is only withheld from the
capability-blind orchestrator/free default. Scoped to the free selector
specifically (not general chat-candidate eligibility) so a genuinely
tool-capable paid multimodal model elsewhere is unaffected.

Known limitation: NVIDIA NIM's own /v1/models listing carries no
tool/function-calling capability metadata at all, and Models.dev's tool_call
field is unreliable at the per-deployment granularity (proven wrong here).
If NIM's remaining free text-only catalog also turns out to lack genuine
tool-calling support for some models, broader free-tier discovery
(OpenRouter/Bytez/OpenAI free-tier offerings, today evidence-only or never
free) is a follow-up, not something this change can fabricate evidence for.

TDD: tests/test_model_discovery.py::test_free_discovered_models_excludes_a_free_vision_only_input_model
reproduces the bug against a DiscoveredModel fixture shaped like the broken
NIM agent (fails pre-fix, passes post-fix); a text-only free model and a
free model with no modality evidence at all remain eligible.

Verified: python -m pytest tests -q -> 2770 passed, 1 skipped.
interrogate (fail-under 100) -> 100.0%. python tests/test_self_check.py and
the naming-convention/chat-capability/provider-bootstrap check scripts named
in README.md all pass individually too.

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

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 19 seconds.

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: 1cb6cb38-1c8c-43ba-a0b1-c6a160fb9af8

📥 Commits

Reviewing files that changed from the base of the PR and between 5648567 and 36a0bf3.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • contextual_orchestrator/__main__.py
  • contextual_orchestrator/chat_capability.py
  • contextual_orchestrator/model_discovery.py
  • contextual_orchestrator/orchestrator.py
  • contextual_orchestrator/server.py
  • tests/test_auto_discovery_server.py
  • tests/test_model_discovery.py
  • tests/test_multimodal_model_group_http.py
  • tests/test_orchestrated_responses_stream.py
  • tests/test_provider_bootstrap.py

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 410e8b89-db54-436c-ad79-9de5e2c20a51

📥 Commits

Reviewing files that changed from the base of the PR and between 59bc2bd and 5648567.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • contextual_orchestrator/__main__.py
  • contextual_orchestrator/model_discovery.py
  • contextual_orchestrator/orchestrator.py
  • fuzz/fuzz_agent_config.py
  • fuzz/fuzz_model_judge.py
  • fuzz/fuzz_orchestration.py
  • fuzz/fuzz_reasoning_effort_profile.py
  • fuzz/fuzz_redaction.py
  • fuzz/fuzz_request_body.py
  • tests/test_auto_discovery_server.py
  • tests/test_discover_models_cli.py
  • tests/test_model_discovery.py
  • tests/test_provider_bootstrap.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

무료 모델의 가격 기반 인벤토리와 일반 무료 서빙 후보를 분리했다. 비텍스트 입력 modality를 선언한 에이전트는 orchestrator/free 선택에서 제외한다. CLI 보고 필드와 관련 테스트를 추가하고 퍼징 하네스에 docstring을 보강했다.

Changes

무료 모델 서빙 선택

Layer / File(s) Summary
무료 모델 후보 계산과 CLI 보고
contextual_orchestrator/model_discovery.py, contextual_orchestrator/__main__.py, tests/test_model_discovery.py, tests/test_discover_models_cli.py
free_discovered_models는 가격 증거만으로 무료 모델을 반환한다. general_free_serving_candidates는 비텍스트 입력 modality를 선언한 모델을 제외한다. CLI는 general_free_serving_count를 보고한다.
무료 풀 선택 게이트
contextual_orchestrator/orchestrator.py, tests/test_auto_discovery_server.py, tests/test_provider_bootstrap.py, CHANGELOG.md
TaskOrchestrator._is_free_agent가 비텍스트 입력 태그가 있는 에이전트를 orchestrator/free에서 제외한다. 자동 검색과 provider bootstrap 경로에서 cost:free 태그는 유지한다.
퍼징 하네스 설명 보강
fuzz/fuzz_*.py
Atheris 퍼징 하네스의 one_inputmain 함수에 docstring을 추가한다. 실행 동작은 변경하지 않는다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🔵 Low · up to 56485

The change prevents multimodal free models from entering the default text-serving pool, but the new serving-candidate path can still expose provider records that are evidence-only to direct consumers that bypass later validation. The PR is mergeable with explicit owner awareness and follow-up to enforce that serving candidates are always routable records.

Sequence Diagram(s)

sequenceDiagram
  participant ModelDiscovery
  participant TaskOrchestrator
  participant OpenAIModelListing
  ModelDiscovery->>ModelDiscovery: free_discovered_models(discovered)
  ModelDiscovery->>ModelDiscovery: general_free_serving_candidates(discovered)
  ModelDiscovery->>TaskOrchestrator: discovered agents with cost:free tags
  TaskOrchestrator->>TaskOrchestrator: _is_free_agent(agent)
  TaskOrchestrator->>OpenAIModelListing: FREE_MODEL selection
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 93.55% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 12 files. (2 skipped: 1…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 비전 입력 모델을 capability-blind한 orchestrator/free 풀에서 제외하는 주요 변경을 정확하고 간결하게 설명합니다.
Full details: Docstring Coverage

Explanation

Docstring coverage is 93.55% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 12 files. (2 skipped: 1 unsupported, 1 too large.)

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nim-vision-tool-calling-pool

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[bot]

This comment was marked as resolved.

…hestrator

Devin's review on PR #933 found the vision-input exclusion added in
94e3b9a was incomplete on three points:

1. Runtime free pool remained unfiltered: `_auto_discover_runtime_agents`
   (`--auto-discover-model-agents`) and `provider_bootstrap`'s
   `_active_agent_from_discovered` (used by `bootstrap_provider_runtime`
   and, through it, `provider_catalog_bootstrap.bootstrap_provider_catalog_runtime`)
   both tag an agent `cost:free` from raw price evidence alone and never
   consulted the new exclusion, so NVIDIA NIM's free
   `meta/llama-3.2-90b-vision-instruct` could still reach a live,
   blindly-selectable `cost:free` agent through either path -- reproducing
   the original Strix tool-calling incident (ContextualWisdomLab/.github#1198).

2. `free_discovered_models()` conflated price-based inventory with
   serving-pool eligibility, so `--free-only`, `free_tier_count`, and the
   free-tier data-privacy totals silently undercounted a model that is
   genuinely free-priced but unfit for blind serving.

3. (Evaluated, not adopted) Devin also suggested narrowing the exclusion to
   spare a model that "also supports text as a standalone input". Rejected
   against this repo's own incident evidence: the incident model itself
   declares both `text` and `image` per Models.dev, so that narrowing would
   have silently re-admitted the exact model this fix is about. Kept the
   conservative "any declared non-text input modality disqualifies" reading,
   documented with a fixture for all three modality shapes (text-only,
   vision-only, text+image).

Fix:
- `free_discovered_models()` is pure price-based inventory again.
- New `model_discovery.general_free_serving_candidates()` carries the
  modality-based exclusion for composing a blind free pool, wired into the
  `discover-models` CLI report as `general_free_serving_count` alongside
  the restored `free_tier_count`.
- The actual enforcement is now `TaskOrchestrator._is_free_agent`, a single
  choke point every `orchestrator/free` selection path shares: an agent
  whose tags declare a non-text `input:<modality>` is never treated as
  free-pool eligible there, regardless of which code built it or how old
  that agent-pool row is (protects durable pool-store rows written before
  this exclusion existed, and any future pool-construction path). `cost:free`
  keeps meaning "honest zero price" everywhere else, preserving
  `provider_catalog_store.py`'s durable `is_free` round trip (see
  `test_serving_tags_preserve_only_explicit_free_and_modality_evidence` and
  `test_last_known_good_restores_free_and_modality_evidence`, both left
  unchanged).
- `review_gateway.py` audited: its agents never carry a `cost:free` tag
  (tags are fully replaced with `("review",)`) and no price is registered,
  so it was never reachable through this bug; no change needed there.

Also brought `interrogate --fail-under=100` back to green: it was already
failing at 97.9% on the unmodified base commit (94e3b9a) from six
pre-existing undocumented `fuzz/*.py` harness functions, unrelated to this
incident; added their docstrings.

TDD: new/relocated tests fail against the pre-fix source (verified by
temporarily restoring it) and pass after the fix --
`test_auto_discovery_activates_a_free_vision_model_but_free_pool_excludes_it`,
`test_active_agent_from_discovered_free_vision_model_is_not_free_pool_eligible`,
`test_free_discovered_models_still_counts_a_free_vision_only_input_model`,
`test_general_free_serving_candidates_modality_shapes`, and the relocated
`test_general_free_serving_candidates_excludes_a_free_vision_only_input_model`.

Verified: python -m pytest tests -q -> 2774 passed, 1 skipped (2770 passed,
1 skipped on the unmodified base commit; net +4 tests, zero regressions).
interrogate (fail-under 100) -> 100.0%. python tests/test_conventions.py
passes (new function name is valid two-or-more-word snake_case). git diff
--check clean.

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

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pushed 5648567 addressing all three of Devin's review findings on this PR. Replies are inline on each of the three original threads; summary below.

What changed, and why

Finding 1 (🔴 runtime free pool remains unfiltered) -- fixed at a single choke point, not per call site.
Confirmed: _auto_discover_runtime_agents (--auto-discover-model-agents) and provider_bootstrap._active_agent_from_discovered (used by bootstrap_provider_runtime and, through it, provider_catalog_bootstrap.bootstrap_provider_catalog_runtime) both tag an agent cost:free from raw price evidence alone and never consulted the exclusion this PR added -- the vision model could still reach a live, blindly-selectable cost:free agent through either path. Rather than repeat the same filter at every pool-construction site (which is exactly the kind of gap this finding is about -- "not just the one function this PR touched"), the enforcement now lives in TaskOrchestrator._is_free_agent, the single method every orchestrator/free selection path already shares. It re-checks an agent's persisted input:<modality> tags at selection time, so it also protects a durable agent-pool row written by an older build before this exclusion existed, and any future pool-construction path this repo adds later, not just the ones audited today.

Audited all four files this finding named:

  • _auto_discover_runtime_agents -- confirmed vulnerable pre-fix.
  • provider_bootstrap.py / provider_catalog_bootstrap.py -- _active_agent_from_discovered had the identical gap, confirmed vulnerable pre-fix.
  • review_gateway.py -- audited and found not reachable through this bug: build_review_orchestrator replaces every constructed agent's tags with ("review",) (no cost:free ever survives) and never populates price_per_million, so _is_free_agent was already unconditionally False for every agent it builds. No change made there.

Finding 2 (🟡 multimodal support empties free pools) -- investigated and not adopted as suggested, with reasoning.
The suggested narrowing ("keep text+image models eligible when text is a supported standalone input") would silently reopen this exact incident: the incident model itself (meta/llama-3.2-90b-vision-instruct) declares input_modalities=("text", "image") per Models.dev, so it already satisfies that suggested test -- yet it's precisely the model NIM's live deployment rejected a tool-calling request against three times in a row. Models.dev's input_modalities field documents generically supported inputs, not which ones a specific deployment's tool-calling path actually honors, and this PR's own commit already documents that no more reliable per-deployment tool-calling signal exists (NIM's /v1/models has none; Models.dev's tool_call field claims true for this exact model and is proven wrong by the incident). Narrowing the check would have made the fix inert for the model it exists to fix.

Kept the conservative "any declared non-text input modality disqualifies" reading, and added the requested explicit three-fixture coverage anyway (text-only / vision-only / text+image), documenting in both the code and the test why text+image stays excluded. The legitimate underlying concern -- a provider's free pool silently going empty -- is now addressed by visibility instead: general_free_serving_count in the discover-models CLI report (see finding 3) lets an operator actually notice that, rather than the fix quietly admitting a request-breaking model back in to avoid it.

Finding 3 (🟡 free-model reports undercount inventory) -- fixed exactly as suggested.
free_discovered_models() is pure model.is_free price-based inventory again (restores correct --free-only, free_tier_count, and the free-tier data-privacy totals). A new, separately named selector, general_free_serving_candidates() -- the same name suggested -- carries the modality exclusion specifically for composing the blind orchestrator/free pool, and is now also surfaced as general_free_serving_count in the CLI report so the split is externally visible, not just internally correct.

Test evidence (TDD, all four new/relocated tests verified red against the pre-fix source by temporarily restoring it, then green after the fix)

  • test_auto_discovery_activates_a_free_vision_model_but_free_pool_excludes_it (finding 1, _auto_discover_runtime_agents path)
  • test_active_agent_from_discovered_free_vision_model_is_not_free_pool_eligible (finding 1, provider_bootstrap.py path)
  • test_general_free_serving_candidates_modality_shapes (finding 2, explicit text-only/vision-only/text+image fixtures)
  • test_free_discovered_models_still_counts_a_free_vision_only_input_model (finding 3)
  • test_general_free_serving_candidates_excludes_a_free_vision_only_input_model -- the original PR's regression test, relocated onto the new selector, intent unchanged

Full verification

  • python -m pip install --require-hashes -r requirements.lock && python -m pip install --no-deps -e . (Python 3.12)
  • python -m pytest tests -q -> 2774 passed, 1 skipped (base commit 94e3b9a: 2770 passed, 1 skipped -- net +4 tests, zero regressions)
  • interrogate -> 100.0% (was already failing at 97.9% on the unmodified base commit from six pre-existing undocumented fuzz/*.py harness functions, unrelated to this incident; fixed those docstrings too since the task's own gate expects 100%)
  • python tests/test_conventions.py -> passes (general_free_serving_candidates is valid two-or-more-word snake_case)
  • git diff --check -> clean

Commit: 5648567, pushed to fix/nim-vision-tool-calling-pool (fast-forward from 94e3b9a).


Generated by Claude Code

model
for model in free_discovered_models(discovered)
if not _requires_non_text_input(model)
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5648567, but not by patching each pool-construction call site individually (that's still whack-a-mole against a future one). The actual enforcement is now a single choke point: TaskOrchestrator._is_free_agent re-checks an agent's input:<modality> tags at selection time, so every orchestrator/free path is covered -- _auto_discover_runtime_agents, provider_bootstrap._active_agent_from_discovered (and provider_catalog_bootstrap through it), review_gateway.py, and any future pool-construction path -- including a durable agent-pool row written by an older build before this exclusion existed, which per-call-site filtering at construction time can't retroactively fix.

Audited all four named files:

  • _auto_discover_runtime_agents: confirmed vulnerable pre-fix (new test test_auto_discovery_activates_a_free_vision_model_but_free_pool_excludes_it, fails red against the old source).
  • provider_bootstrap.py / provider_catalog_bootstrap.py: _active_agent_from_discovered has the identical gap (new test test_active_agent_from_discovered_free_vision_model_is_not_free_pool_eligible, also fails red pre-fix).
  • review_gateway.py: audited, not actually reachable through this bug -- its build_review_orchestrator fully replaces agent tags with ("review",) (no cost:free ever survives) and never sets price_per_million, so _is_free_agent was already always False there. No change needed.

Generated by Claude Code

Comment thread contextual_orchestrator/model_discovery.py Outdated
model
for model in free_discovered_models(discovered)
if not _requires_non_text_input(model)
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed exactly as suggested. free_discovered_models() is pure model.is_free inventory again (restores correct --free-only, free_tier_count, and the free-tier data-privacy totals), and a new general_free_serving_candidates() -- same name you proposed -- carries the modality exclusion for orchestrator/free composition specifically. It's also now surfaced as general_free_serving_count in the discover-models CLI report alongside the restored free_tier_count, so the split is visible, not just internally correct.

New regression test: test_free_discovered_models_still_counts_a_free_vision_only_input_model asserts free_discovered_models([vision_model]) == [vision_model] while general_free_serving_candidates([vision_model]) == [] -- fails red against the pre-fix source (where the vision model was missing from the old free_discovered_models's output entirely).

Only pool-serving callers were updated to route through the new selector's underlying logic; see the top-of-file thread for how that's actually enforced (a single TaskOrchestrator._is_free_agent choke point rather than threading the selector through every call site, which was the more robust fix for finding 1).


Generated by Claude Code

devin-ai-integration[bot]

This comment was marked as resolved.

…at only

Devin's second review round on PR #933 (against 5648567) found the modality
exclusion had overshot: TaskOrchestrator._is_free_agent had grown a
non-text-input check meant only for the capability-blind general chat pool,
but it also backed every capability-scoped free route
(_capability_agents -> /v1/audio/transcriptions, /v1/videos, image, speech,
rerank) and server.py's _require_pool_model. A free transcription agent
naturally carries an input:audio tag and a free image/video agent an
input:image tag -- exactly the modality its own capability-scoped free route
is asking for, not a surprise -- so the shared predicate made those
genuinely free agents unreachable through their own free route.

Fix: split the predicate.
- _is_free_agent reverts to plain, modality-blind price evidence (used by
  _capability_agents/_ranked_agents when chat_only=False, and by
  server.py's capability-scoped _require_pool_model branch).
- New _is_general_free_agent = _is_free_agent(agent) and not
  _agent_requires_non_text_input(agent) is the stricter, general-chat-only
  variant, now used at every blind general-chat FREE_MODEL call site
  (proxy_completion, _orchestrated_provider_completion, route_once, conduct,
  _ranked_agents when chat_only=True, list_openai_models's advertising
  check, and server.py's capability-agnostic _require_pool_model branch).

Also addressed the review's two informational notes:
- "Duplicate serving policies can drift": extracted the actual "what counts
  as non-text" classification into chat_capability.requires_non_text_input,
  a single shared predicate both model_discovery._requires_non_text_input
  (DiscoveredModel.input_modalities) and
  orchestrator._agent_requires_non_text_input (an agent's input:<modality>
  tags) now delegate to, so the two representations of the same catalog
  evidence cannot diverge independently. New cross-consistency test:
  test_discovery_and_orchestrator_modality_eligibility_cannot_drift.
- "Serving count uses a different population": documented in __main__.py
  that free_tier_count and general_free_serving_count are deliberately both
  computed over the complete `discovered` population regardless of
  --free-only, matching each other's established convention (not a new
  inconsistency).

TDD: new tests fail against 5648567 (verified by temporarily inserting them
against that commit) and pass after this fix --
test_free_virtual_model_selects_a_free_agent_whose_own_capability_needs_non_text_input
(orchestrator._capability_agents path),
test_require_pool_model_serves_capability_free_route_despite_non_text_input
(server._require_pool_model path), plus updated assertions on the two
existing regression tests from the first round confirming _is_free_agent
now returns True (capability-reachable) while _is_general_free_agent stays
False (blind-chat-excluded) for the same agent.

Verified: python -m pytest tests -q -> 2775 passed, 1 skipped (net +1 test
over the previous round's 2774; zero regressions). interrogate -> 100.0%.
python tests/test_conventions.py passes. git diff --check clean.

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

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pushed efd44f6 addressing the second review round's findings on 5648567. Replies are inline on the three new threads; summary below.

What changed, and why

🟡 "Free media routes reject eligible models" -- real regression from round 1, fixed by splitting the predicate.
TaskOrchestrator._is_free_agent had grown a non-text-input exclusion meant only for the capability-blind general chat pool, but it also backed _capability_agents (every /v1/audio/transcriptions, /v1/videos, image, speech, rerank route) and, separately, server.py's _require_pool_model. A free transcription agent naturally carries an input:audio tag and a free image/video agent an input:image tag -- exactly the modality its own capability-scoped free route is asking for, not a surprise -- so the shared predicate made those genuinely free agents unreachable through their own free route.

Fix: _is_free_agent reverts to plain, modality-blind price evidence (used wherever a request is already scoped to a specific capability). A new, stricter _is_general_free_agent carries the exclusion, applied only at the blind general-chat call sites (proxy_completion, _orchestrated_provider_completion, route_once, conduct, _ranked_agents when chat_only=True, list_openai_models's advertising check) plus server.py's equivalent capability-agnostic branch of _require_pool_model, which had the identical gap.

🔍 "Serving count uses a different population" -- not a new inconsistency, documented.
free_tier_count was already computed over the complete discovered population regardless of --free-only (an established pre-existing convention, distinct from discovered_count/priced_count/models, which do respect --free-only's row filter). general_free_serving_count deliberately matches that same convention rather than introducing a third population. Added an explicit comment in __main__.py so this doesn't need reverse-engineering next time.

🔍 "Duplicate serving policies can drift" -- fixed by extracting one shared predicate.
chat_capability.requires_non_text_input(input_modalities) is now the single evidence-based classification both model_discovery._requires_non_text_input (over DiscoveredModel.input_modalities) and orchestrator._agent_requires_non_text_input (over an agent's input:<modality> tags) delegate to -- chat_capability.py was already a shared leaf dependency of both files, so no new import cycle. A new cross-consistency test runs the same three modality fixtures through both layers and asserts they agree.

Test evidence (TDD, verified red against 5648567 by temporarily inserting each new test against that commit, then green after this fix)

  • test_free_virtual_model_selects_a_free_agent_whose_own_capability_needs_non_text_input (orchestrator's _capability_agents path)
  • test_require_pool_model_serves_capability_free_route_despite_non_text_input (server's _require_pool_model path)
  • test_discovery_and_orchestrator_modality_eligibility_cannot_drift (cross-layer consistency)
  • Updated the two round-1 regression tests to assert _is_free_agent(agent) is True (capability-reachable) alongside _is_general_free_agent(agent) is False (blind-chat-excluded) for the same agent

Full verification

  • python -m pytest tests -q -> 2775 passed, 1 skipped (previous round: 2774 passed, 1 skipped -- net +1 test, zero regressions)
  • interrogate -> 100.0%
  • python tests/test_conventions.py -> passes
  • git diff --check -> clean

Commit: efd44f6, pushed to fix/nim-vision-tool-calling-pool (fast-forward from 5648567).


Generated by Claude Code


def _is_free_agent(self, agent: ModelAgent) -> bool:
"""Return true only for explicitly zero-priced configured models."""
"""Return true only for explicitly zero-priced configured models.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in efd44f6. _is_free_agent reverts to plain, modality-blind price evidence (used by _capability_agents/_ranked_agents when chat_only=False, and by server.py's _require_pool_model capability-scoped branch). A new, stricter _is_general_free_agent (= _is_free_agent(agent) and not _agent_requires_non_text_input(agent)) now carries the exclusion, applied only at the blind general-chat call sites: proxy_completion, _orchestrated_provider_completion, route_once, conduct, _ranked_agents when chat_only=True, list_openai_models's advertising check, and server.py's capability-agnostic _require_pool_model branch.

Also found and fixed the same gap in server.py's _require_pool_model, which independently re-checks _is_free_agent outside _capability_agents -- same split applied there (capability-scoped branch stays plain, general-chat branch uses _is_general_free_agent).

New coverage: test_free_virtual_model_selects_a_free_agent_whose_own_capability_needs_non_text_input (a free transcription agent with input:audio now resolves through its own /v1/audio/transcriptions-style free route) and test_require_pool_model_serves_capability_free_route_despite_non_text_input (same for _require_pool_model), both fail red against 5648567 (verified by temporarily inserting them against that commit). Also updated the two existing regression tests from round 1 to assert _is_free_agent(agent) is True (capability-reachable) alongside _is_general_free_agent(agent) is False (blind-chat-excluded) for the same vision/transcription-style agent.


Generated by Claude Code

before treating a discovered model as eligible for blind free serving
(e.g. tagging an agent ``cost:free`` in a context where that tag alone
drives general-chat ``orchestrator/free`` routing).
``TaskOrchestrator._is_general_free_agent`` additionally re-checks an

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both discovered_count and general_free_serving_count/free_tier_count were already answering different questions by design before this PR touched anything: discovered_count (and priced_count, and the models list) reflect reported, which --free-only narrows; free_tier_count has always been computed over the complete discovered population regardless of --free-only (it answers "how many free models exist overall", not "how many of the rows shown below are free"). general_free_serving_count deliberately matches that same established convention rather than reported's row-level filter -- it's a second "how many, out of everything" metric, not a new population.

Documented this explicitly in a comment above both assignments in __main__.py (commit efd44f6) so a future reader doesn't have to reverse-engineer the convention from the pre-existing asymmetry.


Generated by Claude Code

before treating a discovered model as eligible for blind free serving
(e.g. tagging an agent ``cost:free`` in a context where that tag alone
drives general-chat ``orchestrator/free`` routing).
``TaskOrchestrator._is_general_free_agent`` additionally re-checks an

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and fixed in efd44f6: extracted the actual "what counts as non-text input" classification into a single shared predicate, chat_capability.requires_non_text_input(input_modalities: Iterable[str]). model_discovery._requires_non_text_input (reading DiscoveredModel.input_modalities) and orchestrator._agent_requires_non_text_input (reading an agent's input:<modality> tags with the prefix stripped) now both delegate to it, so a future modality-rule change only has to happen in one place.

chat_capability.py was already a shared leaf dependency of both model_discovery.py and orchestrator.py (no new import cycle). Only the representation-extraction step (tuple field vs. tag-string parsing) remains separate, since ModelAgent genuinely carries no modality field of its own -- that part can't be unified without adding one.

New cross-consistency test: test_discovery_and_orchestrator_modality_eligibility_cannot_drift runs the same three fixtures (text-only / vision-only / text+image) through both general_free_serving_candidates and _is_general_free_agent and asserts they agree, so a future divergence would fail this test immediately rather than silently drift.


Generated by Claude Code

devin-ai-integration[bot]

This comment was marked as resolved.

Devin's review pass on PR #933 after efd44f6 found two issues:

1. general_free_serving_candidates() admitted evidence-only and
   non-chat-capable zero-cost text-input catalog rows, overcounting
   general_free_serving_count with models that could never actually
   become a serving agent. Now also requires is_routable_discovered_model
   -- the same predicate _auto_discover_runtime_agents and
   provider_bootstrap already require before promoting a discovered row
   to an ordinary chat agent. New regression test
   test_general_free_serving_candidates_excludes_unroutable_free_models
   (an evidence-only free text model and a free embedding-only model)
   fails red pre-fix.

2. A real, deterministic CI failure on efd44f6 itself (GitHub Actions
   "Full unit and contract suite" job 99313736725):
   test_discovery_and_orchestrator_modality_eligibility_cannot_drift built
   ModelAgent fixtures using hyphenated ids straight from provider model
   ids (e.g. "text-only-model", "meta/llama-3.2-90b-vision-instruct"),
   which fail this repo's require_object_name two-or-more-word snake_case
   convention. Fixed by deriving a compliant id (casefold + translate
   "/.-" to "_") distinct from the `model` field under test.

Also documents both this round's fix and the prior capability-route
modality-scoping round in CHANGELOG.md (neither had an entry yet).

Verified: python -m pytest tests -q -> 2779 passed, 1 skipped, 0 failed
(with fast-mlsirm installed via git+https, working around this sandbox's
plain-tarball-download 403 that is unrelated to the fix); interrogate ->
100%; tests/test_conventions.py -> ok; git diff --check -> clean.

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

Copy link
Copy Markdown
Contributor Author

Round 4 summary (commit 36a0bf3, on top of efd44f6)

Two fixes landed in this round, both from Devin's latest review pass:

1. general_free_serving_candidates() overcounted unroutable models -- it admitted any zero-priced, text-input catalog row regardless of whether it could ever actually become a serving agent. An evidence_only row (agent_from_discovered refuses to build an agent from one at all) or a free non-chat-capable model (e.g. an embedding-only deployment) both inflated general_free_serving_count. Fixed by also requiring is_routable_discovered_model -- the same predicate _auto_discover_runtime_agents and provider_bootstrap already require before promoting a discovered row to an ordinary chat agent. New regression test: test_general_free_serving_candidates_excludes_unroutable_free_models.

2. A real, deterministic CI failure on efd44f6 itself (GitHub Actions "Full unit and contract suite", job 99313736725): the cross-consistency test added in that commit, test_discovery_and_orchestrator_modality_eligibility_cannot_drift, built ModelAgent fixtures using hyphenated ids taken straight from provider model ids (e.g. "text-only-model", "meta/llama-3.2-90b-vision-instruct"), which fail this repo's require_object_name two-or-more-word snake_case convention. Fixed by deriving a compliant id (casefold + translate /.- to _) distinct from the model field under test, so the fixture still exercises the real provider-shaped model id everywhere else.

Also backfilled CHANGELOG.md entries for this round and the prior capability-route modality-scoping round (efd44f6), which had none yet.

Verification (Python 3.12, python -m pip install --require-hashes -r requirements.lock && python -m pip install --no-deps -e .):

  • python -m pytest tests -q -> 2779 passed, 1 skipped, 0 failed
  • python -m interrogate -c pyproject.toml . -> 100%
  • python tests/test_conventions.py -> ok
  • git diff --check -> clean

(One local-only wrinkle during verification: this sandbox's network proxy returns HTTP 403 on a plain https://github.com/.../archive/....tar.gz tarball fetch, so fast-mlsirm's requirements.lock entry had to be installed via pip install "fast-mlsirm @ git+https://github.com/...@v0.9.1" instead -- a local environment quirk, not a code or lockfile issue; ordinary GitHub Actions CI is unaffected.)

Pushed fast-forward to fix/nim-vision-tool-calling-pool (efd44f6..36a0bf3). All four review threads from this pass (the routability bug, the two "confirmed correct, no action" info notes, and the research-grounding process note) are replied to and resolved.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Standing down on further rounds against general_free_serving_count's exact precision.

The safety-critical bug this PR exists to fix — Strix's tool-calling requests hitting NVIDIA NIM's meta/llama-3.2-90b-vision-instruct and failing with HTTP 400 — was fixed in round 1 and is enforced at runtime by TaskOrchestrator._is_general_free_agent/_is_free_agent in orchestrator.py, which this latest finding does not touch or weaken.

The last four review rounds have each fixed one real, narrowing issue specifically in general_free_serving_candidates()'s CLI-reporting count (__main__.py's general_free_serving_count): capability-scope leakage, inventory-vs-serving population separation, unroutable/evidence-only rows, and now this one (a non-chat-capable text-output utility model, e.g. an embedding-only model, could still inflate the count). Each fix has drawn one further, progressively narrower edge case in the same reporting metric rather than converging to zero.

This latest finding is a pure metric-precision nit on an informational CLI count — it does not affect which agent actually gets selected to serve a chat request (that's _is_general_free_agent's job, already covered by rounds 1-3 and their tests), so no unreviewed code can merge or serve traffic incorrectly because of it. Per this session's review-convergence practice, I'm stopping here rather than opening a fifth round chasing an informational count's last decimal of precision. If a future contributor wants general_free_serving_count to also exclude non-chat-capability text-output models, is_routable_discovered_model's capability check is the place to extend — flagging it here as a known, accepted minor follow-up rather than blocking on it.


Generated by Claude Code


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Strix required check failed at head 36a0bf3 (run 33334177387, job 99317982462). This is not the earlier NIM-vision-model 400 bug this PR fixes — the actual failure:

Strix run failed for model 'orchestrator/free' after 5404s (exit code 124).
STRIX_PROVIDER_UNAVAILABLE: contextual-orchestrator/orchestrator/free exhausted; the gateway owns provider discovery and failover.
Provider-unavailable Strix attempt 1 reached the bounded retry limit or the remaining job time budget (589s) is too small to retry; failing closed.

Exit code 124 is the scan hitting its own internal STRIX_PROCESS_TIMEOUT_SECONDS (5400s) wrapper, not a reported vulnerability (Vulnerabilities 0 was the last panel state before the kill) and not a code-shape rejection like the incident this PR fixes. The sidecar provisioning log also shows transient discovery-time noise before the scan even started (request_too_large from one provider probe, a bytez 500, a few HTTPError/TimeoutError entries — all logged as non-fatal sidecar startup warnings). This reads as a slow/flaky run classified as provider-unavailable, not a deterministic regression from this PR's changes — it died without completing rather than reporting a real finding.

Re-ran the failed job once (rerun_failed_jobs on this run) per this session's flake-handling policy. If it fails again with the same or a different deterministic signal, that's real and I'll root-cause it properly rather than re-running again.


Generated by Claude Code


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Re-run failed identically (run 33334177387, job 99331265840): exit code 124 at 5403s, Vulnerabilities 0, same discovery-time noise as the first attempt. Per this session's policy, a second identical failure is real, not a flake — root-causing it now instead of re-running again.

This is a deterministic timeout, not a code defect in this PR's diff: Strix's own internal process budget (STRIX_PROCESS_TIMEOUT_SECONDS=5400 in the central .github strix.yml) is consistently insufficient to complete a full scan of this repository — both attempts hit the exact same ~90-minute wall with zero reported vulnerabilities, meaning the scan is being killed mid-analysis rather than concluding. Nothing in this PR's ~115-300 line diff plausibly explains a scan-duration regression, and this org's own standing operating directive already anticipates and accepts this: "중앙 OpenCode, Strix, Noema는 모델당 두 시간 이상 걸릴 수 있음을 수용한다" (central OpenCode/Strix/Noema may take over two hours per model — accepted).

Root cause: the central strix.yml workflow's time budgets (STRIX_PROCESS_TIMEOUT_SECONDS=5400s / STRIX_TOTAL_TIMEOUT_SECONDS=5700s, timeout-minutes: 100 on the Run Strix step, timeout-minutes: 120 on the job) are too tight for a repository of this size, not something a PR to this repo can fix — the fix belongs in ContextualWisdomLab/.github. I'm dispatching that central fix now (raising the budgets, consistent with the org's own stated tolerance, and updating the contract tests that pin those exact values) rather than widening this PR with an unrelated workflow change. This PR stays on its own merits; I'll keep this PR's Strix check watched until the central budget fix lands and this repo gets a fresh scan under it.


Generated by Claude Code


Generated by Claude Code

seonghobae pushed a commit to ContextualWisdomLab/.github that referenced this pull request Aug 31, 2026
… on large repos

The central required Strix Security Scan was deterministically timing out on
contextual-orchestrator (a large, actively-growing repo: ~10,000+ statements,
thousands of tests). Two consecutive real runs
(ContextualWisdomLab/contextual-orchestrator#933, jobs 99317982462 and
99331265840) both failed identically with exit code 124 -- the scanner's own
process-timeout wrapper killing an in-progress, zero-finding scan, not a
reported vulnerability or a genuine provider outage.

This org's standing operating directive (docs/product-goal-directive.md)
explicitly accepts that central OpenCode/Strix/Noema scans may take more than
two hours per model. The previous budgets (90-minute process / 95-minute
total / 100-minute outer deadline / 100-minute step / 120-minute job) did not
honor that tolerance for a repo this size.

Raise every budget in the same chain, preserving proportional ordering and
buffers (process < total < outer-deadline < step-timeout < job-timeout):

- process_budget_seconds: 5400 -> 9000 (150 min)
- STRIX_TOTAL_TIMEOUT_SECONDS: 5700 -> 9300 (155 min)
- strix_gate_deadline outer bound: +6000 -> +9600 (160 min)
- "Run Strix (quick)" step timeout-minutes: 100 -> 170
- strix job timeout-minutes: 120 -> 200

200 minutes stays comfortably under GitHub Actions' 360-minute hosted-runner
job timeout cap, with ~30 minutes of margin for the job's other steps
(checkout, sidecar provisioning, artifact upload).

Retry/backoff mechanics (STRIX_GATE_RETRY_BACKOFF_SECONDS, the 3-attempt
bounded retry count) are untouched -- this is purely a time-ceiling fix, not
a retry-logic change.

Updates the matching contract assertions in
scripts/ci/test_strix_quick_gate.sh so the pinned numeric strings stay in
lockstep with strix.yml.

Verified: actionlint on strix.yml (clean), full `coverage run -m pytest
tests` (1903 passed, 1 skipped, 21 subtests, no regressions -- the one
pre-existing pingora_edge_policy.py coverage gap was already fixed upstream
on this branch by the time of push), `interrogate` (100%), `bash -n` on the
edited script, and a full real run of
`bash scripts/ci/test_strix_quick_gate.sh` (PASS, ~5 min with the fast CI
fixture env vars).

Refs: ContextualWisdomLab/contextual-orchestrator#933

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX
seonghobae added a commit to ContextualWisdomLab/.github that referenced this pull request Aug 31, 2026
…1433)

Adds free_family_diversity evidence for Strix free-pool routing decisions, and raises the central Strix Security Scan's time budgets (90/95/100/100/120 min -> 150/155/160/170/200 min) to fix a deterministic timeout reproduced twice on ContextualWisdomLab/contextual-orchestrator#933 (org's product-goal-directive.md already accepts 2+ hour scans per model).

Bypass-merged per explicit user authorization: all substantive checks (Strix, Noema, Security Scan, SAST, CodeQL, OSV, SBOM, Scorecard, coverage) are green on the exact current head; only the OpenCode formal-verdict dispatch had not completed, and this fix is foundational/blocking for Strix across every repo in the org.
@seonghobae

Copy link
Copy Markdown
Contributor Author

설계 결정: 추상화는 사용하지 않습니다. 와 는 같은 endpoint를 쓰더라도 서로 다른 모델 catalog를 노출할 수 있는 독립 credential/account boundary이므로 각각 discovery·보존·실패 판정을 수행해야 합니다. 모델 간 동적 선택과 latency/quality evidence 공유는 provider 이름 추정이 아니라 오직 명시적 경계에서만 수행하십시오. 이 PR이 해당 전제를 포함하면 제거하거나 현재 head에서 재검증해 주세요.

@seonghobae

Copy link
Copy Markdown
Contributor Author

일반화: 앞 코멘트의 NVIDIA는 예시일 뿐입니다. 모든 KV credential은 독립 provider-account/catalog boundary입니다. 같은 vendor/endpoint/model ID라도 서로 다른 key의 모델·권한·가격·privacy·가용성·실패 상태가 같다고 추정하거나 dedupe하지 마십시오. 개념은 제거하며, 논리적 모델 동등성과 latency/quality evidence 공유는 오직 operator가 명시한 으로만 성립합니다.

Copy link
Copy Markdown
Contributor Author

Confirmed: _provider_family()'s nvidia_nim/nvidia_nim_sub collapse (model_discovery.py:1512-1516) is pre-existing (introduced in dcdb04b7, unrelated to this PR's history) and is not touched, introduced, or relied upon by this PR's diff. This PR's own fix (free_discovered_models/general_free_serving_candidates/TaskOrchestrator._is_free_agent/_is_general_free_agent) evaluates each DiscoveredModel's own input_modalities evidence independently, per model, regardless of which credential discovered it — it never merges or shares catalog/evidence across NVIDIA_NIM_API_KEY and NVIDIA_NIM_API_KEY_SUB. No premise to remove here.

For visibility: _provider_family's collapse is used by select_top_n_cheapest_discovered_agents (same file) and provider_bootstrap.select_provider_diverse_models to cap "provider-diverse" bootstrap selections to one model per family — meaning a genuinely distinct model available only through NVIDIA_NIM_API_KEY_SUB's own account entitlements could currently be deferred/dropped in favor of an already-selected NVIDIA_NIM_API_KEY model, purely because they share the nvidia_nim family label. The docstring's stated intent is outage-domain diversity (defensible on its own terms — a NIM endpoint outage takes both credentials down together), but whether that should also govern catalog-completeness/model-selection is a separate, real design question this PR doesn't touch. Filing that as its own follow-up rather than folding it in here.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Re: the generalization (NVIDIA was just the example, the principle is any two KV credentials must never be assumed/deduped as equivalent) — checked the general dedup path this applies to: _deduplicate_discovered_models/_serving_identity (model_discovery.py:445-447) already keys on (provider_name, model_id), and this repo's discovery layer already assigns nvidia_nim vs nvidia_nim_sub distinct provider_name values specifically so two credentials never collide under that key — so the general dedup mechanism already respects credential independence.

The one place that re-collapses that distinction is _provider_family(), which explicitly maps both back to "nvidia_nim" for provider-diversity-capped selection (select_top_n_cheapest_discovered_agents, provider_bootstrap.select_provider_diverse_models) — the same function my previous comment flagged. I've dispatched a dedicated investigation/fix task against exactly that function (not scoped to NVIDIA specifically — the agent is checking whether the fix should be general, e.g. keying the diversity cap by credential_name rather than a hand-maintained family map, so it holds for any future credential pair sharing a vendor, not just NIM/NIM_SUB). Still out of scope for this PR's own diff; tracking separately.


Generated by Claude Code

@seonghobae

Copy link
Copy Markdown
Contributor Author

최종 일반 계약: 모든 KV credential은 독립 provider-account/catalog boundary입니다. 같은 vendor, endpoint, model ID라도 서로 다른 key의 모델, 권한, 가격, privacy, 가용성, 실패 상태를 같다고 추정하거나 dedupe하지 않습니다. provider family는 없으며, 논리적 모델 동등성과 latency/quality evidence 공유는 오직 operator가 명시한 model_group으로만 성립합니다.

@seonghobae
seonghobae merged commit ba5e00c into main Aug 31, 2026
36 of 40 checks passed
@seonghobae
seonghobae deleted the fix/nim-vision-tool-calling-pool branch August 31, 2026 02:27
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.

2 participants