diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 8723cdaac2e3..38738c71bcb1 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -2,11 +2,111 @@ import os from "os" import { InstallationVersion } from "../../installation/version" import { Effect } from "effect" import { define } from "../internal" +import { ModelV2 } from "../../model" import { ProviderV2 } from "../../provider" +import type { Service as LocationService } from "../../location" -export const GitLabPlugin = define({ +type WorkflowModel = { + readonly id: string + readonly name: string + readonly ref: string + readonly context: number + readonly output: number +} + +type WorkflowDiscovery = { + readonly models: readonly WorkflowModel[] +} + +async function discoverWorkflowModels(input: { + readonly instanceUrl: string + readonly token: string + readonly auth: "key" | "oauth" + readonly directory: string +}) { + try { + const gitlab = await import("gitlab-ai-provider") + const result = (await gitlab.discoverWorkflowModels( + { + instanceUrl: input.instanceUrl, + getHeaders: (): Record => + input.auth === "oauth" ? { Authorization: `Bearer ${input.token}` } : { "PRIVATE-TOKEN": input.token }, + }, + { workingDirectory: input.directory }, + )) as WorkflowDiscovery + return result.models + } catch { + return [] + } +} + +export const GitLabPlugin = define({ id: "gitlab", effect: Effect.fn(function* (ctx) { + const locationModule = yield* Effect.promise(() => import("../../location")) + const location = yield* locationModule.Service + const connection = yield* ctx.integration.connection.active("gitlab") + const credential = connection + ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined))) + : undefined + const auth = credential?.type === "oauth" ? "oauth" : "key" + const token = + credential?.type === "oauth" + ? credential.access + : credential?.type === "key" + ? credential.key + : process.env.GITLAB_TOKEN + + yield* ctx.catalog.transform( + Effect.fn(function* (catalog) { + const provider = catalog.provider.get(ProviderV2.ID.gitlab) + if (!provider) return + + const configuredToken = + typeof provider.provider.request.body.apiKey === "string" ? provider.provider.request.body.apiKey : token + if (!configuredToken) return + + const instanceUrl = + typeof provider.provider.request.body.instanceUrl === "string" + ? provider.provider.request.body.instanceUrl + : (process.env.GITLAB_INSTANCE_URL ?? + (typeof provider.provider.api.url === "string" ? provider.provider.api.url : "https://gitlab.com")) + const models = yield* Effect.promise(() => + discoverWorkflowModels({ + instanceUrl, + token: configuredToken, + auth, + directory: location.directory, + }), + ) + + for (const item of models) { + const modelID = ModelV2.ID.make(item.id) + if (catalog.model.get(ProviderV2.ID.gitlab, modelID)) continue + catalog.model.update(ProviderV2.ID.gitlab, modelID, (model) => { + model.name = `Agent Platform (${item.name})` + model.family = ModelV2.Family.make("") + model.api = { + id: modelID, + type: "aisdk", + package: "gitlab-ai-provider", + url: instanceUrl, + } + model.request.body.workflowRef = item.ref + model.capabilities = { + tools: true, + input: ["text", "image", "pdf"], + output: ["text"], + } + model.cost = [{ input: 0, output: 0, cache: { read: 0, write: 0 } }] + model.limit = { context: item.context, output: item.output } + model.status = "active" + model.enabled = true + }) + } + }), + ) + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "gitlab-ai-provider") return diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index f9880844d3d9..9bbf841eb542 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -11,6 +11,14 @@ import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const gitlabSDKOptions: Record[] = [] +const discoveredWorkflowModels: { + id: string + name: string + ref: string + context: number + output: number +}[] = [] +const gitlabDiscoveryCalls: { instanceUrl: string; headers: Record; workingDirectory: string }[] = [] const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { @@ -50,11 +58,69 @@ void mock.module("gitlab-ai-provider", () => ({ workflowChat: (id: string, options: unknown) => ({ id, options, type: "workflow" }), } }, - discoverWorkflowModels: async () => ({ models: [], project: undefined }), + discoverWorkflowModels: async ( + options: { instanceUrl: string; getHeaders: () => Record }, + context: { workingDirectory: string }, + ) => { + gitlabDiscoveryCalls.push({ + instanceUrl: options.instanceUrl, + headers: options.getHeaders(), + workingDirectory: context.workingDirectory, + }) + return { models: discoveredWorkflowModels, project: undefined } + }, isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact", })) describe("GitLabPlugin", () => { + it.effect("discovers workflow models into the catalog", () => + withEnv( + { + GITLAB_INSTANCE_URL: "https://gitlab.example.com", + GITLAB_TOKEN: "env-token", + }, + () => + Effect.gen(function* () { + discoveredWorkflowModels.splice(0, discoveredWorkflowModels.length, { + id: "duo-workflow-sonnet", + name: "Sonnet workflow", + ref: "claude_sonnet_4_6", + context: 200_000, + output: 64_000, + }) + gitlabDiscoveryCalls.length = 0 + + const catalog = yield* Catalog.Service + yield* catalog.transform((draft) => { + draft.provider.update(ProviderV2.ID.gitlab, (provider) => { + provider.api = { type: "aisdk", package: "gitlab-ai-provider", url: "https://gitlab.default.example" } + }) + }) + yield* addPlugin() + + const model = yield* catalog.model.get(ProviderV2.ID.gitlab, ModelV2.ID.make("duo-workflow-sonnet")) + expect(model).toMatchObject({ + name: "Agent Platform (Sonnet workflow)", + api: { + type: "aisdk", + package: "gitlab-ai-provider", + url: "https://gitlab.example.com", + }, + request: { body: { workflowRef: "claude_sonnet_4_6" } }, + capabilities: { tools: true, input: ["text", "image", "pdf"], output: ["text"] }, + limit: { context: 200_000, output: 64_000 }, + }) + expect(gitlabDiscoveryCalls).toEqual([ + { + instanceUrl: "https://gitlab.example.com", + headers: { "PRIVATE-TOKEN": "env-token" }, + workingDirectory: expect.any(String), + }, + ]) + }), + ), + ) + it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () => withEnv( {