Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
26bf879
feat: enable Gitlab Agent platform
vglafirov Mar 18, 2026
7201ac6
fix: moved discoveryModel to provider
vglafirov Mar 18, 2026
d0b106c
Merge branch 'dev' into dev
vglafirov Mar 18, 2026
dc3a63a
Merge branch 'dev' into dev
vglafirov Mar 18, 2026
26b243a
Merge branch 'dev' into dev
vglafirov Mar 18, 2026
edbade5
Merge branch 'dev' into dev
vglafirov Mar 18, 2026
13907a9
Merge branch 'dev' into dev
vglafirov Mar 19, 2026
eeb9821
fix: update bun.lock
vglafirov Mar 19, 2026
493a486
Merge branch 'dev' into dev
rekram1-node Mar 19, 2026
31136e0
Merge branch 'dev' into dev
rekram1-node Mar 19, 2026
86bf278
fix: scope model discovery to gitlab provider only
vglafirov Mar 19, 2026
ca7870c
docs: update GitLab Duo provider documentation
vglafirov Mar 19, 2026
490b5c6
chore: migrate opencode-gitlab-auth to unscoped package v2.0.0
vglafirov Mar 19, 2026
da92a09
Merge branch 'dev' into dev
vglafirov Mar 19, 2026
5b69e96
cleanup bun.lock diff
rekram1-node Mar 19, 2026
6e189cc
fix: incorrect working directory bug
rekram1-node Mar 19, 2026
20f05bd
Merge branch 'dev' into dev
vglafirov Mar 19, 2026
a4620fb
Merge branch 'dev' into dev
rekram1-node Mar 20, 2026
86ef408
Merge branch 'dev' into dev
vglafirov Mar 20, 2026
246d431
fix: centralize gitlab model discovery into state() init and upgrade …
vglafirov Mar 20, 2026
41bace6
Merge branch 'dev' into dev
vglafirov Mar 20, 2026
03e3414
test: add comprehensive tests for GitLab workflow model routing, disc…
vglafirov Mar 20, 2026
138b047
refactor: remove unused exported discoverModels function
vglafirov Mar 20, 2026
76bc8ed
test: remove toolExecutor tests that only tested their own mocks
vglafirov Mar 20, 2026
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
26 changes: 15 additions & 11 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@
"@ai-sdk/xai": "2.0.51",
"@aws-sdk/credential-providers": "3.993.0",
"@clack/prompts": "1.0.0-alpha.1",
"gitlab-ai-provider": "5.2.2",
"opencode-gitlab-auth": "2.0.0",
"@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { CodexAuthPlugin } from "./codex"
import { Session } from "../session"
import { NamedError } from "@opencode-ai/util/error"
import { CopilotAuthPlugin } from "./copilot"
import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"

export namespace Plugin {
const log = Log.create({ service: "plugin" })
Expand Down
128 changes: 114 additions & 14 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ 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, VERSION as GITLAB_PROVIDER_VERSION } from "@gitlab/gitlab-ai-provider"
import {
createGitLab,
VERSION as GITLAB_PROVIDER_VERSION,
isWorkflowModel,
discoverWorkflowModels,
} from "gitlab-ai-provider"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { GoogleAuth } from "google-auth-library"
import { ProviderTransform } from "./transform"
Expand Down Expand Up @@ -124,18 +129,20 @@ export namespace Provider {
"@ai-sdk/togetherai": createTogetherAI,
"@ai-sdk/perplexity": createPerplexity,
"@ai-sdk/vercel": createVercel,
"@gitlab/gitlab-ai-provider": createGitLab,
"gitlab-ai-provider": createGitLab,
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
}

type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
type CustomDiscoverModels = () => Promise<Record<string, Model>>
type CustomLoader = (provider: Info) => Promise<{
autoload: boolean
getModel?: CustomModelLoader
vars?: CustomVarsLoader
options?: Record<string, any>
discoverModels?: CustomDiscoverModels
}>

