Skip to content
Merged
125 changes: 125 additions & 0 deletions packages/opencode/src/altimate/plugin/databricks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { Auth, OAUTH_DUMMY_KEY } from "@/auth"

/**
* Databricks workspace host regex.
* Matches patterns like: myworkspace.cloud.databricks.com, adb-1234567890.12.azuredatabricks.net
*/
export const VALID_HOST_RE = /^[a-zA-Z0-9._-]+\.(cloud\.databricks\.com|azuredatabricks\.net|gcp\.databricks\.com)$/

/** Parse a `host::token` credential string for Databricks PAT auth. */
export function parseDatabricksPAT(code: string): { host: string; token: string } | null {
const sep = code.indexOf("::")
if (sep === -1) return null
const host = code.substring(0, sep).trim()
const token = code.substring(sep + 2).trim()
if (!host || !token) return null
if (!VALID_HOST_RE.test(host)) return null
return { host, token }
}

/**
* Transform a Databricks request body string.
* Databricks Foundation Model APIs use max_tokens (OpenAI-compatible),
* but some endpoints may prefer max_completion_tokens.
*/
export function transformDatabricksBody(bodyText: string): { body: string } {
const parsed = JSON.parse(bodyText)

// Databricks uses max_tokens for most endpoints, but some newer ones
// expect max_completion_tokens. Normalize to max_tokens for compatibility.
if ("max_completion_tokens" in parsed && !("max_tokens" in parsed)) {
parsed.max_tokens = parsed.max_completion_tokens
delete parsed.max_completion_tokens
}

return { body: JSON.stringify(parsed) }
}

export async function DatabricksAuthPlugin(_input: PluginInput): Promise<Hooks> {
return {
auth: {
provider: "databricks",
async loader(getAuth, provider) {
const auth = await getAuth()
if (auth.type !== "oauth") return {}

for (const model of Object.values(provider.models)) {
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } }
}

return {
apiKey: OAUTH_DUMMY_KEY,
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
const currentAuth = await getAuth()
if (currentAuth.type !== "oauth") return fetch(requestInput, init)

const headers = new Headers()
if (init?.headers) {
if (init.headers instanceof Headers) {
init.headers.forEach((value, key) => headers.set(key, value))
} else if (Array.isArray(init.headers)) {
for (const [key, value] of init.headers) {
if (value !== undefined) headers.set(key, String(value))
}
} else {
for (const [key, value] of Object.entries(init.headers)) {
if (value !== undefined) headers.set(key, String(value))
}
}
}

headers.set("authorization", `Bearer ${currentAuth.access}`)

let body = init?.body
if (body) {
try {
let text: string
if (typeof body === "string") {
text = body
} else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
text = new TextDecoder().decode(body)
} else {
text = ""
}
if (text) {
const result = transformDatabricksBody(text)
body = result.body
headers.delete("content-length")
}
} catch {
// JSON parse error — pass original body through untransformed
}
}

return fetch(requestInput, { ...init, headers, body })
},
}
},
methods: [
{
label: "Databricks PAT",
type: "oauth",
authorize: async () => ({
url: "https://accounts.cloud.databricks.com",
instructions:
"Enter your credentials as: <workspace-host>::<PAT-token>\n e.g. myworkspace.cloud.databricks.com::dapi1234567890abcdef\n Create a PAT in Databricks: Settings → Developer → Access Tokens → Generate New Token",
method: "code" as const,
callback: async (code: string) => {
const parsed = parseDatabricksPAT(code)
if (!parsed) return { type: "failed" as const }
return {
type: "success" as const,
access: parsed.token,
refresh: "",
// Databricks PATs can be configured with custom TTLs; use 90-day default
expires: Date.now() + 90 * 24 * 60 * 60 * 1000,
accountId: parsed.host,
}
},
}),
},
],
},
}
}
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 — databricks plugin import
import { DatabricksAuthPlugin } from "../altimate/plugin/databricks"
// altimate_change end
// altimate_change start — altimate backend auth plugin
import { AltimateAuthPlugin } from "../altimate/plugin/altimate"
// altimate_change end
Expand All @@ -28,8 +31,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 and altimate backend internal plugins
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin as unknown as PluginInstance, SnowflakeCortexAuthPlugin, AltimateAuthPlugin]
// altimate_change start — snowflake cortex, databricks, and altimate backend internal plugins
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin as unknown as PluginInstance, SnowflakeCortexAuthPlugin, DatabricksAuthPlugin, AltimateAuthPlugin]
// altimate_change end

