Skip to content

Commit c3fc44d

Browse files
suryaiyer95claude
andauthored
fix: polish Altimate connect dialog — default URL, rename labels, fix placeholder (#724)
* fix: polish Altimate connect dialog — default URL, rename labels, fix placeholder - parseAltimateKey now accepts `instance-name::api-key` (URL defaults to https://api.myaltimate.com); `api-url::instance-name::api-key` still works for custom/self-hosted instances. - Provider display name: "Altimate" → "Altimate AI". - Default model display name: "Altimate AI" → "Altimate LLM Gateway". - TUI input placeholder for altimate-backend changed from "API key" to "instance-name::api-key" so it matches the expected format. - Dialog copy, example, and validation error message updated to reflect the new default-URL form with custom-URL fallback. - Docs (docs/docs/getting-started.md) and tests updated accordingly. Internal IDs (`altimate-backend`, `altimate-default`) are intentionally left unchanged to preserve auth-store / config compatibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: parseAltimateKey uses part-count, not URL sniffing Drop the `://` heuristic for detecting whether the user supplied an API URL. Rule is now purely positional: - split("::").length == 2 → URL omitted → default to https://api.myaltimate.com - length >= 3 → first segment IS the URL; validated as http(s):// This matches the mental model "if split on :: gives 2, no URL". It also means a 2-part API key cannot contain `::`; that's an acceptable trade-off because Altimate keys don't use `::`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: rename model ID altimate-default → altimate-llm-gateway The display name is already "Altimate LLM Gateway" — the ID should match instead of staying as the opaque "altimate-default". Touched: - packages/opencode/src/provider/provider.ts (model registration + default-selection + log message) - packages/opencode/src/acp/agent.ts (default-selection) - packages/opencode/test/provider/provider.test.ts (assertions) - packages/opencode/test/skill/release-v0.5.20-adversarial.test.ts (parseModel test fixture) - docs/docs/configure/providers.md (tip text) Provider ID `altimate-backend` intentionally left alone — that one is an auth-storage / credential-file boundary and renaming it has a much larger blast radius. CHANGELOG.md left alone — historical record. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: say 'select Altimate AI' to match renamed provider display Addresses review feedback from Copilot and cubic-dev-ai: the getting-started guide instructed users to select **Altimate** in the /connect TUI, but the provider display name is now **Altimate AI**. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add JSDoc to parseAltimateKey describing the two accepted formats Addresses Coderabbit pre-merge docstring-coverage check by documenting the 2-part (default URL) vs 3+ part (custom URL) behavior of parseAltimateKey. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: revert model ID rename (backward-compat) + resolve review comments Addresses two review findings: 1. cubic P1 — renaming the internal model ID from `altimate-default` to `altimate-llm-gateway` broke backward compatibility. Opencode persists selected model IDs to `model.json` (favorites, recents, variants) and users can pin `model: altimate-backend/altimate-default` in their opencode.json. After the rename those references would silently go stale. Keep the ID as `altimate-default`; the polish the user actually wanted was the display name, which stays as "Altimate LLM Gateway". Added a comment explaining why the ID is preserved. Rephrased the auto-selection tip in `docs/docs/configure/providers.md` to refer to the display name rather than the internal ID. 2. Coderabbit — the custom-URL example in `docs/docs/getting-started.md` used `api-url::instance-name::api-key` which is ambiguous; the parser requires `http(s)://`. Replaced with a concrete `https://api.example.com::...` example and added a sentence stating the scheme is required. Also reverted provider.ts, acp/agent.ts, provider.test.ts, and release-v0.5.20-adversarial.test.ts to use the original `altimate-default` ID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f7b9497 commit c3fc44d

6 files changed

Lines changed: 91 additions & 17 deletions

File tree

docs/docs/configure/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Managed LLM access with dynamic routing across Sonnet 4.6, Opus 4.6, GPT-5.4, GP
3939
For pricing, security, and data handling details, see the [Altimate LLM Gateway guide](https://datamates-docs.myaltimate.com/user-guide/components/llm-gateway/).
4040

4141
!!! tip "Automatic model selection"
42-
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.
42+
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.
4343

4444
## Anthropic
4545

docs/docs/getting-started.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,25 @@ For all warehouse types (Snowflake, BigQuery, Databricks, PostgreSQL, Redshift,
8383

8484
### Connecting to Altimate
8585

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

8888
```text
89-
instance-url::instance-name::api-key
89+
instance-name::api-key
9090
```
9191

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

94-
- **Instance URL**`https://api.myaltimate.com` or `https://api.getaltimate.com` depending on your dashboard domain
9594
- **Instance Name** — the subdomain from your Altimate dashboard URL (e.g. `acme` from `https://acme.app.myaltimate.com`)
9695
- **API Key** — go to **Settings > API Keys** in your Altimate dashboard and click **Copy**
9796

97+
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:
98+
99+
```text
100+
https://api.example.com::instance-name::api-key
101+
```
102+
103+
For example: `https://api.getaltimate.com::acme::your-api-key`
104+
98105
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.
99106

100107
**`altimate.json` schema:**

packages/opencode/src/altimate/api/client.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { Global } from "../../global"
44
import { Filesystem } from "../../util/filesystem"
55

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

811
const AltimateCredentials = z.object({
912
altimateUrl: z.string(),
@@ -81,18 +84,40 @@ export namespace AltimateApi {
8184
}
8285
}
8386

87+
/**
88+
* Parse a user-entered Altimate credential string into its component fields.
89+
*
90+
* Accepts two `::`-delimited forms:
91+
* - 2 parts: `instance-name::api-key` — URL defaults to {@link DEFAULT_ALTIMATE_URL}.
92+
* - 3+ parts: `api-url::instance-name::api-key` — first segment is the API base URL
93+
* and must be http(s)://. Extra `::` segments are joined back into the API key.
94+
*
95+
* Returns `null` if the input is malformed (fewer than 2 parts, empty fields,
96+
* or a non-http(s) URL in the 3-part form).
97+
*/
8498
export function parseAltimateKey(value: string): {
8599
altimateUrl: string
86100
altimateInstanceName: string
87101
altimateApiKey: string
88102
} | null {
89103
const parts = value.trim().split("::")
90-
if (parts.length < 3) return null
91-
const url = parts[0].trim()
92-
const instance = parts[1].trim()
93-
const key = parts.slice(2).join("::").trim()
104+
// altimate_change start — 2 parts means no URL was given (use default); 3+ parts means URL was given
105+
if (parts.length < 2) return null
106+
let url: string
107+
let instance: string
108+
let key: string
109+
if (parts.length === 2) {
110+
url = DEFAULT_ALTIMATE_URL
111+
instance = parts[0].trim()
112+
key = parts[1].trim()
113+
} else {
114+
url = parts[0].trim()
115+
instance = parts[1].trim()
116+
key = parts.slice(2).join("::").trim()
117+
}
94118
if (!url || !instance || !key) return null
95119
if (!url.startsWith("http://") && !url.startsWith("https://")) return null
120+
// altimate_change end
96121
return { altimateUrl: url, altimateInstanceName: instance, altimateApiKey: key }
97122
}
98123

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,14 @@ function ApiMethod(props: ApiMethodProps) {
217217
const [validationError, setValidationError] = createSignal<string | null>(null)
218218
// altimate_change end
219219

220+
// altimate_change start — altimate-backend placeholder matches the credential format
221+
const placeholder = props.providerID === "altimate-backend" ? "instance-name::api-key" : "API key"
222+
// altimate_change end
223+
220224
return (
221225
<DialogPrompt
222226
title={props.title}
223-
placeholder="API key"
227+
placeholder={placeholder}
224228
description={
225229
{
226230
opencode: (
@@ -252,10 +256,13 @@ function ApiMethod(props: ApiMethodProps) {
252256
Enter your Altimate credentials in this format:
253257
</text>
254258
<text fg={theme.text}>
255-
instance-url::instance-name::api-key
259+
instance-name::api-key
260+
</text>
261+
<text fg={theme.textMuted}>
262+
e.g. mycompany::abc123 (uses https://api.myaltimate.com)
256263
</text>
257264
<text fg={theme.textMuted}>
258-
e.g. https://api.getaltimate.com::mycompany::abc123
265+
For a custom API URL, use: api-url::instance-name::api-key
259266
</text>
260267
<Show when={validationError()}>
261268
<text fg={theme.error}>{validationError()!}</text>
@@ -271,7 +278,7 @@ function ApiMethod(props: ApiMethodProps) {
271278
if (props.providerID === "altimate-backend") {
272279
const parsed = AltimateApi.parseAltimateKey(value)
273280
if (!parsed) {
274-
setValidationError("Invalid format — use: instance-url::instance-name::api-key")
281+
setValidationError("Invalid format — use: instance-name::api-key (or api-url::instance-name::api-key for a custom URL)")
275282
return
276283
}
277284
const validation = await AltimateApi.validateCredentials(parsed)

packages/opencode/src/provider/provider.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,10 +1115,14 @@ export namespace Provider {
11151115
// altimate_change start — register altimate-backend as an OpenAI-compatible provider
11161116
if (!database["altimate-backend"]) {
11171117
const backendModels: Record<string, Model> = {
1118+
// ID "altimate-default" is kept for backward compatibility — existing
1119+
// users have it persisted in their model.json favorites/recents and in
1120+
// opencode.json `model:` entries. Display name ("Altimate LLM Gateway")
1121+
// is what the TUI actually shows, so branding stays correct.
11181122
"altimate-default": {
11191123
id: ModelID.make("altimate-default"),
11201124
providerID: ProviderID.make("altimate-backend"),
1121-
name: "Altimate AI",
1125+
name: "Altimate LLM Gateway",
11221126
family: "openai",
11231127
api: { id: "altimate-default", url: "", npm: "@ai-sdk/openai-compatible" },
11241128
status: "active",
@@ -1141,7 +1145,7 @@ export namespace Provider {
11411145
}
11421146
database["altimate-backend"] = {
11431147
id: ProviderID.make("altimate-backend"),
1144-
name: "Altimate",
1148+
name: "Altimate AI",
11451149
source: "custom",
11461150
env: [],
11471151
options: {},

packages/opencode/test/altimate/datamate.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,10 +270,41 @@ describe("parseAltimateKey", () => {
270270
expect(r?.altimateApiKey).toBe("key::extra")
271271
})
272272

273-
test("returns null for too few parts", () => {
274-
expect(AltimateApi.parseAltimateKey("https://api.getaltimate.com::mycompany")).toBeNull()
273+
test("returns null for a single part (no separator)", () => {
274+
expect(AltimateApi.parseAltimateKey("justonevalue")).toBeNull()
275275
})
276276

277+
// altimate_change start — default URL for 2-part input
278+
test("parses 2-part input with default URL (https://api.myaltimate.com)", () => {
279+
const r = AltimateApi.parseAltimateKey("mycompany::abc123")
280+
expect(r).toEqual({
281+
altimateUrl: "https://api.myaltimate.com",
282+
altimateInstanceName: "mycompany",
283+
altimateApiKey: "abc123",
284+
})
285+
})
286+
287+
test("trims whitespace for 2-part input", () => {
288+
const r = AltimateApi.parseAltimateKey(" mycompany :: abc123 ")
289+
expect(r?.altimateUrl).toBe("https://api.myaltimate.com")
290+
expect(r?.altimateInstanceName).toBe("mycompany")
291+
expect(r?.altimateApiKey).toBe("abc123")
292+
})
293+
294+
test("returns null for empty instance name in 2-part input", () => {
295+
expect(AltimateApi.parseAltimateKey("::abc123")).toBeNull()
296+
})
297+
298+
test("returns null for empty api key in 2-part input", () => {
299+
expect(AltimateApi.parseAltimateKey("mycompany::")).toBeNull()
300+
})
301+
302+
test("3+ parts always treat first segment as URL — non-http rejected", () => {
303+
// `mycompany::key::extra` → 3 parts → url=mycompany (fails http(s) check)
304+
expect(AltimateApi.parseAltimateKey("mycompany::key::extra")).toBeNull()
305+
})
306+
// altimate_change end
307+
277308
test("returns null for empty url", () => {
278309
expect(AltimateApi.parseAltimateKey("::mycompany::key")).toBeNull()
279310
})

0 commit comments

Comments
 (0)