main
md 15.4 KB

name: sigit-code-cloud

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."

siGit Code Cloud

siGit Code Cloud is the hosted chat for siGit Code: interactive, signed-in, model in the cloud instead of on the device. The product framing is deliberate:

  • siGit Code Cloud is the product, like Copilot Chat. The user signs in, picks a quality tier, and it works. They never see an API key, a base URL, or the name of any model provider.
  • Onde Cloud is the infrastructure, like Azure. It is the OpenAI-compatible inference service siGit Code Cloud runs on, and it is an implementation detail.
  • Onde Cloud, in turn, routes to upstream providers (today Anthropic) and hides them too, see the onde-cloud router/anthropic modules.

This is the chat product, not the agent. The autonomous, sandboxed task -> pull request product is siGit Code Cloud Agent (see the sigit-code-cloud-agent skill). Chat's work unit is a session; the agent's is a run. Keep them separate. Full product map: docs/product/product-overview.md.

Keep the names straight (see the branding skill): the local agent is siGit Code, the hosted chat is siGit Code Cloud, the autonomous one is siGit Code Cloud Agent, the CLI is sigit, the company is smbCloud, the inference infrastructure is Onde Cloud / Onde Inference, and the on-device Rust crate is onde. sigit.si is Git hosting; code.sigit.si is the home of siGit Code.

The layering (why it's built this way)

The original instinct was right: abstract the infrastructure away entirely. The engine underneath is intentionally generic so that "Onde Cloud = swappable infra" is literally true, but that generality is an internal seam, not the product.

┌─ Product surface (Copilot) ────────────────────────────────────────┐
│  sigit login → pick a tier (Fast / Balanced / Large) → just works   │
│  No key, no URL, no provider names. Token is the only credential.   │
└────────────────────────────────────────────────────────────────────┘
┌─ Engine (generic, swappable) ──────────────────────────────────────┐
│  InferenceBackend trait                                             │
│    ├─ LocalBackend   → on-device onde::ChatEngine                   │
│    └─ OpenAiBackend  → any {base_url, api_key, model} over HTTP     │
└────────────────────────────────────────────────────────────────────┘
┌─ Infrastructure (hidden) ──────────────────────────────────────────┐
│  Onde Cloud (OpenAI-compatible) → router → Anthropic (never named)  │
└────────────────────────────────────────────────────────────────────┘

The generic engine is also what enables the BYO-endpoint escape hatch: an advanced user or enterprise can point siGit Code at their own OpenAI/Azure endpoint. That is a power-user override, not the front door.

Backend selection: identity and model choice are separate

Being signed in does not change which model runs. Login is auth; the model is the user's explicit choice in /models. The startup default (provider::active_provider()) is local-first, with one exception:

  1. Override (power user / BYO): OPENAI_BASE_URL + OPENAI_API_KEY, or the active profile in ~/.config/sigit/providers.toml. This is the only thing that changes the startup backend.
  2. On-device by default: no override means local, signed in or not.

The user then picks a model in /models (see below), which swaps the running backend in place. OPENAI_BASE_URL set without OPENAI_API_KEY logs a warning and stays on-device, rather than silently looking like "the cloud didn't work."

/models: on-device models and cloud tiers together

/models always lists the on-device models and the siGit Code Cloud tiers (Fast/Balanced/Large), so the cloud option is discoverable to everyone. Access is gated at selection, not visibility:

  • Pick a cloud tier while signed in → the backend hot-swaps to that tier immediately (no restart). provider::cloud_tier_provider(tier) builds the client from the stored token.
  • Pick a cloud tier while signed out → a prompt: sign in with /login, or create an account at sigit.si. The backend does not change.
  • Pick an on-device model → loads and routes locally, as before.

The cloud tiers are real picker entries backed by a synthetic GgufModelConfig whose id is sigit-cloud:<tier> (no /, so it never collides with a HuggingFace id and on-device matching code stays inert). cloud_tier: Option<String> on ModelPickerItem distinguishes them. The tier maps to a wire model id via provider::tier_to_model (fastonde-fast, etc.); the UI shows siGit Code Cloud · Balanced, never the model id or "Anthropic".

Cloud tiers are a TUI-picker feature today. The ACP session-config paths use models::local_picker_items() (cloud filtered out), because cloud inference over ACP still needs the trait rewire (see Open items).

Account / auth (via sigit.si)

Account management is exposed as slash commands (/login <email> <password>, /logout, /whoami) so it works inside an ACP session (Zed) and the terminal TUI alike, since siGit Code has no separate place to run a CLI verb in those contexts. Both surfaces parse /-prefixed input and call the I/O-free core functions in sigit/src/account.rs (authenticate, end_session, status_line). Slash input is not echoed to the transcript, so the password does not persist there. /login only signs in; it does not change the active model. To use the cloud after signing in, pick a tier in /models.

  • Trust boundary is sigit.si. It holds the confidential smbCloud app_secret server-side and returns the client only a bearer token. The client must never hold app_secret (it is a public/shipped binary, see the smbcloud-auth skill's public-client rule).
  • Base URL: $SIGIT_API_URL, else https://sigit.si (dev: http://localhost:8088).
  • Endpoints (sigit.si /api/v1, token-based): POST /api/v1/auth/sign_in returns an AccountStatus ("NotFound" | {"Ready":{"access_token":…}} | {"Incomplete":{"status":<u32>}}); GET /api/v1/me returns {id,email,created_at,updated_at}; DELETE /api/v1/auth/sign_out returns 204. Failures return ErrorResponse {error_code, message}. These shapes are a shared contract with the desktop client; keep them in sync (see smbcloud-auth). The controllers live at app/controllers/api/v1/{sessions,me}_controller.rb.
  • Credential store: sigit/src/credentials.rs writes ~/.config/sigit/credentials.toml ($SIGIT_CONFIG_DIR override), mode 0600, holding access_token (+ email for display).

Cloud Sessions (persisted chat) and the web app

siGit Code Cloud chat conversations are saved as Cloud Sessions (short: sessions), so they sync across signed-in surfaces and can be resumed. "Session" means a chat conversation and belongs to this product only; the agent's unit is a "run" (see sigit-code-cloud-agent).

  • Models (sigit-si): CloudSession (UUID primary key, since it appears in a shareable URL) has_many :cloud_messages; append_message! auto-titles from the first user turn and tracks activity. CloudMessage roles are user/assistant/system.
  • API: Api::V1::CloudSessionsController, token-scoped to current_user, at path: "sessions" (kept distinct from the auth SessionsController): GET/POST /api/v1/sessions, GET/PATCH/DELETE /api/v1/sessions/:id, and POST /api/v1/sessions/:id/messages.
  • Streaming is unchanged: clients still stream from /api/v1/chat/completions; these endpoints only persist the transcript. The client appends the user message on send and the assistant message when the stream finishes.
  • Web app: sigit-app/apps/code-cloud-web is the SvelteKit client at code.sigit.si. /cloud is the chat; /cloud/sessions and /cloud/sessions/<uuid> are the saved history. Same /api/v1 surface, same-origin in production. (Root / is the on-device dashboard for signed-in users and the marketing landing for signed-out ones.)

Onde Cloud side (the API the cloud tier calls)

Repo: onde-cloud (Rust/axum, OpenAI-compatible).

  • Endpoints: POST /v1/chat/completions, GET /v1/models.
  • Routing: router.rs resolves a request model to a backend. Neutral public ids (onde-fast/balanced/large) map to upstream models; raw claude-* ids are accepted as backward-compatible aliases but never advertised. /v1/models reports owned_by: "onde-inference": the upstream provider is never disclosed.
  • Tool calling: anthropic.rs maps OpenAI function-calling ↔ Anthropic tool_use/tool_result (assistant tool_callstool_use blocks; coalesced tool messages → one tool_result user turn; parametersinput_schema; stop_reasonfinish_reason). This is what makes the cloud tier a real agent backend, not just chat.
  • Error hygiene: client-facing errors are neutral ("Onde Cloud upstream error (NNN)"); full upstream detail is logged server-side only. Never echo provider names or upstream error bodies to the client.
  • Auth: validates app_id:app_secret against smbCloud (DEV_MODE=true bypasses for local dev and accepts any bearer).

Local dev / testing

# 1. Onde Cloud API (needs ANTHROPIC_API_KEY in onde-cloud/.env for the cloud tier)
cd ~/Repositories/onde-cloud && set -a && . ./.env && set +a && ./target/debug/onde-cloud
#   → listens on $PORT (e.g. 8090); DEV_MODE=true accepts any bearer

# 2a. Product path: point siGit Code Cloud at the local API, start sigit
SIGIT_CLOUD_URL=http://localhost:8090/v1 SIGIT_API_URL=http://localhost:8088 sigit
#   then inside the session:
#     /login <email> <password>      # sign in
#     /models                        # pick a siGit Code Cloud tier (hot-swaps)

# 2b. BYO/override path (no login needed): explicit endpoint
OPENAI_BASE_URL=http://localhost:8090/v1 OPENAI_API_KEY=dev:dev SIGIT_TIER=fast sigit
#   (or ~/.config/sigit/providers.toml with active = "<profile>")

SIGIT_CLOUD_URL overrides the baked-in cloud endpoint for dev. Watch routing in the server log: chat: routing app_id=... to upstream provider. Use the fast tier while testing to keep upstream spend low.

Key files

  • sigit/src/account.rs: account core (authenticate/end_session/status_line), sigit.si /api/v1 client. Surfaced as /login, /logout, /whoami.
  • sigit/src/credentials.rs: local token store.
  • sigit/src/provider.rs: backend precedence, tiers, cloud default.
  • sigit/src/backend.rs: InferenceBackend trait, LocalBackend, OpenAiBackend.
  • onde-cloud/src/{router,anthropic,chat,types}.rs: the hosted API + mapping.

Open items / known gaps

  • Inference token ↔ Onde Cloud auth. Onde Cloud currently validates app_id:app_secret; the product needs it to accept the sigit.si-issued user token (sigit.si fronts/mints inference auth, mirroring the git_token exchange in sigit-app). Until then, dev relies on DEV_MODE.
  • sigit.si/api/v1 auth routes exist (sessions/me controllers). The remaining gap is the inference-token link above, not the account endpoints.
  • Usage/billing: onde-cloud returns zeroed usage; wire Anthropic's returned token counts through for metering.
  • Tool-call id fingerprint: Onde Cloud passes Anthropic's toolu_… tool-call ids straight through (OpenAI uses call_…). Reversibly rewrite before any external launch so the id format doesn't reveal the backend.
  • ACP path: the Zed/ACP server in sigit/src/main.rs still calls onde directly; route it through InferenceBackend too.

Billing — siGit's own, gating siGit Code Cloud

siGit Code is local-first and free; siGit Code Cloud (the Fast/Balanced/Large tiers) requires a paid plan. This is siGit's own Stripe billing, separate from Onde Inference's — siGit pays Onde as a customer; here siGit charges its end users.

  • Gate: Api::V1::ChatCompletionsController#enforce_cloud_entitlement!CloudCatalog.cloud_tier?(model) (the onde-*/claude-* ids) requires current_user.entitled_to_cloud?, else 402. On-device GGUF ids pass free.
  • Entitlement: User#entitled_to_cloud?Subscription#entitled_to_cloud? (pro/team && active/trialing). No subscription = Free = local only.
  • Stripe: StripeService (checkout/portal/construct_event/sync_subscription), Api::V1::BillingController (GET billing, POST billing/checkout|portal), StripeWebhooksController (POST /stripe/webhooks, signature-verified). Stripe is the source of truth; the subscriptions table caches plan + status.
  • Allowance: enforce_cloud_allowance! caps cloud requests per billing period (Subscription::CLOUD_ALLOWANCE — pro 2000, team 6000; tunable). CloudUsage counts per (user, billing_period_key), so it resets each cycle; over the cap → 429. GET /api/v1/billing returns cloud_requests_used + cloud_allowance.
  • Plans: product siGit Code in siGit's own Stripe account (acct_1TlaO9LwK8mGvn31, Splitfire AB); prices sigit_code_pro_monthly ($20/mo) and sigit_code_team_monthly ($40/mo), resolved by lookup key.
  • Env: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (siGit account). Checkout is web-initiated (desktop/CLI open the returned URL) to avoid the App Store cut.
  • Remaining: a web/desktop billing UI; metering by tokens (the usage Onde Cloud returns) instead of request count, if finer cost control is needed.

Common mistakes

  • Making BYO-endpoint (env / providers.toml) the default instead of the sigit login product experience.
  • Exposing a model id, base URL, or provider name in product UI/errors.
  • Treating the account access token as a model API key, or shipping smbCloud app_secret in the sigit binary (public-client violation).
  • Letting a persisted local model name show in the title while routing to the cloud (guard with InferenceBackend::is_remote()).
  • Mixing the names: siGit Code (local agent), siGit Code Cloud (hosted chat), siGit Code Cloud Agent (autonomous, task -> PR), sigit (CLI), smbCloud (company), Onde Cloud (infrastructure).
  • Calling a chat conversation a "run", or the agent's work a "session". Chat has sessions; the agent has runs.