Skip to content

feat(jobs): optional output_location param to set location for job artifacts#878

Open
nv-odrulea wants to merge 6 commits into
mainfrom
od/jobs-output-fileset-subpath
Open

feat(jobs): optional output_location param to set location for job artifacts#878
nv-odrulea wants to merge 6 commits into
mainfrom
od/jobs-output-fileset-subpath

Conversation

@nv-odrulea

@nv-odrulea nv-odrulea commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Optional output_location for platform jobs

Adds an optional output_location field to job creation so a caller can nest a job's
artifacts (logs + results) under a caller-supplied fileset instead of the auto-created
throwaway job-fileset-<name>. Backend only; no data migration.

Behavior

  • output_location unset → auto-create job-fileset-<name> (unchanged).
  • output_location set → must be an existing fileset in the workspace; the job's
    artifacts nest there via the existing inner path scheme (results/<attempt_id>/,
    logs/job=<id>/). Missing fileset → 422 (never auto-created — caller owns the lifecycle).
  • Value is a bare fileset name (workspace implied). The name is value-agnostic and
    forward-compatible with a future <fileset>#<subpath> form (no rename needed).

Ownership & delete

  • Create records provenance: output_location is persisted on the PlatformJob entity
    (migration-free — the entity store stores non-base fields as a dynamic property bag).
  • Delete cleans up only the fileset the job owns: output_location is None → whole
    delete_fileset; otherwise the caller's fileset is left intact. Symmetric with create,
    and consistent with "delete an entity → delete only the storage it owns."
  • Provenance (not a name heuristic) is what gates deletion, so a caller passing
    output_location="job-fileset-<their-job>" can't trick delete into destroying their fileset.

Validation

  • Syntactic (pydantic field_validator): rejects empty / / (workspace-qualified) / # (subpath, reserved).
  • Semantic (dispatcher): fileset must exist → else JobOutputLocationError → 422.

Files

  • nemo_platform_plugin/jobs/api_factory.py — field + validator + create-forward
  • nemo_platform_plugin/jobs/types.pyCreatePlatformJobRequest.output_location
  • core/jobs/.../entities.pyPlatformJob.output_location (persisted provenance)
  • core/jobs/.../app/dispatcher.py — create: validate + persist; delete: own-vs-leave
  • core/jobs/.../api/v2/jobs/endpoints.pyJobOutputLocationError → 422 mapping

Explain it to me like I'm 5 yrs old

How output_location nesting works: the full journey

The analogy: qualified-gray is a shared filing cabinet. Every job gets its own drawer inside it (named after the job), and each drawer has two folders: results and logs. The catch: the results clerk and the logs clerk work very differently, so they learn "which drawer" in different ways.


The 3 new props

Prop Lives on Plain meaning
output_location the job request + the job entity "Put my stuff in this shared cabinet." Your choice. If empty → job makes its own throwaway cabinet.
log_subpath the step's marching-orders (PlatformJobStepWithContext) A sticky note the dispatcher hands the running job: "write your logs into the <job-name>/ drawer." Only set if output_location was set.
subpath the log write + read calls The actual <job-name> drawer name used when saving/finding log files. Same value on both ends.

output_location = the cabinet. The drawer name is always just the job's name — nobody passes it; the server already knows it.


Route A — Creating the job (where the props get set)

  1. You POST {spec, output_location: "qualified-gray"} → the plugin builds BaseJobRequest (api_factory.py). Its field_validator checks the value isn't empty / no / / no #.
  2. The create handler copies it into CreatePlatformJobRequest(output_location=...) and calls core-jobs.
  3. JobDispatcher.create_job (dispatcher.py):
    • output_location set? → files.get_fileset(...) to check the cabinet exists (else 422). Set fileset = "qualified-gray".
    • Save PlatformJob(fileset="qualified-gray", output_location="qualified-gray", …). ← the cabinet choice is now written on the job, forever.
  4. Later, when the job runs, the dispatcher builds the step's marching orders:
    PlatformJobStepWithContext(fileset=job.fileset, log_subpath = job.name if job.output_location else None, …) (dispatcher.py:720). ← the "write logs into the <job-name>/ drawer" sticky note.

Route B — The RESULTS clerk (easy: he asks)

