From 9fa4057929c17b61d58a44549c59f79f952f3c58 Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Thu, 13 Aug 2026 14:58:33 -0700 Subject: [PATCH 1/2] feat(optimization): Support filesets and inline config in optimization job Signed-off-by: Sean Teramae --- plugins/nemo-agents/openapi/openapi.yaml | 46 +++- .../src/nemo_optimization/jobs/optimize.py | 171 +++++++++++- .../src/nemo_optimization/schemas/optimize.py | 34 ++- .../tests/test_optimize_job.py | 244 ++++++++++++++++++ 4 files changed, 478 insertions(+), 17 deletions(-) diff --git a/plugins/nemo-agents/openapi/openapi.yaml b/plugins/nemo-agents/openapi/openapi.yaml index ee018f54fc..dce724ae32 100644 --- a/plugins/nemo-agents/openapi/openapi.yaml +++ b/plugins/nemo-agents/openapi/openapi.yaml @@ -3752,9 +3752,17 @@ components: OptimizeSpec: properties: optimize_config: - type: string title: Optimize Config - description: Absolute path to the Fabric-native optimization YAML file. + description: Absolute path to the Fabric-native optimization YAML file on + the platform host. + type: string + optimize_config_inline: + title: Optimize Config Inline + description: The Fabric-native optimization config inline, with the same + shape as the YAML file. Use instead of optimize_config when submitting + from a remote client. + additionalProperties: true + type: object workspace: type: string title: Workspace @@ -3764,13 +3772,39 @@ components: agent: title: Agent description: Optional platform agent reference ('name' or 'workspace/name'). - When omitted, optimize_config must include an inline Fabric agent package. + When omitted, the optimization config must include an inline Fabric agent + package. + type: string + output: + title: Output + description: "Where to publish the study artifacts (optimized config, trials\ + \ dataframe, pareto plots, ATIF evidence) once the study succeeds \u2014\ + \ either a local directory (path-shaped: starts with '/', './', '../',\ + \ '~/') or a NeMo Platform fileset reference ('name' or 'workspace/name').\ + \ Filesets are created on demand if missing. This is in addition to\ + \ the per-job artifacts that ``ctx.results.save`` always registers; it\ + \ gives remote clients a stable, addressable location to read from." type: string type: object - required: - - optimize_config title: OptimizeSpec - description: Spec for an Agents optimize study (``nemo agents optimize``). + description: 'Spec for an Agents optimize study (``nemo agents optimize``). + + + The optimization config arrives one of two ways, and exactly one must be + + set. ``optimize_config`` is a path on the platform host, which only the + + co-located CLI can satisfy; ``optimize_config_inline`` carries the same + + document in the request body, for remote clients (e.g. Studio) that have no + + access to that filesystem. They are separate fields rather than a union + + because the CLI flag generator only collapses unions whose arms share a + + scalar base, and would skip ``--optimize-config`` entirely for ``str | + + dict``.' PaginationData: properties: page: diff --git a/plugins/nemo-optimization/src/nemo_optimization/jobs/optimize.py b/plugins/nemo-optimization/src/nemo_optimization/jobs/optimize.py index d0c02d6013..9049e78889 100644 --- a/plugins/nemo-optimization/src/nemo_optimization/jobs/optimize.py +++ b/plugins/nemo-optimization/src/nemo_optimization/jobs/optimize.py @@ -9,8 +9,13 @@ from __future__ import annotations +import contextlib +import copy import logging import os +import re +import shutil +from collections.abc import Iterator, Mapping from pathlib import Path from typing import Any, ClassVar @@ -20,6 +25,13 @@ from nemo_platform_plugin.job_context import JobContext from nemo_platform_plugin.jobs.api_factory import PlatformJobSpec from nemo_platform_plugin.jobs.exceptions import PlatformJobCompilationError +from nemo_platform_plugin.refs import ( + FILESET_REF_PATTERN, + FilesetRef, + LocalDir, + classify_output_target, +) +from nemo_platform_plugin.run_dependencies import LocalRunError from pydantic import BaseModel from nemo_optimization.agents import resolve_agent_config @@ -40,7 +52,7 @@ class OptimizeJob(NemoJob): spec_schema: ClassVar[type[BaseModel]] = OptimizeSpec @classmethod - async def compile( # type: ignore[override] + async def compile( # ty: ignore[invalid-method-override] cls, *, workspace: str, @@ -61,7 +73,8 @@ async def compile( # type: ignore[override] PERSISTENT_JOB_STORAGE_PATH_ENVVAR, ) - if not Path(spec.optimize_config).is_absolute(): + # optimize_config_inline needs no host filesystem; only the path form does. + if spec.optimize_config is not None and not Path(spec.optimize_config).is_absolute(): raise PlatformJobCompilationError("optimize_config must be an absolute path.") spec_dict = spec.model_dump(mode="json") @@ -88,7 +101,7 @@ async def compile( # type: ignore[override] def run(self, config: dict, *, ctx: JobContext, sdk: NeMoPlatform | None = None) -> dict: spec = OptimizeSpec.model_validate(config) - optimize_config = _load_yaml(Path(spec.optimize_config)) + optimize_config = _resolve_optimize_config(spec) agent_config = resolve_agent_config(spec.agent, workspace=spec.workspace, sdk=sdk) preflight_validate_llm_models( optimize_config, @@ -96,13 +109,34 @@ def run(self, config: dict, *, ctx: JobContext, sdk: NeMoPlatform | None = None) sdk=sdk, agent_config=agent_config, ) - logger.info("Dispatching agents optimize study via OptimizeRouter") - return OptimizeRouter.dispatch( - agent_config=agent_config, - optimize_config=optimize_config, + with _staged_dataset( + optimize_config, + workspace=spec.workspace, ctx=ctx, sdk=sdk, - ) + ) as staged_config: + logger.info("Dispatching agents optimize study via OptimizeRouter") + result = OptimizeRouter.dispatch( + agent_config=agent_config, + optimize_config=staged_config, + ctx=ctx, + sdk=sdk, + ) + + published = _publish_results(spec.output, workspace=spec.workspace, ctx=ctx, sdk=sdk) + return result if published is None else {**result, "output": published} + + +def _resolve_optimize_config(spec: OptimizeSpec) -> dict[str, Any]: + """Normalize either config source into a mapping. + + Both forms get ``${VAR}`` expansion so an inline config can reference the + task subprocess's environment (e.g. ``api_key_env``) the same way a + file-based one does. ``OptimizeSpec`` guarantees exactly one is set. + """ + if spec.optimize_config_inline is not None: + return _expand_env(spec.optimize_config_inline) + return _load_yaml(Path(str(spec.optimize_config))) def _load_yaml(path: Path) -> dict[str, Any]: @@ -112,6 +146,127 @@ def _load_yaml(path: Path) -> dict[str, Any]: return _expand_env(raw) +@contextlib.contextmanager +def _staged_dataset( + optimize_config: dict[str, Any], + *, + workspace: str, + ctx: JobContext, + sdk: NeMoPlatform | None, +) -> Iterator[dict[str, Any]]: + """Yield *optimize_config* with a fileset dataset reference replaced by a local path. + + ``eval.general.dataset`` may be a plain host path (CLI runs) or a + ``workspace/fileset#path`` reference (remote submitters, who have no host + filesystem). For the reference form the fileset is downloaded to a tempdir + for the duration of the study and the config is rewritten in place, so + everything downstream keeps seeing a plain readable path. + """ + ref = _dataset_fileset_ref(optimize_config) + if ref is None: + yield optimize_config + return + + # Soft dependency, mirroring nemo_optimization.agents' lazy imports. + from nemo_agents_plugin.jobs.fileset_io import resolve_staged_config + + fileset_ref, _, object_path = ref.partition("#") + with resolve_staged_config( + object_path, + fileset_ref, + workspace=workspace, + ctx=ctx, + sdk=sdk, + kind="optimize-dataset", + ) as local_path: + yield _with_dataset_path(optimize_config, str(local_path)) + + +def _dataset_fileset_ref(optimize_config: Mapping[str, Any]) -> str | None: + """Return ``eval.general.dataset`` when it is a ``workspace/fileset#path`` ref.""" + dataset = _dataset_node(optimize_config) + value = dataset if isinstance(dataset, str) else None + if isinstance(dataset, Mapping): + candidate = dataset.get("file_path") or dataset.get("path") + value = candidate if isinstance(candidate, str) else None + if value is None or not re.match(FILESET_REF_PATTERN, value): + return None + return value + + +def _dataset_node(optimize_config: Mapping[str, Any]) -> Any: + general = optimize_config.get("eval", {}) + general = general.get("general") if isinstance(general, Mapping) else None + return general.get("dataset") if isinstance(general, Mapping) else None + + +def _with_dataset_path(optimize_config: dict[str, Any], local_path: str) -> dict[str, Any]: + """Copy *optimize_config* with the dataset location swapped for *local_path*.""" + updated = copy.deepcopy(optimize_config) + general = updated["eval"]["general"] + dataset = general.get("dataset") + general["dataset"] = {"file_path": local_path} if isinstance(dataset, str) else {**dataset, "file_path": local_path} + return updated + + +def _publish_results( + output: str | None, + *, + workspace: str, + ctx: JobContext, + sdk: NeMoPlatform | None, +) -> dict[str, str] | None: + """Copy the study's artifacts to *output*, returning a pointer for the job result. + + The backends write everything under ``ctx.storage.persistent / "results"`` + and register it via ``ctx.results.save``, which on the platform lands in the + job's own fileset under ``results//``. That is addressable only + through ``sdk.jobs.results``, so a remote client that wants to read the + optimized config back — or hand it to a follow-up job — needs a stable + location it names up front. Publishing the whole ``results`` tree keeps + this backend-agnostic: no ``RESULT_NAME`` coupling, and the ``ga`` backend + gets it for free. + + Returns ``None`` when no target was requested. + """ + if output is None: + return None + + # Soft dependency, mirroring nemo_optimization.agents' lazy imports. + from nemo_agents_plugin.jobs.fileset_io import split_fileset_ref, upload_to_fileset + + try: + artifacts = ctx.storage.persistent / "results" + except RuntimeError as exc: + raise LocalRunError( + "Publishing optimize results requires persistent storage, which this job did not " + "request. This is a platform-run-only feature; drop 'output' for local runs." + ) from exc + + if not artifacts.is_dir(): + raise FileNotFoundError( + f"Optimize study reported success but wrote no artifacts to {artifacts}; nothing to publish." + ) + + if classify_output_target(output) is LocalDir: + local = Path(output).expanduser().resolve() + local.mkdir(parents=True, exist_ok=True) + shutil.copytree(artifacts, local, dirs_exist_ok=True) + logger.info("Published optimize results from %s to local dir %s", artifacts, local) + return {"type": "local_dir", "path": str(local)} + + ws, name = split_fileset_ref(FilesetRef(output), workspace) + if sdk is None: + raise LocalRunError( + f"Publishing optimize results to fileset '{ws}/{name}' requires a 'sdk: NeMoPlatform', " + "but no platform SDK was available. Set NMP_BASE_URL, pass sdk via " + "NemoJobScheduler.run_local(sdk=...), or use a local output directory instead." + ) + upload_to_fileset(artifacts, fileset=name, workspace=ws, sdk=sdk) + logger.info("Published optimize results from %s to fileset %s/%s", artifacts, ws, name) + return {"type": "fileset", "fileset": f"{ws}/{name}"} + + def _expand_env(value: Any) -> Any: if isinstance(value, dict): return {k: _expand_env(v) for k, v in value.items()} diff --git a/plugins/nemo-optimization/src/nemo_optimization/schemas/optimize.py b/plugins/nemo-optimization/src/nemo_optimization/schemas/optimize.py index 76b1daf1be..0e5a4db850 100644 --- a/plugins/nemo-optimization/src/nemo_optimization/schemas/optimize.py +++ b/plugins/nemo-optimization/src/nemo_optimization/schemas/optimize.py @@ -5,13 +5,24 @@ from __future__ import annotations -from pydantic import BaseModel, Field +from typing import Any + +from nemo_platform_plugin.refs import OutputTarget +from pydantic import BaseModel, Field, model_validator class OptimizeSpec(BaseModel): """Spec for an Agents optimize study (``nemo agents optimize``).""" - optimize_config: str = Field(description="Absolute path to the Fabric-native optimization YAML file.") + optimize_config: str | None = Field( + default=None, + description="Absolute path to the Fabric-native optimization YAML file on the platform host.", + ) + optimize_config_inline: dict[str, Any] | None = Field( + default=None, + description="The Fabric-native optimization config inline, with the same shape as the " + "YAML file. Use instead of optimize_config when submitting from a remote client.", + ) workspace: str = Field( default="default", description="Workspace used to fetch a platform agent and for VirtualModel preflight.", @@ -19,5 +30,22 @@ class OptimizeSpec(BaseModel): agent: str | None = Field( default=None, description="Optional platform agent reference ('name' or 'workspace/name'). " - "When omitted, optimize_config must include an inline Fabric agent package.", + "When omitted, the optimization config must include an inline Fabric agent package.", + ) + output: OutputTarget | None = Field( + default=None, + description="Where to publish the study artifacts (optimized config, trials dataframe, " + "pareto plots, ATIF evidence) once the study succeeds — either a local directory " + "(path-shaped: starts with '/', './', '../', '~/') or a NeMo Platform fileset " + "reference ('name' or 'workspace/name'). Filesets are created on demand if missing. " + "This is in addition to the per-job artifacts that ``ctx.results.save`` always " + "registers; it gives remote clients a stable, addressable location to read from.", ) + + @model_validator(mode="after") + def _exactly_one_config_source(self) -> "OptimizeSpec": + if self.optimize_config is None and self.optimize_config_inline is None: + raise ValueError("Set exactly one of optimize_config or optimize_config_inline; got neither.") + if self.optimize_config is not None and self.optimize_config_inline is not None: + raise ValueError("Set exactly one of optimize_config or optimize_config_inline; got both.") + return self diff --git a/plugins/nemo-optimization/tests/test_optimize_job.py b/plugins/nemo-optimization/tests/test_optimize_job.py index e58cddbead..a9da991414 100644 --- a/plugins/nemo-optimization/tests/test_optimize_job.py +++ b/plugins/nemo-optimization/tests/test_optimize_job.py @@ -4,6 +4,7 @@ from __future__ import annotations from pathlib import Path +from types import SimpleNamespace from typing import Any, cast from unittest.mock import MagicMock, patch @@ -15,6 +16,7 @@ from nemo_platform_plugin.job_context import JobContext from nemo_platform_plugin.jobs.exceptions import PlatformJobCompilationError from nemo_platform_plugin.run_dependencies import LocalRunError +from pydantic import ValidationError FABRIC_AGENT = { "schema_version": "fabric.agent/v1alpha1", @@ -140,6 +142,248 @@ class _StubSDK: assert agent_config["models"]["judge"]["model"] == "demo-model" +@pytest.mark.asyncio +async def test_compile_accepts_inline_optimize_config() -> None: + spec = OptimizeSpec(optimize_config_inline={"optimizer": {"numeric": {"enabled": True}}}) + platform_spec = await OptimizeJob.compile( + workspace="staging", + spec=spec, + entity_client=MagicMock(), + job_name=None, + async_sdk=MagicMock(), + ) + step = next(iter(platform_spec["steps"])) + assert step["config"]["optimize_config_inline"] == {"optimizer": {"numeric": {"enabled": True}}} + + +def test_run_accepts_inline_optimize_config(ctx: JobContext) -> None: + with patch( + "nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", return_value={"status": "completed"} + ) as dispatch: + result = OptimizeJob().run( + { + "optimize_config_inline": {"optimizer": {"numeric": {"enabled": True, "n_trials": 8}}}, + "workspace": "default", + }, + ctx=ctx, + ) + + assert result["status"] == "completed" + assert dispatch.call_args.kwargs["optimize_config"]["optimizer"]["numeric"]["n_trials"] == 8 + + +def test_run_expands_env_vars_in_inline_config(ctx: JobContext, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPTIMIZE_TEST_MODEL", "demo-model") + + with patch( + "nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", return_value={"status": "completed"} + ) as dispatch: + OptimizeJob().run( + { + "optimize_config_inline": {"models": {"default": {"model": "${OPTIMIZE_TEST_MODEL}"}}}, + "workspace": "default", + }, + ctx=ctx, + ) + + assert dispatch.call_args.kwargs["optimize_config"]["models"]["default"]["model"] == "demo-model" + + +def test_spec_requires_exactly_one_config_source() -> None: + with pytest.raises(ValidationError, match="got neither"): + OptimizeSpec() + with pytest.raises(ValidationError, match="got both"): + OptimizeSpec(optimize_config="/abs/optimize.yml", optimize_config_inline={"optimizer": {}}) + + +def _inline_config_with_dataset(dataset: Any) -> dict[str, Any]: + return { + "optimizer": {"numeric": {"enabled": True}}, + "eval": {"general": {"dataset": dataset, "max_concurrency": 1}}, + } + + +def test_run_stages_dataset_from_fileset_ref(ctx: JobContext) -> None: + downloaded: dict[str, Any] = {} + + class _StubFiles: + def download(self, *, local_path: str, fileset: str, workspace: str) -> None: + downloaded.update(fileset=fileset, workspace=workspace) + (Path(local_path) / "rows.json").write_text('[{"question": "q", "answer": "a"}]') + + class _StubSDK: + files = _StubFiles() + + with patch( + "nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", return_value={"status": "completed"} + ) as dispatch: + OptimizeJob().run( + { + "optimize_config_inline": _inline_config_with_dataset({"file_path": "default/evals#rows.json"}), + "workspace": "default", + }, + ctx=ctx, + sdk=cast(NeMoPlatform, _StubSDK()), + ) + + assert downloaded == {"fileset": "evals", "workspace": "default"} + staged = dispatch.call_args.kwargs["optimize_config"]["eval"]["general"]["dataset"]["file_path"] + assert staged.endswith("rows.json") + assert Path(staged).is_absolute() + # Sibling keys survive the rewrite. + assert dispatch.call_args.kwargs["optimize_config"]["eval"]["general"]["max_concurrency"] == 1 + + +def test_run_leaves_plain_dataset_path_untouched(ctx: JobContext) -> None: + with patch( + "nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", return_value={"status": "completed"} + ) as dispatch: + OptimizeJob().run( + { + "optimize_config_inline": _inline_config_with_dataset({"file_path": "/data/rows.json"}), + "workspace": "default", + }, + ctx=ctx, + ) + + dataset = dispatch.call_args.kwargs["optimize_config"]["eval"]["general"]["dataset"] + assert dataset == {"file_path": "/data/rows.json"} + + +INLINE_MINIMAL = {"optimizer": {"numeric": {"enabled": True}}} + + +def _write_study_artifacts(ctx: JobContext) -> Path: + """Stand in for what a backend leaves behind under /results.""" + artifacts = ctx.storage.persistent / "results" / "optimizer_results" + artifacts.mkdir(parents=True) + (artifacts / "study_summary.json").write_text('{"status": "completed"}') + (artifacts / "optimized_config.yml").write_text("optimizer: {}\n") + return artifacts + + +def test_run_publishes_results_to_fileset(ctx: JobContext) -> None: + uploaded: dict[str, Any] = {} + + class _StubFiles: + def upload(self, *, local_path: str, fileset: str, workspace: str, fileset_auto_create: bool) -> Any: + uploaded.update( + local_path=local_path, + fileset=fileset, + workspace=workspace, + auto_create=fileset_auto_create, + names=sorted(p.name for p in Path(local_path).rglob("*") if p.is_file()), + ) + return SimpleNamespace(name=fileset) + + class _StubSDK: + files = _StubFiles() + + def _dispatch(**kwargs: Any) -> dict[str, Any]: + _write_study_artifacts(ctx) + return {"status": "completed", "best_trial": 3} + + with patch("nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", side_effect=_dispatch): + result = OptimizeJob().run( + {"optimize_config_inline": INLINE_MINIMAL, "workspace": "default", "output": "tuned-results"}, + ctx=ctx, + sdk=cast(NeMoPlatform, _StubSDK()), + ) + + assert uploaded["fileset"] == "tuned-results" + assert uploaded["workspace"] == "default" + assert uploaded["auto_create"] is True + # Trailing slash uploads contents, not the dir itself. + assert uploaded["local_path"].endswith("/") + assert uploaded["names"] == ["optimized_config.yml", "study_summary.json"] + # The study's own summary survives alongside the new pointer. + assert result["best_trial"] == 3 + assert result["output"] == {"type": "fileset", "fileset": "default/tuned-results"} + + +def test_run_publishes_results_to_local_dir(ctx: JobContext, tmp_path: Path) -> None: + dest = tmp_path / "published" + + def _dispatch(**kwargs: Any) -> dict[str, Any]: + _write_study_artifacts(ctx) + return {"status": "completed"} + + with patch("nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", side_effect=_dispatch): + result = OptimizeJob().run( + {"optimize_config_inline": INLINE_MINIMAL, "workspace": "default", "output": str(dest)}, + ctx=ctx, + ) + + assert (dest / "optimizer_results" / "study_summary.json").is_file() + assert result["output"] == {"type": "local_dir", "path": str(dest.resolve())} + + +def test_run_without_output_publishes_nothing(ctx: JobContext) -> None: + def _dispatch(**kwargs: Any) -> dict[str, Any]: + _write_study_artifacts(ctx) + return {"status": "completed"} + + with patch("nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", side_effect=_dispatch): + result = OptimizeJob().run( + {"optimize_config_inline": INLINE_MINIMAL, "workspace": "default"}, + ctx=ctx, + ) + + assert result == {"status": "completed"} + + +def test_run_does_not_publish_when_study_fails(ctx: JobContext) -> None: + """A crashed study must not leave partial artifacts in the target fileset.""" + + class _StubFiles: + def upload(self, **kwargs: Any) -> Any: + raise AssertionError("upload must not run when the study raises") + + class _StubSDK: + files = _StubFiles() + + with ( + patch("nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", side_effect=RuntimeError("study blew up")), + pytest.raises(RuntimeError, match="study blew up"), + ): + OptimizeJob().run( + {"optimize_config_inline": INLINE_MINIMAL, "workspace": "default", "output": "tuned-results"}, + ctx=ctx, + sdk=cast(NeMoPlatform, _StubSDK()), + ) + + +def test_run_rejects_fileset_output_without_sdk(ctx: JobContext) -> None: + def _dispatch(**kwargs: Any) -> dict[str, Any]: + _write_study_artifacts(ctx) + return {"status": "completed"} + + with ( + patch("nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", side_effect=_dispatch), + pytest.raises(LocalRunError, match="requires a 'sdk: NeMoPlatform'"), + ): + OptimizeJob().run( + {"optimize_config_inline": INLINE_MINIMAL, "workspace": "default", "output": "tuned-results"}, + ctx=ctx, + ) + + +def test_run_reports_missing_artifacts_on_publish(ctx: JobContext, tmp_path: Path) -> None: + """A backend that claims success but writes nothing is a bug, not an empty upload.""" + with ( + patch("nemo_optimization.jobs.optimize.OptimizeRouter.dispatch", return_value={"status": "completed"}), + pytest.raises(FileNotFoundError, match="wrote no artifacts"), + ): + OptimizeJob().run( + { + "optimize_config_inline": INLINE_MINIMAL, + "workspace": "default", + "output": str(tmp_path / "published"), + }, + ctx=ctx, + ) + + def test_run_rejects_endpoint_agent(tmp_path: Path, ctx: JobContext) -> None: optimize_yaml = tmp_path / "optimize.yml" optimize_yaml.write_text("optimizer:\n numeric:\n enabled: true\n") From 6af064227d0f2aea42a3c18f69a82aed739c071e Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Thu, 13 Aug 2026 15:37:47 -0700 Subject: [PATCH 2/2] fix lint Signed-off-by: Sean Teramae --- plugins/nemo-agents/openapi/openapi.yaml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/plugins/nemo-agents/openapi/openapi.yaml b/plugins/nemo-agents/openapi/openapi.yaml index dce724ae32..e545d705d8 100644 --- a/plugins/nemo-agents/openapi/openapi.yaml +++ b/plugins/nemo-agents/openapi/openapi.yaml @@ -3787,24 +3787,7 @@ components: type: string type: object title: OptimizeSpec - description: 'Spec for an Agents optimize study (``nemo agents optimize``). - - - The optimization config arrives one of two ways, and exactly one must be - - set. ``optimize_config`` is a path on the platform host, which only the - - co-located CLI can satisfy; ``optimize_config_inline`` carries the same - - document in the request body, for remote clients (e.g. Studio) that have no - - access to that filesystem. They are separate fields rather than a union - - because the CLI flag generator only collapses unions whose arms share a - - scalar base, and would skip ``--optimize-config`` entirely for ``str | - - dict``.' + description: Spec for an Agents optimize study (``nemo agents optimize``). PaginationData: properties: page: