@ondeinference / cli / commits / 855e1ac

Unify CLAUDE.md into AGENTS.md and symlink it

- Merge the heavy-test env knobs, finetune gradient-safety warning, GGUF format details, and the onde dependency description from CLAUDE.md into .agents/AGENTS.md, since those were missing there - Replace the standalone CLAUDE.md with a symlink to .agents/AGENTS.md, matching the convention already used in smbcloud and smbcloud-cli Co-Authored-By: siGit Code <297239231+sigitc@users.noreply.github.com>

keypair34 committed Jul 12, 2026 at 08:11 UTC 855e1ac295d1e229b92c5394d7b508c7b09ae6f8
3 files changed +28 -74
.agents/AGENTS.md
+27 -8
@@ -14,23 +14,38 @@ cargo fmt -- --check
14
15 The build **requires** these env vars (read from `.env` via `build.rs`): `ONDE_APP_ID`, `ONDE_APP_SECRET`, `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`. `HF_TOKEN` is optional. They are baked in at compile time via `env!(...)` in `src/app.rs`. Changing `.env` triggers a full rebuild.
16
17 -On macOS, candle's `metal` + `accelerate` features are added automatically. **Fine-tuning always runs on CPU** — Metal's backward pass in candle 0.10.2 produces corrupt gradients. Use `ONDE_FINETUNE_METAL=1` only to test newer candle versions, and verify with `ONDE_FINETUNE_GRAD_DEBUG=1`.
17 +On macOS, candle's `metal` + `accelerate` features are added automatically. **Fine-tuning always runs on CPU** — Metal's backward pass in candle 0.10.2 produces corrupt gradients (1e6–1e29 in magnitude, where CPU gives ~40–65 for identical losses). Use `ONDE_FINETUNE_METAL=1` only to test newer candle versions, and verify with `ONDE_FINETUNE_GRAD_DEBUG=1` (per-step loss/max_abs/norm).
18
19 Linux CI requires `libclang-dev` (`sudo apt-get install -y libclang-dev`).
20
21 +### Tests that load real models
22 +
23 +`src/gguf.rs` has two `#[ignore]` integration tests that export a ~1GB GGUF and run inference through `onde::mistralrs`. They need a Qwen3-0.6B model cached in the Onde App Group container and skip gracefully when it is absent.
24 +
25 +```sh
26 +cargo test gguf::tests::exported_qwen3_gguf_is_runnable -- --ignored --nocapture
27 +cargo test gguf::tests::finetune_merge_export_run -- --ignored --nocapture
28 +```
29 +
30 +Env knobs for these: `ONDE_TEST_MODEL_DIR`, `ONDE_TEST_DTYPE` (`f16`/`q8_0`), `ONDE_TEST_LR`, `ONDE_TEST_DATA_PATH`, `ONDE_TEST_EPOCHS`, `ONDE_TEST_MAX_SEQ_LEN`, `ONDE_TEST_EVAL_PROMPT` (`||`-separated prompts), `ONDE_TEST_SYSTEM_PROMPT`. This makes the full-pipeline test double as a headless fine-tune runner for a custom dataset.
31 +
32 +### Debugging the TUI
33 +
34 +`main.rs` redirects both stdout and stderr to `~/.cache/onde/debug.log` before ratatui takes the alternate screen, because `mistral.rs` writes to both fds and would otherwise tear up the display. To see what the app or the inference engine is doing, tail that log; `println!`/`eprintln!`/`log::*` all land there, not on screen.
35 +
36 ## Architecture — `src/`
37
38 | File(s) | Responsibility |
39 |---|---|
40 | `main.rs` | Entry point; redirects stdout/stderr to `~/.cache/onde/debug.log` before ratatui takes over |
26 -| `app.rs` | `App` struct + `Screen` state machine + `AuthEvent` channel; `App::apply` folds background progress into state |
41 +| `app.rs` | `App` struct + `Screen` state machine + `AuthEvent` channel (`tokio::select!` over an `mpsc` channel and a `crossterm` `EventStream`); `App::apply` folds background progress into state |
42 | `ui.rs` | Pure ratatui render of `App`; no SDK imports (re-exports types through `app.rs`) |
28 -| `finetune.rs` | Hand-written LoRA trainer (candle, CPU). Writes `lora_adapter.safetensors` |
29 -| `merge.rs` | Folds LoRA adapter into base weights; writes `model.safetensors` |
30 -| `gguf.rs` | Hand-rolled GGUF writer (no llama.cpp). Tensor dims innermost-first; `head_dim` → `attention.key_length/value_length` |
31 -| `chat.rs` | Local GGUF chat via `onde::mistralrs::GgufModelBuilder` |
32 -| `hf*.rs` | Hugging Face Hub: upload, clone, search, cache resolution |
33 -| `gresiq.rs` | `smbcloud-gresiq-sdk` wrapper; model assignment (`assign_model`) |
43 +| `finetune.rs` | Hand-written LoRA trainer (candle, CPU): Qwen forward pass (RMSNorm, RoPE, GQA, optional Qwen3 QK-norm), LoRA A/B trained on q/v projections in F32, writes `lora_adapter.safetensors`. Gradients are sanitized (non-finite elements zeroed) and globally norm-clipped in a scaled space before each AdamW step; a step is skipped (loudly) only when the norm is zero or non-finite. **Skipping this guard corrupts every weight after the first bad step.** |
44 +| `merge.rs` | Folds LoRA adapter into base weights (`W + scale·(B@A)`); writes `model.safetensors` plus copied config/tokenizer |
45 +| `gguf.rs` | Hand-rolled GGUF writer (no llama.cpp). Tensor dims are written innermost-first — reverse of safetensors shape, because candle reverses on read; `head_dim` comes from config and is emitted as `attention.key_length`/`value_length` (Qwen3 decouples it from `hidden_size/num_heads`); `token_type` is an INT32 array; `general.architecture` is chosen from `model_type` so Qwen3 routes to candle's `quantized_qwen3` loader |
46 +| `chat.rs` | Local GGUF chat via `onde::mistralrs::GgufModelBuilder`, the same engine the Onde SDK uses, so a model can be tested before publishing |
47 +| `hf_upload.rs` / `hf_clone.rs` / `hf_search.rs` / `hf.rs` | Hugging Face Hub: upload the GGUF, check/create a repo, search models, and resolve/merge the local HF cache (incl. the macOS Onde App Group container) |
48 +| `gresiq.rs` | `smbcloud-gresiq-sdk` wrapper for apps and the model catalog. "Deploying" a model means `assign_model(app_id, model_id)` against a catalog entry; the end app then fetches that assignment through the Onde SDK's `load_assigned_model` and downloads the GGUF. The CLI can only assign models that already exist in the GresIQ catalog |
49 | `token.rs` | Auth token persistence |
50 | `project.rs` | Per-project fine-tune workspaces under `~/.onde` |
51
@@ -38,6 +53,10 @@ Linux CI requires `libclang-dev` (`sudo apt-get install -y libclang-dev`).
53
54 **Adding a feature pattern:** new `Screen` variant → key handler → `AuthEvent` variant → background task that streams progress events.
55
56 +### The `onde` dependency
57 +
58 +The `onde` crate provides the inference engine (`onde::mistralrs`, a vendored mistral.rs) and `onde::inference::models::SUPPORTED_MODEL_INFO` (the supported-model catalog the inference picker mirrors). It is normally the published crate; `Cargo.toml` has commented `path`/`[patch.crates-io]` blocks for developing against local checkouts of `onde`, the `smbcloud-*` crates, and `candle` — never commit with those uncommented.
59 +
60 ## Conventions
61
62 - **Git:** merge feature/release/hotfix branches with `--no-ff`. Tag the merge commit on `main`. Never tag a branch tip that hasn't been merged. See `.agents/skills/git/SKILL.md`.
CLAUDE.md deleted
-66
@@ -1,66 +0,0 @@
1 -# CLAUDE.md
2 -
3 -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 -
5 -## What this is
6 -
7 -`onde-cli` is a native Rust terminal UI (binary name `onde`) for managing an Onde Inference account and running a local model pipeline: fine-tune a safetensors small language model with LoRA, merge the adapter, export to GGUF, test it in a local chat, upload it to Hugging Face, and assign it to an Onde app. All packaging (npm, PyPI, NuGet, pub.dev, Homebrew, crates.io) is thin wrappers around this one binary; the Rust crate is the source of truth.
8 -
9 -## Build, run, test
10 -
11 -```sh
12 -cargo build # debug build
13 -cargo run # launches the TUI
14 -cargo test # unit tests only (the heavy ones are #[ignore])
15 -cargo clippy --all-targets
16 -```
17 -
18 -The build **bakes credentials in at compile time** via `build.rs`, which reads `.env` (or the environment in CI). The build fails if any of these are missing: `ONDE_APP_ID`, `ONDE_APP_SECRET`, `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`. `HF_TOKEN` is optional (defaults to empty). Changing `.env` triggers a rebuild. These are exposed in code as `env!(...)` constants in `src/app.rs`.
19 -
20 -On macOS the build pulls in candle's `metal` and `accelerate` features. Inference runs on Metal; **training deliberately runs on CPU everywhere** — candle 0.10.2's Metal backward pass produces garbage gradient magnitudes (1e6–1e29 where CPU gives ~40–65 for identical losses), and Accelerate is ~2x faster per training step anyway. `ONDE_FINETUNE_METAL=1` re-enables Metal training for testing newer candle versions; verify gradients with `ONDE_FINETUNE_GRAD_DEBUG=1` (per-step loss/max_abs/norm) before trusting it.
21 -
22 -### Tests that load real models
23 -
24 -`src/gguf.rs` has two `#[ignore]` integration tests that export a ~1GB GGUF and run inference through `onde::mistralrs`. They need a Qwen3-0.6B model cached in the Onde App Group container and skip gracefully when it is absent.
25 -
26 -```sh
27 -cargo test gguf::tests::exported_qwen3_gguf_is_runnable -- --ignored --nocapture
28 -cargo test gguf::tests::finetune_merge_export_run -- --ignored --nocapture
29 -```
30 -
31 -Env knobs for these: `ONDE_TEST_MODEL_DIR`, `ONDE_TEST_DTYPE` (`f16`/`q8_0`), `ONDE_TEST_LR`, `ONDE_TEST_DATA_PATH`, `ONDE_TEST_EPOCHS`, `ONDE_TEST_MAX_SEQ_LEN`, `ONDE_TEST_EVAL_PROMPT` (`||`-separated prompts), `ONDE_TEST_SYSTEM_PROMPT`. This makes the full-pipeline test double as a headless fine-tune runner for a custom dataset.
32 -
33 -### Debugging the TUI
34 -
35 -`main.rs` redirects both stdout and stderr to `~/.cache/onde/debug.log` before ratatui takes the alternate screen, because `mistral.rs` writes to both fds and would otherwise tear up the TUI. To see what the app or the inference engine is doing, tail that log; `println!`/`eprintln!`/`log::*` all land there, not on screen.
36 -
37 -## Architecture
38 -
39 -### TUI event loop (`app.rs`, `ui.rs`, `main.rs`)
40 -
41 -The whole app is one `App` struct plus a `Screen` enum acting as a state machine. `app::run` owns a single `tokio::sync::mpsc` channel of `AuthEvent`s and a `crossterm` `EventStream`, multiplexed with `tokio::select!`. Keystrokes mutate `App` synchronously; anything slow (network calls, downloads, fine-tune, merge, GGUF export, chat inference) is spawned as a background tokio task or OS thread that streams progress back as `AuthEvent`s, which `App::apply` folds into state. `ui.rs` is a pure render of `App` and intentionally does not depend on the SDK directly (it re-exports `OndeApp`/`OndeModel` through `app.rs`). When adding a feature, the pattern is: add a `Screen` variant, a key handler, an `AuthEvent` variant, and a background task that emits progress.
42 -
43 -Background work uses two task kinds deliberately: network/IO uses `tokio::spawn`; CPU-heavy tensor work (`finetune`, `merge`, `gguf`) uses `std::thread::spawn` so it does not starve the async runtime.
44 -
45 -### The local model pipeline
46 -
47 -These modules form the fine-tune-to-deploy chain, each running on a background thread and streaming a `*Progress` enum:
48 -
49 -- `finetune.rs` — hand-written LoRA trainer (candle). Builds a Qwen forward pass (RMSNorm, RoPE, GQA, optional Qwen3 QK-norm), trains LoRA A/B on q/v projections in F32, writes `lora_adapter.safetensors`. Gradients are sanitized (non-finite elements zeroed) and globally norm-clipped before each AdamW step; the norm is computed in a scaled space so huge-but-finite gradients clip instead of overflowing the f32 sum to `inf`. A step is skipped (loudly) only when the norm is zero or non-finite. Skipping this guard corrupts every weight after the first bad step. Training runs on CPU — see the Metal note above.
50 -- `merge.rs` — folds the LoRA adapter back into base weights (`W + scale·(B@A)`), writes a merged `model.safetensors` plus copied config/tokenizer.
51 -- `gguf.rs` — **hand-rolled GGUF writer** (no llama.cpp). Conventions that must hold for mistral.rs/candle to load and run the file correctly: tensor dims are written innermost-first (reverse of safetensors shape, because candle reverses on read); `head_dim` comes from config and is emitted as `attention.key_length`/`value_length` (Qwen3 decouples it from `hidden_size/num_heads`); `token_type` is an INT32 array; `general.architecture` is chosen from `model_type` so Qwen3 routes to candle's `quantized_qwen3` loader.
52 -- `chat.rs` — loads a local GGUF via `onde::mistralrs::GgufModelBuilder` (the same engine the Onde SDK uses) and streams a multi-turn chat, so a model can be tested before publishing.
53 -- `hf_upload.rs` / `hf_clone.rs` / `hf_search.rs` / `hf.rs` — Hugging Face Hub: upload the GGUF, check/create a repo, search models, and resolve/merge the local HF cache (incl. the macOS Onde App Group container).
54 -
55 -### Account / deploy side (`gresiq.rs`, `token.rs`, `project.rs`)
56 -
57 -`gresiq.rs` wraps `smbcloud-gresiq-sdk` for apps and the model catalog. "Deploying" a model means `assign_model(app_id, model_id)` against a catalog entry; the end app (e.g. `rumilearnpersian`) then fetches that assignment through the Onde SDK's `load_assigned_model` and downloads the GGUF. The CLI can only assign models that already exist in the GresIQ catalog. `token.rs` persists the auth token; `project.rs` manages per-project fine-tune workspaces under `~/.onde`.
58 -
59 -### The `onde` dependency
60 -
61 -The `onde` crate provides the inference engine (`onde::mistralrs`, a vendored mistral.rs) and `onde::inference::models::SUPPORTED_MODEL_INFO` (the supported-model catalog the inference picker mirrors). It is normally the published crate; `Cargo.toml` has commented `path`/`[patch.crates-io]` blocks for developing against local checkouts of `onde`, the `smbcloud-*` crates, and `candle`. Never commit with those uncommented.
62 -
63 -## Conventions
64 -
65 -- Git: merge feature/release/hotfix branches with `--no-ff` (explicit merge commits). Tag the merge commit that holds the final release state. See `.agents/skills/git/SKILL.md`.
66 -- Distribution changes: see `.agents/skills/distribution/SKILL.md` before touching any wrapper package; keep all channel versions aligned with the Rust crate version.
CLAUDE.md new
+1
@@ -0,0 +1 @@
1 +.agents/AGENTS.md
\ No newline at end of file