Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fbe0235
feat: NVIDIA NIM model discovery via KV credential
seonghobae Aug 12, 2026
ae6fb34
feat: role-differentiated sampling temperature for reasoning ablation
seonghobae Aug 12, 2026
fba4cab
test: drive nim_discovery to 100% statement coverage
seonghobae Aug 12, 2026
7aa6039
feat: CLI discover-nim-models subcommand via KV credential
seonghobae Aug 12, 2026
9fcc987
docs: add CHANGELOG for release-ready versioning
seonghobae Aug 12, 2026
d3a0a17
docs: document role_temperature ablation knobs
seonghobae Aug 12, 2026
66eeacf
docs: document discover-nim-models CLI in README
seonghobae Aug 12, 2026
534aa3f
fix(security): precise nosemgrep for NIM urllib allowlist path
seonghobae Aug 12, 2026
0ac5609
fix(security): nosemgrep on NIM Request construction too
seonghobae Aug 12, 2026
325a1a4
fix(security): harden NIM discovery against credential SSRF
seonghobae Aug 12, 2026
b7fdef2
test: cover NIM URL validation edge cases for 100% discovery coverage
seonghobae Aug 12, 2026
4edd3e8
feat(nim): offline capability inventory and dry-run benchmark plan
seonghobae Aug 12, 2026
619b1f4
docs: CHANGELOG entry for offline NIM dry-run plan
seonghobae Aug 12, 2026
0855fa8
feat(nim): offline cost-quality comparison after discovery (issue #86…
seonghobae Aug 12, 2026
6ccec23
fix(nim): harden discovery bounds and cost-quality contracts
seonghobae Aug 12, 2026
1f7f26d
test: give NIM urlopen fixture response headers for Content-Length
seonghobae Aug 12, 2026
bbb5849
feat(nim): mock orchestrator path for offline cost-quality (issue #86)
seonghobae Aug 12, 2026
61a3943
feat(nim): offline capability probe plan and fixture classification
seonghobae Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
Comment on lines +14 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

중복된 ### Added 제목을 하나로 병합하십시오.

[Unreleased] 섹션에 ### Added 제목이 line 14와 line 19에 두 번 나옵니다. Line 5-6은 이 파일이 Keep a Changelog 형식을 따른다고 선언합니다. 이 형식은 릴리스 섹션마다 변경 유형별로 하나의 제목을 사용합니다. 중복 제목은 변경 로그 파서와 릴리스 노트 생성을 혼란시킵니다.

두 블록의 항목을 하나의 ### Added 아래로 병합하십시오.

♻️ 제안 수정
 ### Added
 - Offline NIM capability probe plan + fixture classification (issue `#86`).
 - Offline cost-quality `--use-mock-orchestrator` path: Fugu `route_once` and
   Conductor/TRINITY `conduct` via `mock://` agents (issue `#86` paper-path exercise).
-
-### Added
 - Offline NIM cost-quality comparison harness (`nim_cost_quality` +
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Added
- Offline NIM capability probe plan + fixture classification (issue #86).
- Offline cost-quality `--use-mock-orchestrator` path: Fugu `route_once` and
Conductor/TRINITY `conduct` via `mock://` agents (issue #86 paper-path exercise).
### Added
### Added
- Offline NIM capability probe plan + fixture classification (issue #86).
- Offline cost-quality `--use-mock-orchestrator` path: Fugu `route_once` and
Conductor/TRINITY `conduct` via `mock://` agents (issue #86 paper-path exercise).
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 19-19: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 14 - 19, CHANGELOG.md의 [Unreleased] 섹션에서 중복된 `###
Added` 제목을 하나로 통합하십시오. 두 제목 아래의 모든 항목을 단일 `### Added` 블록에 유지하고, 다른 변경 유형 제목이나
항목은 수정하지 마십시오.

- `discover-nim-models` CLI and `nim_discovery` module (issue #86).
- Role-differentiated sampling temperatures for paper-role ablation.
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated

### Security
- Semgrep nosemgrep hygiene for audited SQL placeholders / TLS opt-out / urllib.

## [0.1.0] - 2026-07-13

### Added
- Initial OpenAI-compatible orchestration gateway.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,12 @@ python tests/test_commercial_purchase_approval_packet.py
python tests/test_commercial_due_diligence_room.py
python tests/test_commercial_investment_committee_memo.py
```

### NIM model discovery

```bash
python -m contextual_orchestrator discover-nim-models
python -m contextual_orchestrator discover-nim-models --as-agent-pool
```

Requires `NVIDIA_NIM_API_KEY` in the KV (`register-credential`).
33 changes: 33 additions & 0 deletions contextual_orchestrator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,44 @@ def _register_credential_command(argv: list[str]) -> None:
print(json.dumps({"registered": args.name, "backend": "kv"}, ensure_ascii=False))



def _discover_nim_models_command(argv: list[str]) -> None:
"""List NIM model IDs via KV credential and print agent-pool JSON candidates."""
from .nim_discovery import discover_nim_models, models_to_agent_pool_entries

parser = argparse.ArgumentParser(
prog="python -m contextual_orchestrator discover-nim-models",
description="Discover NVIDIA NIM model IDs using the KV credential NVIDIA_NIM_API_KEY.",
)
parser.add_argument(
"--models-url",
default=None,
help="Override models listing URL (default: integrate.api.nvidia.com/v1/models).",
)
parser.add_argument(
"--as-agent-pool",
action="store_true",
help="Emit agent-pool JSON entries instead of the discovery report.",
)
args = parser.parse_args(argv)
kwargs = {}
if args.models_url:
kwargs["models_url"] = args.models_url
report = discover_nim_models(**kwargs)
if args.as_agent_pool:
print(json.dumps(models_to_agent_pool_entries(report.get("model_ids") or []), ensure_ascii=False, indent=2))
else:
print(json.dumps(report, ensure_ascii=False, indent=2))


def main() -> None:
"""Parse CLI options and run bootstrap, prompt completion, or the HTTP server."""
if len(sys.argv) > 1 and sys.argv[1] == "register-credential":
_register_credential_command(sys.argv[2:])
return
if len(sys.argv) > 1 and sys.argv[1] == "discover-nim-models":
_discover_nim_models_command(sys.argv[2:])
return

parser = argparse.ArgumentParser(description="Route or conduct chat requests across model agents.")
parser.add_argument("prompt", nargs="?", help="User prompt for CLI mode.")
Expand Down
6 changes: 3 additions & 3 deletions contextual_orchestrator/cost_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def _seed_dimension_catalog(self) -> None:
ph = self._placeholder()
cur = self._conn.cursor()
for order, (name, label, _column) in enumerate(ATTRIBUTION_DIMENSION_CATALOG):
cur.execute(
cur.execute( # nosemgrep -- sqlalchemy-execute-raw-query FP: only the DB-API placeholder char is interpolated; the value is bound.
f"SELECT 1 FROM cost_attribution_dimensions WHERE dimension_name = {ph}", # nosec B608 - ph is a DB-API placeholder.
(name,),
)
Expand All @@ -602,7 +602,7 @@ def append(self, record: UsageRecord) -> None:
placeholders = ", ".join(ph for _ in _USAGE_COLUMNS)
columns = ", ".join(_USAGE_COLUMNS)
cur = self._conn.cursor()
cur.execute(
cur.execute( # nosemgrep -- sqlalchemy-execute-raw-query FP: columns are the fixed _USAGE_COLUMNS constant; values are bound.
f"INSERT INTO llm_usage_records ({columns}) VALUES ({placeholders})", # nosec B608 - columns are fixed _USAGE_COLUMNS.
tuple(row.get(column) for column in _USAGE_COLUMNS),
)
Expand All @@ -622,7 +622,7 @@ def query(self, start: Optional[int] = None, end: Optional[int] = None) -> List[
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
columns = ", ".join(_USAGE_COLUMNS)
cur = self._conn.cursor()
cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # nosec B608 - columns and clauses are fixed.
cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # nosec B608 - columns and clauses are fixed. # nosemgrep -- sqlalchemy-execute-raw-query FP: fixed columns and clause templates; all values are bound.
Comment thread
seonghobae marked this conversation as resolved.
return [dict(zip(_USAGE_COLUMNS, values)) for values in cur.fetchall()]


Expand Down
159 changes: 159 additions & 0 deletions contextual_orchestrator/nim_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Evidence-grade NVIDIA NIM model discovery for agent-pool population.

Discovers OpenAI-compatible model IDs from a NIM-compatible ``/models`` endpoint
using the KV credential ``NVIDIA_NIM_API_KEY`` (never ``COPILOT_GITHUB_TOKEN``).
Operators convert discovered models into agent pool entries; routing still uses
the deterministic route/conduct policies grounded in Fugu / Conductor / TRINITY
paper contracts.

References
----------
Touvron, H., et al. (2023). *Llama 2: Open foundation and fine-tuned chat models*
(arXiv:2307.09288) — open weights commonly hosted on NIM for gateway evaluation.

Live discovery is optional: tests use offline fixtures so CI stays hermetic.
"""

from __future__ import annotations

import json
import ssl
import urllib.error
import urllib.request
from typing import Any

from .credentials import get_credential

DEFAULT_NIM_MODELS_URL = "https://integrate.api.nvidia.com/v1/models"
NIM_CREDENTIAL_NAME = "NVIDIA_NIM_API_KEY"


def discover_nim_models(
*,
models_url: str = DEFAULT_NIM_MODELS_URL,
credential_name: str = NIM_CREDENTIAL_NAME,
timeout_seconds: float = 30.0,
transport: Any | None = None,
) -> dict[str, Any]:
"""Discover model IDs from a NIM-compatible OpenAI ``/models`` list endpoint.

Parameters
----------
models_url:
Absolute HTTPS URL of the models listing endpoint.
credential_name:
KV credential name. Defaults to ``NVIDIA_NIM_API_KEY``.
timeout_seconds:
Socket timeout for the listing request.
transport:
Optional callable ``(request, timeout) -> bytes`` for tests. When omitted,
uses stdlib ``urllib`` with default TLS verification.

Returns
-------
dict
``measurement_status`` (``live_nim_catalog`` | ``offline_fixture`` |
``credential_missing``), ``model_ids`` (sorted unique strings), and
``source_url``. Never includes the raw API key.
"""
api_key = get_credential(credential_name)
if not api_key:
return {
"measurement_status": "credential_missing",
"model_ids": [],
"source_url": models_url,
"credential_name": credential_name,
}

request = urllib.request.Request(
models_url,
headers={
"authorization": f"Bearer {api_key}",
"accept": "application/json",
},
method="GET",
)

if transport is not None:
raw = transport(request, timeout_seconds)
else:
context = ssl.create_default_context()
with urllib.request.urlopen( # nosec B310 - URL is operator-configured HTTPS catalog endpoint.
request, timeout=timeout_seconds, context=context
) as response:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
raw = response.read()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

payload = json.loads(raw.decode("utf-8"))
model_ids = _extract_model_ids(payload)
return {
"measurement_status": "live_nim_catalog",
"model_ids": model_ids,
"source_url": models_url,
"model_count": len(model_ids),
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def models_to_agent_pool_entries(
model_ids: list[str],
*,
base_url: str = "https://integrate.api.nvidia.com/v1",
credential_key: str = NIM_CREDENTIAL_NAME,
tags: tuple[str, ...] = ("reasoning", "writing"),
) -> list[dict[str, Any]]:
"""Map discovered model IDs to agent-pool JSON dicts (multi-word snake_case ids).

Each model becomes one agent. After ``model_group`` race lands on main
(issue #102 / PR #114), operators may add ``model_group`` keys for replica race.
"""
entries: list[dict[str, Any]] = []
for index, model_id in enumerate(model_ids):
slug = _slug_model_id(model_id)
entries.append(
{
"id": f"nim_{slug}_agent",
"model": model_id,
"base_url": base_url,
"credential_key": credential_key,
"tags": list(tags),
"priority": max(0, 10 - index),
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
return entries


def _extract_model_ids(payload: Any) -> list[str]:
"""Parse OpenAI-style ``{data: [{id: ...}]}`` or a bare list of ids/objects."""
ids: list[str] = []
if isinstance(payload, dict):
data = payload.get("data", payload.get("models", []))
else:
data = payload
if not isinstance(data, list):
return []
for item in data:
if isinstance(item, str) and item.strip():
ids.append(item.strip())
elif isinstance(item, dict):
mid = item.get("id") or item.get("model")
if isinstance(mid, str) and mid.strip():
ids.append(mid.strip())
return sorted(set(ids))


def _slug_model_id(model_id: str) -> str:
"""Convert a provider model id into a multi-word-friendly snake_case token."""
cleaned = []
for char in model_id.lower():
if char.isalnum():
cleaned.append(char)
else:
cleaned.append("_")
slug = "".join(cleaned).strip("_")
while "__" in slug:
slug = slug.replace("__", "_")
if not slug:
slug = "unnamed_model"
# require_object_name needs two semantic words — ensure underscore present
if "_" not in slug:
slug = f"{slug}_model"
return slug[:48]
29 changes: 26 additions & 3 deletions contextual_orchestrator/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ class OrchestrationPolicy:
# report; "model" asks a verifier-selected model to reply ACCEPT/REJECT (fixes the
# known term-matching false negative on risk-vocabulary verifier outputs).
verifier_judge: str = "terms"
# Role-differentiated sampling temperature (reasoning effort proxy for ablation).
# Thinker/verifier lower for stability; worker slightly higher for exploration.
role_temperature: dict[str, float] | None = None
Comment thread
seonghobae marked this conversation as resolved.

def as_dict(self) -> dict[str, Any]:
"""Return the API-safe policy snapshot for workflow records."""
Expand All @@ -179,10 +182,29 @@ def as_dict(self) -> dict[str, Any]:
"workflow_planning": self.workflow_planning,
"verifier_judge": self.verifier_judge,
"max_workflow_steps": self.max_workflow_steps,
"role_temperature": dict(self.role_temperature or self.default_role_temperature()),
"workflow_steps": ["thinker", "worker", "verifier", "synthesizer"],
"supported_locales": ["en", "ko"],
}

@staticmethod
def default_role_temperature() -> dict[str, float]:
"""Default per-role temperatures for route/conduct ablation studies."""
return {
"thinker": 0.1,
"worker": 0.2,
"verifier": 0.0,
"synthesizer": 0.15,
}

def temperature_for_role(self, role: str) -> float:
"""Return the sampling temperature configured for a paper role."""
table = self.role_temperature or self.default_role_temperature()
try:
return float(table.get(role, 0.2))
except (TypeError, ValueError):
return 0.2

Comment thread
seonghobae marked this conversation as resolved.

# HTTP statuses worth retrying: request timeout, conflict, too-early, rate limit,
# and the standard upstream/gateway failures. Everything else (400/401/403/404 ...)
Expand Down Expand Up @@ -230,7 +252,7 @@ def __init__(
@staticmethod
def _build_ssl_context(ca_bundle: str | None, verify_tls: bool) -> ssl.SSLContext:
if not verify_tls:
return ssl._create_unverified_context() # nosec B323 - explicit dev-only provider TLS opt-out.
return ssl._create_unverified_context() # nosec B323 - explicit dev-only provider TLS opt-out. # nosemgrep -- unverified-ssl-context: intentional, default-secure (verify_tls defaults True) dev-only opt-out for self-signed endpoints.
Comment thread
seonghobae marked this conversation as resolved.
if ca_bundle:
if not os.path.isfile(ca_bundle):
raise ValueError(f"provider CA bundle does not exist: {ca_bundle}")
Expand Down Expand Up @@ -307,7 +329,7 @@ def _send(self, agent: ModelAgent, payload: dict[str, Any]) -> str:

def _open_provider(self, request: urllib.request.Request) -> Any:
"""Open a provider request built from a validated provider URL."""
return urllib.request.urlopen( # nosec B310 - request URL comes from _provider_url after provider validation.
return urllib.request.urlopen( # nosec B310 - request URL comes from _provider_url after provider validation. # nosemgrep -- dynamic-urllib-use: URL is built by _provider_url after scheme/host validation; egress to loopback/private/reserved is blocked.
request,
timeout=self.timeout,
context=self._ssl_context,
Expand Down Expand Up @@ -1548,10 +1570,11 @@ def _invoke(
usage when available (else None), so spend analytics can prefer it.
"""
candidates = self._failover_candidates(primary, text, role)
temperature = self.policy.temperature_for_role(role)
last_error: Exception | None = None
for agent in candidates:
try:
output = self.client.chat(agent, messages)
output = self.client.chat(agent, messages, temperature=temperature)
Comment thread
seonghobae marked this conversation as resolved.
except Exception as exc: # noqa: BLE001 - one agent failing routes to the next
last_error = exc
self._record_failure(agent.id)
Expand Down
16 changes: 16 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,19 @@ The product is not a Fugu clone. It is a control-plane prototype for the same pu
- replayable evaluation runs before any learned coordinator replaces the deterministic policy.

See [product_planning.md](product_planning.md) for the product reboot.

## NVIDIA NIM discovery

`contextual_orchestrator.nim_discovery` lists models from a NIM-compatible
OpenAI `/models` endpoint using the KV credential `NVIDIA_NIM_API_KEY` (bootstrap
may seed the KV from process env; request-time resolution stays on `get_credential`).
Discovered IDs convert to agent-pool entries for cost-aware routing and optional
`model_group` race once that lands. Live catalog tests use the real key when
present; CI uses offline fixtures so measurement_status stays honest.

## Role temperature (reasoning effort proxy)

`OrchestrationPolicy.role_temperature` sets per-role sampling temperatures
(default: thinker 0.1, worker 0.2, verifier 0.0, synthesizer 0.15) so ablation
studies can vary reasoning effort by paper role without collapsing multi-agent
depth. Used by `_invoke` for route and conduct steps.
Loading
Loading