From c32c2e8a8f892b375dd8c47cbd303dbe37913f4f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 18 Mar 2026 18:16:00 -0500 Subject: [PATCH 1/3] feat: add model reconciliation hook --- packages/opencode/src/plugin/index.ts | 6 +++--- packages/plugin/src/index.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 8790efac49b..a57084e2e79 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -110,9 +110,9 @@ export namespace Plugin { }) export async function trigger< - Name extends Exclude, "auth" | "event" | "tool">, - Input = Parameters[Name]>[0], - Output = Parameters[Name]>[1], + Name extends Exclude, "auth" | "event" | "tool" | "provider">, + Input = Parameters[Name], (...args: any) => any>>[0], + Output = Parameters[Name], (...args: any) => any>>[1], >(name: Name, input: Input, output: Output): Promise { if (!name) return output for (const hook of await state().then((x) => x.hooks)) { diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 7e5ae7a6ec5..2ab5195fba7 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -159,6 +159,16 @@ export type AuthOuathResult = { url: string; instructions: string } & ( } ) +export type ProviderHook = { + id: string + models?: { + reconcile?: (input: { + provider: Provider + models: Record + }) => Promise | undefined> + } +} + export interface Hooks { event?: (input: { event: Event }) => Promise config?: (input: Config) => Promise @@ -166,6 +176,7 @@ export interface Hooks { [key: string]: ToolDefinition } auth?: AuthHook + provider?: ProviderHook /** * Called when a new message is received */ From b3182149c8a93474a3cb68b1a3975198401605e9 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 18 Mar 2026 23:59:50 -0500 Subject: [PATCH 2/3] gitlab integration poc --- bun.lock | 14 ++--- gitlab-ai-provider | 1 + packages/opencode/package.json | 2 +- packages/opencode/src/plugin/codex.ts | 2 +- packages/opencode/src/plugin/gitlab.ts | 86 ++++++++++++++++++++++++++ packages/opencode/src/plugin/index.ts | 3 +- packages/plugin/src/index.ts | 2 + 7 files changed, 100 insertions(+), 10 deletions(-) create mode 160000 gitlab-ai-provider create mode 100644 packages/opencode/src/plugin/gitlab.ts diff --git a/bun.lock b/bun.lock index 115100e1048..707330adf61 100644 --- a/bun.lock +++ b/bun.lock @@ -325,7 +325,6 @@ "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", "@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:", @@ -358,6 +357,7 @@ "drizzle-orm": "1.0.0-beta.16-ea816b6", "effect": "catalog:", "fuzzysort": "3.1.0", + "gitlab-ai-provider": "5.2.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -1108,8 +1108,6 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], - "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="], - "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], @@ -3028,6 +3026,8 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "gitlab-ai-provider": ["gitlab-ai-provider@5.2.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7uUbITj7tqNF+AG4AgVEYpkIsXGCCt0BLHULghaXktyP7DOqqMYc3967AnbYZQW04uC8MXeAxCCaaWZ/frwk3A=="], + "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -5054,10 +5054,6 @@ "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], - "@gitlab/gitlab-ai-provider/openai": ["openai@6.27.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ=="], - - "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -5452,6 +5448,10 @@ "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "gitlab-ai-provider/openai": ["openai@6.27.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ=="], + + "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], diff --git a/gitlab-ai-provider b/gitlab-ai-provider new file mode 160000 index 00000000000..294158bb7be --- /dev/null +++ b/gitlab-ai-provider @@ -0,0 +1 @@ +Subproject commit 294158bb7bebb917c47c7ee0e3e3b259bf3d4628 diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 049573e3e52..253a71ee385 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -82,7 +82,7 @@ "@ai-sdk/xai": "2.0.51", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.6.0", + "gitlab-ai-provider": "5.2.0", "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 943295e64c7..aacfe388b5b 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -1,7 +1,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import { Log } from "../util/log" import { Installation } from "../installation" -import { Auth, OAUTH_DUMMY_KEY } from "../auth" +import { OAUTH_DUMMY_KEY } from "../auth" import os from "os" import { ProviderTransform } from "@/provider/transform" import { ModelID, ProviderID } from "@/provider/schema" diff --git a/packages/opencode/src/plugin/gitlab.ts b/packages/opencode/src/plugin/gitlab.ts new file mode 100644 index 00000000000..a97c88604a2 --- /dev/null +++ b/packages/opencode/src/plugin/gitlab.ts @@ -0,0 +1,86 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin" +import { discoverWorkflowModels } from "gitlab-ai-provider" +import { Log } from "@/util/log" +import { ProviderTransform } from "@/provider/transform" +import { ModelID, ProviderID } from "@/provider/schema" + +const log = Log.create({ service: "plugin.gitlab" }) + +function str(value: unknown, fallback: string) { + if (typeof value === "string" && value) return value + return fallback +} + +export async function GitlabPlugin(input: PluginInput): Promise { + return { + provider: { + id: "gitlab", + models: { + async reconcile(args) { + const url = str(args.options?.instanceUrl, "https://gitlab.com") + + const token = str(args.options?.apiKey, "") + if (!token) return + const headers = (): Record => { + if (args.auth?.type === "api") return { "PRIVATE-TOKEN": token } + return { Authorization: `Bearer ${token}` } + } + + try { + log.info("gitlab model discovery starting", { instanceUrl: url }) + const res = await discoverWorkflowModels( + { instanceUrl: url, getHeaders: headers }, + { workingDirectory: input.directory }, + ) + + if (!res.models.length) { + log.info("gitlab model discovery skipped: no models found", { + project: res.project ? { id: res.project.id, path: res.project.pathWithNamespace } : null, + }) + return + } + for (const model of res.models) { + if (args.models[model.id]) { + continue + } + + const m = { + id: ModelID.make(model.id), + providerID: ProviderID.make("gitlab"), + name: `Agent Platform (${model.name})`, + api: { + id: model.id, + url, + npm: "gitlab-ai-provider", + }, + status: "active" as const, + headers: {}, + options: { workflowRef: model.ref }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: model.context, output: model.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: {} as Record>, + } + m.variants = ProviderTransform.variants(m) + args.models[model.id] = m + } + + return args.models + } catch (err) { + log.warn("gitlab model discovery failed", { error: err }) + return + } + }, + }, + }, + } +} diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index a57084e2e79..96549464fe7 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -12,6 +12,7 @@ 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 { GitlabPlugin } from "./gitlab" export namespace Plugin { const log = Log.create({ service: "plugin" }) @@ -19,7 +20,7 @@ export namespace Plugin { const BUILTIN = ["opencode-anthropic-auth@0.0.13"] // Built-in plugins that are directly imported (not installed from npm) - const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin] + const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin, GitlabPlugin] const state = Instance.state(async () => { const client = createOpencodeClient({ diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 2ab5195fba7..450c402389a 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -165,6 +165,8 @@ export type ProviderHook = { reconcile?: (input: { provider: Provider models: Record + auth?: Auth + options?: Record }) => Promise | undefined> } } From 2c968ed1083f1166410034c382fa074aeb5fa203 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 19 Mar 2026 00:00:02 -0500 Subject: [PATCH 3/3] rm --- gitlab-ai-provider | 1 - 1 file changed, 1 deletion(-) delete mode 160000 gitlab-ai-provider diff --git a/gitlab-ai-provider b/gitlab-ai-provider deleted file mode 160000 index 294158bb7be..00000000000 --- a/gitlab-ai-provider +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 294158bb7bebb917c47c7ee0e3e3b259bf3d4628