| 1 | --- |
| 2 | name: sigit-code-cloud |
| 3 | description: "Reference for siGit Code Cloud, the hosted CHAT product for siGit Code (interactive, signed-in, no local model). Use when working on the cloud chat surface: the sigit login/logout/whoami account commands, the credential store, provider/backend selection (on-device vs siGit Code Cloud vs BYO endpoint), neutral quality tiers, the persisted Cloud Sessions API, or the onde-cloud API behind it. Covers the Copilot-Chat/Azure layering, the InferenceBackend engine, auth via sigit.si/api/v1, and what must never leak to the client. The autonomous task->PR product is siGit Code Cloud Agent; see the sigit-code-cloud-agent skill." |
| 4 | --- |
| 5 | |
| 6 | # siGit Code Cloud |
| 7 | |
| 8 | **siGit Code Cloud** is the hosted **chat** for **siGit Code**: interactive, |
| 9 | signed-in, model in the cloud instead of on the device. The product framing is |
| 10 | deliberate: |
| 11 | |
| 12 | - **siGit Code Cloud is the product**, like Copilot **Chat**. The user signs in, |
| 13 | picks a quality tier, and it works. They never see an API key, a base URL, or |
| 14 | the name of any model provider. |
| 15 | - **Onde Cloud is the infrastructure**, like Azure. It is the OpenAI-compatible |
| 16 | inference service siGit Code Cloud runs on, and it is an implementation detail. |
| 17 | - **Onde Cloud, in turn, routes to upstream providers** (today Anthropic) and |
| 18 | hides them too, see the onde-cloud `router`/`anthropic` modules. |
| 19 | |
| 20 | **This is the chat product, not the agent.** The autonomous, sandboxed |
| 21 | task -> pull request product is **siGit Code Cloud Agent** (see the |
| 22 | `sigit-code-cloud-agent` skill). Chat's work unit is a **session**; the agent's is |
| 23 | a **run**. Keep them separate. Full product map: |
| 24 | [`docs/product/product-overview.md`](../../../docs/product/product-overview.md). |
| 25 | |
| 26 | Keep the names straight (see the `branding` skill): the local agent is |
| 27 | `siGit Code`, the hosted chat is `siGit Code Cloud`, the autonomous one is |
| 28 | `siGit Code Cloud Agent`, the CLI is `sigit`, the company is `smbCloud`, the |
| 29 | inference infrastructure is `Onde Cloud` / `Onde Inference`, and the on-device |
| 30 | Rust crate is `onde`. `sigit.si` is Git hosting; `code.sigit.si` is the home of |
| 31 | siGit Code. |
| 32 | |
| 33 | ## The layering (why it's built this way) |
| 34 | |
| 35 | The original instinct was right: abstract the infrastructure away entirely. The |
| 36 | engine underneath is intentionally generic so that "Onde Cloud = swappable infra" |
| 37 | is literally true, but that generality is an *internal* seam, not the product. |
| 38 | |
| 39 | ``` |
| 40 | ┌─ Product surface (Copilot) ────────────────────────────────────────┐ |
| 41 | │ sigit login → pick a tier (Fast / Balanced / Large) → just works │ |
| 42 | │ No key, no URL, no provider names. Token is the only credential. │ |
| 43 | └────────────────────────────────────────────────────────────────────┘ |
| 44 | ┌─ Engine (generic, swappable) ──────────────────────────────────────┐ |
| 45 | │ InferenceBackend trait │ |
| 46 | │ ├─ LocalBackend → on-device onde::ChatEngine │ |
| 47 | │ └─ OpenAiBackend → any {base_url, api_key, model} over HTTP │ |
| 48 | └────────────────────────────────────────────────────────────────────┘ |
| 49 | ┌─ Infrastructure (hidden) ──────────────────────────────────────────┐ |
| 50 | │ Onde Cloud (OpenAI-compatible) → router → Anthropic (never named) │ |
| 51 | └────────────────────────────────────────────────────────────────────┘ |
| 52 | ``` |
| 53 | |
| 54 | The generic engine is also what enables the **BYO-endpoint escape hatch**: an |
| 55 | advanced user or enterprise can point siGit Code at their own OpenAI/Azure |
| 56 | endpoint. That is a power-user override, **not** the front door. |
| 57 | |
| 58 | ## Backend selection: identity and model choice are separate |
| 59 | |
| 60 | Being signed in does **not** change which model runs. Login is auth; the model |
| 61 | is the user's explicit choice in `/models`. The startup default |
| 62 | (`provider::active_provider()`) is local-first, with one exception: |
| 63 | |
| 64 | 1. **Override (power user / BYO):** `OPENAI_BASE_URL` + `OPENAI_API_KEY`, or the |
| 65 | active profile in `~/.config/sigit/providers.toml`. This is the only thing that |
| 66 | changes the *startup* backend. |
| 67 | 2. **On-device by default:** no override means local, signed in or not. |
| 68 | |
| 69 | The user then picks a model in `/models` (see below), which swaps the running |
| 70 | backend in place. `OPENAI_BASE_URL` set without `OPENAI_API_KEY` logs a warning |
| 71 | and stays on-device, rather than silently looking like "the cloud didn't work." |
| 72 | |
| 73 | ## `/models`: on-device models and cloud tiers together |
| 74 | |
| 75 | `/models` always lists the on-device models **and** the siGit Code Cloud tiers |
| 76 | (`Fast`/`Balanced`/`Large`), so the cloud option is discoverable to everyone. |
| 77 | Access is gated at **selection**, not visibility: |
| 78 | |
| 79 | - **Pick a cloud tier while signed in** → the backend hot-swaps to that tier |
| 80 | immediately (no restart). `provider::cloud_tier_provider(tier)` builds the |
| 81 | client from the stored token. |
| 82 | - **Pick a cloud tier while signed out** → a prompt: sign in with `/login`, or |
| 83 | create an account at sigit.si. The backend does not change. |
| 84 | - **Pick an on-device model** → loads and routes locally, as before. |
| 85 | |
| 86 | The cloud tiers are real picker entries backed by a synthetic |
| 87 | `GgufModelConfig` whose id is `sigit-cloud:<tier>` (no `/`, so it never collides |
| 88 | with a HuggingFace id and on-device matching code stays inert). `cloud_tier: |
| 89 | Option<String>` on `ModelPickerItem` distinguishes them. The tier maps to a wire |
| 90 | model id via `provider::tier_to_model` (`fast`→`onde-fast`, etc.); the UI shows |
| 91 | `siGit Code Cloud · Balanced`, never the model id or "Anthropic". |
| 92 | |
| 93 | Cloud tiers are a TUI-picker feature today. The ACP session-config paths use |
| 94 | `models::local_picker_items()` (cloud filtered out), because cloud inference over |
| 95 | ACP still needs the trait rewire (see Open items). |
| 96 | |
| 97 | ## Account / auth (via sigit.si) |
| 98 | |
| 99 | Account management is exposed as **slash commands** (`/login <email> <password>`, |
| 100 | `/logout`, `/whoami`) so it works inside an ACP session (Zed) and the terminal |
| 101 | TUI alike, since siGit Code has no separate place to run a CLI verb in those |
| 102 | contexts. Both surfaces parse `/`-prefixed input and call the I/O-free core |
| 103 | functions in `sigit/src/account.rs` (`authenticate`, `end_session`, |
| 104 | `status_line`). Slash input is not echoed to the transcript, so the password |
| 105 | does not persist there. `/login` only signs in; it does not change the active |
| 106 | model. To use the cloud after signing in, pick a tier in `/models`. |
| 107 | |
| 108 | - **Trust boundary is `sigit.si`.** It holds the confidential smbCloud |
| 109 | `app_secret` server-side and returns the client only a **bearer token**. The |
| 110 | client must never hold `app_secret` (it is a public/shipped binary, see the |
| 111 | `smbcloud-auth` skill's public-client rule). |
| 112 | - **Base URL:** `$SIGIT_API_URL`, else `https://sigit.si` (dev: |
| 113 | `http://localhost:8088`). |
| 114 | - **Endpoints (sigit.si `/api/v1`, token-based):** `POST /api/v1/auth/sign_in` |
| 115 | returns an `AccountStatus` (`"NotFound"` | `{"Ready":{"access_token":…}}` | |
| 116 | `{"Incomplete":{"status":<u32>}}`); `GET /api/v1/me` returns |
| 117 | `{id,email,created_at,updated_at}`; `DELETE /api/v1/auth/sign_out` returns 204. |
| 118 | Failures return `ErrorResponse {error_code, message}`. These shapes are a shared |
| 119 | contract with the desktop client; keep them in sync (see `smbcloud-auth`). The |
| 120 | controllers live at `app/controllers/api/v1/{sessions,me}_controller.rb`. |
| 121 | - **Credential store:** `sigit/src/credentials.rs` writes |
| 122 | `~/.config/sigit/credentials.toml` (`$SIGIT_CONFIG_DIR` override), mode `0600`, |
| 123 | holding `access_token` (+ email for display). |
| 124 | |
| 125 | ## Cloud Sessions (persisted chat) and the web app |
| 126 | |
| 127 | siGit Code Cloud chat conversations are saved as **Cloud Sessions** (short: |
| 128 | sessions), so they sync across signed-in surfaces and can be resumed. "Session" |
| 129 | means a chat conversation and belongs to this product only; the agent's unit is a |
| 130 | "run" (see `sigit-code-cloud-agent`). |
| 131 | |
| 132 | - **Models (sigit-si):** `CloudSession` (UUID primary key, since it appears in a |
| 133 | shareable URL) `has_many :cloud_messages`; `append_message!` auto-titles from |
| 134 | the first user turn and tracks activity. `CloudMessage` roles are |
| 135 | `user`/`assistant`/`system`. |
| 136 | - **API:** `Api::V1::CloudSessionsController`, token-scoped to `current_user`, at |
| 137 | `path: "sessions"` (kept distinct from the auth `SessionsController`): |
| 138 | `GET/POST /api/v1/sessions`, `GET/PATCH/DELETE /api/v1/sessions/:id`, and |
| 139 | `POST /api/v1/sessions/:id/messages`. |
| 140 | - **Streaming is unchanged:** clients still stream from `/api/v1/chat/completions`; |
| 141 | these endpoints only persist the transcript. The client appends the user message |
| 142 | on send and the assistant message when the stream finishes. |
| 143 | - **Web app:** `sigit-app/apps/code-cloud-web` is the SvelteKit client at |
| 144 | **code.sigit.si**. `/cloud` is the chat; `/cloud/sessions` and |
| 145 | `/cloud/sessions/<uuid>` are the saved history. Same `/api/v1` surface, |
| 146 | same-origin in production. (Root `/` is the on-device dashboard for signed-in |
| 147 | users and the marketing landing for signed-out ones.) |
| 148 | |
| 149 | ## Onde Cloud side (the API the cloud tier calls) |
| 150 | |
| 151 | Repo: `onde-cloud` (Rust/axum, OpenAI-compatible). |
| 152 | |
| 153 | - **Endpoints:** `POST /v1/chat/completions`, `GET /v1/models`. |
| 154 | - **Routing:** `router.rs` resolves a request `model` to a backend. Neutral public |
| 155 | ids (`onde-fast/balanced/large`) map to upstream models; raw `claude-*` ids are |
| 156 | accepted as backward-compatible aliases but never advertised. `/v1/models` |
| 157 | reports `owned_by: "onde-inference"`: the upstream provider is never disclosed. |
| 158 | - **Tool calling:** `anthropic.rs` maps OpenAI function-calling ↔ Anthropic |
| 159 | `tool_use`/`tool_result` (assistant `tool_calls` → `tool_use` blocks; coalesced |
| 160 | `tool` messages → one `tool_result` user turn; `parameters` → `input_schema`; |
| 161 | `stop_reason` → `finish_reason`). This is what makes the cloud tier a real |
| 162 | *agent* backend, not just chat. |
| 163 | - **Error hygiene:** client-facing errors are neutral ("Onde Cloud upstream error |
| 164 | (NNN)"); full upstream detail is logged server-side only. Never echo provider |
| 165 | names or upstream error bodies to the client. |
| 166 | - **Auth:** validates `app_id:app_secret` against smbCloud (`DEV_MODE=true` |
| 167 | bypasses for local dev and accepts any bearer). |
| 168 | |
| 169 | ## Local dev / testing |
| 170 | |
| 171 | ```bash |
| 172 | # 1. Onde Cloud API (needs ANTHROPIC_API_KEY in onde-cloud/.env for the cloud tier) |
| 173 | cd ~/Repositories/onde-cloud && set -a && . ./.env && set +a && ./target/debug/onde-cloud |
| 174 | # → listens on $PORT (e.g. 8090); DEV_MODE=true accepts any bearer |
| 175 | |
| 176 | # 2a. Product path: point siGit Code Cloud at the local API, start sigit |
| 177 | SIGIT_CLOUD_URL=http://localhost:8090/v1 SIGIT_API_URL=http://localhost:8088 sigit |
| 178 | # then inside the session: |
| 179 | # /login <email> <password> # sign in |
| 180 | # /models # pick a siGit Code Cloud tier (hot-swaps) |
| 181 | |
| 182 | # 2b. BYO/override path (no login needed): explicit endpoint |
| 183 | OPENAI_BASE_URL=http://localhost:8090/v1 OPENAI_API_KEY=dev:dev SIGIT_TIER=fast sigit |
| 184 | # (or ~/.config/sigit/providers.toml with active = "<profile>") |
| 185 | ``` |
| 186 | |
| 187 | `SIGIT_CLOUD_URL` overrides the baked-in cloud endpoint for dev. Watch routing in |
| 188 | the server log: `chat: routing app_id=... to upstream provider`. Use the `fast` tier |
| 189 | while testing to keep upstream spend low. |
| 190 | |
| 191 | ## Key files |
| 192 | |
| 193 | - `sigit/src/account.rs`: account core (`authenticate`/`end_session`/`status_line`), sigit.si `/api/v1` client. Surfaced as `/login`, `/logout`, `/whoami`. |
| 194 | - `sigit/src/credentials.rs`: local token store. |
| 195 | - `sigit/src/provider.rs`: backend precedence, tiers, cloud default. |
| 196 | - `sigit/src/backend.rs`: `InferenceBackend` trait, `LocalBackend`, `OpenAiBackend`. |
| 197 | - `onde-cloud/src/{router,anthropic,chat,types}.rs`: the hosted API + mapping. |
| 198 | |
| 199 | ## Open items / known gaps |
| 200 | |
| 201 | - **Inference token ↔ Onde Cloud auth.** Onde Cloud currently validates |
| 202 | `app_id:app_secret`; the product needs it to accept the sigit.si-issued user |
| 203 | token (sigit.si fronts/mints inference auth, mirroring the `git_token` exchange |
| 204 | in `sigit-app`). Until then, dev relies on `DEV_MODE`. |
| 205 | - **`sigit.si/api/v1` auth routes exist** (`sessions`/`me` controllers). The |
| 206 | remaining gap is the inference-token link above, not the account endpoints. |
| 207 | - **Usage/billing:** `onde-cloud` returns zeroed `usage`; wire Anthropic's |
| 208 | returned token counts through for metering. |
| 209 | - **Tool-call id fingerprint:** Onde Cloud passes Anthropic's `toolu_…` tool-call |
| 210 | ids straight through (OpenAI uses `call_…`). Reversibly rewrite before any |
| 211 | external launch so the id format doesn't reveal the backend. |
| 212 | - **ACP path:** the Zed/ACP server in `sigit/src/main.rs` still calls `onde` |
| 213 | directly; route it through `InferenceBackend` too. |
| 214 | |
| 215 | ## Billing — siGit's own, gating siGit Code Cloud |
| 216 | |
| 217 | siGit Code is local-first and free; **siGit Code Cloud (the Fast/Balanced/Large |
| 218 | tiers) requires a paid plan.** This is siGit's *own* Stripe billing, separate from |
| 219 | Onde Inference's — siGit pays Onde as a customer; here siGit charges its end users. |
| 220 | |
| 221 | - **Gate:** `Api::V1::ChatCompletionsController#enforce_cloud_entitlement!` — |
| 222 | `CloudCatalog.cloud_tier?(model)` (the `onde-*`/`claude-*` ids) requires |
| 223 | `current_user.entitled_to_cloud?`, else **402**. On-device GGUF ids pass free. |
| 224 | - **Entitlement:** `User#entitled_to_cloud?` → `Subscription#entitled_to_cloud?` |
| 225 | (`pro`/`team` && `active`/`trialing`). No subscription = Free = local only. |
| 226 | - **Stripe:** `StripeService` (checkout/portal/`construct_event`/`sync_subscription`), |
| 227 | `Api::V1::BillingController` (`GET billing`, `POST billing/checkout|portal`), |
| 228 | `StripeWebhooksController` (`POST /stripe/webhooks`, signature-verified). Stripe is |
| 229 | the source of truth; the `subscriptions` table caches plan + status. |
| 230 | - **Allowance:** `enforce_cloud_allowance!` caps cloud requests per billing period |
| 231 | (`Subscription::CLOUD_ALLOWANCE` — pro 2000, team 6000; tunable). `CloudUsage` |
| 232 | counts per `(user, billing_period_key)`, so it resets each cycle; over the cap → |
| 233 | **429**. `GET /api/v1/billing` returns `cloud_requests_used` + `cloud_allowance`. |
| 234 | - **Plans:** product `siGit Code` in **siGit's own Stripe account** |
| 235 | (`acct_1TlaO9LwK8mGvn31`, Splitfire AB); prices `sigit_code_pro_monthly` ($20/mo) |
| 236 | and `sigit_code_team_monthly` ($40/mo), resolved by lookup key. |
| 237 | - **Env:** `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` (siGit account). Checkout is |
| 238 | **web-initiated** (desktop/CLI open the returned URL) to avoid the App Store cut. |
| 239 | - **Remaining:** a web/desktop billing UI; metering by tokens (the `usage` Onde |
| 240 | Cloud returns) instead of request count, if finer cost control is needed. |
| 241 | |
| 242 | ## Common mistakes |
| 243 | |
| 244 | - Making BYO-endpoint (env / `providers.toml`) the default instead of the |
| 245 | `sigit login` product experience. |
| 246 | - Exposing a model id, base URL, or provider name in product UI/errors. |
| 247 | - Treating the account access token as a model API key, or shipping smbCloud |
| 248 | `app_secret` in the `sigit` binary (public-client violation). |
| 249 | - Letting a persisted local model name show in the title while routing to the |
| 250 | cloud (guard with `InferenceBackend::is_remote()`). |
| 251 | - Mixing the names: `siGit Code` (local agent), `siGit Code Cloud` (hosted chat), |
| 252 | `siGit Code Cloud Agent` (autonomous, task -> PR), `sigit` (CLI), `smbCloud` |
| 253 | (company), `Onde Cloud` (infrastructure). |
| 254 | - Calling a chat conversation a "run", or the agent's work a "session". Chat has |
| 255 | **sessions**; the agent has **runs**. |