const state = Instance.state(async () => {
Expand Down
93 changes: 93 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ import { ModelID, ProviderID } from "./schema"
// altimate_change start — snowflake cortex account validation
import { VALID_ACCOUNT_RE } from "../altimate/plugin/snowflake"
// altimate_change end
// altimate_change start — databricks host validation
import { VALID_HOST_RE } from "../altimate/plugin/databricks"
// altimate_change end

const DEFAULT_CHUNK_TIMEOUT = 120_000

Expand Down Expand Up @@ -733,6 +736,32 @@ export namespace Provider {
}
},
// altimate_change end
// altimate_change start — databricks provider loader
databricks: async () => {
const auth = await Auth.get("databricks")
if (auth?.type !== "oauth") {
// Fall back to env-based config
const host = Env.get("DATABRICKS_HOST")
const token = Env.get("DATABRICKS_TOKEN")
if (!host || !token) return { autoload: false }
return {
autoload: true,
options: {
baseURL: `https://${host}/serving-endpoints`,
apiKey: token,
},
Comment on lines +740 to +752
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate DATABRICKS_HOST in the env fallback.

The OAuth path applies VALID_HOST_RE, but this branch interpolates raw DATABRICKS_HOST into https://${host}/serving-endpoints. A value like https://adb... or a mistyped domain will silently produce a broken URL and skip the workspace-host guard.

Suggested fix
     databricks: async () => {
       const auth = await Auth.get("databricks")
       if (auth?.type !== "oauth") {
         // Fall back to env-based config
         const host = Env.get("DATABRICKS_HOST")
         const token = Env.get("DATABRICKS_TOKEN")
-        if (!host || !token) return { autoload: false }
+        if (!host || !token || !VALID_HOST_RE.test(host)) return { autoload: false }
         return {
           autoload: true,
           options: {
             baseURL: `https://${host}/serving-endpoints`,
             apiKey: token,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
databricks: async () => {
const auth = await Auth.get("databricks")
if (auth?.type !== "oauth") {
// Fall back to env-based config
const host = Env.get("DATABRICKS_HOST")
const token = Env.get("DATABRICKS_TOKEN")
if (!host || !token) return { autoload: false }
return {
autoload: true,
options: {
baseURL: `https://${host}/serving-endpoints`,
apiKey: token,
},
databricks: async () => {
const auth = await Auth.get("databricks")
if (auth?.type !== "oauth") {
// Fall back to env-based config
const host = Env.get("DATABRICKS_HOST")
const token = Env.get("DATABRICKS_TOKEN")
if (!host || !token || !VALID_HOST_RE.test(host)) return { autoload: false }
return {
autoload: true,
options: {
baseURL: `https://${host}/serving-endpoints`,
apiKey: token,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/provider/provider.ts` around lines 740 - 752, The
env-fallback in the databricks provider uses raw DATABRICKS_HOST without
validation; update the databricks branch (the async databricks provider logic
that calls Auth.get("databricks") and Env.get("DATABRICKS_HOST")) to validate
and normalize the host using the same VALID_HOST_RE used on the OAuth path:
reject or strip protocol if present, ensure it matches VALID_HOST_RE, and only
construct baseURL=`https://${host}/serving-endpoints` when validation passes; if
validation fails, return { autoload: false } (or equivalent) to preserve the
workspace-host guard.

}
}
const host = auth.accountId ?? Env.get("DATABRICKS_HOST")
if (!host || !VALID_HOST_RE.test(host)) return { autoload: false }
return {
autoload: true,
options: {
baseURL: `https://${host}/serving-endpoints`,
},
}
},
// altimate_change end
}

export const Model = z
Expand Down Expand Up @@ -1019,6 +1048,70 @@ export namespace Provider {
}
// altimate_change end

// altimate_change start — databricks provider models
function makeDatabricksModel(
id: string,
name: string,
limits: { context: number; output: number },
caps?: { reasoning?: boolean; attachment?: boolean; toolcall?: boolean; image?: boolean },
): Model {
const m: Model = {
id: ModelID.make(id),
providerID: ProviderID.databricks,
api: {
id,
url: "",
npm: "@ai-sdk/openai-compatible",
},
name,
capabilities: {
temperature: true,
reasoning: caps?.reasoning ?? false,
attachment: caps?.attachment ?? false,
toolcall: caps?.toolcall ?? true,
input: { text: true, audio: false, image: caps?.image ?? false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: limits.context, output: limits.output },
status: "active" as const,
options: {},
headers: {},
release_date: "2024-01-01",
variants: {},
}
m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
return m
}

database["databricks"] = {
id: ProviderID.databricks,
source: "custom",
name: "Databricks",
env: ["DATABRICKS_TOKEN"],
options: {},
Comment on lines +1088 to +1093
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't opt Databricks into the generic provider.env bootstrap.

Line 1271 treats provider.env as “any populated variable is enough”, so DATABRICKS_TOKEN alone materializes this provider. Line 1361 then keeps it alive even when CUSTOM_LOADERS.databricks returns autoload: false, which leaves Databricks registered without any baseURL. Let the custom loader own env activation for this provider.

Suggested fix
     database["databricks"] = {
       id: ProviderID.databricks,
       source: "custom",
       name: "Databricks",
-      env: ["DATABRICKS_TOKEN"],
+      env: [],
       options: {},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
database["databricks"] = {
id: ProviderID.databricks,
source: "custom",
name: "Databricks",
env: ["DATABRICKS_TOKEN"],
options: {},
database["databricks"] = {
id: ProviderID.databricks,
source: "custom",
name: "Databricks",
env: [],
options: {},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/provider/provider.ts` around lines 1088 - 1093, The
Databricks provider is being auto-activated via the generic provider.env
bootstrap because database["databricks"] includes env: ["DATABRICKS_TOKEN"];
remove the env entry from the Databricks provider definition
(database["databricks"]) so it no longer participates in the generic
provider.env activation path, and let CUSTOM_LOADERS.databricks (the custom
loader and its autoload flag) be solely responsible for enabling/registering the
provider; ensure no other bootstrap logic treats Databricks as present based
solely on environment variables.

models: {
// Meta Llama models — tool calling supported
"databricks-meta-llama-3-1-405b-instruct": makeDatabricksModel("databricks-meta-llama-3-1-405b-instruct", "Meta Llama 3.1 405B Instruct", { context: 128000, output: 4096 }),
"databricks-meta-llama-3-1-70b-instruct": makeDatabricksModel("databricks-meta-llama-3-1-70b-instruct", "Meta Llama 3.1 70B Instruct", { context: 128000, output: 4096 }),
"databricks-meta-llama-3-1-8b-instruct": makeDatabricksModel("databricks-meta-llama-3-1-8b-instruct", "Meta Llama 3.1 8B Instruct", { context: 128000, output: 4096 }),
// Claude models via Databricks AI Gateway
"databricks-claude-sonnet-4-6": makeDatabricksModel("databricks-claude-sonnet-4-6", "Claude Sonnet 4.6", { context: 200000, output: 64000 }),
"databricks-claude-opus-4-6": makeDatabricksModel("databricks-claude-opus-4-6", "Claude Opus 4.6", { context: 200000, output: 32000 }),
// GPT models via Databricks AI Gateway
"databricks-gpt-5-4": makeDatabricksModel("databricks-gpt-5-4", "GPT-5-4", { context: 128000, output: 16384 }),
"databricks-gpt-5-mini": makeDatabricksModel("databricks-gpt-5-mini", "GPT-5 Mini", { context: 128000, output: 16384 }),
// Gemini models via Databricks AI Gateway
"databricks-gemini-3-1-pro": makeDatabricksModel("databricks-gemini-3-1-pro", "Gemini 3.1 Pro", { context: 1000000, output: 8192 }),
// DBRX — Databricks native model
"databricks-dbrx-instruct": makeDatabricksModel("databricks-dbrx-instruct", "DBRX Instruct", { context: 32768, output: 4096 }),
// Mixtral via Databricks
"databricks-mixtral-8x7b-instruct": makeDatabricksModel("databricks-mixtral-8x7b-instruct", "Mixtral 8x7B Instruct", { context: 32768, output: 4096 }, { toolcall: false }),
},
}
// altimate_change end

// altimate_change start — register altimate-backend as an OpenAI-compatible provider
if (!database["altimate-backend"]) {
const backendModels: Record<string, Model> = {
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/provider/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const ProviderID = providerIdSchema.pipe(
// altimate_change start — snowflake cortex provider ID
snowflakeCortex: schema.makeUnsafe("snowflake-cortex"),
// altimate_change end
// altimate_change start — databricks provider ID
databricks: schema.makeUnsafe("databricks"),
// altimate_change end
})),
)

Expand Down
Loading
Loading