Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
16 changes: 16 additions & 0 deletions docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ Add a warehouse connection to `.altimate-code/connections.json`. Here's a quick

For all warehouse types (Snowflake, BigQuery, Databricks, PostgreSQL, Redshift, DuckDB, MySQL, SQL Server) and advanced options (key-pair auth, ADC, SSH tunneling), see the [Warehouses reference](configure/warehouses.md).

### Connecting to Altimate

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

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

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

- **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**

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.

## Step 4: Choose an Agent Mode

altimate offers specialized agent modes for different workflows:
Expand Down
83 changes: 82 additions & 1 deletion packages/opencode/src/altimate/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,96 @@ export namespace AltimateApi {
return Filesystem.exists(credentialsPath())
}

function resolveEnvVars(obj: unknown): unknown {
if (typeof obj === "string") {
return obj.replace(/\$\{env:([^}]+)\}/g, (_, envVar) => {
const value = process.env[envVar]
if (!value) throw new Error(`Environment variable ${envVar} not found`)
return value
})
}
if (Array.isArray(obj)) return obj.map(resolveEnvVars)
if (obj && typeof obj === "object")
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, resolveEnvVars(v)]))
return obj
}

export async function getCredentials(): Promise<AltimateCredentials> {
const p = credentialsPath()
if (!(await Filesystem.exists(p))) {
throw new Error(`Altimate credentials not found at ${p}`)
}
const raw = JSON.parse(await Filesystem.readText(p))
const raw = resolveEnvVars(JSON.parse(await Filesystem.readText(p)))
return AltimateCredentials.parse(raw)
}

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()
if (!url || !instance || !key) return null
if (!url.startsWith("http://") && !url.startsWith("https://")) return null
return { altimateUrl: url, altimateInstanceName: instance, altimateApiKey: key }
}

export async function saveCredentials(creds: {
altimateUrl: string
altimateInstanceName: string
altimateApiKey: string
mcpServerUrl?: string
}): Promise<void> {
await Filesystem.writeJson(credentialsPath(), creds, 0o600)
}

const VALID_TENANT_REGEX = /^[a-z_][a-z0-9_-]*$/

/** Validates credentials against the Altimate API.
* Mirrors AltimateSettingsHelper.validateSettings from altimate-mcp-engine. */
export async function validateCredentials(creds: {
altimateUrl: string
altimateInstanceName: string
altimateApiKey: string
}): Promise<{ ok: true } | { ok: false; error: string }> {
if (!VALID_TENANT_REGEX.test(creds.altimateInstanceName)) {
return {
ok: false,
error:
"Invalid instance name (must be lowercase letters, numbers, underscores, hyphens, starting with letter or underscore)",
}
}
try {
const url = `${creds.altimateUrl.replace(/\/+$/, "")}/dbt/v3/validate-credentials`
const res = await fetch(url, {
method: "GET",
headers: {
"x-tenant": creds.altimateInstanceName,
Authorization: `Bearer ${creds.altimateApiKey}`,
"Content-Type": "application/json",
},
})
if (res.status === 401) {
const body = await res.text()
return { ok: false, error: `Invalid API key - ${body}` }
}
if (res.status === 403) {
const body = await res.text()
return { ok: false, error: `Invalid instance name - ${body}` }
}
if (!res.ok) {
return { ok: false, error: `Connection failed (${res.status} ${res.statusText})` }
}
return { ok: true }
} catch {
return { ok: false, error: "Could not reach Altimate API" }
}
}

