Skip to content

feat(models): add typed NemoClient models foundation - #993

Open
maxdubrinsky wants to merge 1 commit into
mainfrom
aircore-876-typed-models-client/md
Open

feat(models): add typed NemoClient models foundation#993
maxdubrinsky wants to merge 1 commit into
mainfrom
aircore-876-typed-models-client/md

Conversation

@maxdubrinsky

@maxdubrinsky maxdubrinsky commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

Introduce the typed NemoClient Models service client that the AIRCORE-876 consumer migration will build on:

  • request/response DTOs (models/types.py)
  • PreparedRequest endpoint builders (models/endpoints.py)
  • the sync/async ModelsClient surface (models/client.py)

Why this is safe to merge now

Purely additive. No existing code imports this module yet, so it changes no runtime behavior and carries zero risk to current consumers. It is split out of the large AIRCORE-876 migration so reviewers can scrutinize the typed contract in isolation, before the mechanical consumer repoint lands.

The breaking, coupled steps land separately: the consumer repoint, the packages/models resources rewrite, and the vendored SDK sync (make vendor, which removes sdk.models.retrieve/create/list/adapters.*).

Also carries the method() descriptor fix this client requires: class-level attribute access now resolves without invoking the wrapped callable, so Mock(spec=ModelsClient) and other introspection tools no longer break. Plus a response.py docstring note on distinguishing 202/204 deletes.

Scope

  • models/{client,endpoints,types}.py
  • client/method.py (descriptor fix), client/response.py (docstring)
  • Tests: tests/models/{test_client,test_endpoints}.py, tests/client/test_method.py

Disjoint from the retry-fix PR; the two can merge in any order.

Verification

  • pytest packages/nemo_platform_plugin/tests -> 1076 passed
  • Endpoint-builder tests cover path/param/body serialization, exclude_unset, conflict-resolver wiring, and response typing
  • ruff check / ruff format --check clean

Summary by CodeRabbit

  • New Features
    • Added typed synchronous and asynchronous clients for managing models, adapters, providers, prompts, deployments, and deployment configurations.
    • Added comprehensive request and response models with validation for model references, inference settings, deployment options, and tool definitions.
    • Added deployment and provider status polling with timeout, failure, and deletion handling.
    • Added helpers for generating OpenAI-compatible provider routes.
    • Improved endpoint introspection, documentation visibility, and class-level access.
  • Documentation
    • Added guidance for distinguishing responses that share multiple successful HTTP status codes.

Introduces the typed Models service client that the AIRCORE-876 consumer
migration will build on: request/response DTOs (types), PreparedRequest
endpoint builders (endpoints), and the sync/async ModelsClient surface
(client).

Purely additive: no existing code imports it yet, so it changes no runtime
behavior and carries zero risk to current consumers. Also carries the
method() descriptor fix that this client requires -- class-level attribute
access now resolves without invoking the wrapped callable, so
Mock(spec=ModelsClient) and other introspection no longer break -- plus a
response docstring note on distinguishing 202/204 deletes.

The consumer repoint, the packages/models resources rewrite, and the
vendored SDK sync land separately as the breaking, coupled steps. Covered
by endpoint-builder, client-surface, and descriptor tests.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@maxdubrinsky
maxdubrinsky requested review from a team as code owners July 30, 2026 17:39
@github-actions github-actions Bot added the feat label Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds typed Models service DTOs and endpoint contracts, synchronous and asynchronous clients, OpenAI route builders, deployment/provider polling, conflict handling, and tests for descriptor introspection, requests, routing, CRUD, and polling.

Changes

Models service client

Layer / File(s) Summary
Descriptor introspection and binding
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/method.py, packages/nemo_platform_plugin/tests/client/test_method.py
EndpointMethod now exposes wrapped endpoint metadata, supports class-level access, and preserves sync/async instance binding and introspection behavior.
Models service DTOs and validation
packages/nemo_platform_plugin/src/nemo_platform_plugin/models/types.py
Adds typed Models service entities, request models, enums, status/configuration schemas, query parameters, and field validators.
Models service endpoint contract
packages/nemo_platform_plugin/src/nemo_platform_plugin/models/endpoints.py
Adds typed CRUD, versioning, status, listing, and conflict-replay endpoint definitions for models, adapters, providers, prompts, deployments, and deployment configs.
Sync and async client behavior
packages/nemo_platform_plugin/src/nemo_platform_plugin/models/client.py
Adds endpoint-backed clients, OpenAI route builders, deployment-provider resolution, and synchronous/asynchronous status polling.
Client and endpoint behavior validation
packages/nemo_platform_plugin/tests/models/test_client.py, packages/nemo_platform_plugin/tests/models/test_endpoints.py
Tests prepared requests, CRUD behavior, conflict handling, deletion responses, URL construction, descriptor behavior, and sync/async polling outcomes.

Sequence Diagram(s)

sequenceDiagram
  participant ModelsClient
  participant ModelsAPI
  participant StatusHistory
  ModelsClient->>ModelsAPI: Fetch deployment or provider status
  ModelsAPI-->>ModelsClient: Return typed status response
  ModelsClient->>StatusHistory: Inspect new history entries
  StatusHistory-->>ModelsClient: Return current status and message
  ModelsClient->>ModelsAPI: Poll until desired, error, deletion, or timeout
Loading

Possibly related PRs

Suggested reviewers: mikeknep, benmccown, tylersbray, ironcommit

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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 The title accurately summarizes the additive typed models client foundation introduced by this PR.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aircore-876-typed-models-client/md

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/nemo_platform_plugin/src/nemo_platform_plugin/models/client.py (1)

245-329: 🩺 Stability & Availability | 🔵 Trivial

Sync polling blocks the calling thread for up to timeout seconds.

wait_for_deployment_status/wait_for_provider_status on ModelsClient block synchronously (default up to 1200s / 60s). Consumer wiring is deferred to a separate PR — worth confirming those call-sites don't invoke this from a request-handling thread; prefer AsyncModelsClient or a NemoJob for long-running polls.

🤖 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 `@packages/nemo_platform_plugin/src/nemo_platform_plugin/models/client.py`
around lines 245 - 329, The wait_for_deployment_status and
wait_for_provider_status methods synchronously block the calling thread for
their full polling timeout. Review their call sites and ensure they are not
invoked from request-handling threads; route long-running polling through
AsyncModelsClient or a NemoJob instead, while preserving the existing polling
behavior for suitable synchronous callers.
packages/nemo_platform_plugin/tests/models/test_client.py (1)

179-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Provider fixtures built with _model_json, not _provider_json.

Both test_create_provider_exist_ok_resolves_conflict (Line 186) and test_provider_route_for_deployment_fetches_provider (Line 240) build the mocked ModelProvider response via _model_json(...) (the ModelEntity-shaped helper) instead of _provider_json(...). It passes today, but it's confusing and fragile if the two DTOs diverge further.

♻️ Proposed fix
-    existing = httpx.Response(
-        200,
-        request=httpx.Request("GET", BASE),
-        json=_model_json("p", host_url="http://x") | {"host_url": "http://x"},
-    )
+    existing = httpx.Response(
+        200,
+        request=httpx.Request("GET", BASE),
+        json=_provider_json(name="p", host_url="http://x"),
+    )
-    http.request.return_value = httpx.Response(
-        200,
-        request=httpx.Request("GET", BASE),
-        json=_model_json("my-provider", host_url="https://api.example.com"),
-    )
+    http.request.return_value = httpx.Response(
+        200,
+        request=httpx.Request("GET", BASE),
+        json=_provider_json(name="my-provider", host_url="https://api.example.com"),
+    )

Also applies to: 235-247

🤖 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 `@packages/nemo_platform_plugin/tests/models/test_client.py` around lines 179 -
198, Update the provider response fixtures in
test_create_provider_exist_ok_resolves_conflict and
test_provider_route_for_deployment_fetches_provider to use _provider_json(...)
instead of _model_json(...), while preserving the existing provider-specific
field overrides and assertions.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/models/client.py`:
- Around line 237-243: Validate that model_provider_id contains the required
“workspace/name” delimiter before unpacking it in both
get_provider_route_openai_url_for_deployment and its async counterpart. Raise a
clear ValueError identifying the deployment and malformed model_provider_id,
while preserving the existing provider lookup for valid identifiers.

In `@packages/nemo_platform_plugin/tests/models/test_endpoints.py`:
- Around line 119-126: Replace the dynamic __import__ calls in
test_create_model_adapter_nested_path and the corresponding
update-model-deployment test with normal top-level imports for
CreateModelAdapterRequest and UpdateModelDeploymentConfigRequest, then
instantiate those imported classes directly at the call sites.

---

Nitpick comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/models/client.py`:
- Around line 245-329: The wait_for_deployment_status and
wait_for_provider_status methods synchronously block the calling thread for
their full polling timeout. Review their call sites and ensure they are not
invoked from request-handling threads; route long-running polling through
AsyncModelsClient or a NemoJob instead, while preserving the existing polling
behavior for suitable synchronous callers.

In `@packages/nemo_platform_plugin/tests/models/test_client.py`:
- Around line 179-198: Update the provider response fixtures in
test_create_provider_exist_ok_resolves_conflict and
test_provider_route_for_deployment_fetches_provider to use _provider_json(...)
instead of _model_json(...), while preserving the existing provider-specific
field overrides and assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 22aa54f6-4522-4cd6-b964-c9dfc8e2671c

📥 Commits

Reviewing files that changed from the base of the PR and between 5fe01ba and 773663e.

📒 Files selected for processing (8)
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/method.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/models/client.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/models/endpoints.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/models/types.py
  • packages/nemo_platform_plugin/tests/client/test_method.py
  • packages/nemo_platform_plugin/tests/models/test_client.py
  • packages/nemo_platform_plugin/tests/models/test_endpoints.py

Comment on lines +237 to +243
def get_provider_route_openai_url_for_deployment(self, deployment: DeploymentLike) -> str:
"""Fetch a deployment's ModelProvider and return its OpenAI route URL."""
if not deployment.model_provider_id:
raise ValueError(f"Deployment '{deployment.name}' has no associated model_provider_id")
workspace, name = deployment.model_provider_id.split("/", 1)
provider = self.get_provider(name=name, workspace=workspace).data()
return self.get_provider_route_openai_url(provider)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unvalidated model_provider_id split raises unclear error.

model_provider_id.split("/", 1) yields a 1-item list if there's no /, and the unpack (workspace, name = ...) throws an opaque ValueError: not enough values to unpack instead of a clear message. Same pattern in both sync (Line 241) and async (Line 338) versions.

🐛 Proposed fix (apply to both sync and async methods)
-        workspace, name = deployment.model_provider_id.split("/", 1)
+        parts = deployment.model_provider_id.split("/", 1)
+        if len(parts) != 2:
+            raise ValueError(
+                f"Deployment '{deployment.name}' has malformed model_provider_id: "
+                f"{deployment.model_provider_id!r}"
+            )
+        workspace, name = parts

Also applies to: 334-340

🤖 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 `@packages/nemo_platform_plugin/src/nemo_platform_plugin/models/client.py`
around lines 237 - 243, Validate that model_provider_id contains the required
“workspace/name” delimiter before unpacking it in both
get_provider_route_openai_url_for_deployment and its async counterpart. Raise a
clear ValueError identifying the deployment and malformed model_provider_id,
while preserving the existing provider lookup for valid identifiers.

Comment on lines +119 to +126
def test_create_model_adapter_nested_path() -> None:
prepared = endpoints.create_model_adapter(
workspace="w",
model_name="base",
body=__import__(
"nemo_platform_plugin.models.types", fromlist=["CreateModelAdapterRequest"]
).CreateModelAdapterRequest(name="a", fileset="w/fs", finetuning_type=FinetuningType.LORA),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Dynamic __import__ instead of normal import.

CreateModelAdapterRequest and UpdateModelDeploymentConfigRequest are fetched at call time via __import__("nemo_platform_plugin.models.types", fromlist=[...]) instead of just adding them to the existing top-level import block.

♻️ Proposed fix
 from nemo_platform_plugin.models.types import (
     Adapter,
     ContainerExecutorConfig,
+    CreateModelAdapterRequest,
     CreateAdapterRequest,
     CreateModelDeploymentConfigRequest,
     CreateModelDeploymentRequest,
     CreateModelEntityRequest,
     CreateModelProviderRequest,
     CreatePromptRequest,
     Engine,
     FinetuningType,
     ModelDeployment,
     ModelDeploymentConfig,
     ModelDeploymentConfigModelSpec,
     ModelDeploymentStatus,
     ModelEntity,
     ModelProvider,
     ModelProviderStatus,
     Prompt,
     UpdateAdapterRequest,
     UpdateModelDeploymentRequest,
+    UpdateModelDeploymentConfigRequest,
     UpdateModelDeploymentStatusRequest,
     UpdateModelEntityRequest,
     UpdateModelProviderStatusRequest,
     UpdatePromptRequest,
     UpsertModelProviderRequest,
 )
-        body=__import__(
-            "nemo_platform_plugin.models.types", fromlist=["CreateModelAdapterRequest"]
-        ).CreateModelAdapterRequest(name="a", fileset="w/fs", finetuning_type=FinetuningType.LORA),
+        body=CreateModelAdapterRequest(name="a", fileset="w/fs", finetuning_type=FinetuningType.LORA),
-        body=__import__(
-            "nemo_platform_plugin.models.types", fromlist=["UpdateModelDeploymentConfigRequest"]
-        ).UpdateModelDeploymentConfigRequest(
+        body=UpdateModelDeploymentConfigRequest(
             engine=Engine.VLLM,
             model_spec=ModelDeploymentConfigModelSpec(model_name="llama"),
             executor_config=ContainerExecutorConfig(gpu=1),
         ),

Also applies to: 289-296

🤖 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 `@packages/nemo_platform_plugin/tests/models/test_endpoints.py` around lines
119 - 126, Replace the dynamic __import__ calls in
test_create_model_adapter_nested_path and the corresponding
update-model-deployment test with normal top-level imports for
CreateModelAdapterRequest and UpdateModelDeploymentConfigRequest, then
instantiate those imported classes directly at the call sites.

@maxdubrinsky
maxdubrinsky marked this pull request as draft July 30, 2026 17:50
@github-actions

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 28896/36779 78.6% 62.7%
Integration Tests 17344/35497 48.9% 20.8%

@maxdubrinsky
maxdubrinsky marked this pull request as ready for review July 30, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant