Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 10 additions & 3 deletions docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,22 @@ For all warehouse types (Snowflake, BigQuery, Databricks, PostgreSQL, Redshift,
If you have an Altimate platform account, run `/connect` in the TUI, select **Altimate**, and enter your credentials in this format:

```text
instance-url::instance-name::api-key
instance-name::api-key
```
Comment thread
suryaiyer95 marked this conversation as resolved.
Outdated

For example: `https://api.getaltimate.com::acme::your-api-key`
For example: `acme::your-api-key` — this uses the default API URL `https://api.myaltimate.com`.

- **Instance URL** — `https://api.myaltimate.com` or `https://api.getaltimate.com` depending on your dashboard domain
- **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**

If your instance uses a different API URL (e.g. a self-hosted or `getaltimate.com` deployment), prepend it:

```text
api-url::instance-name::api-key
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

For example: `https://api.getaltimate.com::acme::your-api-key`

Credentials are validated against the Altimate API before being saved. If you prefer to configure credentials directly (e.g. for CI or environment variable substitution), you can also create `~/.altimate/altimate.json` manually — if that file exists it takes priority over the TUI-entered credentials.

**`altimate.json` schema:**
Expand Down
29 changes: 24 additions & 5 deletions packages/opencode/src/altimate/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { Global } from "../../global"
import { Filesystem } from "../../util/filesystem"

const DEFAULT_MCP_URL = "https://mcpserver.getaltimate.com/sse"
// altimate_change start — default Altimate API URL when user omits it from the TUI credential entry
const DEFAULT_ALTIMATE_URL = "https://api.myaltimate.com"
// altimate_change end

const AltimateCredentials = z.object({
altimateUrl: z.string(),
Expand Down Expand Up @@ -87,12 +90,28 @@ export namespace AltimateApi {
altimateApiKey: string
} | null {
const parts = value.trim().split("::")
if (parts.length < 3) return null
const url = parts[0].trim()
const instance = parts[1].trim()
const key = parts.slice(2).join("::").trim()
if (parts.length < 2) return null
// altimate_change start — support 2-part `instance-name::api-key` with default URL, keep 3-part `url::instance::key` for custom instances
const first = parts[0].trim()
const looksLikeUrl = (s: string) => s.startsWith("http://") || s.startsWith("https://")
let url: string
let instance: string
let key: string
if (first.includes("://")) {
// Leading segment looks like a URL — must be http(s) and the 3-part form
if (!looksLikeUrl(first)) return null
if (parts.length < 3) return null
url = first
instance = parts[1].trim()
key = parts.slice(2).join("::").trim()
} else {
url = DEFAULT_ALTIMATE_URL
instance = first
key = parts.slice(1).join("::").trim()
}
if (!url || !instance || !key) return null
if (!url.startsWith("http://") && !url.startsWith("https://")) return null
if (!looksLikeUrl(url)) return null
// altimate_change end
return { altimateUrl: url, altimateInstanceName: instance, altimateApiKey: key }
}

Expand Down
15 changes: 11 additions & 4 deletions packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,14 @@ function ApiMethod(props: ApiMethodProps) {
const [validationError, setValidationError] = createSignal<string | null>(null)
// altimate_change end

// altimate_change start — altimate-backend placeholder matches the credential format
const placeholder = props.providerID === "altimate-backend" ? "instance-name::api-key" : "API key"
// altimate_change end

return (
<DialogPrompt
title={props.title}
placeholder="API key"
placeholder={placeholder}
description={
{
opencode: (
Expand Down Expand Up @@ -252,10 +256,13 @@ function ApiMethod(props: ApiMethodProps) {
Enter your Altimate credentials in this format:
</text>
<text fg={theme.text}>
instance-url::instance-name::api-key
instance-name::api-key
</text>
<text fg={theme.textMuted}>
e.g. mycompany::abc123 (uses https://api.myaltimate.com)
</text>
<text fg={theme.textMuted}>
e.g. https://api.getaltimate.com::mycompany::abc123
For a custom API URL, use: api-url::instance-name::api-key
</text>
<Show when={validationError()}>
<text fg={theme.error}>{validationError()!}</text>
Expand All @@ -271,7 +278,7 @@ function ApiMethod(props: ApiMethodProps) {
if (props.providerID === "altimate-backend") {
const parsed = AltimateApi.parseAltimateKey(value)
if (!parsed) {
setValidationError("Invalid format — use: instance-url::instance-name::api-key")
setValidationError("Invalid format — use: instance-name::api-key (or api-url::instance-name::api-key for a custom URL)")
return
}
const validation = await AltimateApi.validateCredentials(parsed)
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ export namespace Provider {
"altimate-default": {
id: ModelID.make("altimate-default"),
providerID: ProviderID.make("altimate-backend"),
name: "Altimate AI",
name: "Altimate LLM Gateway",
family: "openai",
api: { id: "altimate-default", url: "", npm: "@ai-sdk/openai-compatible" },
status: "active",
Expand All @@ -1048,7 +1048,7 @@ export namespace Provider {
}
database["altimate-backend"] = {
id: ProviderID.make("altimate-backend"),
name: "Altimate",
name: "Altimate AI",
Comment thread
suryaiyer95 marked this conversation as resolved.
source: "custom",
env: [],
options: {},
Expand Down
39 changes: 38 additions & 1 deletion packages/opencode/test/altimate/datamate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,47 @@ describe("parseAltimateKey", () => {
expect(r?.altimateApiKey).toBe("key::extra")
})

test("returns null for too few parts", () => {
test("returns null for too few parts when leading segment is a URL", () => {
expect(AltimateApi.parseAltimateKey("https://api.getaltimate.com::mycompany")).toBeNull()
})

test("returns null for a single part (no separator)", () => {
expect(AltimateApi.parseAltimateKey("justonevalue")).toBeNull()
})

// altimate_change start — default URL for 2-part input
test("parses 2-part input with default URL (https://api.myaltimate.com)", () => {
const r = AltimateApi.parseAltimateKey("mycompany::abc123")
expect(r).toEqual({
altimateUrl: "https://api.myaltimate.com",
altimateInstanceName: "mycompany",
altimateApiKey: "abc123",
})
})

test("preserves :: in api key for 2-part input", () => {
const r = AltimateApi.parseAltimateKey("mycompany::key::extra")
expect(r?.altimateUrl).toBe("https://api.myaltimate.com")
expect(r?.altimateInstanceName).toBe("mycompany")
expect(r?.altimateApiKey).toBe("key::extra")
})

test("trims whitespace for 2-part input", () => {
const r = AltimateApi.parseAltimateKey(" mycompany :: abc123 ")
expect(r?.altimateUrl).toBe("https://api.myaltimate.com")
expect(r?.altimateInstanceName).toBe("mycompany")
expect(r?.altimateApiKey).toBe("abc123")
})

test("returns null for empty instance name in 2-part input", () => {
expect(AltimateApi.parseAltimateKey("::abc123")).toBeNull()
})

test("returns null for empty api key in 2-part input", () => {
expect(AltimateApi.parseAltimateKey("mycompany::")).toBeNull()
})
// altimate_change end

test("returns null for empty url", () => {
expect(AltimateApi.parseAltimateKey("::mycompany::key")).toBeNull()
})
Expand Down
Loading