The results clerk phones the front desk before filing, so he just asks which drawer.

  1. Job task calls ctx.results.save("ltManager.create_result**(result_manager.py).
  2. _fetch_job_metadata()get.output_location** off the response.
  3. base = job.name if output_location else None.
  4. _result_remote_path(attempt, "summary", base)
    • nested: "nemo-evaluator.agent-xyz/results/<attempt>/summary"
    • flat: "results/<attempt>/summary"
  5. Uploads to job.fileset at that path. ✅ Done — no sticky note needed, he asked.

Route C — The LOGS clerk (hard: hen't ask)

The logs clerk is a running subprocess shipping logs out to a drop-box endpoint. He knows nothing
about the job — only the URL he was twer name must be written on theshipping label at launch.

Write side:

  1. At launch, subprocess._prepare_runtime(step) reads step.log_subpath and calls
    get_logs_endpoint_config_from_fileset(fileset, subpath=step.log_subpath) (base.py) → builds
    the URL:
    …/filesets/qualified-gray/otlp/v1/logs?base=nemo-evaluator.agent-xyzdrawer name baked into
    the label.
  2. The job ships logs to that URL. Files service upload_otlp_logs(base=…) reads ?base=
    insert_logs(subpath=base) → wrib-name>/logs/…`. ✅

Read side (Studio asks for logs):
3. page_job_logs(name) (jobs/ejob, recomputes **subpath = name if
job.output_location else None** → thnt.query_logs → files
query_otlp_logs → **query_logs(subualified-gray/<job-name>/logs/…. ✅

Why it works: write side and read side both compute the drawer name as job.name, gated on output_location. Same recipe on both ends → they always look in the same drawer.


One-line summary

  • output_location = **which cabinethe job).
  • Drawer = job.name (server derives it; never passed by you).
  • Results clerk pulls the cabinjob` → no threading.
  • Logs clerk pushes, so the dra?base=subpath) must be handed to him at launch and re-derived identica

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Job submission now supports an optional output_location to control where job artifacts are written.
    • Added --output-location to the nemo jobs create CLI.
    • Updated public API/OpenAPI contracts for job requests to include output_location.
  • Bug Fixes

    • Invalid output_location values are rejected with HTTP 422.
    • Output locations are normalized (whitespace trimmed) and only supported formats are allowed; ownership/deletion behavior now respects caller-supplied locations.
  • Tests

    • Added validation and end-to-end coverage for API and dispatcher behavior.

@nv-odrulea
nv-odrulea requested review from a team as code owners July 23, 2026 22:18
@nv-odrulea nv-odrulea changed the title Od/jobs output fileset subpath feat(jobs): optional output_location param to set location for job artifacts Jul 23, 2026
@github-actions github-actions Bot added the feat label Jul 23, 2026
@nv-odrulea nv-odrulea self-assigned this Jul 23, 2026
@nv-odrulea
nv-odrulea requested a review from drazvan July 23, 2026 22:24
@nv-odrulea
nv-odrulea force-pushed the od/jobs-output-fileset-subpath branch from 7947ddd to 599e5b4 Compare July 23, 2026 22:26
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 27178/34890 77.9% 62.2%
Integration Tests 15977/33602 47.5% 20.0%

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Job creation now accepts an optional validated output_location, reuses existing workspace filesets, records fileset ownership, preserves caller-managed filesets during deletion, and returns HTTP 422 for invalid locations. Related OpenAPI contracts and CLI support are updated, alongside independent RL and Docker schema changes.

Changes

Output location lifecycle

Layer / File(s) Summary
Request validation and forwarding
packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/api_factory.py, packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py, packages/nemo_platform_ext/src/.../jobs/__init__.py, openapi/..., plugins/*/openapi/openapi.yaml, docs/cli/reference.mdx
Requests validate bare fileset names, expose the CLI option, update job request schemas, and forward valid locations.
Fileset resolution and ownership
services/core/jobs/src/nmp/core/jobs/app/dispatcher.py, services/core/jobs/src/nmp/core/jobs/entities.py, services/core/jobs/tests/test_dispatcher.py
The dispatcher reuses caller-provided filesets or creates owned filesets, persists provenance, and deletes only dispatcher-owned filesets.
API error handling and coverage
services/core/jobs/src/nmp/core/jobs/api/v2/jobs/endpoints.py, services/core/jobs/tests/test_jobs_api.py, packages/nemo_platform_plugin/tests/test_output_location_validation.py
Invalid output locations map to HTTP 422, with validation, forwarding, lifecycle, and response tests.

Customizer RL API contracts

Layer / File(s) Summary
RL job operations and schemas
plugins/nemo-customizer/openapi/openapi.yaml
Customizer job operations move from unsloth to rl, and RL job, output, training, pagination, and monitoring schemas are added or expanded.

Deployment schema additions

Layer / File(s) Summary
Docker volume initialization settings
plugins/nemo-deployments/openapi/openapi.yaml
Docker volume configuration adds initChmod and initImage string properties.

Possibly related PRs

Suggested reviewers: drazvan, briannewsom, mckornfield, marcusds, tylersbray

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.
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 main change: adding an optional output_location parameter for job artifact storage.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch od/jobs-output-fileset-subpath

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py (1)

222-233: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

output_location unvalidated at the core API boundary.

The plugin's BaseJobRequest._validate_output_location (api_factory.py) rejects empty/whitespace, # subpaths, and /-qualified names — but CreatePlatformJobRequest.output_location here has no such validator. Since endpoints.create_job accepts this model directly, any caller bypassing the plugin can submit malformed values (empty string, subpaths, slash-qualified names) that only get "validated" indirectly via a files.get_fileset lookup in the dispatcher, not via an explicit contract check.

🛡️ Mirror the plugin's validation
 from pydantic import BaseModel
+from pydantic import field_validator


 class CreatePlatformJobRequest(BaseModel):
     ...
     output_location: Optional[str] = None
+
+    `@field_validator`("output_location")
+    `@classmethod`
+    def _validate_output_location(cls, value: Optional[str]) -> Optional[str]:
+        if value is None:
+            return None
+        stripped = value.strip()
+        if not stripped:
+            raise ValueError("output_location must not be empty")
+        if "#" in stripped or "/" in stripped:
+            raise ValueError("output_location must be a bare fileset name")
+        return stripped
🤖 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/jobs/types.py` around
lines 222 - 233, Update CreatePlatformJobRequest.output_location to apply the
same explicit validation as BaseJobRequest._validate_output_location, rejecting
empty or whitespace-only values, # subpaths, and slash-qualified names before
endpoints.create_job accepts the request. Reuse the existing validator logic or
shared validation symbol rather than relying on dispatcher fileset lookup.
🤖 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 `@services/core/jobs/src/nmp/core/jobs/app/dispatcher.py`:
- Around line 286-301: Update the output-location lookup in the dispatcher’s
fileset resolution block to also catch ClientPermissionDeniedError alongside
ClientNotFoundError. Translate the permission failure into the established
client-facing JobOutputLocationError, preserving the referenced fileset and
workspace context and chaining the original exception.

---

Outside diff comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py`:
- Around line 222-233: Update CreatePlatformJobRequest.output_location to apply
the same explicit validation as BaseJobRequest._validate_output_location,
rejecting empty or whitespace-only values, # subpaths, and slash-qualified names
before endpoints.create_job accepts the request. Reuse the existing validator
logic or shared validation symbol rather than relying on dispatcher fileset
lookup.
🪄 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: 46383832-d606-41a3-9ded-4d3cc7c7869c

📥 Commits

Reviewing files that changed from the base of the PR and between 774dac1 and 7947ddd.

📒 Files selected for processing (8)
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/api_factory.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py
  • packages/nemo_platform_plugin/tests/test_output_location_validation.py
  • services/core/jobs/src/nmp/core/jobs/api/v2/jobs/endpoints.py
  • services/core/jobs/src/nmp/core/jobs/app/dispatcher.py
  • services/core/jobs/src/nmp/core/jobs/entities.py
  • services/core/jobs/tests/test_dispatcher.py
  • services/core/jobs/tests/test_jobs_api.py

Comment thread services/core/jobs/src/nmp/core/jobs/app/dispatcher.py
@nv-odrulea
nv-odrulea force-pushed the od/jobs-output-fileset-subpath branch from 47bef7c to f3aa1db Compare July 24, 2026 17:42
Signed-off-by: Octavian Drulea <[email protected]>
@nv-odrulea
nv-odrulea requested a review from ironcommit July 24, 2026 19:38

@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)
plugins/nemo-customizer/openapi/openapi.yaml (2)

2365-2367: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

output_location ships with no description or constraints.

Bare type: string gives SDK/doc consumers no hint that the value must be an existing bare fileset name — no workspace prefix, no subpath, non-empty. Same shape at Lines 1328-1330 (AutomodelJobsJobRequest) and Lines 2836-2838 (UnslothJobsJobRequest), so the fix belongs on the shared upstream request model (Field(description=..., min_length=1, pattern=...)), then regenerate.

🤖 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 `@plugins/nemo-customizer/openapi/openapi.yaml` around lines 2365 - 2367, The
shared upstream request model for output_location must define it as a non-empty
existing bare fileset name, excluding workspace prefixes and subpaths, with a
descriptive schema description and validation constraints for minimum length and
pattern. Update that model rather than only the generated OpenAPI entries, then
regenerate the corresponding schemas used by AutomodelJobsJobRequest and
UnslothJobsJobRequest.

2077-2100: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

RlDPOTraining numeric hyperparameters are unconstrained.

learning_rate, weight_decay, adam_beta1, adam_beta2 accept any number — negative LR or beta > 1 passes contract validation and only fails once training is underway. The sibling AutomodelOptimizerSpec (Lines 1442-1470) constrains all four. Add the bounds upstream on the RL plugin schema and regenerate.

♻️ Expected generated shape after adding upstream bounds
         learning_rate:
           type: number
+          exclusiveMinimum: 0.0
           title: Learning Rate
           description: Peak learning rate.
           default: 0.0001
@@
         weight_decay:
           type: number
+          minimum: 0.0
           title: Weight Decay
           description: Weight decay coefficient.
           default: 0.01
@@
         adam_beta1:
           type: number
+          exclusiveMaximum: 1.0
+          minimum: 0.0
           title: Adam Beta1
           description: Adam beta1.
           default: 0.9
@@
         adam_beta2:
           type: number
+          exclusiveMaximum: 1.0
+          minimum: 0.0
           title: Adam Beta2
           description: Adam beta2.
           default: 0.999
🤖 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 `@plugins/nemo-customizer/openapi/openapi.yaml` around lines 2077 - 2100,
Constrain the numeric hyperparameters in the RlDPOTraining schema to match
AutomodelOptimizerSpec: require learning_rate and weight_decay to be
non-negative, and restrict adam_beta1 and adam_beta2 to the valid [0, 1] range.
Add these bounds in the upstream schema source, then regenerate the OpenAPI
output so the displayed RlDPOTraining properties include the corresponding
minimum/maximum validation metadata.
🤖 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 `@openapi/ga/openapi.yaml`:
- Around line 9789-9791: Add schema validation to output_location by retaining
type: string, adding minLength: 1, and applying the existing request-validator
pattern for bare fileset values only. Do not encode fileset existence checks in
the OpenAPI schema; keep those server-side validations unchanged.

In `@openapi/openapi.yaml`:
- Around line 9789-9791: Update the output_location schema property in the
relevant OpenAPI definition to include the same minLength and pattern
constraints enforced by request validation, rejecting empty,
workspace-qualified, and subpath-containing values while preserving server-side
fileset existence validation.

---

Nitpick comments:
In `@plugins/nemo-customizer/openapi/openapi.yaml`:
- Around line 2365-2367: The shared upstream request model for output_location
must define it as a non-empty existing bare fileset name, excluding workspace
prefixes and subpaths, with a descriptive schema description and validation
constraints for minimum length and pattern. Update that model rather than only
the generated OpenAPI entries, then regenerate the corresponding schemas used by
AutomodelJobsJobRequest and UnslothJobsJobRequest.
- Around line 2077-2100: Constrain the numeric hyperparameters in the
RlDPOTraining schema to match AutomodelOptimizerSpec: require learning_rate and
weight_decay to be non-negative, and restrict adam_beta1 and adam_beta2 to the
valid [0, 1] range. Add these bounds in the upstream schema source, then
regenerate the OpenAPI output so the displayed RlDPOTraining properties include
the corresponding minimum/maximum validation metadata.
🪄 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: 49afd0fb-8db2-41f4-b016-020b1fa33e53

📥 Commits

Reviewing files that changed from the base of the PR and between f3aa1db and b32f1d0.

⛔ Files ignored due to path filters (7)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/jobs/__init__.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/inference/container_executor_config.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/inference/container_executor_config_param.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/jobs/job_create_params.py is excluded by !sdk/**
  • sdk/python/nemo-platform/tests/api_resources/test_jobs.py is excluded by !sdk/**
📒 Files selected for processing (13)
  • docs/cli/reference.mdx
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/jobs/__init__.py
  • plugins/nemo-agents/openapi/openapi.yaml
  • plugins/nemo-anonymizer/openapi/openapi.yaml
  • plugins/nemo-auditor/openapi/openapi.yaml
  • plugins/nemo-customizer/openapi/openapi.yaml
  • plugins/nemo-data-designer/openapi/openapi.yaml
  • plugins/nemo-deployments/openapi/openapi.yaml
  • plugins/nemo-evaluator/openapi/openapi.yaml
  • plugins/nemo-safe-synthesizer/openapi/openapi.yaml

Comment thread openapi/ga/openapi.yaml
Comment thread openapi/openapi.yaml
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