Skip to content
Open
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
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