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
119 changes: 119 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.42",
"@clack/prompts": "1.0.0-alpha.1",
"@gitlab/gitlab-ai-provider": "3.0.9",
"@gitlab/opencode-gitlab-auth": "1.2.1",
"@gitlab/opencode-gitlab-plugin": "1.0.6",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
Expand Down
39 changes: 39 additions & 0 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { Server } from "../server/server"
import { BunProc } from "../bun"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import { Env } from "../env"
import path from "path"
import os from "os"
import { CodexAuthPlugin } from "./codex"

export namespace Plugin {
Expand Down Expand Up @@ -44,6 +48,41 @@ export namespace Plugin {
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
plugins.push(...BUILTIN)
}

const shouldLoadGitLabPlugins = await (async () => {
if (config.provider?.["gitlab"]) return true

const auth = await Auth.get("gitlab")
if (auth) return true

const homeDir = os.homedir()
const xdgDataHome = process.env.XDG_DATA_HOME
const authPath = xdgDataHome
? path.join(xdgDataHome, "opencode", "auth.json")
: process.platform !== "win32"
? path.join(homeDir, ".local", "share", "opencode", "auth.json")
: path.join(homeDir, ".opencode", "auth.json")

const file = Bun.file(authPath)
if (await file.exists()) {
const authData = await file.json().catch((err) => {
log.debug("failed to parse auth.json for gitlab plugin check", { err })
return undefined
})
if (authData?.gitlab) return true
}

if (Env.get("GITLAB_TOKEN")) return true

return false
})()

if (shouldLoadGitLabPlugins && !Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
log.info("auto-loading GitLab plugins")
plugins.push("@gitlab/opencode-gitlab-auth@latest")
plugins.push("@gitlab/opencode-gitlab-plugin@latest")
}

for (let plugin of plugins) {
// ignore old codex plugin since it is supported first party now
if (plugin.includes("opencode-openai-codex-auth")) continue
Expand Down
260 changes: 260 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import z from "zod"
import path from "path"
import os from "os"
import fuzzysort from "fuzzysort"
import { Config } from "../config/config"
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
Expand Down Expand Up @@ -35,6 +37,7 @@ import { createGateway } from "@ai-sdk/gateway"
import { createTogetherAI } from "@ai-sdk/togetherai"
import { createPerplexity } from "@ai-sdk/perplexity"
import { createVercel } from "@ai-sdk/vercel"
import { createGitLab } from "@gitlab/gitlab-ai-provider"
import { ProviderTransform } from "./transform"

export namespace Provider {
Expand All @@ -60,6 +63,7 @@ export namespace Provider {
"@ai-sdk/togetherai": createTogetherAI,
"@ai-sdk/perplexity": createPerplexity,
"@ai-sdk/vercel": createVercel,
"@gitlab/gitlab-ai-provider": createGitLab,
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
}
Expand Down Expand Up @@ -389,6 +393,95 @@ export namespace Provider {
},
}
},
async gitlab(input) {
const instanceUrl = Env.get("GITLAB_INSTANCE_URL") || "https://gitlab.com"

const loadOpenCodeAuth = async () => {
const homeDir = os.homedir()
const xdgDataHome = process.env.XDG_DATA_HOME
const authPath = xdgDataHome
? path.join(xdgDataHome, "opencode", "auth.json")
: process.platform !== "win32"
? path.join(homeDir, ".local", "share", "opencode", "auth.json")
: path.join(homeDir, ".opencode", "auth.json")

const file = Bun.file(authPath)
if (!(await file.exists())) return undefined

const authData = await file.json().catch((err) => {
log.debug("failed to parse auth.json", { err })
return undefined
})
if (!authData) return undefined

const gitlabAuth = authData.gitlab
if (!gitlabAuth) return undefined

if (gitlabAuth.type === "oauth") {
const authUrl = gitlabAuth.enterpriseUrl || "https://gitlab.com"
if (authUrl !== instanceUrl && authUrl !== instanceUrl.replace(/\/$/, "")) {
return undefined
}
return {
access: gitlabAuth.access,
refresh: gitlabAuth.refresh,
expires: gitlabAuth.expires,
}
}

if (gitlabAuth.type === "api") {
return { access: gitlabAuth.key }
}

return undefined
}

const auth = await Auth.get(input.id)
const creds = await (async () => {
if (auth?.type === "oauth") {
return { access: auth.access, refresh: auth.refresh, expires: auth.expires }
}
if (auth?.type === "api") {
return { access: auth.key }
}

const stored = await loadOpenCodeAuth()
if (stored) return stored

const envToken = Env.get("GITLAB_TOKEN")
if (envToken) return { access: envToken }

return undefined
})()

const config = await Config.get()
const providerConfig = config.provider?.["gitlab"]

return {
autoload: !!creds,
options: {
instanceUrl,
apiKey: creds?.access,
refreshToken: creds?.refresh,
featureFlags: {
duo_agent_platform_agentic_chat: true,
duo_agent_platform: true,
...(providerConfig?.options?.featureFlags || {}),
},
},
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string, options?: { anthropicModel?: string }) {
const anthropicModel = options?.anthropicModel
return sdk.agenticChat(modelID, {
anthropicModel,
featureFlags: {
duo_agent_platform_agentic_chat: true,
duo_agent_platform: true,
...(providerConfig?.options?.featureFlags || {}),
},
})
},
}
},
"cloudflare-ai-gateway": async (input) => {
const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID")
const gateway = Env.get("CLOUDFLARE_GATEWAY_ID")
Expand Down Expand Up @@ -642,6 +735,173 @@ export namespace Provider {
}
}

// Add GitLab Duo provider with model metadata
// Will use models.dev data once PR #616 is merged
if (!database["gitlab"]) {
database["gitlab"] = {
id: "gitlab",
name: "GitLab Duo",
source: "custom",
env: ["GITLAB_TOKEN"],
options: {},
models: {
"duo-chat-haiku-4-5": {
id: "duo-chat-haiku-4-5",
providerID: "gitlab",
name: "Agentic Chat (Claude Haiku 4.5)",
family: "claude",
api: {
id: "duo-chat-haiku-4-5",
url: "https://gitlab.com",
npm: "@gitlab/gitlab-ai-provider",
},
status: "active",
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: {
text: true,
audio: false,
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: 200000,
output: 8192,
},
options: {
anthropicModel: "claude-haiku-4-5-20251001",
},
headers: {},
release_date: "2026-01-08",
variants: {},
},
"duo-chat-sonnet-4-5": {
id: "duo-chat-sonnet-4-5",
providerID: "gitlab",
name: "Agentic Chat (Claude Sonnet 4.5)",
family: "claude",
api: {
id: "duo-chat-sonnet-4-5",
url: "https://gitlab.com",
npm: "@gitlab/gitlab-ai-provider",
},
status: "active",
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: {
text: true,
audio: false,
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: 200000,
output: 8192,
},
options: {
anthropicModel: "claude-sonnet-4-5-20250929",
},
headers: {},
release_date: "2026-01-08",
variants: {},
},
"duo-chat-opus-4-5": {
id: "duo-chat-opus-4-5",
providerID: "gitlab",
name: "Agentic Chat (Claude Opus 4.5)",
family: "claude",
api: {
id: "duo-chat-opus-4-5",
url: "https://gitlab.com",
npm: "@gitlab/gitlab-ai-provider",
},
status: "active",
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: {
text: true,
audio: false,
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: 200000,
output: 8192,
},
options: {
anthropicModel: "claude-opus-4-5-20251101",
},
headers: {},
release_date: "2026-01-08",
variants: {},
},
},
}
}

function mergeProvider(providerID: string, provider: Partial<Info>) {
const existing = providers[providerID]
if (existing) {
Expand Down
Loading