function useLanguageModel(sdk: any) {
Expand Down Expand Up @@ -533,28 +540,105 @@ export namespace Provider {
...(providerConfig?.options?.aiGatewayHeaders || {}),
}

const featureFlags = {
duo_agent_platform_agentic_chat: true,
duo_agent_platform: true,
...(providerConfig?.options?.featureFlags || {}),
}

return {
autoload: !!apiKey,
options: {
instanceUrl,
apiKey,
aiGatewayHeaders,
featureFlags: {
duo_agent_platform_agentic_chat: true,
duo_agent_platform: true,
...(providerConfig?.options?.featureFlags || {}),
},
featureFlags,
},
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string) {
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string, options?: Record<string, any>) {
if (modelID.startsWith("duo-workflow-")) {
const workflowRef = options?.workflowRef as string | undefined
// Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
const model = sdk.workflowChat(sdkModelID, {
featureFlags,
})
if (workflowRef) {
model.selectedModelRef = workflowRef
}
return model
}
return sdk.agenticChat(modelID, {
aiGatewayHeaders,
featureFlags: {
duo_agent_platform_agentic_chat: true,
duo_agent_platform: true,
...(providerConfig?.options?.featureFlags || {}),
},
featureFlags,
})
},
async discoverModels(): Promise<Record<string, Model>> {
if (!apiKey) {
log.info("gitlab model discovery skipped: no apiKey")
return {}
}

try {
const token = apiKey
const getHeaders = (): Record<string, string> =>
auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }

log.info("gitlab model discovery starting", { instanceUrl })
const result = await discoverWorkflowModels(
{ instanceUrl, getHeaders },
{ workingDirectory: Instance.directory },
)

if (!result.models.length) {
log.info("gitlab model discovery skipped: no models found", {
project: result.project ? { id: result.project.id, path: result.project.pathWithNamespace } : null,
})
return {}
}

const models: Record<string, Model> = {}
for (const m of result.models) {
if (!input.models[m.id]) {
models[m.id] = {
id: ModelID.make(m.id),
providerID: ProviderID.make("gitlab"),
name: `Agent Platform (${m.name})`,
family: "",
api: {
id: m.id,
url: instanceUrl,
npm: "gitlab-ai-provider",
},
status: "active",
headers: {},
options: { workflowRef: m.ref },
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: m.context, output: m.output },
capabilities: {
temperature: false,
reasoning: true,
attachment: true,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: true },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
release_date: "",
variants: {},
}
}
}

log.info("gitlab model discovery complete", {
count: Object.keys(models).length,
models: Object.keys(models),
})
return models
} catch (e) {
log.warn("gitlab model discovery failed", { error: e })
return {}
}
},
}
},
"cloudflare-workers-ai": async (input) => {
Expand Down Expand Up @@ -853,6 +937,9 @@ export namespace Provider {
const varsLoaders: {
[providerID: string]: CustomVarsLoader
} = {}
const discoveryLoaders: {
[providerID: string]: CustomDiscoverModels
} = {}
const sdk = new Map<string, SDK>()

log.info("init")
Expand Down Expand Up @@ -1009,6 +1096,7 @@ export namespace Provider {
if (result && (result.autoload || providers[providerID])) {
if (result.getModel) modelLoaders[providerID] = result.getModel
if (result.vars) varsLoaders[providerID] = result.vars
if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
const opts = result.options ?? {}
const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
mergeProvider(providerID, patch)
Expand Down Expand Up @@ -1070,6 +1158,18 @@ export namespace Provider {
log.info("found", { providerID })
}

const gitlab = ProviderID.make("gitlab")
if (discoveryLoaders[gitlab] && providers[gitlab]) {
await (async () => {
const discovered = await discoveryLoaders[gitlab]()
for (const [modelID, model] of Object.entries(discovered)) {
if (!providers[gitlab].models[modelID]) {
providers[gitlab].models[modelID] = model
}
}
})().catch((e) => log.warn("state discovery error", { id: "gitlab", error: e }))
}

return {
models: languages,
providers,
Expand Down Expand Up @@ -1250,7 +1350,7 @@ export namespace Provider {

try {
const language = s.modelLoaders[model.providerID]
? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
? await s.modelLoaders[model.providerID](sdk, model.api.id, { ...provider.options, ...model.options })
: sdk.languageModel(model.api.id)
s.models.set(key, language)
return language
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/server/routes/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { ProviderID } from "../../provider/schema"
import { mapValues } from "remeda"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
import { Log } from "../../util/log"

const log = Log.create({ service: "server" })

export const ProviderRoutes = lazy(() =>
new Hono()
Expand Down
Loading
Loading