Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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]

### Fixed
- NIM discovery catalog body bound + dry-run call budget uses max_steps;
offline cost-quality rejects malformed scripted answers and zeros failed-cell usage.

### 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
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` 블록에 유지하고, 다른 변경 유형 제목이나
항목은 수정하지 마십시오.

- Offline NIM cost-quality comparison harness (`nim_cost_quality` +
`nim-cost-quality-offline` CLI) for issue #86 post-discovery: locked task
manifest scorers, honest unknown actual/hypothetical cost, policy summaries,
and quality-latency / quality-cost Pareto frontiers without live egress.
- Offline NIM capability inventory + dry-run benchmark plan (issue #86).
- `discover-nim-models` CLI and `nim_discovery` module (issue #86): allowlisted
NVIDIA HTTPS `/v1/models` only; offline fixture status; unique agent ids on
slug collision; live tests require `RUN_LIVE_NIM_TESTS=1`.
- Role-differentiated sampling temperatures for paper-role ablation.

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

## [0.1.0] - 2026-07-13

### Added
- Initial OpenAI-compatible orchestration gateway.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,35 @@ 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
python -m contextual_orchestrator discover-nim-models --benchmark-dry-run
python -m contextual_orchestrator discover-nim-models --capability-probe-plan
python -m contextual_orchestrator discover-nim-models --capability-probe-dry-run examples/nim_capability_probe_fixtures.json
```

Requires `NVIDIA_NIM_API_KEY` in the KV (`register-credential`) for live catalog
listing. Offline dry-run / capability inventory paths stay secret-free.

### Offline NIM cost-quality (issue #86)

```bash
python -m contextual_orchestrator nim-cost-quality-offline \
--task-manifest examples/nim_task_manifest_offline.json
python -m contextual_orchestrator nim-cost-quality-offline \
--task-manifest examples/nim_task_manifest_offline.json \
--pricing-scenario examples/nim_pricing_scenario_offline.json \
--markdown
python -m contextual_orchestrator nim-cost-quality-offline \
--use-mock-orchestrator --agents examples/agents.mock.json --markdown
python tests/test_nim_cost_quality.py
```

Compares route/conduct/direct policies on a locked task set with honest
`unknown` costs unless a pricing scenario covers every model. Optional
`--use-mock-orchestrator` exercises Fugu `route_once` and Conductor/TRINITY
`conduct` on `mock://` agents. Never uses `COPILOT_GITHUB_TOKEN` for model calls.
227 changes: 227 additions & 0 deletions contextual_orchestrator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,238 @@ 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 (
DEFAULT_NIM_MODELS_URL,
NimDiscoveryError,
build_benchmark_plan_dry_run,
build_capability_inventory,
build_capability_probe_plan,
discover_nim_models,
models_to_agent_pool_entries,
run_capability_probes_dry_run,
validate_nim_models_url,
)

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=DEFAULT_NIM_MODELS_URL,
help=(
"HTTPS NVIDIA catalog URL (default: integrate.api.nvidia.com/v1/models). "
"Only allowlisted NVIDIA hosts with path /v1/models are accepted; "
"the API key is never sent to other origins."
),
)
parser.add_argument(
"--as-agent-pool",
action="store_true",
help="Emit agent-pool JSON entries instead of the discovery report.",
)
parser.add_argument(
"--capability-inventory",
action="store_true",
help="Emit offline capability-hint inventory for discovered model ids (issue #86 dry path).",
)
parser.add_argument(
"--benchmark-dry-run",
action="store_true",
help="Emit a fail-closed dry-run benchmark plan with unknown costs (issue #86).",
)
parser.add_argument(
"--hard-request-budget",
type=int,
default=100,
help="Hard call budget for dry-run admission (default: 100).",
)
parser.add_argument(
"--capability-probe-plan",
action="store_true",
help="Emit offline capability probe plan (models x probe kinds) without network.",
)
parser.add_argument(
"--capability-probe-dry-run",
metavar="FIXTURE_JSON",
default=None,
help=(
"Classify offline probe fixtures from JSON list of "
"{model_id, probe_kind, status_code|error_class, body?} rows."
),
)
args = parser.parse_args(argv)
try:
models_url = validate_nim_models_url(args.models_url)
except NimDiscoveryError as exc:
parser.error(str(exc))
if args.capability_probe_dry_run:
try:
with open(args.capability_probe_dry_run, encoding="utf-8") as handle:
fixtures = json.load(handle)
plan = run_capability_probes_dry_run(
fixtures if isinstance(fixtures, list) else fixtures.get("probe_rows") or fixtures.get("fixtures") or [],
hard_request_budget=args.hard_request_budget,
)
except (NimDiscoveryError, OSError, ValueError, TypeError) as exc:
parser.error(str(exc))
print(json.dumps(plan, ensure_ascii=False, indent=2))
return
Comment on lines +126 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

두 서브커맨드에서 운영자 입력 오류가 선언된 예외 계약을 벗어납니다. 각 명령은 입력 오류를 parser.error로 변환하는 계약을 선언합니다. 그러나 두 경로 모두 JSON 입력의 내부 구조를 검사하지 않으므로 except 목록에 없는 예외가 발생하고 CLI가 traceback으로 종료됩니다.

  • contextual_orchestrator/__main__.py#L126-L137: fixture JSON의 최상위 타입을 검사하십시오. list와 dict가 아닌 값에서 .get 호출로 발생하는 AttributeError를 제거하십시오.
  • contextual_orchestrator/__main__.py#L239-L250: load_agents 호출을 감싸서 KeyErrorparser.error로 변환하십시오.
🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 127-127: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(args.capability_probe_dry_run, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[info] 135-135: use jsonify instead of json.dumps for JSON output
Context: json.dumps(plan, ensure_ascii=False, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

📍 Affects 1 file
  • contextual_orchestrator/__main__.py#L126-L137 (this comment)
  • contextual_orchestrator/__main__.py#L239-L250
🤖 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 `@contextual_orchestrator/__main__.py` around lines 126 - 137, Validate the
fixture JSON root in the capability_probe_dry_run path before accessing dict
methods: accept only list or dict values, normalize supported dict keys as
currently intended, and route invalid structures through parser.error without
allowing AttributeError. In contextual_orchestrator/__main__.py lines 126-137,
update the fixture handling accordingly; in lines 239-250, wrap the load_agents
call so KeyError is converted to parser.error.


report = discover_nim_models(models_url=models_url)
model_ids = report.get("model_ids") or []
if args.capability_probe_plan:
try:
plan = build_capability_probe_plan(
model_ids, hard_request_budget=args.hard_request_budget
)
except NimDiscoveryError as exc:
parser.error(str(exc))
print(json.dumps(plan, ensure_ascii=False, indent=2))
return
if args.benchmark_dry_run:
try:
plan = build_benchmark_plan_dry_run(
model_ids, hard_request_budget=args.hard_request_budget
)
except NimDiscoveryError as exc:
parser.error(str(exc))
print(json.dumps(plan, ensure_ascii=False, indent=2))
elif args.capability_inventory:
print(json.dumps(build_capability_inventory(model_ids), ensure_ascii=False, indent=2))
elif args.as_agent_pool:
print(json.dumps(models_to_agent_pool_entries(model_ids), ensure_ascii=False, indent=2))
else:
print(json.dumps(report, ensure_ascii=False, indent=2))


def _nim_cost_quality_offline_command(argv: list[str]) -> None:
"""Run the offline cost-quality harness against a locked task manifest (issue #86)."""
from .nim_cost_quality import (
CostQualityContractError,
build_orchestrator_policy_runners,
build_scripted_policy_runners,
load_pricing_scenario,
load_task_manifest,
locked_evaluation_tasks,
render_cost_quality_markdown,
run_offline_cost_quality,
validate_scripted_answers,
)
from .orchestrator import TaskOrchestrator, load_agents

parser = argparse.ArgumentParser(
prog="python -m contextual_orchestrator nim-cost-quality-offline",
description=(
"Offline cost-quality comparison for issue #86 (post-discovery). "
"Uses scripted answers by default so CI never needs NVIDIA_NIM_API_KEY. "
"Pass --use-mock-orchestrator to drive Fugu route_once vs Conductor "
"conduct through mock:// agents. Never invents prices."
),
)
parser.add_argument(
"--task-manifest",
default="examples/nim_task_manifest_offline.json",
help="Path to the versioned task manifest (locked split only).",
)
parser.add_argument(
"--pricing-scenario",
default=None,
help="Optional USD-per-million-token scenario JSON; omit to keep costs unknown.",
)
parser.add_argument(
"--scripted-answers",
default=None,
help=(
"Optional JSON map {task_id: {policy_name: answer}}. "
"When omitted, answers are empty (scores zero) for structural dry-run only."
),
)
parser.add_argument(
"--use-mock-orchestrator",
action="store_true",
help=(
"Run policies via TaskOrchestrator route_once/conduct on --agents "
"(default examples/agents.mock.json). Mutually exclusive with "
"--scripted-answers."
),
)
parser.add_argument(
"--agents",
default="examples/agents.mock.json",
help="Agent pool JSON for --use-mock-orchestrator (mock:// recommended).",
)
parser.add_argument(
"--model-id",
default="mock-scripted",
help="Model id recorded on cells and used for pricing lookups (default: mock-scripted).",
)
parser.add_argument(
"--markdown",
action="store_true",
help="Emit a short markdown summary instead of the full JSON report.",
)
args = parser.parse_args(argv)
if args.use_mock_orchestrator and args.scripted_answers:
parser.error("--use-mock-orchestrator cannot be combined with --scripted-answers")
try:
manifest = load_task_manifest(args.task_manifest)
tasks = locked_evaluation_tasks(manifest)
pricing = load_pricing_scenario(args.pricing_scenario)
if args.use_mock_orchestrator:
agents = load_agents(args.agents)
if not agents:
parser.error("--agents pool is empty")
non_mock = [a.id for a in agents if not str(a.base_url).startswith("mock://")]
if non_mock:
parser.error(
"--use-mock-orchestrator requires mock:// agents only; "
f"non-mock: {non_mock}"
)
orchestrator = TaskOrchestrator(agents)
runners = build_orchestrator_policy_runners(orchestrator)
model_id = args.model_id if args.model_id != "mock-scripted" else "mock-orchestrator"
else:
answers: dict = {}
if args.scripted_answers:
with open(args.scripted_answers, encoding="utf-8") as handle:
raw_answers = json.load(handle)
answers = validate_scripted_answers(raw_answers)
runners = build_scripted_policy_runners(answers, model_id=args.model_id)
model_id = args.model_id
report = run_offline_cost_quality(
tasks=tasks,
policy_runners=runners,
model_id=model_id,
pricing_scenario=pricing,
)
if args.use_mock_orchestrator:
report["runner_backend"] = "mock_orchestrator"
report["agent_pool_path"] = args.agents
else:
report["runner_backend"] = "scripted_answers"
except (CostQualityContractError, OSError, ValueError) as exc:
parser.error(str(exc))
if args.markdown:
print(render_cost_quality_markdown(report))
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
if len(sys.argv) > 1 and sys.argv[1] == "nim-cost-quality-offline":
_nim_cost_quality_offline_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
8 changes: 4 additions & 4 deletions contextual_orchestrator/cost_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,12 +583,12 @@ 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,),
)
if cur.fetchone() is None:
cur.execute(
cur.execute( # nosemgrep -- sqlalchemy-execute-raw-query FP: only DB-API placeholder chars are interpolated; values are bound.
"INSERT INTO cost_attribution_dimensions "
f"(dimension_name, dimension_label, dimension_order) VALUES ({ph}, {ph}, {ph})", # nosec B608 - ph is a DB-API placeholder.
(name, label, order),
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
Loading
Loading