diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 7584bbeb81..d2c9d72a51 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -46,7 +46,35 @@ The TUI launches with an interactive terminal. On first run, use the `/discover` You can also configure connections manually — see [Warehouse connections](#warehouse-connections) below. -To set up your LLM provider, use the `/connect` command. +### Connecting an LLM provider + +Run `/connect` in the TUI to set up your LLM provider. You'll see a list of popular providers — select one and follow the prompts: + +- **Altimate Code Zen** (Recommended) — single API key for all top coding models at the lowest prices. Get a key at `https://altimate.ai/zen` +- **OpenAI** — ChatGPT Plus/Pro subscription or API key +- **Anthropic** — API key from `console.anthropic.com` +- **Google** — API key from Google AI Studio +- **Altimate** — connect to your Altimate platform instance (see below) + +You can switch providers at any time by running `/connect` again. + +### Connecting to Altimate + +If you have an Altimate platform account, create `~/.altimate/altimate.json` with your credentials: + +```json +{ + "altimateInstanceName": "your-instance", + "altimateApiKey": "your-api-key", + "altimateUrl": "https://api.myaltimate.com" +} +``` + +- **Instance Name** — the subdomain from your Altimate dashboard URL (e.g. `acme` from `https://acme.app.myaltimate.com`) +- **API Key** — go to **Settings > API Keys** in your Altimate dashboard and click **Copy** +- **URL** — matches your dashboard domain: if you access `https://.app.myaltimate.com`, use `https://api.myaltimate.com`; if `https://.app.getaltimate.com`, use `https://api.getaltimate.com` + +Then run `/connect` and select **Altimate** to start using it as your model. ## Configuration diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-altimate-login.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-altimate-login.tsx new file mode 100644 index 0000000000..aedb921339 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-altimate-login.tsx @@ -0,0 +1,198 @@ +import { createSignal, Show, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { TextareaRenderable, TextAttributes } from "@opentui/core" +import { useKeyboard } from "@opentui/solid" +import { useDialog } from "@tui/ui/dialog" +import { useSDK } from "../context/sdk" +import { useSync } from "@tui/context/sync" +import { useTheme } from "../context/theme" +import { AltimateApi } from "@/altimate/api/client" +import { Filesystem } from "@/util/filesystem" + +export function DialogAltimateLogin() { + const dialog = useDialog() + const sdk = useSDK() + const sync = useSync() + const { theme } = useTheme() + const [error, setError] = createSignal("") + const [validating, setValidating] = createSignal(false) + const [store, setStore] = createStore({ + active: "instance" as "instance" | "key" | "url", + }) + + let instanceRef: TextareaRenderable + let keyRef: TextareaRenderable + let urlRef: TextareaRenderable + + const fields = ["instance", "key", "url"] as const + + function focusActive() { + setTimeout(() => { + const ref = { instance: instanceRef, key: keyRef, url: urlRef }[store.active] + if (ref && !ref.isDestroyed) ref.focus() + }, 1) + } + + useKeyboard((evt) => { + if (evt.name === "tab") { + const idx = fields.indexOf(store.active) + const next = fields[(idx + 1) % fields.length] + setStore("active", next) + focusActive() + evt.preventDefault() + } + if (evt.name === "return") { + if (validating()) return + void submit().catch((e) => setError(`Unexpected error: ${e?.message ?? e}`)) + } + }) + + onMount(() => { + dialog.setSize("medium") + focusActive() + }) + + async function submit() { + const instance = instanceRef.plainText.trim() + const key = keyRef.plainText.trim() + const url = urlRef.plainText.trim().replace(/\/+$/, "") + + if (!instance) { + setError("Instance name is required") + setStore("active", "instance") + focusActive() + return + } + if (!key) { + setError("API key is required") + setStore("active", "key") + focusActive() + return + } + if (!url) { + setError("URL is required") + setStore("active", "url") + focusActive() + return + } + + setError("") + setValidating(true) + try { + const res = await fetch(`${url}/auth_health`, { + method: "GET", + headers: { + Authorization: `Bearer ${key}`, + "x-tenant": instance, + }, + }) + if (!res.ok) { + setError("Invalid credentials — check your instance name, API key, and URL") + setValidating(false) + return + } + const data = await res.json() + if (data.status !== "auth_valid") { + setError("Unexpected response from server") + setValidating(false) + return + } + } catch { + setError(`Connection failed — could not reach ${url}`) + setValidating(false) + return + } + + try { + const creds = { + altimateUrl: url, + altimateInstanceName: instance, + altimateApiKey: key, + } + await Filesystem.writeJson(AltimateApi.credentialsPath(), creds, 0o600) + await sdk.client.instance.dispose() + await sync.bootstrap() + dialog.clear() + } finally { + setValidating(false) + } + } + + return ( + + + + Connect to Altimate + + dialog.clear()}> + esc + + + + Find these in Settings > API Keys in your Altimate dashboard + + + Instance Name: + From your URL: https://<instance>.app.myaltimate.com +