Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<instance>.app.myaltimate.com`, use `https://api.myaltimate.com`; if `https://<instance>.app.getaltimate.com`, use `https://api.getaltimate.com`

Then run `/connect` and select **Altimate** to start using it as your model.

## Configuration

Expand Down
11 changes: 11 additions & 0 deletions packages/opencode/src/altimate/bridge/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,19 @@ export namespace Bridge {
async function start() {
await ensureEngine()
const pythonCmd = resolvePython()

// Propagate altimate-code's telemetry opt-out to the Python engine.
// The engine calls altimate_core.init() lazily; this env var ensures
// it won't send telemetry when the user has disabled it here.
await Telemetry.init()
const childEnv = { ...process.env }
if (!Telemetry.isEnabled()) {
childEnv.ALTIMATE_TELEMETRY_DISABLED = "true"
}

child = spawn(pythonCmd, ["-m", "altimate_engine.server"], {
stdio: ["pipe", "pipe", "pipe"],
env: childEnv,
})

buffer = ""
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@ export namespace Telemetry {
return { sessionId, projectId }
}

/** Returns true only after init() has completed and telemetry is enabled. */
export function isEnabled(): boolean {
return initDone && enabled
}

export function track(event: Event) {
// Before init completes: buffer (flushed once init enables, or cleared if disabled).
// After init completed and disabled telemetry: drop silently.
Expand Down
198 changes: 198 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/dialog-altimate-login.tsx
Original file line number Diff line number Diff line change
@@ -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,
},
})
Comment on lines +82 to +88
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 (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Connect to Altimate
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>

<text fg={theme.textMuted}>Find these in Settings &gt; API Keys in your Altimate dashboard</text>

<box>
<text fg={store.active === "instance" ? theme.text : theme.textMuted}>Instance Name:</text>
<text fg={theme.textMuted}> From your URL: https://&lt;instance&gt;.app.myaltimate.com</text>
<textarea
height={3}
ref={(val: TextareaRenderable) => (instanceRef = val)}
placeholder="your-instance"
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.text}
onMouseUp={() => {
setStore("active", "instance")
focusActive()
}}
/>
</box>

<box>
<text fg={store.active === "key" ? theme.text : theme.textMuted}>API Key:</text>
<text fg={theme.textMuted}> Settings &gt; API Keys &gt; Copy</text>
<textarea
height={3}
ref={(val: TextareaRenderable) => (keyRef = val)}
placeholder="your-api-key"
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.text}
onMouseUp={() => {
setStore("active", "key")
focusActive()
}}
/>
</box>

<box>
<text fg={store.active === "url" ? theme.text : theme.textMuted}>URL:</text>
<textarea
height={3}
ref={(val: TextareaRenderable) => (urlRef = val)}
initialValue="https://api.myaltimate.com"
placeholder="https://api.myaltimate.com"
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.text}
onMouseUp={() => {
setStore("active", "url")
focusActive()
}}
/>
</box>

<Show when={error()}>
<text fg={theme.error}>{error()}</text>
</Show>
<Show when={validating()}>
<text fg={theme.textMuted}>Validating credentials...</text>
</Show>

<text fg={theme.textMuted} paddingBottom={1}>
<span style={{ fg: theme.text }}>tab</span> next field{" "}
<span style={{ fg: theme.text }}>enter</span> submit
</text>
</box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DialogModel } from "./dialog-model"
import { useKeyboard } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard"
import { useToast } from "../ui/toast"
import { DialogAltimateLogin } from "./dialog-altimate-login"

const PROVIDER_PRIORITY: Record<string, number> = {
opencode: 0,
Expand All @@ -21,6 +22,7 @@ const PROVIDER_PRIORITY: Record<string, number> = {
"github-copilot": 3,
anthropic: 4,
google: 5,
"altimate-backend": 6,
}

export function createDialogProviderOptions() {
Expand All @@ -35,13 +37,18 @@ export function createDialogProviderOptions() {
title: provider.name,
value: provider.id,
description: {
"altimate-backend": "(API key)",
opencode: "(Recommended)",
anthropic: "(API key)",
openai: "(ChatGPT Plus/Pro or API key)",
"opencode-go": "Low cost subscription for everyone",
}[provider.id],
Comment on lines 37 to 45
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() {
if (provider.id === "altimate-backend") {
dialog.replace(() => <DialogAltimateLogin />)
return
}
const methods = sync.data.provider_auth[provider.id] ?? [
{
type: "api",
Expand Down
58 changes: 58 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { iife } from "@/util/iife"
import { Global } from "../global"
import path from "path"
import { Filesystem } from "../util/filesystem"
import { AltimateApi } from "../altimate/api/client"

// Direct imports for bundled providers
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
Expand Down Expand Up @@ -178,6 +179,26 @@ export namespace Provider {
options: hasKey ? {} : { apiKey: "public" },
}
},
"altimate-backend": async () => {
const isConfigured = await AltimateApi.isConfigured()
if (!isConfigured) return { autoload: false }

try {
const creds = await AltimateApi.getCredentials()
return {
autoload: true,
options: {
baseURL: `${creds.altimateUrl.replace(/\/+$/, "")}/agents/v1`,
apiKey: creds.altimateApiKey,
headers: {
"x-tenant": creds.altimateInstanceName,
},
},
}
} catch {
return { autoload: false }
}
},
openai: async () => {
return {
autoload: false,
Expand Down Expand Up @@ -877,6 +898,43 @@ export namespace Provider {
}
}

// Register altimate-backend as an OpenAI-compatible provider
if (!database["altimate-backend"]) {
const backendModels: Record<string, Model> = {
"altimate-default": {
id: ModelID.make("altimate-default"),
providerID: ProviderID.make("altimate-backend"),
name: "Altimate AI",
family: "openai",
api: { id: "altimate-default", url: "", npm: "@ai-sdk/openai-compatible" },
status: "active",
headers: {},
options: {},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 200000, output: 128000 },
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
release_date: "2025-01-01",
variants: {},
},
}
database["altimate-backend"] = {
id: ProviderID.make("altimate-backend"),
name: "Altimate",
source: "custom",
env: [],
options: {},
models: backendModels,
}
}

Comment on lines +901 to +937
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
const existing = providers[providerID]
if (existing) {
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/util/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export namespace Filesystem {
try {
if (mode) {
await writeFile(p, content, { mode })
await chmod(p, mode)
} else {
await writeFile(p, content)
}
Expand All @@ -63,6 +64,7 @@ export namespace Filesystem {
await mkdir(dirname(p), { recursive: true })
if (mode) {
await writeFile(p, content, { mode })
await chmod(p, mode)
} else {
await writeFile(p, content)
}
Expand Down
Loading