Skip to content

Commit bdc5217

Browse files
committed
feat: add Altimate provider with /login command and credential validation
1 parent 1ca92bc commit bdc5217

File tree

7 files changed

+296
-0
lines changed

7 files changed

+296
-0
lines changed

docs/docs/getting-started.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ You can also configure connections manually — see [Warehouse connections](#war
4848

4949
To set up your LLM provider, use the `/connect` command.
5050

51+
### Connecting to Altimate
52+
53+
If you have an Altimate account, you can connect directly:
54+
55+
1. Run `/login` or select **Altimate** from the `/connect` provider list
56+
2. Enter your credentials:
57+
- **Instance Name** — found in your Altimate URL: `https://<instance>.app.getaltimate.com` or `https://<instance>.app.myaltimate.com`
58+
- **API Key** — go to **Settings > API Keys** in your Altimate dashboard and click **Copy**
59+
- **URL** — defaults to `https://api.myaltimate.com` (use `https://api.getaltimate.com` for enterprise)
60+
61+
Your credentials are validated against the server before being saved to `~/.altimate/altimate.json`. Once connected, Altimate AI is automatically selected as your default model.
62+
5163
## Configuration
5264

5365
altimate uses a JSON config file. Create `altimate-code.json` in your project root or `~/.config/altimate-code/altimate-code.json` globally.

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Flag } from "@/flag/flag"
1010
import { Log } from "@/util/log"
1111
import { DialogProvider, useDialog } from "@tui/ui/dialog"
1212
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
13+
import { DialogAltimateLogin } from "@tui/component/dialog-altimate-login"
1314
import { SDKProvider, useSDK } from "@tui/context/sdk"
1415
import { SyncProvider, useSync } from "@tui/context/sync"
1516
import { LocalProvider, useLocal } from "@tui/context/local"
@@ -580,6 +581,17 @@ function App() {
580581
},
581582
category: "Provider",
582583
},
584+
{
585+
title: "Login to Altimate",
586+
value: "altimate.login",
587+
category: "Provider",
588+
slash: {
589+
name: "login",
590+
},
591+
onSelect: () => {
592+
dialog.replace(() => <DialogAltimateLogin />)
593+
},
594+
},
583595
{
584596
title: "View status",
585597
keybind: "status_view",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { createSignal, Show, onMount } from "solid-js"
2+
import { createStore } from "solid-js/store"
3+
import { TextareaRenderable, TextAttributes } from "@opentui/core"
4+
import { useKeyboard } from "@opentui/solid"
5+
import { useDialog } from "@tui/ui/dialog"
6+
import { useSDK } from "../context/sdk"
7+
import { useSync } from "@tui/context/sync"
8+
import { useLocal } from "@tui/context/local"
9+
import { useTheme } from "../context/theme"
10+
import { AltimateApi } from "@/altimate/api/client"
11+
import { Filesystem } from "@/util/filesystem"
12+
13+
export function DialogAltimateLogin() {
14+
const dialog = useDialog()
15+
const sdk = useSDK()
16+
const sync = useSync()
17+
const local = useLocal()
18+
const { theme } = useTheme()
19+
const [error, setError] = createSignal("")
20+
const [validating, setValidating] = createSignal(false)
21+
const [store, setStore] = createStore({
22+
active: "instance" as "instance" | "key" | "url",
23+
})
24+
25+
let instanceRef: TextareaRenderable
26+
let keyRef: TextareaRenderable
27+
let urlRef: TextareaRenderable
28+
29+
const fields = ["instance", "key", "url"] as const
30+
31+
function focusActive() {
32+
setTimeout(() => {
33+
const ref = { instance: instanceRef, key: keyRef, url: urlRef }[store.active]
34+
if (ref && !ref.isDestroyed) ref.focus()
35+
}, 1)
36+
}
37+
38+
useKeyboard((evt) => {
39+
if (evt.name === "tab") {
40+
const idx = fields.indexOf(store.active)
41+
const next = fields[(idx + 1) % fields.length]
42+
setStore("active", next)
43+
focusActive()
44+
evt.preventDefault()
45+
}
46+
if (evt.name === "return") {
47+
if (validating()) return
48+
submit()
49+
}
50+
})
51+
52+
onMount(() => {
53+
dialog.setSize("medium")
54+
focusActive()
55+
})
56+
57+
async function submit() {
58+
const instance = instanceRef.plainText.trim()
59+
const key = keyRef.plainText.trim()
60+
const url = urlRef.plainText.trim().replace(/\/+$/, "")
61+
62+
if (!instance) {
63+
setError("Instance name is required")
64+
setStore("active", "instance")
65+
focusActive()
66+
return
67+
}
68+
if (!key) {
69+
setError("API key is required")
70+
setStore("active", "key")
71+
focusActive()
72+
return
73+
}
74+
if (!url) {
75+
setError("URL is required")
76+
setStore("active", "url")
77+
focusActive()
78+
return
79+
}
80+
81+
setError("")
82+
setValidating(true)
83+
try {
84+
const res = await fetch(`${url}/auth_health`, {
85+
method: "GET",
86+
headers: {
87+
Authorization: `Bearer ${key}`,
88+
"x-tenant": instance,
89+
},
90+
})
91+
if (!res.ok) {
92+
setError("Invalid credentials — check your instance name, API key, and URL")
93+
return
94+
}
95+
const data = await res.json()
96+
if (data.status !== "auth_valid") {
97+
setError("Unexpected response from server")
98+
return
99+
}
100+
} catch {
101+
setError(`Connection failed — could not reach ${url}`)
102+
return
103+
} finally {
104+
setValidating(false)
105+
}
106+
107+
const creds = {
108+
altimateUrl: url,
109+
altimateInstanceName: instance,
110+
altimateApiKey: key,
111+
}
112+
await Filesystem.writeJson(AltimateApi.credentialsPath(), creds, 0o600)
113+
await sdk.client.instance.dispose()
114+
await sync.bootstrap()
115+
local.model.set({ providerID: "altimate-backend", modelID: "altimate-default" }, { recent: true })
116+
dialog.clear()
117+
}
118+
119+
return (
120+
<box paddingLeft={2} paddingRight={2} gap={1}>
121+
<box flexDirection="row" justifyContent="space-between">
122+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
123+
Login to Altimate
124+
</text>
125+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
126+
esc
127+
</text>
128+
</box>
129+
130+
<text fg={theme.textMuted}>Find these in Settings &gt; API Keys in your Altimate dashboard</text>
131+
132+
<box>
133+
<text fg={store.active === "instance" ? theme.text : theme.textMuted}>Instance Name:</text>
134+
<text fg={theme.textMuted}> From your URL: https://&lt;instance&gt;.app.myaltimate.com</text>
135+
<textarea
136+
height={3}
137+
ref={(val: TextareaRenderable) => (instanceRef = val)}
138+
placeholder="your-instance"
139+
textColor={theme.text}
140+
focusedTextColor={theme.text}
141+
cursorColor={theme.text}
142+
onMouseUp={() => {
143+
setStore("active", "instance")
144+
focusActive()
145+
}}
146+
/>
147+
</box>
148+
149+
<box>
150+
<text fg={store.active === "key" ? theme.text : theme.textMuted}>API Key:</text>
151+
<text fg={theme.textMuted}> Settings &gt; API Keys &gt; Copy</text>
152+
<textarea
153+
height={3}
154+
ref={(val: TextareaRenderable) => (keyRef = val)}
155+
placeholder="your-api-key"
156+
textColor={theme.text}
157+
focusedTextColor={theme.text}
158+
cursorColor={theme.text}
159+
onMouseUp={() => {
160+
setStore("active", "key")
161+
focusActive()
162+
}}
163+
/>
164+
</box>
165+
166+
<box>
167+
<text fg={store.active === "url" ? theme.text : theme.textMuted}>URL:</text>
168+
<textarea
169+
height={3}
170+
ref={(val: TextareaRenderable) => (urlRef = val)}
171+
initialValue="https://api.myaltimate.com"
172+
placeholder="https://api.myaltimate.com"
173+
textColor={theme.text}
174+
focusedTextColor={theme.text}
175+
cursorColor={theme.text}
176+
onMouseUp={() => {
177+
setStore("active", "url")
178+
focusActive()
179+
}}
180+
/>
181+
</box>
182+
183+
<Show when={error()}>
184+
<text fg={theme.error}>{error()}</text>
185+
</Show>
186+
<Show when={validating()}>
187+
<text fg={theme.textMuted}>Validating credentials...</text>
188+
</Show>
189+
190+
<text fg={theme.textMuted} paddingBottom={1}>
191+
<span style={{ fg: theme.text }}>tab</span> next field{" "}
192+
<span style={{ fg: theme.text }}>enter</span> submit
193+
</text>
194+
</box>
195+
)
196+
}

packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { DialogModel } from "./dialog-model"
1313
import { useKeyboard } from "@opentui/solid"
1414
import { Clipboard } from "@tui/util/clipboard"
1515
import { useToast } from "../ui/toast"
16+
import { DialogAltimateLogin } from "./dialog-altimate-login"
1617

1718
const PROVIDER_PRIORITY: Record<string, number> = {
1819
opencode: 0,
@@ -21,6 +22,7 @@ const PROVIDER_PRIORITY: Record<string, number> = {
2122
"github-copilot": 3,
2223
anthropic: 4,
2324
google: 5,
25+
"altimate-backend": 6,
2426
}
2527

2628
export function createDialogProviderOptions() {
@@ -35,13 +37,18 @@ export function createDialogProviderOptions() {
3537
title: provider.name,
3638
value: provider.id,
3739
description: {
40+
"altimate-backend": "(API key)",
3841
opencode: "(Recommended)",
3942
anthropic: "(API key)",
4043
openai: "(ChatGPT Plus/Pro or API key)",
4144
"opencode-go": "Low cost subscription for everyone",
4245
}[provider.id],
4346
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
4447
async onSelect() {
48+
if (provider.id === "altimate-backend") {
49+
dialog.replace(() => <DialogAltimateLogin />)
50+
return
51+
}
4552
const methods = sync.data.provider_auth[provider.id] ?? [
4653
{
4754
type: "api",

packages/opencode/src/cli/cmd/tui/context/local.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
172172
}
173173
}
174174

175+
// Prefer altimate-backend when configured
176+
const altimateProvider = sync.data.provider.find((x) => x.id === "altimate-backend")
177+
if (altimateProvider?.models["altimate-default"]) {
178+
return {
179+
providerID: "altimate-backend",
180+
modelID: "altimate-default",
181+
}
182+
}
183+
175184
for (const item of modelStore.recent) {
176185
if (isModelValid(item)) {
177186
return item

packages/opencode/src/provider/provider.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { iife } from "@/util/iife"
1818
import { Global } from "../global"
1919
import path from "path"
2020
import { Filesystem } from "../util/filesystem"
21+
import { AltimateApi } from "../altimate/api/client"
2122

2223
// Direct imports for bundled providers
2324
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
@@ -178,6 +179,26 @@ export namespace Provider {
178179
options: hasKey ? {} : { apiKey: "public" },
179180
}
180181
},
182+
"altimate-backend": async () => {
183+
const isConfigured = await AltimateApi.isConfigured()
184+
if (!isConfigured) return { autoload: false }
185+
186+
try {
187+
const creds = await AltimateApi.getCredentials()
188+
return {
189+
autoload: true,
190+
options: {
191+
baseURL: `${creds.altimateUrl.replace(/\/+$/, "")}/agents/v1`,
192+
apiKey: creds.altimateApiKey,
193+
headers: {
194+
"x-tenant": creds.altimateInstanceName,
195+
},
196+
},
197+
}
198+
} catch {
199+
return { autoload: false }
200+
}
201+
},
181202
openai: async () => {
182203
return {
183204
autoload: false,
@@ -877,6 +898,43 @@ export namespace Provider {
877898
}
878899
}
879900

901+
// Register altimate-backend as an OpenAI-compatible provider
902+
if (!database["altimate-backend"]) {
903+
const backendModels: Record<string, Model> = {
904+
"altimate-default": {
905+
id: ModelID.make("altimate-default"),
906+
providerID: ProviderID.make("altimate-backend"),
907+
name: "Altimate AI",
908+
family: "openai",
909+
api: { id: "altimate-default", url: "", npm: "@ai-sdk/openai-compatible" },
910+
status: "active",
911+
headers: {},
912+
options: {},
913+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
914+
limit: { context: 200000, output: 128000 },
915+
capabilities: {
916+
temperature: true,
917+
reasoning: false,
918+
attachment: false,
919+
toolcall: true,
920+
input: { text: true, audio: false, image: true, video: false, pdf: false },
921+
output: { text: true, audio: false, image: false, video: false, pdf: false },
922+
interleaved: false,
923+
},
924+
release_date: "2025-01-01",
925+
variants: {},
926+
},
927+
}
928+
database["altimate-backend"] = {
929+
id: ProviderID.make("altimate-backend"),
930+
name: "Altimate",
931+
source: "custom",
932+
env: [],
933+
options: {},
934+
models: backendModels,
935+
}
936+
}
937+
880938
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
881939
const existing = providers[providerID]
882940
if (existing) {

packages/opencode/src/util/filesystem.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export namespace Filesystem {
5555
try {
5656
if (mode) {
5757
await writeFile(p, content, { mode })
58+
await chmod(p, mode)
5859
} else {
5960
await writeFile(p, content)
6061
}
@@ -63,6 +64,7 @@ export namespace Filesystem {
6364
await mkdir(dirname(p), { recursive: true })
6465
if (mode) {
6566
await writeFile(p, content, { mode })
67+
await chmod(p, mode)
6668
} else {
6769
await writeFile(p, content)
6870
}

0 commit comments

Comments
 (0)