Skip to content

Commit 4f49225

Browse files
suryaiyer95claude
andcommitted
feat: add /login command, auto-select Altimate model, fix provider URL
- Add `/login` slash command with 3-step wizard (URL, tenant, API key) that writes credentials to `~/.altimate/altimate.json` - Auto-select `altimate-backend/altimate-default` model when credentials are configured — no provider or model selection needed - Replace 3 separate Claude model entries with single "Altimate AI" model (backend handles model routing via litellm fallback chain) - Fix hardcoded `localhost:8000` URL to use `creds.altimateUrl` from credential file Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9a02f27 commit 4f49225

4 files changed

Lines changed: 166 additions & 0 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Installation } from "@/installation"
99
import { Flag } from "@/flag/flag"
1010
import { DialogProvider, useDialog } from "@tui/ui/dialog"
1111
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
12+
import { DialogAltimateLogin } from "@tui/component/dialog-altimate-login"
1213
import { SDKProvider, useSDK } from "@tui/context/sdk"
1314
import { SyncProvider, useSync } from "@tui/context/sync"
1415
import { LocalProvider, useLocal } from "@tui/context/local"
@@ -513,6 +514,17 @@ function App() {
513514
},
514515
category: "Provider",
515516
},
517+
{
518+
title: "Login to Altimate",
519+
value: "altimate.login",
520+
category: "Provider",
521+
slash: {
522+
name: "login",
523+
},
524+
onSelect: () => {
525+
dialog.replace(() => <DialogAltimateLogin />)
526+
},
527+
},
516528
{
517529
title: "View status",
518530
keybind: "status_view",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { createSignal } from "solid-js"
2+
import { useDialog } from "@tui/ui/dialog"
3+
import { useSDK } from "../context/sdk"
4+
import { useSync } from "@tui/context/sync"
5+
import { useLocal } from "@tui/context/local"
6+
import { DialogPrompt } from "../ui/dialog-prompt"
7+
import { useTheme } from "../context/theme"
8+
import { AltimateApi } from "@/altimate/api/client"
9+
import { Filesystem } from "@/util/filesystem"
10+
11+
export function DialogAltimateLogin() {
12+
const dialog = useDialog()
13+
const sdk = useSDK()
14+
const sync = useSync()
15+
const local = useLocal()
16+
const { theme } = useTheme()
17+
const [step, setStep] = createSignal<"url" | "tenant" | "key">("url")
18+
const [url, setUrl] = createSignal("https://api.myaltimate.com")
19+
const [tenant, setTenant] = createSignal("")
20+
21+
async function saveAndConnect(apiKey: string) {
22+
const creds = {
23+
altimateUrl: url(),
24+
altimateInstanceName: tenant(),
25+
altimateApiKey: apiKey,
26+
}
27+
await Filesystem.writeJson(AltimateApi.credentialsPath(), creds, 0o600)
28+
// Refresh providers to pick up the new altimate-backend
29+
await sdk.client.instance.dispose()
30+
await sync.bootstrap()
31+
// Auto-select the altimate model
32+
local.model.set({ providerID: "altimate-backend", modelID: "altimate-default" }, { recent: true })
33+
dialog.clear()
34+
}
35+
36+
return (
37+
<>
38+
{step() === "url" && (
39+
<DialogPrompt
40+
title="Altimate Backend URL"
41+
placeholder="https://api.myaltimate.com"
42+
value="https://api.myaltimate.com"
43+
description={() => (
44+
<text fg={theme.textMuted}>Enter the URL of your Altimate backend server</text>
45+
)}
46+
onConfirm={(value) => {
47+
if (value) setUrl(value)
48+
setStep("tenant")
49+
}}
50+
/>
51+
)}
52+
{step() === "tenant" && (
53+
<DialogPrompt
54+
title="Instance Name"
55+
placeholder="your-tenant"
56+
description={() => (
57+
<text fg={theme.textMuted}>Enter your Altimate instance (tenant) name</text>
58+
)}
59+
onConfirm={(value) => {
60+
if (!value) return
61+
setTenant(value)
62+
setStep("key")
63+
}}
64+
/>
65+
)}
66+
{step() === "key" && (
67+
<DialogPrompt
68+
title="API Key"
69+
placeholder="your-api-key"
70+
description={() => (
71+
<text fg={theme.textMuted}>Enter your Altimate API key</text>
72+
)}
73+
onConfirm={async (value) => {
74+
if (!value) return
75+
await saveAndConnect(value)
76+
}}
77+
/>
78+
)}
79+
</>
80+
)
81+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
178178
}
179179
}
180180

181+
// Prefer altimate-backend when configured
182+
const altimateProvider = sync.data.provider.find((x) => x.id === "altimate-backend")
183+
if (altimateProvider) {
184+
const altimateModel = Object.values(altimateProvider.models)[0]
185+
if (altimateModel) {
186+
return {
187+
providerID: "altimate-backend",
188+
modelID: altimateModel.id,
189+
}
190+
}
191+
}
192+
181193
const provider = sync.data.provider[0]
182194
if (!provider) return undefined
183195
const defaultModel = sync.data.provider_default[provider.id]

packages/opencode/src/provider/provider.ts

Lines changed: 61 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"
@@ -151,6 +152,28 @@ export namespace Provider {
151152
options: hasKey ? {} : { apiKey: "public" },
152153
}
153154
},
155+
// altimate_change start - altimate-backend OpenAI-compatible provider
156+
"altimate-backend": async () => {
157+
const isConfigured = await AltimateApi.isConfigured()
158+
if (!isConfigured) return { autoload: false }
159+
160+
try {
161+
const creds = await AltimateApi.getCredentials()
162+
return {
163+
autoload: true,
164+
options: {
165+
baseURL: `${creds.altimateUrl}/agents/v1`,
166+
apiKey: creds.altimateApiKey,
167+
headers: {
168+
"x-tenant": creds.altimateInstanceName,
169+
},
170+
},
171+
}
172+
} catch {
173+
return { autoload: false }
174+
}
175+
},
176+
// altimate_change end
154177
openai: async () => {
155178
return {
156179
autoload: false,
@@ -795,6 +818,44 @@ export namespace Provider {
795818
}
796819
}
797820

821+
// altimate_change start - register altimate-backend as an OpenAI-compatible provider
822+
if (!database["altimate-backend"]) {
823+
const backendModels: Record<string, Model> = {
824+
"altimate-default": {
825+
id: "altimate-default",
826+
providerID: "altimate-backend",
827+
name: "Altimate AI",
828+
family: "openai",
829+
api: { id: "altimate-default", url: "", npm: "@ai-sdk/openai-compatible" },
830+
status: "active",
831+
headers: {},
832+
options: {},
833+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
834+
limit: { context: 200000, output: 128000 },
835+
capabilities: {
836+
temperature: true,
837+
reasoning: false,
838+
attachment: false,
839+
toolcall: true,
840+
input: { text: true, audio: false, image: true, video: false, pdf: false },
841+
output: { text: true, audio: false, image: false, video: false, pdf: false },
842+
interleaved: false,
843+
},
844+
release_date: "2025-01-01",
845+
variants: {},
846+
},
847+
}
848+
database["altimate-backend"] = {
849+
id: "altimate-backend",
850+
name: "Altimate Backend",
851+
source: "custom",
852+
env: [],
853+
options: {},
854+
models: backendModels,
855+
}
856+
}
857+
// altimate_change end
858+
798859
function mergeProvider(providerID: string, provider: Partial<Info>) {
799860
const existing = providers[providerID]
800861
if (existing) {

0 commit comments

Comments
 (0)