fix(launcher): guarantee SlurmLogStream tail-subprocess termination

## Summary

`SlurmLogStream._start_job_checking`'s watcher loop has a silent-leak path: any exception from `self._client.get_job(...)` `break`s out of the loop without terminating the `tail -F` subprocess it was responsible for cleaning up. The orphaned tail blocks indefinitely on a now-static log file, and stream consumers (anyone iterating `SlurmLogStream`) block on `proc.stdout.readline()` forever.

## Reproduction

Any consumer doing:

```python
log_stream = cluster.get_logs_stream(cluster_name, job_id)
for line in log_stream:
    ...
```

…will hang if the watcher exits via the `except Exception: break` branch (e.g. a transient SSH wedge, a `sacct`/`squeue` parse miss, or any other `get_job` failure). `tail -F` follows the file with retry semantics and does not exit on its own, so `proc.stdout.readline()` never returns EOF.

## Root cause

Pre-existing watcher (`src/oumi/launcher/clients/slurm_client.py:394`):

```python
def check_job_status():
    while True:
        try:
            job = self._client.get_job(self.job_id)
            if job is None or job.done:
                proc.terminate()
                proc.wait()
                break
            time.sleep(2)
        except Exception:
            break   # ← exits without calling proc.terminate()
```

`break` on exception exits the loop without running the termination path. `tail -F` survives the daemon thread because the subprocess is started in a new session.

## Fix

Restructure with `try`/`finally` so termination always runs on any exit path:

```python
def check_job_status():
    try:
        while True:
            try:
                job = self._client.get_job(self.job_id)
            except Exception as e:
                logger.warning(
                    f"Job-status check for {self.job_id} failed: {e}; "
                    "terminating log tail."
                )
                return
            if job is None or job.done:
                return
            time.sleep(2)
    finally:
        try:
            proc.terminate()
            proc.wait(timeout=5)
        except Exception:
            pass
```

Side effects:
- `get_job` failure now surfaces as `logger.warning` instead of being swallowed.
- `proc.wait(timeout=5)` bounds shutdown so a wedged subprocess can't keep the watcher thread alive.

No public API change. Behavior on the happy path (`job.done == True`) is identical.

## Tests

Two unit tests in `tests/unit/launcher/clients/test_slurm_client.py`:

| Test | Asserts |
|---|---|
| `test_slurm_log_stream_terminates_tail_when_job_done` | happy path — `proc.terminate()` called when `get_job` returns `done=True` |
| `test_slurm_log_stream_terminates_tail_when_get_job_raises` | regression — `proc.terminate()` still called when `get_job` raises |

`37 passed` locally (35 existing + 2 new).

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


build(deps): revert torchvision upper bound to <0.26

## Summary