async function request(creds: AltimateCredentials, method: string, endpoint: string, body?: unknown) {
const url = `${creds.altimateUrl}${endpoint}`
const res = await fetch(url, {
Expand Down
15 changes: 15 additions & 0 deletions packages/opencode/src/altimate/plugin/altimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"

export async function AltimateAuthPlugin(_input: PluginInput): Promise<Hooks> {
return {
auth: {
provider: "altimate-backend",
methods: [
{
type: "api",
label: "Connect to Altimate",
},
],
},
}
}
43 changes: 43 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { DialogModel } from "./dialog-model"
import { useKeyboard } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard"
import { useToast } from "../ui/toast"
// altimate_change start — import AltimateApi for direct credential file write
import { AltimateApi } from "../../../../altimate/api/client"
// altimate_change end

const PROVIDER_PRIORITY: Record<string, number> = {
opencode: 0,
Expand Down Expand Up @@ -210,6 +213,9 @@ function ApiMethod(props: ApiMethodProps) {
const sdk = useSDK()
const sync = useSync()
const { theme } = useTheme()
// altimate_change start — altimate-backend: validation error signal
const [validationError, setValidationError] = createSignal<string | null>(null)
// altimate_change end

return (
<DialogPrompt
Expand Down Expand Up @@ -239,10 +245,47 @@ function ApiMethod(props: ApiMethodProps) {
</text>
</box>
),
// altimate_change start — altimate-backend credential format description
"altimate-backend": (
<box gap={1}>
<text fg={theme.textMuted}>
Enter your Altimate credentials in this format:
</text>
<text fg={theme.text}>
instance-url::instance-name::api-key
</text>
<text fg={theme.textMuted}>
e.g. https://api.getaltimate.com::mycompany::abc123
</text>
<Show when={validationError()}>
<text fg={theme.error}>{validationError()!}</text>
</Show>
</box>
),
// altimate_change end
}[props.providerID] ?? undefined
}
onConfirm={async (value) => {
if (!value) return
// altimate_change start — altimate-backend: validate then write credentials file directly
if (props.providerID === "altimate-backend") {
const parsed = AltimateApi.parseAltimateKey(value)
if (!parsed) {
setValidationError("Invalid format — use: instance-url::instance-name::api-key")
return
}
const validation = await AltimateApi.validateCredentials(parsed)
if (!validation.ok) {
setValidationError(validation.error)
return
}
await AltimateApi.saveCredentials(parsed)
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.replace(() => <DialogModel providerID={props.providerID} />)
return
}
// altimate_change end
await sdk.client.auth.set({
providerID: props.providerID,
auth: {
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-au
// altimate_change start — snowflake cortex plugin import
import { SnowflakeCortexAuthPlugin } from "../altimate/plugin/snowflake"
// altimate_change end
// altimate_change start — altimate backend auth plugin
import { AltimateAuthPlugin } from "../altimate/plugin/altimate"
// altimate_change end

export namespace Plugin {
const log = Log.create({ service: "plugin" })
Expand All @@ -25,8 +28,8 @@ export namespace Plugin {
// GitlabAuthPlugin uses a different version of @opencode-ai/plugin (from npm)
// vs the workspace version, causing a type mismatch on internal HeyApiClient.
// The types are structurally compatible at runtime.
// altimate_change start — snowflake cortex internal plugin
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin as unknown as PluginInstance, SnowflakeCortexAuthPlugin]
// altimate_change start — snowflake cortex and altimate backend internal plugins
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin as unknown as PluginInstance, SnowflakeCortexAuthPlugin, AltimateAuthPlugin]
// altimate_change end

const state = Instance.state(async () => {
Expand Down
81 changes: 81 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { iife } from "@/util/iife"
import { Global } from "../global"
import path from "path"
import { Filesystem } from "../util/filesystem"
import { AltimateApi } from "../altimate/api/client"

// Direct imports for bundled providers
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
Expand Down Expand Up @@ -181,6 +182,47 @@ export namespace Provider {
options: hasKey ? {} : { apiKey: "public" },
}
},
// altimate_change start — Altimate backend provider: ~/.altimate/altimate.json first, auth store (TUI-configured) as fallback
"altimate-backend": async () => {
// Path 1: ~/.altimate/altimate.json (primary — manual file or env-var substitution, never overwritten)
const isConfigured = await AltimateApi.isConfigured()
if (isConfigured) {
try {
const creds = await AltimateApi.getCredentials()
return {
autoload: true,
options: {
baseURL: `${creds.altimateUrl.replace(/\/+$/, "")}/agents/v1`,
apiKey: creds.altimateApiKey,
headers: {
"x-tenant": creds.altimateInstanceName,
},
},
}
} catch {
return { autoload: false }
}
}
// Path 2: auth store (populated by TUI entry, file not yet written)
const auth = await Auth.get(ProviderID.make("altimate-backend"))
if (auth?.type === "api") {
const parsed = AltimateApi.parseAltimateKey(auth.key)
if (parsed) {
return {
autoload: true,
options: {
baseURL: `${parsed.altimateUrl.replace(/\/+$/, "")}/agents/v1`,
apiKey: parsed.altimateApiKey,
headers: {
"x-tenant": parsed.altimateInstanceName,
},
},
}
}
}
return { autoload: false }
},
// altimate_change end
openai: async () => {
return {
autoload: false,
Expand Down Expand Up @@ -973,6 +1015,45 @@ export namespace Provider {
}
// altimate_change end

// altimate_change start — register altimate-backend as an OpenAI-compatible provider
if (!database["altimate-backend"]) {
const backendModels: Record<string, Model> = {
"altimate-default": {
id: ModelID.make("altimate-default"),
providerID: ProviderID.make("altimate-backend"),
name: "Altimate AI",
family: "openai",
api: { id: "altimate-default", url: "", npm: "@ai-sdk/openai-compatible" },
status: "active",
headers: {},
options: {},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 200000, output: 128000 },
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
release_date: "2025-01-01",
variants: {},
},
}
database["altimate-backend"] = {
id: ProviderID.make("altimate-backend"),
name: "Altimate",
source: "custom",
env: [],
options: {},
models: backendModels,
}
}
// altimate_change end


function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
const existing = providers[providerID]
if (existing) {
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/util/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export namespace Filesystem {
try {
if (mode) {
await writeFile(p, content, { mode })
// altimate_change start — upstream_fix: writeFile { mode } option does not reliably set permissions; explicit chmod ensures correct mode is applied
await chmod(p, mode)
// altimate_change end
} else {
await writeFile(p, content)
}
Expand All @@ -63,6 +66,9 @@ export namespace Filesystem {
await mkdir(dirname(p), { recursive: true })
if (mode) {
await writeFile(p, content, { mode })
// altimate_change start — upstream_fix: writeFile { mode } option does not reliably set permissions; explicit chmod ensures correct mode is applied
await chmod(p, mode)
// altimate_change end
} else {
await writeFile(p, content)
}
Expand Down
Loading
Loading