Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/docs/configure/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Managed LLM access with dynamic routing across Sonnet 4.6, Opus 4.6, GPT-5.4, GP
For pricing, security, and data handling details, see the [Altimate LLM Gateway guide](https://datamates-docs.myaltimate.com/user-guide/components/llm-gateway/).

!!! tip "Automatic model selection"
When Altimate credentials are configured and no model is explicitly chosen, `altimate-backend/altimate-default` is selected automatically. You can override this by setting `model` in your config or by restricting the `provider` section to specific providers only.
When Altimate credentials are configured and no model is explicitly chosen, the Altimate LLM Gateway is selected automatically. You can override this by setting `model` in your config or by restricting the `provider` section to specific providers only.

## Anthropic

Expand Down
15 changes: 11 additions & 4 deletions docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,25 @@ For all warehouse types (Snowflake, BigQuery, Databricks, PostgreSQL, Redshift,

### Connecting to Altimate

If you have an Altimate platform account, run `/connect` in the TUI, select **Altimate**, and enter your credentials in this format:
If you have an Altimate platform account, run `/connect` in the TUI, select **Altimate AI**, and enter your credentials in this format:

```text
instance-url::instance-name::api-key
instance-name::api-key
```

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 the full URL — it must include the `http://` or `https://` scheme, hostname-only values will fail validation:

```text
https://api.example.com::instance-name::api-key
```

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
33 changes: 29 additions & 4 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 @@ -81,18 +84,40 @@ export namespace AltimateApi {
}
}

/**
* Parse a user-entered Altimate credential string into its component fields.
*
* Accepts two `::`-delimited forms:
* - 2 parts: `instance-name::api-key` — URL defaults to {@link DEFAULT_ALTIMATE_URL}.
* - 3+ parts: `api-url::instance-name::api-key` — first segment is the API base URL
* and must be http(s)://. Extra `::` segments are joined back into the API key.
*
* Returns `null` if the input is malformed (fewer than 2 parts, empty fields,
* or a non-http(s) URL in the 3-part form).
*/
export function parseAltimateKey(value: string): {
altimateUrl: string
altimateInstanceName: string
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()
// altimate_change start — 2 parts means no URL was given (use default); 3+ parts means URL was given
if (parts.length < 2) return null
let url: string
let instance: string
let key: string
if (parts.length === 2) {
url = DEFAULT_ALTIMATE_URL
instance = parts[0].trim()
key = parts[1].trim()
} else {
url = parts[0].trim()
instance = parts[1].trim()
key = parts.slice(2).join("::").trim()
}
if (!url || !instance || !key) return null
if (!url.startsWith("http://") && !url.startsWith("https://")) 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
8 changes: 6 additions & 2 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,10 +1022,14 @@ export namespace Provider {
// altimate_change start — register altimate-backend as an OpenAI-compatible provider
if (!database["altimate-backend"]) {
const backendModels: Record<string, Model> = {
// ID "altimate-default" is kept for backward compatibility — existing
// users have it persisted in their model.json favorites/recents and in
// opencode.json `model:` entries. Display name ("Altimate LLM Gateway")
// is what the TUI actually shows, so branding stays correct.
"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 +1052,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
35 changes: 33 additions & 2 deletions packages/opencode/test/altimate/datamate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,41 @@ describe("parseAltimateKey", () => {
expect(r?.altimateApiKey).toBe("key::extra")
})

test("returns null for too few parts", () => {
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("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()
})

test("3+ parts always treat first segment as URL — non-http rejected", () => {
// `mycompany::key::extra` → 3 parts → url=mycompany (fails http(s) check)
expect(AltimateApi.parseAltimateKey("mycompany::key::extra")).toBeNull()
})
// altimate_change end

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