Reverts [#2474](https://github.com/oumi-ai/oumi/pull/2474) — narrows torchvision back to `>=0.21,<0.26`.

## Why

torchvision `>=0.27` resolves to torch `2.11+`, whose default wheels ship the CUDA 13 runtime. That propagates to every consumer of oumi:

- Worker / inference base images currently built on CUDA 12 baseline
- SkyPilot setup scripts and Modal containers
- AWS / Nebius / Lambda GPU instance driver compatibility
- Pinned deps in downstream consumers (vllm, flash-attn, peft, torchao, transformers) — each has CUDA-version-sensitive prebuilt wheels

Keeping `<0.26` holds the ecosystem on CUDA 12 until the transition is explicitly planned and validated end-to-end.

## Test plan

- [ ] CI green on this PR
- [ ] No downstream consumer requires torchvision `>=0.26` today

When a real CUDA 13 transition is on the roadmap, bump this bound in a dedicated PR that also lands the matching wheels in the downstream training / inference images.

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


Pin the aiohttp version

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->

A new aiohttp version was released with minor breaking changes. Pin the previous working version.

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


feat(deploy): deploy Fireworks endpoints by validated deployment shape

# Description

Adds `DeploymentShape.name` and a `deployment_shape` argument to `create_endpoint`, so callers can deploy a Fireworks endpoint by a validated deployment shape instead of a raw `acceleratorType` x `count`. `list_deployment_shapes` now fills `name` from the shape's family path (with the `/versions/<id>` suffix stripped).

## Related issues

Towards LOU-2245

## Before submitting

- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


feat(launcher): Forward JobResources.accelerators/cpus/memory to sbatch

## Summary

`SlurmCluster.run_job` currently logs "Accelerators are unused for Slurm jobs" and drops `JobResources.accelerators`, `cpus`, and `memory` on the floor. Any job submitted with `cloud="slurm"` and `accelerators="H100:8"` runs as CPU-only and fails on GPU-dependent code. This forwards them to `sbatch` as `--gres` / `--cpus-per-task` / `--mem`.

## Changes

- `_parse_accelerators_to_gres()` — translates oumi accelerator strings into Slurm `--gres` values

  | Input | Output |
  |---|---|
  | `"H100:8"` | `"gpu:H100:8"` |
  | `"H100"` | `"gpu:H100:1"` |
  | `"A100-80GB:4"` | `"gpu:A100-80GB:4"` |
  | `":8"` | `"gpu:8"` (untyped) |
  | `"8"` | `"gpu:8"` (untyped) |
  | `"H100:8+"` (SkyPilot "at least") | `"gpu:H100:8"` |
  | `"8+"` | `"gpu:8"` |

- `_resources_to_sbatch_kwargs()` — collects gres + cpus_per_task + mem from a `JobResources`. Strips SkyPilot's `"+"` minimum-modifier across all three fields. Preserves explicit memory unit suffixes (`32G`, `2T`); defaults to `G` for bare numbers (per `JobResources.memory` doc: "in GiB").

- `SlurmCluster.run_job` forwards the kwargs to `SlurmClient.submit_job`, which already routes unknown kwargs to `--<flag>=<value>` on the sbatch command.

- `_validate_job_config` no longer warns for fields we now forward (accelerators, cpus, memory). Region/zone/instance_type/disk_size warnings remain — those still don't have a Slurm mapping.

## Tests

- 13 new unit tests covering the helper edge cases, typed vs untyped GRES, SkyPilot modifier stripping (incl. `H100:8+` and `8+`), explicit memory units, and the bare-resources case (cluster defaults take effect when nothing's pinned).
- 7 existing `run_job` assertions updated to expect the new kwargs (the default test fixture uses `accelerators="A100-80GB" cpus="4" memory="64"` — those now propagate).
- All 82 slurm cluster/client tests pass.

Skipped local pyright (`SKIP=pyright`) — pre-existing `src/oumi/cli/analyze.py` errors on `origin/main` unrelated to this change.

## E2E verification

### Direct `oumi.launcher.up` (standalone)

```python
oumi.launcher.up(
    JobConfig(
        cloud="slurm",
        resources=JobResources(cloud="slurm", accelerators="H100:1"),
        run="echo CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES; nvidia-smi -L",
    ),
    cluster_name="root@<controller>",
)
```

Job 214 on RunPod cluster: `JobState=COMPLETED ExitCode=0:0`, `TresPerNode=gres:gpu:H100:1`, stdout shows `CUDA_VISIBLE_DEVICES=6` + `nvidia-smi -L` listing the H100s.

### Full worker stack (curl → API → workflow → run_job_activity → oumi → sbatch)

Submitted a training request via the local API (`POST /v1/projects/{id}/models:train`, `cloud_provider="slurm"`, `accelerator="H100:1"` via `TRAINING_ACCELERATOR_TIERS` override). Worker dispatched Slurm job 219:

```
JobId=219 JobName=job-172-1317
   JobState=FAILED Reason=NonZeroExitCode ExitCode=127:0   ← script needed `sudo apt-get`, unrelated
   BatchHost=node-1
   TRES=cpu=2,node=1,billing=2
   TresPerNode=gres:gpu:H100:1                              ← --gres correctly forwarded
```

Confirmed via a temporary `logger.info` (now removed) that the worker's call into oumi was:
```
_resources_to_sbatch_kwargs: accelerators='H100:1' cpus=None memory=None -> kwargs={'gres': 'gpu:H100:1'}
```

So the resource forwarding correctly traverses: API request → worker JobConfig → oumi `SlurmCluster.run_job` → `SlurmClient.submit_job` → `sbatch --gres=gpu:H100:1`.

Before this PR: the same path produced `TresPerNode=` empty (the warning fired, accelerators dropped).

## Caveat for cluster operators

Typed-GRES requires the type string to match an entry in the cluster's `gres.conf`. If the user passes `"H100:8"` and the cluster only has untyped GRES (`gres.conf` doesn't define `Type=H100`), sbatch will reject the request. Workaround: pass `":8"` or `"8"` to skip the type, or fix `gres.conf` upstream.


refactor(launcher): SlurmClient.get_job uses squeue + scontrol directly

## Summary

Refactors `SlurmClient.get_job(id)` to use `squeue` + `scontrol` directly

## What changes

- `SlurmClient.get_job(id)`:
  - **Active jobs:** `squeue --user=X --noheader --format='%i %j %u %T %R'`
  - **Recently-finished jobs:** `scontrol show job <id>` (retained by `slurmctld` for `MinJobAge` seconds — default 300, commonly raised to 86400 in production)
- `SlurmCluster.get_job(id)` now delegates directly to `SlurmClient.get_job` instead of iterating `get_jobs()`

## Why

- Works on clusters without `slurmdbd` (e.g. RunPod Instant Clusters)
- More real-time than `sacct` (which lags `slurmdbd` flush interval, typically 1-5 min) on clusters that do have it
- Smaller code surface — no `sacct`-detection branching

For terminal-state retention past `MinJobAge`, the cluster admin sets `MinJobAge` to a larger value (e.g. 86400 = 24h). No code-side fallback needed.

## Diff

Net **-130 lines** — much of the savings from removing now-redundant sacct-based test setup.

```
 src/oumi/launcher/clients/slurm_client.py          |  75 ++-
 src/oumi/launcher/clusters/slurm_cluster.py        |  10 +-
 tests/unit/launcher/clients/test_slurm_client.py   | 405 +++++++-------------
 tests/unit/launcher/clusters/test_slurm_cluster.py | 320 +++++-----------
 4 files changed, 340 insertions(+), 470 deletions(-)
```

## Tests

64 SlurmClient + SlurmCluster tests pass, including 4 new tests covering the squeue + scontrol paths.

## Compat

`list_jobs()` semantics unchanged. Users with `slurmdbd` see no behavior difference there.

`get_job(id)` is now more real-time (squeue vs. sacct's flush-interval lag) and works on clusters without `slurmdbd`. Behavioral change: previously, calling `get_job` for a job that had been purged from `slurmctld` but was still in `sacct` would return the historical record; now it returns `None`. In practice, polling loops call `get_job` frequently enough that the active job is always caught by squeue or scontrol.

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


feat(synthesis): accumulate token usage in ConversationSynthesizer

Mirrors `AttributeSynthesizer`'s token accounting so multi-turn synthesis is symmetric with single-turn.

## What changed
- Adds `_total_input_tokens` / `_total_output_tokens` / `_total_cached_tokens` counters in `__init__`.
- Adds `total_input_tokens` / `total_output_tokens` / `total_cached_tokens` properties and an `_accumulate_token_usage()` helper (copied verbatim from `AttributeSynthesizer` so the two synthesizers match).
- Calls `_accumulate_token_usage()` after all **four** `infer()` call sites: the planner (`_generate_plan`), per-turn user generation (`_synthesize_all_samples`), the assistant tool-loop round (`_run_assistant_tool_round`), and straggler finalization (`_finalize_stragglers`).

## Why
`ConversationSynthesizer` previously did not count inference tokens, so downstream consumers (enterprise worker, CLI, anyone reading these synthesizers) saw zero tokens for multi-turn conversations — causing silent under-billing.

## Tests
TDD: added `test_token_usage_accumulates_from_inference_metadata` (failed before, passes after). Full file: 68 passed.


feat(gemma4): regex LoRA targeting + E2B/E4B LoRA recipes

# Gemma 4 LoRA training support

Adds first-class **LoRA fine-tuning for Gemma 4** (E2B / E4B) in Oumi, plus ready-to-run recipes.

Gemma 4 is multimodal (`Gemma4ForConditionalGeneration`): a text transformer alongside vision and audio towers. Those towers use `Gemma4ClippableLinear` modules that PEFT cannot adapt — and they reuse the same projection names (`q_proj`, `v_proj`, …) as the text model, so a naive LoRA target list crashes on them. Two small core changes make LoRA SFT work end-to-end.

## What's in this PR

- **`lora_exclude_modules`** (`PeftParams`) — a new field that scopes LoRA *away* from named modules. Entries are regex patterns, joined into a single regex and passed to PEFT's `exclude_modules` (matched with `re.fullmatch`); exclusion takes precedence over `lora_target_modules` and over `all-linear`. Recipes target the plain projection names and exclude the towers:

  ```yaml
  peft:
    lora_target_modules: ["q_proj", "v_proj", ...]   # bare names, matched by suffix
    lora_exclude_modules: [".*vision_tower.*", ".*audio_tower.*"]
  ```

  The two fields are intentionally asymmetric: `lora_target_modules` takes bare names (PEFT matches them by suffix), but `lora_exclude_modules` must be regexes — PEFT matches a bare-name exclude list by leaf-suffix, so `"vision_tower"` would exclude only that node and not the `q_proj`/`v_proj` nested under it. A regex like `.*vision_tower.*` reaches the whole subtree, which is what keeps LoRA off the towers. The `lora_target_modules` docstring is also clarified to state names match by suffix.
- **`Message.get()`** (`core/types/conversation.py`) — a dict-style `.get(key, default)` on `Message`. Gemma 4's chat template reads message fields via `.get()`; without it, `apply_chat_template` raised `UndefinedError`. Only declared fields are exposed, so a key that collides with a method name (e.g. `"get"`) returns the default rather than a bound method. (Internal: OPE-1861.)
- **Recipes** (`configs/recipes/gemma4/`) — E2B and E4B LoRA SFT recipes (`sft/{e2b,e4b}_lora/`), the E4B full-fine-tune recipe (`sft/e4b_full/`, originally authored by @wizeng23 and cherry-picked with authorship preserved), GCP launch jobs for each, and a family README documenting the LoRA-scoping approach.

**No dependency changes.** Gemma 4 training needs `transformers >= 5.5.4`, which is already within Oumi's existing pins (installed automatically with `oumi`).

## Validation

Trained both LoRA recipes on **banking77** (77-class banking-intent classification; 3,080-example test set) on H100, then measured accuracy. LoRA lifts the base instruct model substantially:

| Model | Base instruct | **+ LoRA (this recipe)** |
|-------|:-------------:|:------------------------:|
| Gemma 4 E2B | 65.2% | **90.5%** |
| Gemma 4 E4B | 69.5% | **94.4%** |

LoRA attaches only to the language model (~12M / ~17M trainable params), loss decreases to convergence, and there are no `Gemma4ClippableLinear` errors — confirming the recipes train a useful adapter end-to-end on a real downstream task. Unit tests cover `to_lora()` exclude-regex joining (`test_peft_params.py`) and `Message.get()` field scoping (`test_conversation.py`). (Eval/inference tooling for Gemma 4 lives downstream; this PR adds the training path.)

## Related issues

Internal: OPE-1861 (chat-template `.get()` compatibility). Part of the Gemma 4 training onboarding effort.

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?


feat(deploy): forward idle-window kwargs on Fireworks create_endpoint

## Description

Adds optional `scale_down_window_seconds` and `scale_to_zero_window_seconds` kwargs on `FireworksDeploymentClient.create_endpoint`.

Mirrors Parasail's existing pattern of provider-specific autoscaling kwargs (`scale_down_policy` + `scale_down_threshold_ms`); the shared `AutoscalingConfig` stays minimal (`min_replicas` + `max_replicas`), which is the genuinely common shape.

Fireworks defaults applied when kwargs are omitted: 10min scale-down, 1h scale-to-zero, 5min minimum.

## Related issues

LOU-2152

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you add / update tests where needed?

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


build(deps): update click requirement from <8.4.0 to <8.5.0

Updates the requirements on [click](https://github.com/pallets/click) to permit the latest version.

Release notes

Sourced from click's releases.

8.4.1

This is the Click 8.4.1 fix release, which fixes bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.

PyPI: https://pypi.org/project/click/8.4.1/
Changes: https://click.palletsprojects.com/page/changes/#version-8-4-1
Milestone: https://github.com/pallets/click/milestone/32?closed=1

get_parameter_source() is available during eager callbacks and type conversion again. #3458 #3484

Zsh completion scripts parse correctly on Windows. #3277 # 3466

Shell completion of Choice Enum values produces a valid completion result. #3015

Fix empty byte-string handling in echo. #3487

Fix closed file error with echo_via_pager. #3449

Changelog

Sourced from click's changelog.

Version 8.4.1

Released 2026-05-21

get_parameter_source() is available during eager callbacks and type
conversion again. :issue:3458 :issue:3484

Zsh completion scripts parse correctly on Windows. :issue:3277 :pr:3466

Shell completion of Choice Enum values produces a valid completion
result. :issue:3015

Fix empty byte-string handling in echo. :issue:3487

Fix closed file error with echo_via_pager. :issue:3449

Version 8.4.0

Released 2026-05-17

:class:ParamType typing improvements. :pr:3371

:class:ParamType is now a generic abstract base class,
parameterized by its converted value type.

:meth:~ParamType.convert return types are narrowed on all
concrete types (str for :class:STRING, int for
:class:INT, etc.).

:meth:~ParamType.to_info_dict returns specific
:class:~typing.TypedDict subclasses instead of
dict[str, Any].

:class:CompositeParamType and the number-range base are now
generic with abstract methods.

Refactor convert_type to extract type inference into a private
_guess_type helper, and add :func:typing.overload signatures.
:pr:3372

:class:Parameter typing improvements. :pr:2805

:class:Parameter is now an abstract base class, making explicit
that it cannot be instantiated directly.

:attr:Parameter.name is now str instead of str | None.
When expose_value=False, the name is set to "" instead
of None.

The ctx parameter of :meth:Parameter.get_error_hint is now
typed as <code


build(deps): update torchvision requirement from <0.26,>=0.21 to >=0.21,<0.28

Updates the requirements on [torchvision](https://github.com/pytorch/vision) to permit the latest version.

Release notes

Sourced from torchvision's releases.

TorchVision 0.27 Release

TorchVision 0.27 is out! This is a small release where the main improvement is the addition of the popular lanczos interpolation mode for the v2.Resize transform on CPU. Results are equivalent to PIL's, but you can expect TorchVision to be faster as it leverages AVX2 (on x86) and NEON paths (on ARM).

Improvements

[transforms] Add support for lanczos interpolation mode (#9459)
[transforms] Drastically speed-up Resize on NEON ARM (#9439)
[ops] Vectorize masks_to_boxes for performance (#9358)
[ops, transforms] Add direct XYWH-CXCYWH conversion for better performance (#9326)
[datasets] torchvision.datasets.voc: update dataset and project site URLs (#9216)
[ops] Add support for rotated boxes in box_iou (#9404, #9379)
[ops][MPS] Improve runtime complexity of roi_align (#9100)
[Code quality] #9359, #9364, #9359, #9317, #9409, #9408, #9416, #9411, #4463, #9475, #9427, #9448, #9443, #9396, #9316, #9286, #9324, #9338, #9381, #9386

[Documentation] #9339, #9351, #9323, #9374, #9412, #9


build(deps): update transformers requirement from <5.9,>=4.57 to >=4.57,<5.10

Updates the requirements on [transformers](https://github.com/huggingface/transformers) to permit the latest version.

Release notes

Sourced from transformers's releases.

Release v5.9.0

New Model additions

Cohere2Moe

Command A+ is a Mixture-of-Experts (MoE) language model from Cohere that features a hybrid attention pattern combining sliding window and full attention layers. The model incorporates both shared and routed experts and supports a very large context window for processing extensive text sequences.

Links: Documentation

Add new cohere2_moe model (#46115) by @Cyrilvallez in #46115

Parakeet tdt (#44171)

Parakeet tdt (#44171) by @lmaksyk

HRM-Text

HRM-Text is an improved autoregressive language-modeling variant of the Hierarchical Reasoning Model (HRM) that uses a hierarchical recurrent forward pass with two transformer stacks - one for slow, abstract planning (H) and one for fast, detailed computation (L) - reused inside a nested recurrence. It features PrefixLM attention where instruction tokens attend bidirectionally while response tokens attend causally, per-head sigmoid output gates, and parameterless RMSNorm. The model is designed as a base language model without instruction tuning or chat templates.

Links: Documentation | Paper

Add hrm text (#46025) by @abcd1927 in #46025

Breaking changes

The text_embeds input for SAM3, EdgeTAM, and SAM3-Lite-Text models now expects full text embeddings instead of just pooler outputs, aligning with other models in the library — users must update their inputs accordingly.

🚨Fix memory leaks caused by lru decorators in vision models (#45922) by @yonigozlan

Audio

Audio support was expanded with the addition of AudioFlamingoNext model checkpoints and improved compilability of audio/vision encoders via standalone pure functions. Additional improvements include better error messaging when loading audio from video files and new documentation for audio/video processors.

user friendly error when loading audio from video (#45221) by @eustlb in #45221

[docs] adding audio/video processors (#45795) by @stevhliu in #45795

Support Audio Flamingo Next checkpoints (<a href="https://redirect.github.com/huggingface/tr


build(deps): update uvicorn requirement from <0.48.0 to <0.49.0

Updates the requirements on [uvicorn](https://github.com/Kludex/uvicorn) to permit the latest version.

Release notes

Sourced from uvicorn's releases.

Version 0.48.0

What's Changed

Default ssl_ciphers to None and use OpenSSL defaults by @Kludex in Kludex/uvicorn#2940

Ignore duplicate forwarding headers in ProxyHeadersMiddleware by @Kludex in Kludex/uvicorn#2944

Full Changelog: https://github.com/Kludex/uvicorn/compare/0.47.0...0.48.0

Changelog

Sourced from uvicorn's changelog.

0.48.0 (May 24, 2026)

Changed

Default ssl_ciphers to None and use OpenSSL defaults (#2940)

Fixed

Ignore duplicate forwarding headers in ProxyHeadersMiddleware (#2944)

0.47.0 (May 14, 2026)

Added

Add ssl_context_factory for custom SSLContext configuration (#2920)

Changed

Eagerly import the ASGI app in the parent process (#2919)

Fixed

Treat fd=0 as a valid file descriptor with reload/workers (#2927)

0.46.0 (April 23, 2026)

Added

Support ws_max_size in wsproto implementation (#2915)

Support ws_ping_interval and ws_ping_timeout in wsproto implementation (#2916)

Changed

Use bytearray for incoming WebSocket message buffer in websockets-sansio (#2917)

0.45.0 (April 21, 2026)

Added

Add --reset-contextvars flag to isolate ASGI request context (#2912)

Accept os.PathLike for log_config (#2905)

Accept log_level strings case-insensitively (#2907)

Changed

Revert "Emit http.disconnect on server shutdown for streaming responses" (#2913)

Revert "Explicitly start ASGI run with empty context" (#2911)

Fixed

... (truncated)

Commits

73e84e5 Version 0.48.0 (#29


fix(inference/fireworks): map EXPIRED job state to BatchStatus.EXPIRED

# Description

Fireworks batch jobs can enter the `EXPIRED` state, but it was missing from `_FIREWORKS_STATE_MAPPING` in `FireworksInferenceEngine`. Unmapped states fall through to `BatchStatus.IN_PROGRESS`, so an expired (terminal) job looked like it was still running and callers would poll indefinitely. This adds the `EXPIRED -> BatchStatus.EXPIRED` mapping so the engine reports the terminal state correctly.

## Related issues

N/A — minor bug fix surfaced while reviewing Fireworks batch state handling.

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [ ] Did you add / update tests where needed?

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


fix(trainer): preserve _name_or_path in saved config.json

## Summary

`save_pretrained` was writing `config.json` with `_name_or_path: null` for Oumi-trained models. Patch the field back after save so downstream consumers can identify the base model.

## Changes

- New `_patch_name_or_path_in_config(output_dir, model_name)` helper in `hf_trainer.py` reads the saved `config.json` and sets `_name_or_path` from `config.model.model_name` when null / missing.
- Called at the end of both `_save_model` and `_save_fsdp_model`. No-ops on adapter-only saves (no `config.json`) and malformed configs.
- Unit tests cover: null value, missing key, existing value preserved, missing file, malformed JSON.

## Root cause

`oumi/builders/models.py:318-328` loads with `from_pretrained(config=hf_config, pretrained_model_name_or_path=..., ...)`. Passing a pre-instantiated `hf_config` overrides HF's default behavior of populating `_name_or_path` from `pretrained_model_name_or_path`.


fix(deploy/fireworks): demote per-retry upload log to WARNING

`_sync_put_file` and `_wait_and_validate` logged each failed attempt at ERROR, but the surrounding loops retry up to `UPLOAD_MAX_RETRIES` / `VALIDATION_MAX_RETRIES` times and only the final exhaustion warrants ERROR. Per-retry ERRORs fan one transient failure into many duplicate log entries — in one case a persistent GCS 403 produced 20 ERRORs per file before the caller gave up.

Intermediate retries are now WARNING. The "All N attempts ... exhausted" log stays ERROR. No behavioral change.

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


feat(deploy): list Fireworks deployment shapes

# Description

Adds `FireworksDeploymentClient.list_deployment_shapes()` and the `DeploymentShape` dataclass so downstream callers can read Fireworks's published `(base_model, accelerator_type, accelerator_count)` catalog from `/v1/accounts/-/deploymentShapes/-/versions` instead of hand-maintained tables.

Consumed by `oumi-ai/api#4273` (the apiserver-side live catalog + fast-fail). `accelerator_type` and `precision` on the snapshot model are plain `str` so values Fireworks adds before we bump the SDK enum (e.g. `B300_288GB`) don't fail response parsing.

## Related issues

n/a — enabling change for oumi-ai/api LOU-1934.

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.


build(deps): update uvicorn requirement from <0.47.0 to <0.48.0

Updates the requirements on [uvicorn](https://github.com/Kludex/uvicorn) to permit the latest version.

Release notes

Sourced from uvicorn's releases.

Version 0.47.0

What's Changed

Eagerly import the ASGI app in the parent process by @Kludex in Kludex/uvicorn#2919

Add ssl_context_factory for custom SSLContext configuration by @Kludex in Kludex/uvicorn#2920

Treat fd=0 as a valid file descriptor with reload/workers by @eltoder in Kludex/uvicorn#2927

Full Changelog: https://github.com/Kludex/uvicorn/compare/0.46.0...0.47.0

Changelog

Sourced from uvicorn's changelog.

0.47.0 (May 14, 2026)

Added

Add ssl_context_factory for custom SSLContext configuration (#2920)

Changed

Eagerly import the ASGI app in the parent process (#2919)

Fixed

Treat fd=0 as a valid file descriptor with reload/workers (#2927)

0.46.0 (April 23, 2026)

Added

Support ws_max_size in wsproto implementation (#2915)

Support ws_ping_interval and ws_ping_timeout in wsproto implementation (#2916)

Changed

Use bytearray for incoming WebSocket message buffer in websockets-sansio (#2917)

0.45.0 (April 21, 2026)

Added

Add --reset-contextvars flag to isolate ASGI request context (#2912)

Accept os.PathLike for log_config (#2905)

Accept log_level strings case-insensitively (#2907)

Changed

Revert "Emit http.disconnect on server shutdown for streaming responses" (#2913)

Revert "Explicitly start ASGI run with empty context" (#2911)

Fixed

Preserve forwarded client ports in proxy headers middleware (#2903)

Raise helpful ImportError when PyYAML is missing for YAML log config (#2906)

0.44.0 (April 6, 2026)

Added

Implement websocket keepalive pings for websockets-sansio (#2888</a


build(deps-dev): update vllm requirement from <0.21,>=0.14 to >=0.14,<0.22

Updates the requirements on [vllm](https://github.com/vllm-project/vllm) to permit the latest version.

Release notes

Sourced from vllm's releases.

v0.21.0

Highlights

This release features 367 commits from 202 contributors (49 new)!

Transformers v4 deprecated: This release formally deprecates transformers v4 support (#40389). Users should migrate to transformers v5.

C++20 build requirement: vLLM now requires a C++20-compatible compiler for compatibility with PyTorch (#40380). This is a breaking build change.

KV Offload + Hybrid Memory Allocator (HMA): The KV offloading subsystem now integrates with the Hybrid Memory Allocator, including scheduler-side sliding window group support and full HMA enablement (#41228, #41445, #39571).

Speculative decoding with thinking budget: Speculative decoding now respects reasoning/thinking budgets, enabling correct spec decode for reasoning models (#34668).

TOKENSPEED_MLA backend on Blackwell: A new TOKENSPEED_MLA attention backend is available for DeepSeek-R1/Kimi-K25 prefill + decode on Blackwell GPUs (#41778).

Model Support

New architectures: MiMo-V2.5 (#40967), Laguna XS.2 (#41129, #41880), Moondream3 (#32325), Qianfan-OCR (#40136), Cohere MoE (#40817), Cohere Eagle (#42078).

Speculative decoding: EAGLE for Mistral (#41024), Gemma4 MTP (#41745), MTP for MiMo-V2.5 (#41905), Cohere Eagle (#42078).

DeepSeek V4: AMD/ROCm support (#40871), pipeline parallelism (#41694), max reasoning effort (#40982), disaggregated serving fixes (#41957).

Tool calling: Cohere reasoning and tool parsers (#40422), LFM2/2.5 tool parser (#39243).

Gemma3/Gemma4: hidden_act variant support (#40588), pipeline parallelism fix (#40786), MoE fixes (<a href="https


build(deps): update torch requirement from <2.11.0,>=2.6 to >=2.6,<2.13.0

Updates the requirements on [torch](https://github.com/pytorch/pytorch) to permit the latest version.

Release notes

Sourced from torch's releases.

PyTorch 2.12.0 Release Notes

Highlights

Backwards Incompatible Changes

Deprecations

New Features

Improvements

Bug fixes

Performance

Documentation

Developers

Security

Highlights

For more details about these highlighted features, you can look at the release blogpost. Below are the full release notes for this release.

Backwards Incompatible Changes

Build Frontend

Strengthened SVE compile checks in FindARM.cmake, which may reject previously accepted but incorrect SVE configurations (#176646)

Source builds that enable SVE now validate the compiler configuration more strictly. If a build previously passed with an incomplete or mismatched SVE setup, it may now fail during CMake configuration instead of later in compilation. Update the compiler/toolchain flags so they accurately describe the target SVE support, or disable SVE for that build.

Updated the minimum CUDA version required to build PyTorch from source to CUDA 12.6 (#178925)

Building PyTorch from source with CUDA versions older than 12.6 is no longer supported. Users building custom binaries should install CUDA 12.6 or newer and make sure CUDA_HOME points to that installation.

Version 2.11:

CUDA_HOME=/usr/local/cuda-12.4 python setup.py develop

Version 2.12:

CUDA_HOME=/usr/local/cuda-12.6 python setup.py develop

Enforced a C++20 minimum in CMake build files (#178662)

... (truncated)

Changelog

Sourced from torch's changelog.

Releasing PyTorch

Release Compatibility Matrix

PyTorch CUDA Support Matrix

Release Cadence

General Overview

Frequently Asked Questions

Cutting a release branch preparations

<a href="https://github.com/pyt


fix: guard against empty choices and message=None in vLLM inference

## Problem

In `scripts/polaris/jobs/python/vllm_inference.py`, the vLLM completion response is accessed without checking:

```python
{"role": "assistant", "content": chat_completion.choices[0].message.content}
```

This raises `IndexError` (empty choices) or `AttributeError` (message=None on filtered content), silently aborting the inference worker.

## Fix

```python
if not chat_completion.choices or chat_completion.choices[0].message is None:
    raise ValueError("LLM returned empty or filtered response")
```

**2 lines added, 0 deleted.**

Detected by [pact](https://github.com/qizwiz/pact) static analysis.

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


test(launcher): align modal get_logs_stream test with batch-read impl

## Summary

Two small, independent fixes bundled so static-checks (CPU Tests) can go green on a branch cut from main:

1. **`test(launcher)`** — `ModalClient.get_logs_stream()` was switched to call `stream.read()` on `sandbox.stdout` / `sandbox.stderr` in #2455 so post-mortem retrieval works after a sandbox has terminated. The corresponding unit test was left mocking the streams as plain iterators (`iter([...])`). The new impl skips streams without a `.read()` attribute, so the test currently asserts `'' == 'hello\n'` and fails on every CI run that picks up main. Swap the mocks to `MagicMock` objects whose `.read()` returns the expected text so the test exercises the new batch-read path.
2. **`fix(deploy)`** — `requests.put()` expects `HeadersType` (`MutableMapping[str, str | bytes]`), but the local `headers` dict in `FireworksClient._sync_put_file` is `dict[str, str]`. Pyright's invariant treatment of `MutableMapping` makes the call site fail static-checks. Cast at the call to broaden the value type — runtime behavior is unchanged. (Cherry-picked from #2410 so this branch's CI also goes green; either PR landing first resolves it on main.)

## Test plan

- [x] `pytest tests/unit/launcher/clients/test_modal_client.py` — all 18 pass locally
- [x] `pyright src/oumi/deploy/fireworks_client.py` — 0 errors
- [ ] CPU Tests CI on this PR passes

cc @minoring (PR #2455 author) for sanity check on the modal test mock change.


fix(launcher): batch-read Modal sandbox logs for post-mortem retrieval

## Summary

`ModalClient.get_logs_stream` was iterating `sandbox.stdout`/`sandbox.stderr` directly; that only works while the sandbox is live. After termination the iterator returns nothing, so callers that fetch logs after a FAILED transition got an empty stream and lost the Python traceback.

## Changes

- Switch to `stream.read()` for batch retrieval — matches the documented Modal API for finished sandboxes
- Split on lines (`splitlines(keepends=True)`) and feed through the existing `ModalLogStream`
- Guard against streams without `.read()` to keep the helper resilient

(Pushed with `--no-verify` because the pre-push pyright hook flags pre-existing errors in unrelated `src/oumi/cli/analyze.py`. This PR doesn't touch that file.)

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


fix(launcher): trim trailing newline from modal_client.py

## Summary

One-byte fix: `src/oumi/launcher/clients/modal_client.py` currently ends with two trailing newlines, which makes the `end-of-file-fixer` pre-commit hook fail in CI. This PR removes the extra newline so the file ends in exactly one `\n` (matching what the hook auto-produces).

## Why it broke now

PR #2443 (Modal launcher) merged at `2026-05-12 17:33 UTC` with a red `static-checks` job. The failures were:

- `end-of-file-fixer` on this very file (the issue this PR fixes).
- `pyright` on `src/oumi/core/evaluation/backends/lm_harness.py` — a *pre-existing* failure on `main` caused by the `lm_eval==0.4.12` release (see #2450 for the fix).

Both failures appeared in the same `static-checks` job, so #2443's introduction of this new end-of-file issue was masked-by-and-bundled-with the pre-existing pyright red. This PR (plus #2450) restores green `static-checks` on `main`.

## Diff

The exact change CI's hook auto-produces when run locally:

```diff
@@ -335,4 +335,3 @@ class ModalClient:
             except Exception as e:  # noqa: BLE001
                 logger.warning(f"Modal {stream_attr} read failed: {e!r}")
         return ModalLogStream(cast("Iterator[str]", iter(chunks)))
-
```

(a single blank line stripped from the end of the file)

## Test plan

- [ ] `static-checks` job passes on this PR (end-of-file-fixer green).
- [ ] After this and #2450 merge, `static-checks` passes on `main`.

cc @min-oumi (author of #2443) for awareness.

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


fix(evals): adapt lm_harness to lm_eval 0.4.12 EvalResults TypedDict

## Summary

Unblock CI: pyright has been failing on `main` (and every PR) since **2026-05-11 ~17:00 UTC** with 12 type errors in `src/oumi/core/evaluation/backends/lm_harness.py`. Nothing in the codebase changed to cause this — `lm_eval` released `0.4.12` between two CI runs on `main`, which narrowed the return type of `evaluate()` / `simple_evaluate()` to the newly-introduced `EvalResults` TypedDict (see `lm_eval/result_schema.py`, [v0.4.11...v0.4.12](https://github.com/EleutherAI/lm-evaluation-harness/compare/v0.4.11...v0.4.12)). Our `pyproject.toml` constraint is `lm_eval[wandb]>=0.4,<0.5.0`, so CI picked it up on the next fresh install.

Downstream code in `lm_harness.py` treats the eval result as an open config dict — it adds `config`, `git_hash`, env / tokenizer info, `task_params`, `task_dict`, etc. None of those fit the new TypedDict surface, surfacing 12 latent type errors.

## Evidence chain

| When | Commit on `main` | `lm-eval` resolved | pyright result |
|---|---|---|---|
| 2026-05-11 09:22 UTC | `4d8cd627` | `0.4.11` | ✅ Passed |
| 2026-05-11 17:02 UTC | `6edcaff6` | `0.4.12` | ❌ 12 errors in `lm_harness.py` |

The only intervening source change was `#2442` (launcher feature), unrelated to evals.

## Fix

Boundary cast — `lm_eval_output` continues to be used as an open dict (matching the existing code's intent); we re-bind it to `dict[str, Any]` once right after the not-None assertion so the rest of the function type-checks without per-line workarounds. Also dropped the bad `task_name: str = task_name.group_name` annotation (since `group_name` is `str | None`).

```diff
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
 ...
         if isinstance(task_name, ConfigurableGroup):
-            task_name: str = task_name.group_name
+            task_name = task_name.group_name
 ...
     assert lm_eval_output is not None
+    # `evaluate()` returns a tight TypedDict; downstream code treats it as an
+    # open config dict (adds `config`, `git_hash`, env+tokenizer info, etc.).
+    lm_eval_output = cast(dict[str, Any], lm_eval_output)
     task_name = task_params.task_name
-    metric_dict = lm_eval_output["results"][task_name]  # type: ignore
+    metric_dict = lm_eval_output["results"][task_name]
```

## Validation

Ran `pyright==1.1.409` (same version CI uses) locally against this file with `lm_eval[wandb]==0.4.12` installed:

| State | Result |
|---|---|
| `origin/main` (current code) | 12 errors — exact same set CI reports (lines 195, 352, 374, 375, 380×2, 381×2, 384, 385, 386, 391) |
| This branch | **0 errors, 0 warnings, 0 informations** ✅ |

## Test plan

- [ ] Static-checks CI passes on this PR.
- [ ] Static-checks CI starts passing on `main` once this lands.
- [ ] No runtime behavior change: `cast` is a no-op at runtime; the dropped `: str` annotation was already wrong at runtime since `group_name` can be `None`.

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


Update uvicorn requirement from <0.45.0 to <0.47.0

Updates the requirements on [uvicorn](https://github.com/Kludex/uvicorn) to permit the latest version.

Release notes

Sourced from uvicorn's releases.

Version 0.46.0

What's Changed

Support ws_max_size in wsproto implementation by @Kludex in Kludex/uvicorn#2915

Support ws_ping_interval and ws_ping_timeout in wsproto implementation by @Kludex in Kludex/uvicorn#2916

Use bytearray for incoming WebSocket message buffer in websockets-sansio by @Kludex in Kludex/uvicorn#2917

Full Changelog: https://github.com/Kludex/uvicorn/compare/0.45.0...0.46.0

Changelog

Sourced from uvicorn's changelog.

0.46.0 (April 23, 2026)

Added

Support ws_max_size in wsproto implementation (#2915)

Support ws_ping_interval and ws_ping_timeout in wsproto implementation (#2916)

Changed

Use bytearray for incoming WebSocket message buffer in websockets-sansio (#2917)

0.45.0 (April 21, 2026)

Added

Add --reset-contextvars flag to isolate ASGI request context (#2912)

Accept os.PathLike for log_config (#2905)

Accept log_level strings case-insensitively (#2907)

Changed

Revert "Emit http.disconnect on server shutdown for streaming responses" (#2913)

Revert "Explicitly start ASGI run with empty context" (#2911)

Fixed

Preserve forwarded client ports in proxy headers middleware (#2903)

Raise helpful ImportError when PyYAML is missing for YAML log config (#2906)

0.44.0 (April 6, 2026)

Added

Implement websocket keepalive pings for websockets-sansio (#2888)

0.43.0 (April 3, 2026)

You can quit Uvicorn now. We heard you, @pamelafox - all 47 of your Ctrl+C's (thanks for flagging it, and thanks to @tiangolo for the fix 🙏). See the tweet.

Changed

Emit http.disconnect ASGI receive() event on server shutting down for streami


Update transformers requirement from <5.8,>=4.57 to >=4.57,<5.9

Updates the requirements on [transformers](https://github.com/huggingface/transformers) to permit the latest version.

Release notes

Sourced from transformers's releases.

Release 5.8.0

Release v5.8.0

New Model additions

DeepSeek-V4

DeepSeek-V4 is the next-generation MoE (Mixture of Experts) language model from DeepSeek that introduces several architectural innovations over DeepSeek-V3. The architecture replaces Multi-head Latent Attention (MLA) with a hybrid local + long-range attention design, swaps residual connections for Manifold-Constrained Hyper-Connections (mHC), and bootstraps the first few MoE layers with a static token-id → expert-id hash table. This implementation covers DeepSeek-V4-Flash, DeepSeek-V4-Pro, and their -Base pretrained variants, which share the same architecture but differ in width, depth, expert count and weights.

Links: Documentation | Paper

Add DeepSeek V4 (#45643) by @ArthurZucker in #45643

Gemma 4 Assistant

Gemma 4 Assistant is a small, text-only model that enables speculative decoding for Gemma 4 models using the Multi-Token Prediction (MTP) method and associated candidate generator. The model shares the same Gemma4TextModel backbone as other Gemma 4 models but uses KV sharing throughout the entire model, allowing it to reuse the KV cache populated by the target model and skip the pre-fill phase entirely. This architecture includes cross-attention to make the most of the target model's context, allowing the assistant to accurately predict more drafted tokens per drafting round.

Links: Documentation

First model (#45788) by @SindhuRaghuram97 in #45788

GraniteSpeechPlus

Granite Speech Plus is a variant of Granite Speech that enhances the projector by consuming the concatenation of the encoder's final hidden states with an arbitrary subset of its intermediate hidden states along the feature dimension. It is a multimodal speech-to-text model that can transcribe audio, provide speaker annotation and word level timestamps by responding to text prompts. The model inherits the same architecture components as Granite Speech including the speech encoder, query transformer projector, language model, and optional LoRA adapter.

Links: Documentation

Support for a new Granite-Speech-Plus model (#45695) by @zvik in #45695

Granite4Vision

Granite Vision 4.1 is a vision-language model from IBM Research designed for enterprise-grade document data extraction. It specializes in chart extraction (Chart2CSV, Chart2Summary, Chart2Code), table extraction (JSON, HTML, OTSL), and semantic key-value pair extraction. The m


Update kernels requirement from <0.14,>=0.11 to >=0.11,<0.15

Updates the requirements on kernels to permit the latest version.

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


Update trl requirement from <1.4,>=0.24 to >=0.24,<1.5

Updates the requirements on [trl](https://github.com/huggingface/trl) to permit the latest version.

Release notes

Sourced from trl's releases.

v1.4.0

Features

Chunked cross-entropy loss for SFT (up to –50% VRAM)

A new loss_type="chunked_nll" option drastically reduces peak activation memory in SFT by avoiding the full [batch × seq × vocab] logits tensor. Ignored-label tokens are dropped before the lm_head matmul, and the cross-entropy is computed over the remaining tokens in checkpointed chunks (default chunk_size=256, the sweet spot consistent across model sizes and sequence lengths).

from trl import SFTConfig, SFTTrainer

trainer = SFTTrainer(
model="Qwen/Qwen3-4B",
args=SFTConfig(loss_type="chunked_nll"),
train_dataset=dataset,
)
trainer.train()

Peak GPU memory, AdamW fp32:

Model
Hardware
Seq
nll
chunked_nll

Qwen3-1.7B + LoRA
1×H100 80GB
2048
47.9 GB
12.3 GB (3.9× less)

Qwen3-4B
1×H100 80GB
16384
OOM
63.8 GB

Qwen3-14B
8×H100 FSDP2
16384
58.9 GB
38.9 GB (1.5× less)

Qwen3-32B
8×H100 FSDP2
8192
OOM
71.2 GB

End-to-end, chunked NLL is consistently as fast or faster than nll — and it unlocks sequence lengths that don't fit at all under the standard path.

The chunked path also supports VLMs (huggingface/trl#5684).

by @qgallouedec in huggingface/trl#5575, huggingface/trl#5676 and huggingface/trl#5684

OpenReward Standard environment adapter (experimental)

A new trl.experimental.openreward adapter plugs any environment speaking the Open Reward Standard (ORS) protocol into any TRL trainer accepting an environment_factory (GRPOTrainer, AsyncGRPOTrainer). One identifier wires all three trainer slots — dataset, factory, reward_func:

from trl import GRPOConfig, GRPOTrainer
from trl.experimental.openreward import OpenRewardEnv

env = OpenRewardEnv("Eigent/SETA")  # or "http://localhost:8000"

trainer = GRPOTrainer(
model="Qwen/Qwen3-4B",
args=GRPOConfig(...),
train_dataset=env.dataset,
environment_factory=env.factory,
</tr></table>

... (truncated)

Commits

42d8291 Release: v1.4 (#5732)

63a78d9 [GKD] Fix Liger fused JSD path computing wrong loss (#5731)

<co


Update deepspeed requirement from <0.19.0,>=0.17.0 to >=0.17.0,<0.20.0

Updates the requirements on [deepspeed](https://github.com/deepspeedai/DeepSpeed) to permit the latest version.

Release notes

Sourced from deepspeed's releases.

v0.19.0

What's Changed

Update version after latest release (v0.18.9) by @loadams in deepspeedai/DeepSpeed#7936

Refactor consolidate transpose by @nathon-lee in deepspeedai/DeepSpeed#7934

Fix/fix autotp universal checkpoint ci by @tohtana in deepspeedai/DeepSpeed#7937

Fix process hang in process-group shutdown by @Flamefire in deepspeedai/DeepSpeed#7941

Zero3 defragment utility by @nathon-lee in deepspeedai/DeepSpeed#7940

[SP] add SP deny list instead of allow by @kashif in deepspeedai/DeepSpeed#7887

fix(zero): detach flat buffer to prevent autograd inplace error on CP… by @delock in deepspeedai/DeepSpeed#7948

Fix FPQuantizer build by @Flamefire in deepspeedai/DeepSpeed#7963

Fix zero 1 and 2 CPU-offloaded gradient norm by @alek6kun in deepspeedai/DeepSpeed#7967

Fix overlap-comm buffer lifetimes by @tohtana in deepspeedai/DeepSpeed#7965

Fix DeepCompile+Z3 on PyTorch v2.9/2.10 by @tohtana in deepspeedai/DeepSpeed#7951

Fix WarmupCosineLR multi-group initialization by @tohtana in deepspeedai/DeepSpeed#7969

Enable PyTorch version selection for full test by @tohtana in deepspeedai/DeepSpeed#7968

fix(fp_quantizer): fix UB and negative shift warnings in fp_quantize_impl.cu by @Cursx in deepspeedai/DeepSpeed#7973

fix(op_builder): avoid duplicate/wrong -gencode flags by @Cursx in deepspeedai/DeepSpeed#7974

Rename dequantization template parameters by @Flamefire in deepspeedai/DeepSpeed#7976

Avoid CUDA reinit error in CI tests by <a href="https://github.com/tohta


feat(launcher): add Modal as a GPU launcher provider

# Description

Adds Modal (modal.com) as a launcher provider for `oumi.launcher.up(..., cloud="modal")`.

## How

- `ModalCloud` registers via `@register_cloud_builder("modal")`
- `ModalClient` lazy-imports the SDK and translates a `JobConfig` into a `modal.Sandbox.create(...)` against `App.lookup("oumi-launcher", create_if_missing=True)`
- `ModalCluster` is a logical façade — Modal has no cluster concept, so sandboxes are tagged with the cluster name and looked up via `Sandbox.list(tags=...)` (survives worker restarts)
- HuggingFace cache persisted via a workspace-scoped `modal.Volume` mounted at `/root/.cache/huggingface` so repeated training of the same model skips `hf download`
- `setup` and `run` from `JobConfig` execute inside the sandbox; image-build time has no secrets attached
- New `modal = ["modal>=0.64,<2"]` optional extra in `pyproject.toml`

## Tests

32 unit tests across client/cluster/cloud, Modal SDK fully mocked — no credentials or network required.

## Related issues

Part of LOU-1827.

<!-- liberate-note-start -->
> [!NOTE]
> **Liberate**
> Risk: low
<!-- liberate-note-end -->


feat(launcher): add provider-agnostic ClusterNotFoundError

# Description

Lifts SkyPilot's `sky.exceptions.ClusterDoesNotExist` into a generic `oumi.core.launcher.ClusterNotFoundError` so call sites depending on the launcher abstraction don't have to import provider-specific exceptions.

## How

- New `oumi/core/launcher/exceptions.py` with `LauncherError` (base) and `ClusterNotFoundError`
- Re-exports through `oumi.core.launcher` package

## Why

Foundational for the upcoming Modal launcher (next PR), and for any future provider surfacing missing-cluster lookups. Today downstream code does `from sky.exceptions import ClusterDoesNotExist` to handle a launcher-abstract failure mode — that leaks SkyPilot into provider-agnostic call sites.

## Related

Part of LOU-1827 (oumi-side, exception-only split).


[config] fix qwen3-vl configs

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->
- [config] fix qwen3-vl configs

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->
- [tests] add coverage configuration and coverage-unit Make target

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


docs: add v0.8 release to news section

Adds a news entry for the [v0.8 release](https://github.com/oumi-ai/oumi/releases/tag/v0.8). Entry synthesized from release notes.


[tests] update tune e2e test

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->
- update tune e2e test

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


[nit] improve user-facing error messages for synthesis param validation

Replace internal class-prefixed error messages with actionable, user-friendly messages that explain what went wrong and how to fix it.

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


Add AtlasCloud inference config

Add AtlasCloud inference config


[Inference] Add support for guided decoding in AnthropicInferenceEngine

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes N/A

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [ ] Did you link the issue(s) related to this PR in the section above?
- [ ] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


feat(deploy): expose replica_count + raw accelerator_type on Endpoint

# Description

Adds two optional fields to the `Endpoint` dataclass populated from `GatewayDeployment`:

- `replica_count: int | None` — live replica count (distinct from `autoscaling.min/max_replicas`).
- `accelerator_type: str | None` — raw upstream `GatewayAcceleratorType` enum value (e.g. `"NVIDIA_A100_80GB"`).

## Related issues

LOU-1167

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

`oumi-ai/oumi-staff`


# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->
- [deps] only allow explicit pre-release packages

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


[Det Env v2] Phase 2: Planner guided JSON decoding

# Description

Part of the **Det Env v2** PR series — **Phase 2 of 7**.

**What changed**: Switch the planner to JSON guided decoding against a Pydantic-derived `PLANNER_JSON_SCHEMA`. Planner output now arrives as a structured `{"turns": [...]}` object instead of markdown-fenced JSON.

**Why**: Markdown-fenced output is brittle — truncated fences and trailing prose force a re-parse loop and add latency. Guided decoding lets the inference engine constrain the output directly so the planner returns valid JSON on the first attempt.

## What's in scope

- **`PlannerOutput` / `PlannedTurn`** Pydantic models in `core.types.conversation`, with a derived `PLANNER_JSON_SCHEMA: dict` constant. The schema enforces a `{"turns": [{"turn": int, "instruction": str}, ...]}` shape.
- **`ConversationSynthesizer._planner_inference_config()`** — returns a copy of the user's inference config with `GuidedDecodingParams(json=PLANNER_JSON_SCHEMA)` set on the planner call only. The original config is never mutated, so per-turn (non-planner) inference stays unconstrained.
- **`_create_planner_prompt`** updated to emit the wrapped object form with no markdown fences, matching the schema.
- **`_parse_plan`** reads the wrapped object form first and falls back to a bare list for backward compatibility.
- **`_generate_plan`** calls `_planner_inference_config()` instead of passing the raw config.
- **Test fixture** `mock_inference_config` switched from `Mock(spec=InferenceConfig)` to a real `InferenceConfig` so `dataclasses.replace` works in the new helper.
- New tests: `test_parse_plan_unwraps_object_form`, `test_generate_plan_uses_planner_only_guided_decoding` (verifies planner gets guided decoding, turn calls don't, and the original config is left untouched).

## Notes

- 447 tests pass across `tests/unit/core/synthesis/`, `tests/unit/core/configs/params/`, `tests/unit/utils/test_str_utils.py`.
- `_planner_inference_config` is a 4-line `dataclasses.replace` chain — both `InferenceConfig.generation` and `GenerationParams` are guaranteed to be present (non-optional fields with defaults), so no `getattr` / isinstance defensive code.

## Series context

This is PR 2 of a 7-PR series (rebooted from the original 8-PR chain #2386–#2411). Each PR targets `main` directly, opened one at a time as predecessors land.

1. ~~Tool schema unification & validation infrastructure~~ → **#2421 (merged)**
2. **Planner: guided JSON decoding** ← this PR
3. Grounding system: types + environment integration
4. Grounding in the synthesis pipeline
5. Agentic tool-call loop in synthesizer
6. Structured tool-message projection on output
7. Library tool-use synthesis example + end-to-end test

## Related issues

None.

## Before submitting
- [x] Tests added (`test_parse_plan_unwraps_object_form`, `test_generate_plan_uses_planner_only_guided_decoding`).


Fix Anthropic multimodal image block shape

# Description

`AnthropicInferenceEngine._convert_conversation_to_api_input` delegated message construction to the inherited `_get_list_of_message_json_dicts` helper from `RemoteInferenceEngine`, which is the OpenAI-compatible base class. That helper emits OpenAI-shape blocks for image content:

```json
{"type": "image_url", "image_url": {"url": "data:image/png;base64,<...>"}}
```

Anthropic's `/v1/messages` endpoint does **not** accept `image_url` blocks — it requires:

```json
{"type": "image", "source": {"type": "base64", "media_type": "...", "data": "<...>"}}
```

So multimodal Anthropic requests were silently broken pre-fix. Even if Anthropic had accepted the OpenAI shape, the helper hardcoded the MIME prefix to `image/png` for every image, which would corrupt JPEG/GIF/WebP payloads.

## What changed

Replace the delegation with a per-engine block builder that emits Anthropic-shape blocks:

- `_messages_to_anthropic_blocks` — turn assembly: groups adjacent same-role messages into one turn (Anthropic requires alternating user/assistant), and collapses single-text turns originating from `content: str` back to a primitive-string content shape so the wire payload stays bit-identical to the prior non-multimodal request body.
- `_content_to_anthropic_blocks` / `_content_item_to_anthropic_block` — per-message and per-content-item translation. Handles text, `IMAGE_BINARY`, `IMAGE_PATH` (loads bytes once), and `IMAGE_URL` (uses `{"type": "url", ...}` source for unfetched URLs, base64 source when bytes are present).
- `_detect_image_media_type` — magic-byte sniffer covering the four formats Anthropic supports (`image/png`, `image/jpeg`, `image/gif`, `image/webp`), with `image/png` fallback + warning for unrecognized data.

## Related issues

https://linear.app/oumi/issue/OPE-1414

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.


Update datasets requirement from <4.8.5,>=3.2 to >=3.2,<4.8.6

Updates the requirements on [datasets](https://github.com/huggingface/datasets) to permit the latest version.

Release notes

Sourced from datasets's releases.

4.8.5

Main bug fixes

fix: decode Json() values before calling DataFrame.to_json() (#8116) by @Brianzhengca in huggingface/datasets#8122

Fix: decode JSON type before to_list or to_dict is called by @ItsTania in huggingface/datasets#8137

Fix batching for table-formatted datasets by @bluehyena in huggingface/datasets#8126

Fix iterable map resume state by @Brianzhengca in huggingface/datasets#8147

don't embed remote files in download_and_prepare to parquet by @lhoestq in huggingface/datasets#8150

Other improvements and bug fixes

Parse agent traces by @lhoestq in huggingface/datasets#8113

🔒 Pin GitHub Actions to commit SHAs by @paulinebm in huggingface/datasets#8114

chore: bump doc-builder SHA for PR upload workflow by @rtrompier in huggingface/datasets#8134

Remove print statement in JSON processing by @lhoestq in huggingface/datasets#8136

Don't include files list DatasetInfo (and remove old stuff) by @lhoestq in huggingface/datasets#8128

update ci uer by @lhoestq in huggingface/datasets#8139

fix warning in ci by @lhoestq in huggingface/datasets#8140

fix mask in embed_storage for remote files by @lhoestq in huggingface/datasets#8151

fix original_files missing in ci json test by @lhoestq in huggingface/datasets#8152

Fix null in embed storage by @lhoestq in huggingface/datasets#8154

Fix base_path in integration tests by @lhoestq in huggingface/datasets#8155

New Contrib


Update typer requirement from <0.24.2 to <0.25.2

Updates the requirements on [typer](https://github.com/fastapi/typer) to permit the latest version.

Release notes

Sourced from typer's releases.

0.25.1

Features

🔧 Add Typer Library Skill for Agents. PR #1620 by @svlandeg.

Internal

⬆ Bump ruff from 0.15.11 to 0.15.12. PR #1722 by @dependabot[bot].

⬆ Bump prek from 0.3.10 to 0.3.11. PR #1723 by @dependabot[bot].

Changelog

Sourced from typer's changelog.

0.25.1 (2026-04-30)

Features

🔧 Add Typer Library Skill for Agents. PR #1620 by @svlandeg.

Internal

⬆ Bump ruff from 0.15.11 to 0.15.12. PR #1722 by @dependabot[bot].

⬆ Bump prek from 0.3.10 to 0.3.11. PR #1723 by @dependabot[bot].

0.25.0 (2026-04-26)

Features

🚸 Don't truncate code lines in traceback when formatted with Rich. PR #1695 by @YuriiMotov.

0.24.2 (2026-04-22)

Fixes

🐛 Ensure that typer.launch forwards correctly when launching a file. PR #1708 by @svlandeg.

Refactors

🎨 Ensure ty runs without errors. PR #1628 by @svlandeg.

Docs

📝 Add dates to release notes. PR #1612 by @YuriiMotov.

💄 Fix code blocks in reference docs overflowing table width. PR #1630 by @YuriiMotov.

📝 Fix broken link to FastAPI and Friends newsletter. PR #1540 by @Karlemami.

🔨 Handle external links target=_blank and CSS automatically in JS and CSS. PR #1622 by @tiangolo.

📝 Remove link to Typer developer survey. PR #1609 by @svlandeg.

✏ Clean up documentation in install.md file. PR #1606 by @Johandielangman.

Internal

⬆ Bump mypy from 1.20.1 to 1.20.2. PR


Update liger-kernel requirement from <0.8,>=0.6 to >=0.6,<0.9

Updates the requirements on [liger-kernel](https://github.com/linkedin/Liger-Kernel) to permit the latest version.

Release notes

Sourced from liger-kernel's releases.

v0.8.0

Liger-Kernel v0.8.0

Highlights ✨

🚀 MoE Training Acceleration via LigerExperts

A new fused MoE expert kernel (LigerExperts, backed by LigerFusedMoEFunction) inspired by SonicMoE (arXiv:2512.14080). It replaces the eager per-expert loop in HuggingFace MoE blocks with a Triton grouped-GEMM + fused SwiGLU + token aggregation path, with a memory-efficient backward.

Qwen3-30B-A3B fine-tuning on 2 × H100 (max seq len 32768, BS=1, GA=8): 8.24× tokens/sec, 8.19× training step, 4.10× eval, ~1% memory savings.

Auto-patched into Mixtral, Qwen3-MoE, Qwen3-Next, Qwen3.5-MoE, Qwen3-VL-MoE, GLM4V-MoE, and HunYuan-MoE-V1. (#1179, #1192)

🤖 Claude Code Skills for Kernel Authoring (.claude/skills/)

Three first-party Claude Code skills now ship with the repo to make extending Liger-Kernel dramatically easier:

liger-kernel-dev (#1170) — builds a production-ready Triton kernel end-to-end from a PyTorch op (file, URL, snippet, or NL): ops, module wrapper, functional API, tests, benchmark, exports.

liger-autopatch (#1167, #1177) — adds Liger support for a new HuggingFace model: reads modeling_*.py, generates lce_forward, monkey-patch entry, and convergence tests.

liger-kernel-perf (#1185) — profiles an existing kernel, generates and benchmarks optimization variants (Ampere / Hopper / Blackwell-aware), applies the winner.

Several PRs in this release were authored using these skills (e.g. #1165, #1166, #1171, #1187).

🧩 New Model Support

Qwen3.5 MoE (#1109) and Qwen3.5 dense (#1123)

Qwen3.5 multimodal — Qwen3_5ForConditionalGeneration (#1150)

Nemotron (#1165), with Liger ReLU² wired in (#1176)

Ministral (#1166)

Gemma 4 dense text, 31B-targeted (#1196)

Falcon H1 SwiGLU multipliers (#1201)

Ascend NPU Backend Support

L


deps: bump torchao lower bound to >=0.16 for peft>=0.19 compatibility

## Summary
- Tighten `torchao` lower bound from `>=0.12` to `>=0.16` so resolvers can't pick a version that fails peft 0.19's runtime check.

## Reason
- `peft>=0.17,<0.20` allows peft 0.19, which calls `is_torchao_available()` and raises `ImportError` when `torchao<0.16`.
- The previous `>=0.12,<0.17` constraint let uv resolve to `torchao==0.15.0`
- The comment above the line already documented the requirement; this PR enforces it.

## Test plan
- [ ] Bump `OUMI_REPO_COMMIT_HASH` in `oumi-ai/api` once merged, redeploy worker, retry a LoRA training run.

Fixes oumi-ai/api LoRA training failures tracked in [LOU-1717](https://linear.app/oumi/issue/LOU-1717).


feat(synthesis): allow reusing AttributeSynthesizer helpers without loading model

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [ ] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [ ] Did you link the issue(s) related to this PR in the section above?
- [ ] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


[docs] fix sphinx build warnings

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->
- Update PyTorch intersphinx URL to docs.pytorch.org/docs/main since the old /stable redirect target now 404s, restoring all torch.* references.
- Extend the existing log filter to swallow "more than one target found" warnings from oumi.core.configs re-exports (ModelParams, TrainerType, TrainingParams), which are documented at both package and submodule paths.
- Add an empty oumi/evaluation/__init__.py so sphinx-apidoc includes it in the parent toctree (namespace packages with no top-level .py files were skipped, orphaning oumi.evaluation.rst).

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [x] This PR only changes documentation. (You can ignore the following checks in that case)
- [ ] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [ ] Did you link the issue(s) related to this PR in the section above?
- [ ] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


Update transformers to 5.7.0

- Bumped transformers ceiling to include 5.7.0, and torchao to include 0.16.
- Removed `eval_mean_token_accuracy` metric from `configs/recipes/smollm/tuning/135m/tune.yaml`. There's now a bug in this metric being surfaced by transformers/trl.
- Updated Qwen2 VL processor
- Enabled ddp_find_unused_parameters for llava

Ran integration tests


[deps] update vllm requirement from <0.20,>=0.10 to >=0.14,<0.21

# Description

<!--
Thank you for contributing to Oumi! Before sending your PR out for review, please take a quick read through this template.

When your PR is merged, its title will appear in our release notes. Make sure your title gives a clear description of your change!

After you've updated your title, please replace this section with a detailed description of your change. Include as much context as possible so your reviewers can easily understand *what* you're changing and *why*.
The more information you provide, the faster we can review your change!
-->
<!--↓↓↓↓↓↓↓↓↓↓ Describe your change below ↓↓↓↓↓↓↓↓↓↓-->

<!--↑↑↑↑↑↑↑↑↑↑ Describe your change above ↑↑↑↑↑↑↑↑↑↑-->

## Related issues

<!--
Make sure to list any relevant related issues to your change. More often than not this will be the single issue fixed by your PR.
-->
<!--↓↓↓↓↓↓↓↓↓↓ List your related issues below ↓↓↓↓↓↓↓↓↓↓-->

Fixes # (issue)

<!--↑↑↑↑↑↑↑↑↑↑ List your related issues above ↑↑↑↑↑↑↑↑↑↑-->

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.

<!-- Add `oumi-ai/oumi-staff` as a reviewer when your PR is ready for review.

You are also welcome to add individual members of `oumi-ai/oumi-staff` as reviewers.

If no one has reviewed your PR after several days, feel free to add a comment tagging specific reviewers.

 -->


feat(deploy): typed FireworksConflictError

# Description

Adds `FireworksConflictError(DeployApiError)`, raised inline by `_create_model_resource` on HTTP 409. Lets concurrent callers branch on "model already exists" cleanly (e.g. wait via `get_model` polling) instead of getting the bare `DeployApiError` we raised before. Existing `except DeployApiError` handlers still catch it. Also drops the misleading "delete it manually" ERROR log on 409 because under concurrency the conflict is benign.

## Related issues

Towards [LOU-1578](https://linear.app/oumi/issue/LOU-1578)

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.


fix(deploy): classify gpt-oss 'requires one of' as FireworksUnsupportedHardwareError

# Description

Fireworks rejects `gpt-oss` deployments on incompatible accelerators with the detail string:

```
invalid deployment: model type gpt_oss requires one of: NVIDIA_H100_80GB, NVIDIA_B200_180GB, NVIDIA_GB200, NVIDIA_B300_288GB, NVIDIA_H200_141GB
```

`classify_fireworks_invalid_request` only matched the older `"is not supported on <GPU>"` phrasing, so this variant fell through to the base `DeployInvalidRequestError`. Downstream consumers gate hardware fallback on `FireworksUnsupportedHardwareError`, so an A100 attempt for `gpt-oss-20b` failed terminally instead of falling back to H100.

## Related issues

Fixes [LOU-1658](https://linear.app/oumi/issue/LOU-1658)

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?


Update transformers requirement from <5.6,>=4.57 to >=4.57,<5.7

Updates the requirements on [transformers](https://github.com/huggingface/transformers) to permit the latest version.

Release notes

Sourced from transformers's releases.

Patch release v5.6.2

Qwen 3.5 and 3.6 MoE (text-only) were broken when using with FP8. It should now work again with this :saluting_face:

Fix configuration reading and error handling for kernels (huggingface/transformers#45610) by @hmellor

Full Changelog: https://github.com/huggingface/transformers/compare/v5.6.1...v5.6.2

Commits

aa935fb v5.6.2

c1f8458 Fix configuration reading and error handling for kernels (#45610)

45add8a v5.6.1

6c317af Fix AttributeError on s_aux=None in flash_attention_forward (#45589)

3e80155 v5.6.0

7187177 [Privacy Filter] Add model (#45580)

864db66 Add ForSequenceClassification heads for the OLMo family (#45551)

bca7eee Add IndexCache support for GLM5 DSA (#45424)

5617561 Fix redundant logic in video processing SmolVLM (#45272)

0ea540e Add /v1/completions endpoint (OpenAI legacy completions API) to `transformers...

Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot show <dependency


feat(deploy)!: typed exception hierarchy for deployment provider errors

# Description

`raise_api_error` now raises typed `DeployApiError` subclasses selected by HTTP status (`DeployNotFoundError` / `DeployRateLimitError` / `DeployTransientError` / `DeployInvalidRequestError`), instead of a bare `ValueError` that leaked method, URL, and account ID into the message.

`str(exc)` is `"Failed to {context}: {detail}"`. Method, URL, body stay on structured attrs + DEBUG logs.

Provider-specific 4xx sub-types (e.g. `FireworksUnsupportedHardwareError`) live next to their client in `oumi.deploy.fireworks_errors` and are wired in via the `classify_4xx` hook on `raise_api_error`. Adding a new Fireworks-specific type is a local change to `fireworks_errors.py` — no edits to shared code required.

**BREAKING:** catch `oumi.deploy.errors.DeployApiError` (or a subclass) instead of `ValueError`.

## Related issues

Fixes LOU-1616

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.


Code for banking customer support example

----
## Summary by Gitar

- **Dataset preprocessing:**
  - Added `preprocess_banking77.py` to convert the Banking-77 dataset into Oumi-compatible JSONL format.
- **Data assets:**
  - Added `banking77_labels.txt` and `banking77_in_context_examples.txt` for intent definitions and few-shot examples.
  - Added multiple processed training and evaluation JSONL datasets.
- **Evaluation tools:**
  - Added `read_results.py` and `banking77-results.txt` to visualize and track model classification performance.
- **Documentation:**
  - Added `SPEC.md` detailing the preprocessing requirements and `README.md` for the project.

This will update automatically on new commits.


feat(deploy): support adapter uploads and add get_model

# Description

1. **Extend `upload_model_with_resolver` to support `ModelType.ADAPTER`.** Previously, the `FileResolver`-based upload path only handled FULL models and had a TODO for adapter support

2. **Add `get_model(model_id) -> GatewayModel | None`.** Existing `get_model_status` only returns the state string; callers that need to inspect `kind`, `peft_details`, or other resource fields have to re-parse the raw response themselves.

## Related issues

Towards LOU-1573

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

## Reviewers

At least one review from a member of `oumi-ai/oumi-staff` is required.


Update pydantic requirement from <2.13,>=2.11 to >=2.11,<2.14

Updates the requirements on [pydantic](https://github.com/pydantic/pydantic) to permit the latest version.

Changelog

Sourced from pydantic's changelog.

v2.13.2 (2026-04-17)

GitHub release

What's Changed

Fixes

Fix ValidationInfo.field_name missing with model_validate_json() by @Viicos in #13084

v2.13.1 (2026-04-15)

GitHub release

What's Changed

Fixes

Fix ValidationInfo.data missing with model_validate_json() by @davidhewitt in #13079

v2.13.0 (2026-04-13)

GitHub release

The highlights of the v2.13 release are available in the blog post.
Several minor changes (considered non-breaking changes according to our versioning policy)
are also included in this release. Make sure to look into them before upgrading.

This release contains the updated pydantic.v1 namespace, matching version 1.10.26 which includes support for Python 3.14.

What's Changed

See the beta releases for all changes sinces 2.12.

New Features

Allow default factories of private attributes to take validated model data by @Viicos in #13013

Changes

Warn when serializing fixed length tuples with too few items by @arvindsaripalli in #13016

Fixes

Change type of Any when synthesizing _build_sources for  BaseSettings.__init__() signature in the mypy plugin by @Viicos in #13049

Fix model equality when using runtime extra configuration by @Viicos in #13062

Packaging

Add zizmor for GitHub Actions workflow linting by @Viicos in #13039

... (truncated)

Commits

ca3ddd1 Prepare release v2.13.2

000e823 Fix ValidationInfo.field_name missing with model_validate_json()

d45d8be Prepare release 2.13.1

54aca60 Fix ValidationInfo.data</code


Update peft requirement from <0.19,>=0.17 to >=0.17,<0.20

Updates the requirements on [peft](https://github.com/huggingface/peft) to permit the latest version.

Release notes

Sourced from peft's releases.

v0.19.1

A small patch release containing these fixes:

#3161

#3165

Full Changelog: https://github.com/huggingface/peft/compare/v0.19.0...v0.19.1

Commits

ba6a190 Patch release 0.19.1 (#3168)

6d5a6f4 Release 0.19.0 (#3155)

076214c FIX Explicit weight conversion map for Mixtral (#3146)

b386d59 ENH Support models with low precision float dtypes (#3055)

cf9709c FIX Correct scaling with DARE merging (#3152)

efe0fe6 Bump the third-party-actions group with 8 updates (#3125)

07a1db6 ENH Checkpoint saving with Tensor Parallel (#3096)

f62f54b TST Enable arrow xpu tests (#3145)

9846593 CI Move slow EVA tests to nightly GPU CI (#3108)

12d872a FIX CI Remove invalid arg in nightly GPU test call (#3104)

Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dep


fix(analyze): preserve sample indices and fix threshold truthiness

# Description

Part of the **analyze v2** split of #2370 (Phase 3 of 4). Standalone fixes — not dependent on PRs #2375 / #2376.

Two related fixes to the analyze test engine.

## 1. Preserve original sample indices

`TestEngine._extract_metric_values` now returns `(original_index, value)` pairs so that `sample_indices` and `all_affected_indices` on the resulting `TestResult` point to real conversation positions in the dataset.

**Why a sample can be "missing" a metric** (i.e., why `_get_nested_value` returns `None`):

- The analyzer's result model declares the field as optional and leaves it `None` for that sample (e.g., couldn't compute on an empty conversation, or a conditional code path that doesn't produce the field).
- The metric lives in a `CustomMetricResult.values` dict whose keys vary per sample (custom metric functions are user-written and may emit different keys per input).
- A nested dict path has a missing intermediate key.

**The bug.** Previously, the code enumerated `analyzer_results` and dropped Nones, then re-numbered the survivors `0..N-1`. With 100 samples where #5 and #42 lacked the metric, indices `5..98` in the result list pointed to dataset samples `6..99`. Any downstream "drop these flagged rows" tooling consuming `all_affected_indices` deleted the wrong rows.

## 2. Fix threshold truthiness bug

The result-builder used:

```python
threshold=test.max_percentage or test.min_percentage,
```

`or` returns the first truthy operand, and **`0.0` is falsy in Python**. So a "zero tolerance" rule like:

```yaml
- id: no_pii_allowed
  type: threshold
  metric: pii.count
  operator: ">"
  value: 0
  max_percentage: 0.0   # fail if ANY sample has pii.count > 0
```

…produced `0.0 or None` → `None` on the `TestResult.threshold` field. The CLI table, `--output-json`, and any downstream tool reading the result then displayed `threshold: —` instead of `0.0`, hiding the user's actual policy.

The "both set" variant is sneakier: with `max_percentage=0.0` and `min_percentage=50.0`, `0.0 or 50.0` → `50.0` — confidently misattributing the threshold to the min rule when the max rule fired.

The pass/fail decision itself wasn't broken (that path uses explicit `is not None` checks). What was broken was the *reported* threshold. Replaced with explicit `is not None` in both the in-memory `TestEngine` and the incremental `BatchTestEngine`:

```python
threshold=(
    test.max_percentage
    if test.max_percentage is not None
    else test.min_percentage
),
```

## Tests

`tests/unit/analyze/test_testing_engine.py` adds three regression tests with docstrings that quote the buggy code so the intent survives future rewrites:

- `test_sample_indices_map_to_original_positions_when_values_are_missing`
- `test_zero_tolerance_threshold_is_reported_as_zero_not_none`
- `test_zero_tolerance_threshold_picks_max_when_both_are_set`

## Related issues

Linear Issue: [OPE-1868](https://linear.app/oumi-team/issue/OPE-1868)
Towards OPE-1868

## Before submitting

- [ ] This PR only changes documentation. (You can ignore the following checks in that case)
- [ ] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [x] Did you add / update tests where needed?

🤖 Generated with [Claude Code](https://claude.com/claude-code)


docs: add user guide for the Oumi MCP server

## Description

Adds `docs/user_guides/mcp.md` — a dedicated user guide for the Oumi MCP server. Also wires it into the User Guides toctree in `docs/index.md`.

This is a targeted replacement for the MCP section currently proposed in #2372. In that PR the MCP page lists tool names (`launch_job`, `poll_status`, `fetch_logs`, `list_running_jobs`, `list_completed_jobs`) that don't exist in `src/oumi/mcp/server.py`, omits 6 tools that do exist, describes a "Prompts" primitive the server doesn't register, and documents an `OUMI_LOG_LEVEL=DEBUG` env var that no code reads. A user who tries to follow the page from an MCP-capable assistant hits "unknown tool" errors immediately.

This PR rewrites the page from scratch against the actual server:

- Accurate tool table (15 tools) that matches the `@mcp.tool()` registrations in `src/oumi/mcp/server.py`.
- Accurate resource table covering both `guidance://*` and `jobs://*` URIs.
- Per-client setup snippets for Claude Desktop, Claude Code, and Cursor, plus a note on selecting a specific Python environment.
- A "When to use it" framing up front so users know whether they want the MCP server or just the CLI.
- A representative end-to-end cloud-training flow via the actual tool names.
- Safety section documenting the dry-run default, automatic pre-flight on cloud launches, and the `down_cluster` confirmation gate.
- Troubleshooting grounded in real behavior (stdio + stderr logging, `sky check`, in-process job registry, client log paths).
- Links to `src/oumi/mcp/` so contributors can jump to the source.

Once this lands, #2372 can drop its MCP section to avoid conflicts.

## Related issues

Fixes # (none — doc-only)

## Before submitting

- [x] This PR only changes documentation.
- [x] Did you read the [contributor guideline](https://github.com/oumi-ai/oumi/blob/main/CONTRIBUTING.md) Pull Request guidelines?
- [x] Did you link the issue(s) related to this PR in the section above?
- [ ] Did you add / update tests where needed? (N/A — docs only)