Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions configs/ci/integration/alphabet_sort.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ args = { min_turns = 2, max_turns = 2, min_names_per_turn = 1, max_names_per_tur
# Mirror of Qwen/Qwen3-0.6B; PI's template patch always re-emits prior
# <think> blocks. Match that with the qwen3 renderer's preserve_all_thinking.
[orchestrator.renderer]
name = "auto"
preserve_all_thinking = true
1 change: 1 addition & 0 deletions configs/gsm8k/rl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ args = { dataset_name = "openai/gsm8k", dataset_subset = "main", math_verify_max
# Mirror of Qwen/Qwen3-0.6B; PI's template patch always re-emits prior
# <think> blocks. Match that with the qwen3 renderer's preserve_all_thinking.
[orchestrator.renderer]
name = "auto"
preserve_all_thinking = true
15 changes: 7 additions & 8 deletions configs/multimodal/rl_color_codeword_feat_renderer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ gpus_per_node = 2
[orchestrator]
batch_size = 16
group_size = 8
use_renderer = true
# 64 concurrent rollouts (batch_size=16 × group_size=4) want
# more than one tokenizer slot to avoid serialization queueing. The
# image processor (CPU-bound) dominates for VLMs so returns diminish
# past 4; bump to 4 as the default for multimodal runs.
pool_size = 4

# Track zero-advantage groups but don't drop them — we're validating the
# multimodal renderer path on 20 steps, not optimizing training efficiency.
Expand All @@ -58,13 +62,8 @@ max_completion_tokens = 64
id = "color-codeword"
args = { images_per_turn = 2, max_turns = 2, num_examples = 100, seed = 42 }

[orchestrator.renderer]
name = "auto"
# 64 concurrent rollouts (batch_size=16 × group_size=4) want
# more than one tokenizer slot to avoid serialization queueing. The
# image processor (CPU-bound) dominates for VLMs so returns diminish
# past 4; bump to 4 as the default for multimodal runs.
pool_size = 4
# Default renderer (AutoRendererConfig) resolves Qwen3-VL-4B-Instruct from
# MODEL_RENDERER_MAP to Qwen3VLRenderer at runtime; no explicit name needed.

[trainer]

Expand Down
2 changes: 1 addition & 1 deletion deps/verifiers
Submodule verifiers updated 77 files
+32 −38 assets/lab/environments/AGENTS.md
+21 −23 docs/byo-harness.md
+6 −2 docs/development.md
+32 −38 docs/environments.md
+25 −28 docs/overview.md
+32 −38 environments/AGENTS.md
+2 −2 pyproject.toml
+25 −0 tests/test_client_multimodal_types.py
+43 −89 tests/test_harbor_env_mcp.py
+80 −0 tests/test_init_script.py
+10 −8 tests/test_lean_task.py
+35 −44 tests/test_opencode_rlm_env.py
+89 −31 tests/test_openenv_client.py
+5 −5 tests/test_prime_plugin.py
+45 −14 tests/test_renderer_client.py
+2 −2 tests/test_renderer_e2e.py
+0 −24 tests/test_rlm_env.py
+105 −9 tests/test_v1_config_extension.py
+93 −73 tests/test_v1_runtime_lifecycle.py
+37 −61 uv.lock
+9 −1 verifiers/__init__.py
+1 −5 verifiers/cli/plugins/prime.py
+27 −44 verifiers/clients/anthropic_messages_client.py
+12 −14 verifiers/clients/client.py
+1 −6 verifiers/clients/openai_chat_completions_client.py
+14 −17 verifiers/clients/openai_chat_completions_token_client.py
+13 −18 verifiers/clients/openai_responses_client.py
+41 −68 verifiers/clients/renderer_client.py
+6 −16 verifiers/envs/environment.py
+13 −21 verifiers/envs/experimental/composable/composable_env.py
+7 −8 verifiers/envs/experimental/composable/harnesses/rlm.py
+12 −19 verifiers/envs/experimental/composable/swe_debug_env.py
+9 −18 verifiers/envs/experimental/composable/task.py
+5 −18 verifiers/envs/experimental/composable/tasksets/lean/lean_task.py
+1 −10 verifiers/envs/experimental/composable/tasksets/swe/log_parser.py
+3 −7 verifiers/envs/experimental/composable/tasksets/swe/multi_swe.py
+2 −2 verifiers/envs/experimental/composable/tasksets/swe/r2e_gym.py
+24 −34 verifiers/envs/experimental/composable/tasksets/swe/swe_rebench_v2.py
+34 −44 verifiers/envs/experimental/composable/tasksets/swe/swe_rebench_v2_log_parsers.py
+22 −19 verifiers/envs/experimental/gym_env.py
+17 −28 verifiers/envs/experimental/harbor_env/mcp.py
+6 −13 verifiers/envs/experimental/mcp_env.py
+9 −16 verifiers/envs/experimental/opencode_rlm_env.py
+40 −62 verifiers/envs/experimental/rlm_env.py
+13 −31 verifiers/envs/experimental/utils/git_checkout_cache.py
+75 −126 verifiers/envs/integrations/openenv_env.py
+1 −5 verifiers/envs/multiturn_env.py
+6 −14 verifiers/gepa/gepa_utils.py
+7 −12 verifiers/rubrics/rubric.py
+17 −29 verifiers/scripts/build.py
+91 −59 verifiers/scripts/init.py
+4 −0 verifiers/serve/server/env_server.py
+0 −7 verifiers/serve/server/env_worker.py
+18 −5 verifiers/types.py
+19 −31 verifiers/utils/client_utils.py
+10 −17 verifiers/utils/data_utils.py
+2 −6 verifiers/utils/display_utils.py
+79 −21 verifiers/utils/env_utils.py
+21 −38 verifiers/utils/eval_utils.py
+4 −9 verifiers/utils/import_utils.py
+10 −11 verifiers/utils/install_utils.py
+9 −11 verifiers/utils/interception_utils.py
+11 −17 verifiers/utils/logging_utils.py
+9 −14 verifiers/utils/message_utils.py
+58 −50 verifiers/utils/response_utils.py
+13 −21 verifiers/utils/save_utils.py
+2 −15 verifiers/utils/thread_utils.py
+2 −2 verifiers/utils/threaded_sandbox_client.py
+54 −61 verifiers/v1/ENVIRONMENT_BEST_PRACTICES.md
+5 −0 verifiers/v1/__init__.py
+17 −21 verifiers/v1/packages/harnesses/command.py
+1 −1 verifiers/v1/packages/harnesses/opencode.py
+6 −10 verifiers/v1/packages/harnesses/pi.py
+12 −18 verifiers/v1/packages/harnesses/rlm.py
+8 −13 verifiers/v1/packages/harnesses/terminus_2.py
+24 −37 verifiers/v1/runtime.py
+1 −4 verifiers/v1/taskset.py
2 changes: 1 addition & 1 deletion docs/multimodal.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ To add permanent support for a new model family, add an entry to `VLM_REGISTRY`

## How Multi-Turn VLM RL Training Works

VLM rollouts go through the renderer-backed TITO client (`orchestrator.use_renderer = true`, the default and required for VLMs). The renderer owns the HuggingFace processor per-slot and emits multimodal tensors alongside tokens.
VLM rollouts go through the renderer-backed TITO client (`orchestrator.renderer` set, the default and required for VLMs). The renderer owns the HuggingFace processor per-slot and emits multimodal tensors alongside tokens.

1. **Render**: For each trajectory step, the renderer tokenizes messages and emits per-image multimodal tensors (e.g. `pixel_values`, `image_grid_thw` for Qwen3-VL) as `multi_modal_data`.
2. **Pack**: `interleave_rollout` concatenates the per-image tensors emitted across a sample's merged step range into a single `mm_kwargs` dict on the `TrainingSample`. Per-token `mm_token_type_ids` (0=text, 1=image, 2=video) come from `renderer.mm_token_type_id_map`.
Expand Down
7 changes: 1 addition & 6 deletions packages/prime-rl-configs/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ requires-python = "~=3.12.0"
dependencies = [
"pydantic>=1.10.13",
"prime-pydantic-config>=0.3.0.dev82",
# OrchestratorConfig validates renderer.name='auto' against
# renderers.base.MODEL_RENDERER_MAP. Renderers itself is light; its only
# weighty transitive (transformers) is lazy-imported via the validator,
# so plain `from prime_rl.configs.orchestrator import OrchestratorConfig`
# still doesn't pull it into sys.modules.
"renderers>=0.1.8.dev4",
"renderers>=0.1.8.dev28",
"tomli>=2.2.1",
"tomli-w>=1.2.0",
]
Expand Down
104 changes: 53 additions & 51 deletions packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from pathlib import Path
from typing import Annotated, Any, Literal, TypeAlias

from pydantic import AliasChoices, Field, model_validator
from pydantic import AliasChoices, Field, model_serializer, model_validator
from pydantic_core.core_schema import SerializerFunctionWrapHandler
from renderers import AutoRendererConfig, RendererConfig

from prime_rl.configs.shared import (
BaseModelConfig,
Expand All @@ -12,7 +14,6 @@
HeartbeatConfig,
LogConfig,
PrimeMonitorConfig,
RendererConfig,
TransportConfig,
WandbWithExtrasConfig,
)
Expand Down Expand Up @@ -570,8 +571,30 @@ class OrchestratorConfig(BaseConfig):

tokenizer: TokenizerConfig = TokenizerConfig()

renderer: RendererConfig = RendererConfig()
"""Client-side renderer configuration. Only consumed when ``use_renderer=true``."""
renderer: RendererConfig | None = AutoRendererConfig()
"""Typed renderer config (``renderers.RendererConfig`` discriminated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we make this docstring a bit more concise

union). Defaults to ``"auto"``, which resolves from
``tokenizer.name_or_path`` via ``MODEL_RENDERER_MAP``. ``None``
opts into MITO (``openai_chat_completions``); SFT mode forces this."""

pool_size: int | None = Field(None, ge=1)
"""Number of renderer slots shared across concurrent rollouts. Bump
for long multi-turn prompts where client-side jinja tokenization
serializes. Only meaningful when ``renderer`` is not ``None``."""

@model_serializer(mode="wrap")
def _preserve_mito_renderer(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
"""Emit ``renderer = "None"`` (string) when MITO so
``model_dump(exclude_none=True)`` round-trips: dumped TOML has
``renderer = "None"``, and on reload
``BaseConfig._none_str_to_none`` coerces it back to ``None``.
Without this, a MITO orchestrator config saved to
``control/orch.toml`` would lose the renderer key entirely and
reload as the default ``AutoRendererConfig()`` (TITO)."""
result = handler(self)
if self.renderer is None:
result["renderer"] = "None"
return result
Comment on lines +585 to +597
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hmm this is code smell from the pydantic-config side. can merge for now but can you put an issue to fix this one


optim: OptimizerConfig = OptimizerConfig()
"""Per-run optimizer configuration for multi-run training."""
Expand Down Expand Up @@ -647,9 +670,6 @@ class OrchestratorConfig(BaseConfig):
heartbeat: HeartbeatConfig | None = None
"""BetterStack heartbeat configuration for monitoring training progress."""

use_renderer: bool = True
"""Use the renderer-backed TITO client (client-side tokenization via the ``renderers`` package, served by ``/v1/generate``). When True, the ``[orchestrator.renderer]`` block (name / tool_parser / reasoning_parser / pool_size) applies. Default for both text-only and VLM rollouts; VLMs require it. False falls back to MITO (``openai_chat_completions``)."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing breaking config changelog

Medium Severity

This PR removes orchestrator.use_renderer, moves renderer pool sizing to orchestrator.renderer_pool_size, and replaces the flat [orchestrator.renderer] schema with the typed renderers.RendererConfig union (MITO via renderer = "None"). Those are breaking config changes, but CHANGELOG.md was not updated with a migration entry.

Fix in Cursor Fix in Web

Triggered by project rule: BugBot Instructions

Reviewed by Cursor Bugbot for commit 2072afe. Configure here.

env_install_prerelease: bool = False
"""Allow pre-release versions when installing environments (e.g. ``verifiers>=0.1.12.dev5``). Passes ``--prerelease`` to ``prime env install``."""

Expand Down Expand Up @@ -777,11 +797,11 @@ def validate_unique_filter_types(self):
@model_validator(mode="after")
def _force_no_renderer_for_sft(self):
"""SFT rolls out via the teacher's plain chat-completions endpoint; the
renderer client doesn't apply. Force use_renderer=False so the user
renderer client doesn't apply. Force ``renderer=None`` so the user
doesn't have to remember to set it. Declared before the renderer
validators below so they see the corrected value."""
if self.training_mode == "sft":
self.use_renderer = False
self.renderer = None
return self

@model_validator(mode="after")
Expand All @@ -795,33 +815,15 @@ def validate_training_mode(self):
return self

@model_validator(mode="after")
def validate_renderer_args(self):
"""``[orchestrator.renderer]`` knobs are only meaningful when
``use_renderer=True``. Reject otherwise so callers don't silently
pass them and wonder why they're ignored."""
if self.use_renderer:
return self

renderer_args_set = []
if self.renderer.name != "auto":
renderer_args_set.append(f"renderer.name={self.renderer.name!r}")
if self.renderer.tool_parser is not None:
renderer_args_set.append(f"renderer.tool_parser={self.renderer.tool_parser!r}")
if self.renderer.reasoning_parser is not None:
renderer_args_set.append(f"renderer.reasoning_parser={self.renderer.reasoning_parser!r}")
if self.renderer.pool_size is not None:
renderer_args_set.append(f"renderer.pool_size={self.renderer.pool_size!r}")
if self.renderer.preserve_all_thinking:
renderer_args_set.append(f"renderer.preserve_all_thinking={self.renderer.preserve_all_thinking!r}")
if self.renderer.preserve_thinking_between_tool_calls:
renderer_args_set.append(
f"renderer.preserve_thinking_between_tool_calls={self.renderer.preserve_thinking_between_tool_calls!r}"
)

if renderer_args_set:
def validate_pool_size(self):
"""``pool_size`` is only meaningful when the renderer is enabled
(``renderer is not None``). Reject otherwise so callers don't
silently pass it and wonder why it's ignored."""
if self.renderer is None and self.pool_size is not None:
raise ValueError(
"Renderer-specific args set without orchestrator.use_renderer=True: "
f"{', '.join(renderer_args_set)}. Either enable the renderer client or remove these knobs."
f"orchestrator.pool_size={self.pool_size!r} is set but "
"orchestrator.renderer is None (MITO mode). Either configure a renderer "
"or remove pool_size."
)
return self

Expand All @@ -833,9 +835,9 @@ def vlm_requires_renderer(self):
tokens, and ships generic ``mm_kwargs`` keyed by whatever the
model's forward signature expects.
"""
if self.student.model.vlm is not None and not self.use_renderer:
if self.student.model.vlm is not None and self.renderer is None:
raise ValueError(
"orchestrator.use_renderer must be true when model.vlm is set. "
"orchestrator.renderer must be set when model.vlm is set. "
"VLMs must go through a renderer (e.g. Qwen3VLRenderer) that owns the processor."
)
return self
Expand All @@ -844,36 +846,36 @@ def vlm_requires_renderer(self):
def validate_renderer_auto_resolves(self):
"""Reject the silent DefaultRenderer fallback at config time.

When ``use_renderer=True`` with ``renderer.name='auto'`` and the
model isn't in ``MODEL_RENDERER_MAP``, ``create_renderer`` would
fall back to ``DefaultRenderer``. That fallback doesn't fix the
position-dependent chat-template bug the renderer client exists to
solve, and rejects envs that pass tools (the rollout dies with
"RendererPool does not support tools") unless
``renderer.tool_parser`` is configured. Surface at config time so
``--dry-run`` reports the error.
When ``renderer.name='auto'`` and the model isn't in
``MODEL_RENDERER_MAP``, ``create_renderer`` would fall back to
``DefaultRenderer``. That fallback doesn't fix the
position-dependent chat-template bug the renderer client exists
to solve, and rejects envs that pass tools (the rollout dies
with "RendererPool does not support tools") unless
``DefaultRendererConfig.tool_parser`` is configured. Surface at
config time so ``--dry-run`` reports the error.
"""
if not self.use_renderer or self.renderer.name != "auto":
if self.renderer is None or self.renderer.name != "auto":
return self
from renderers.base import MODEL_RENDERER_MAP

model_id = self.tokenizer.name or self.student.model.name
if model_id in MODEL_RENDERER_MAP:
return self
raise ValueError(
f"orchestrator.use_renderer=True with renderer.name='auto' but "
f"orchestrator.renderer.name='auto' but "
f"{model_id!r} is not in renderers.base.MODEL_RENDERER_MAP, so it "
f"would silently fall back to DefaultRenderer. Pick one: "
f"(a) [orchestrator.renderer] name='default' — for fine-tunes / "
f"vendored mirrors with custom chat templates (DefaultRenderer "
f"calls apply_chat_template); pair with tool_parser=<name> if "
f"the env uses tools. "
f"calls apply_chat_template); set tool_parser=<name> if the env "
f"uses tools. "
f"(b) [orchestrator.renderer] name=<model-specific renderer> — "
f"if {model_id!r} is template-identical to a mapped family "
f"(and ideally also add it upstream to "
f"renderers.base.MODEL_RENDERER_MAP). "
f"(c) orchestrator.use_renderer=false — opt out of the renderer "
f"client entirely."
f"(c) orchestrator.renderer='none' — opt out of the renderer "
f"client entirely (MITO)."
)

@model_validator(mode="after")
Expand Down
64 changes: 10 additions & 54 deletions packages/prime-rl-configs/src/prime_rl/configs/sft.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from typing import Annotated, Literal, TypeAlias

from pydantic import Field, model_validator
from renderers import RendererConfig

from prime_rl.configs.shared import (
HeartbeatConfig,
RendererConfig,
SlurmConfig,
TrainerLogConfig,
WandbConfig,
Expand Down Expand Up @@ -175,11 +175,13 @@ class SFTConfig(BaseConfig):

tokenizer: TokenizerConfig = TokenizerConfig()

renderer: RendererConfig = RendererConfig()
"""Client-side renderer configuration. Only consumed when ``use_renderer=true``."""

use_renderer: bool = False
"""Tokenize SFT samples through the ``renderers`` library (single ``render()`` + ``message_indices`` mask) instead of the default ``build_incremental_token_mask`` path. Required for chat templates that render position-dependently (e.g. Qwen3, Qwen3.5)."""
renderer: RendererConfig | None = None
"""Typed renderer config (``renderers.RendererConfig`` discriminated
union). When set, SFT tokenizes samples through the ``renderers``
library (single ``render()`` + ``message_indices`` mask) instead of
the default ``build_incremental_token_mask`` path. Required for chat
templates that render position-dependently (e.g. Qwen3, Qwen3.5).
``None`` (default) uses the legacy tokenization path."""

data: DataConfig = SFTDataConfig()

Expand Down Expand Up @@ -319,59 +321,13 @@ def dont_do_massive_traces(self):

@model_validator(mode="after")
def validate_renderer_vs_vlm(self):
if self.use_renderer and self.model.vlm is not None:
if self.renderer is not None and self.model.vlm is not None:
raise ValueError(
"use_renderer is not supported for VLMs. The renderer tokenizes "
"renderer is not supported for VLMs in SFT. The renderer tokenizes "
"text-only message dicts client-side and cannot handle image inputs."
)
return self

@model_validator(mode="after")
def validate_renderer_args(self):
# pool_size is orchestrator-only. An in-process renderer pool exists
# to amortize tokenization across concurrent rollouts in the
# orchestrator (many async requests render at once, HF fast
# tokenizers release the GIL during Rust encoding, so a pool of N
# tokenizer copies parallelizes well). SFT has no such concurrency:
# the StatefulDataLoader is constructed with num_workers=0, so the
# main process tokenizes one example at a time, between training
# steps. Across DP, each rank already owns its own renderer — an
# implicit pool of size world_size. Pooling within a rank gives
# nothing on top of that. Reject so callers don't silently set a
# knob that does nothing; if SFT tokenization ever becomes a
# bottleneck the fix is num_workers on the dataloader, not a pool.
if self.renderer.pool_size is not None:
raise ValueError(
f"renderer.pool_size={self.renderer.pool_size!r} is only used by the orchestrator. "
"SFT tokenizes synchronously (num_workers=0) and already gets one renderer per DP "
"rank — an in-process pool adds nothing. If tokenization is a bottleneck, raise "
"num_workers on the dataloader instead."
)

if self.use_renderer:
return self

renderer_args_set = []
if self.renderer.name != "auto":
renderer_args_set.append(f"renderer.name={self.renderer.name!r}")
if self.renderer.tool_parser is not None:
renderer_args_set.append(f"renderer.tool_parser={self.renderer.tool_parser!r}")
if self.renderer.reasoning_parser is not None:
renderer_args_set.append(f"renderer.reasoning_parser={self.renderer.reasoning_parser!r}")
if self.renderer.preserve_all_thinking:
renderer_args_set.append(f"renderer.preserve_all_thinking={self.renderer.preserve_all_thinking!r}")
if self.renderer.preserve_thinking_between_tool_calls:
renderer_args_set.append(
f"renderer.preserve_thinking_between_tool_calls={self.renderer.preserve_thinking_between_tool_calls!r}"
)

if renderer_args_set:
raise ValueError(
"Renderer-specific args set without use_renderer=True: "
f"{', '.join(renderer_args_set)}. Either enable the renderer or remove these knobs."
)
return self

@model_validator(mode="after")
def validate_lora_adapter_saving(self):
if self.ckpt and self.ckpt.weights and self.ckpt.weights.save_adapter_separately:
Expand Down
20 changes: 0 additions & 20 deletions packages/prime-rl-configs/src/prime_rl/configs/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,26 +84,6 @@ def is_vlm(self) -> bool:
return self.vlm is not None


class RendererConfig(BaseConfig):
name: str = "auto"
"""Renderer used for chat-template tokenization. One of: ``auto`` (detect from tokenizer), ``qwen3``, ``qwen3_vl``, ``qwen3.5``, ``glm5``, ``glm4.5``, ``minimax-m2``, ``deepseek_v3``, ``kimi_k2``, ``kimi_k25``, ``nemotron3``, ``gpt_oss``, ``default``."""

tool_parser: str | None = None
"""Tool parser from ``renderers.parsers``. Only consumed by DefaultRenderer; model-specific renderers bake their own parsing in. Options: ``qwen3``, ``qwen3.5``, ``glm``, ``deepseek_v3``."""

reasoning_parser: str | None = None
"""Reasoning parser from ``renderers.parsers``. Only consumed by DefaultRenderer. Options: ``think``."""

pool_size: int | None = Field(None, ge=1)
"""Number of renderer slots shared across concurrent rollouts. Bump for long multi-turn prompts where client-side jinja tokenization serializes."""

preserve_all_thinking: bool = False
Comment thread
cursor[bot] marked this conversation as resolved.
"""Re-emit every past-assistant turn's ``reasoning_content`` between ``<think>``/``</think>`` (or the model's equivalent), even when the chat template would drop it. Strict superset of preserve_thinking_between_tool_calls."""

preserve_thinking_between_tool_calls: bool = False
"""Preserve past-assistant ``reasoning_content`` only inside the current tool cycle — the contiguous assistant→tool→…→assistant block after the most recent user message, when that block contains at least one tool response. A new user turn closes the block."""


class ElasticConfig(BaseConfig):
hostname: str
"""DNS hostname that resolves to inference server IPs."""
Expand Down
Loading
Loading