-
Notifications
You must be signed in to change notification settings - Fork 1
feat: NVIDIA NIM model discovery (issue #86 scaffold) #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
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 ae6fb34
feat: role-differentiated sampling temperature for reasoning ablation
seonghobae fba4cab
test: drive nim_discovery to 100% statement coverage
seonghobae 7aa6039
feat: CLI discover-nim-models subcommand via KV credential
seonghobae 9fcc987
docs: add CHANGELOG for release-ready versioning
seonghobae d3a0a17
docs: document role_temperature ablation knobs
seonghobae 66eeacf
docs: document discover-nim-models CLI in README
seonghobae 534aa3f
fix(security): precise nosemgrep for NIM urllib allowlist path
seonghobae 0ac5609
fix(security): nosemgrep on NIM Request construction too
seonghobae 325a1a4
fix(security): harden NIM discovery against credential SSRF
seonghobae b7fdef2
test: cover NIM URL validation edge cases for 100% discovery coverage
seonghobae 4edd3e8
feat(nim): offline capability inventory and dry-run benchmark plan
seonghobae 619b1f4
docs: CHANGELOG entry for offline NIM dry-run plan
seonghobae 0855fa8
feat(nim): offline cost-quality comparison after discovery (issue #86…
seonghobae 6ccec23
fix(nim): harden discovery bounds and cost-quality contracts
seonghobae 1f7f26d
test: give NIM urlopen fixture response headers for Content-Length
seonghobae bbb5849
feat(nim): mock orchestrator path for offline cost-quality (issue #86)
seonghobae 61a3943
feat(nim): offline capability probe plan and fixture classification
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| - `discover-nim-models` CLI and `nim_discovery` module (issue #86). | ||
| - Role-differentiated sampling temperatures for paper-role ablation. | ||
|
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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| raw = response.read() | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
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), | ||
| } | ||
|
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), | ||
| } | ||
|
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] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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아래로 병합하십시오.♻️ 제안 수정
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 19-19: Multiple headings with the same content
(MD024, no-duplicate-heading)
🤖 Prompt for AI Agents