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
102 changes: 101 additions & 1 deletion packages/core/src/plugin/provider/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> =>
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<LocationService>({
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
Expand Down
68 changes: 67 additions & 1 deletion packages/core/test/plugin/provider-gitlab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"

const gitlabSDKOptions: Record<string, unknown>[] = []
const discoveredWorkflowModels: {
id: string
name: string
ref: string
context: number
output: number
}[] = []
const gitlabDiscoveryCalls: { instanceUrl: string; headers: Record<string, string>; workingDirectory: string }[] = []
const it = testEffect(PluginTestLayer)

const addPlugin = Effect.fn(function* () {
Expand Down Expand Up @@ -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<string, string> },
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(
{
Expand Down
Loading