feat(core): port provider integrations to v2

This commit is contained in:
Aiden Cline 2026-07-12 18:40:56 +00:00
commit df04a0133b
31 changed files with 2000 additions and 152 deletions

View file

@ -218,14 +218,16 @@ export function integrationHost(integration: Integration.Interface): PluginConte
method: {
list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
update: (input) => {
if ("authorize" in input) {
const methodID = Integration.MethodID.make(input.method.id)
const refresh = input.refresh
if (input.method.type === "oauth") {
const oauth =
input as import("@opencode-ai/plugin/v2/effect/integration").IntegrationOAuthMethodRegistration
const methodID = Integration.MethodID.make(oauth.method.id)
const refresh = oauth.refresh
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, id: methodID },
authorize: (inputs) =>
input.authorize(inputs).pipe(
integrationID: Integration.ID.make(oauth.integrationID),
method: { ...oauth.method, id: methodID },
authorize: (inputs: import("@opencode-ai/plugin/v2/effect/integration").IntegrationInputs) =>
oauth.authorize(inputs).pipe(
Effect.map((authorization) => {
if (authorization.mode === "auto") {
return {
@ -267,7 +269,7 @@ export function integrationHost(integration: Integration.Interface): PluginConte
),
}
: {}),
...(input.label ? { label: input.label } : {}),
...(oauth.label ? { label: oauth.label } : {}),
})
return
}
@ -278,9 +280,19 @@ export function integrationHost(integration: Integration.Interface): PluginConte
})
return
}
const registration =
input as import("@opencode-ai/plugin/v2/effect/integration").IntegrationKeyMethodRegistration
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
integrationID: Integration.ID.make(registration.integrationID),
method: registration.method,
...(registration.authorize
? {
authorize: (key, inputs) =>
registration.authorize!(key, inputs).pipe(
Effect.map((credential) => Credential.Key.make(credential)),
),
}
: {}),
})
},
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),

View file

@ -3,6 +3,7 @@ import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
@ -60,6 +61,55 @@ function fakeSelectorSdk(calls: string[]) {
}
describe("AzurePlugin", () => {
it.effect("registers an API key method and stores prompted resource metadata", () =>
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* addPlugin()
expect((yield* integrations.get(Integration.ID.make("azure")))?.methods).toEqual([
{
type: "key",
label: "API key",
prompts: [
{
type: "text",
key: "resourceName",
message: "Enter Azure Resource Name",
placeholder: "e.g. my-models",
},
],
},
])
yield* integrations.connection.key({
integrationID: Integration.ID.make("azure"),
key: "secret",
inputs: { resourceName: "my-models" },
})
const connection = required(yield* integrations.connection.active(Integration.ID.make("azure")))
expect(yield* integrations.connection.resolve(connection)).toMatchObject({
type: "key",
key: "secret",
metadata: { resourceName: "my-models" },
})
}),
),
)
it.effect("omits the resource prompt when Azure resource env is configured", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* addPlugin()
expect((yield* integrations.get(Integration.ID.make("azure")))?.methods).toContainEqual({
type: "key",
label: "API key",
prompts: [],
})
}),
),
)
it.effect("resolves resourceName from env", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {

View file

@ -1,6 +1,7 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
@ -102,6 +103,69 @@ mock.module("ai-gateway-provider/providers/unified", () => ({
}))
describe("CloudflareAIGatewayPlugin", () => {
it.effect("registers a gateway token method and stores prompted gateway metadata", () =>
withEnv(
cloudflareEnv({
CLOUDFLARE_ACCOUNT_ID: undefined,
CLOUDFLARE_GATEWAY_ID: undefined,
CLOUDFLARE_API_TOKEN: undefined,
}),
() =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* addPlugin()
const integrationID = Integration.ID.make("cloudflare-ai-gateway")
expect((yield* integrations.get(integrationID))?.methods).toEqual([
{
type: "key",
label: "Gateway API token",
prompts: [
{
type: "text",
key: "accountId",
message: "Enter your Cloudflare Account ID",
placeholder: "e.g. 1234567890abcdef1234567890abcdef",
},
{
type: "text",
key: "gatewayId",
message: "Enter your Cloudflare AI Gateway ID",
placeholder: "e.g. my-gateway",
},
],
},
])
yield* integrations.connection.key({
integrationID,
key: "secret",
inputs: { accountId: "acct", gatewayId: "gateway" },
})
const connection = yield* integrations.connection.active(integrationID)
if (!connection) throw new Error("Expected connection")
expect(yield* integrations.connection.resolve(connection)).toMatchObject({
type: "key",
key: "secret",
metadata: { accountId: "acct", gatewayId: "gateway" },
})
}),
),
)
it.effect("omits gateway prompts backed by Cloudflare env", () =>
withEnv(cloudflareEnv(), () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* addPlugin()
expect((yield* integrations.get(Integration.ID.make("cloudflare-ai-gateway")))?.methods).toContainEqual({
type: "key",
label: "Gateway API token",
prompts: [],
})
}),
),
)
it.effect("requires account, gateway, and token before creating the unified SDK", () =>
withEnv(
{

View file

@ -2,6 +2,7 @@ import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
@ -79,6 +80,56 @@ function cloudflareHeaders(sdk: unknown, modelID = "@cf/model") {
}
describe("CloudflareWorkersAIPlugin", () => {
it.effect("registers an API key method and stores prompted account metadata", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined }, () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* addPlugin()
const integrationID = Integration.ID.make("cloudflare-workers-ai")
expect((yield* integrations.get(integrationID))?.methods).toEqual([
{
type: "key",
label: "API key",
prompts: [
{
type: "text",
key: "accountId",
message: "Enter your Cloudflare Account ID",
placeholder: "e.g. 1234567890abcdef1234567890abcdef",
},
],
},
])
yield* integrations.connection.key({
integrationID,
key: "secret",
inputs: { accountId: "acct" },
})
const connection = required(yield* integrations.connection.active(integrationID))
expect(yield* integrations.connection.resolve(connection)).toMatchObject({
type: "key",
key: "secret",
metadata: { accountId: "acct" },
})
}),
),
)
it.effect("omits the account prompt when Cloudflare account env is configured", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct" }, () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* addPlugin()
expect((yield* integrations.get(Integration.ID.make("cloudflare-workers-ai")))?.methods).toContainEqual({
type: "key",
label: "API key",
prompts: [],
})
}),
),
)
it.effect("maps account ID to endpoint URL and creates an OpenAI-compatible SDK", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {

View file

@ -0,0 +1,95 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { DigitalOceanPlugin } from "@opencode-ai/core/plugin/provider/digitalocean"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const integrationID = Integration.ID.make("digitalocean")
const providerID = ProviderV2.ID.make("digitalocean")
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
yield* DigitalOceanPlugin.effect(host)
})
describe("DigitalOceanPlugin", () => {
it.effect("registers implicit OAuth and manual model access keys", () =>
Effect.gen(function* () {
yield* addPlugin()
expect((yield* (yield* Integration.Service).get(integrationID))?.methods).toEqual([
{
id: Integration.MethodID.make("implicit"),
type: "oauth",
label: "Login with DigitalOcean",
},
{ type: "key", label: "Paste Model Access Key" },
])
}),
)
it.effect("adds cached inference routers to the DigitalOcean catalog", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const credentials = yield* Credential.Service
yield* catalog.transform((draft) => {
draft.provider.update(providerID, (provider) => {
provider.name = "DigitalOcean"
})
})
yield* credentials.create({
integrationID,
value: Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("implicit"),
refresh: "token",
access: "token",
expires: Date.now() + 60_000,
metadata: {
routers: [
{ name: "production", uuid: "router-1" },
{ name: "support", description: "Support router" },
],
routersFetchedAt: Date.now(),
},
}),
})
yield* addPlugin()
const model = yield* catalog.model.get(providerID, ModelV2.ID.make("router:production"))
expect(model).toMatchObject({
id: "router:production",
modelID: "router:production",
providerID: "digitalocean",
name: "production",
family: "digitalocean-inference-routers",
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
settings: { baseURL: "https://inference.do-ai.run/v1" },
capabilities: { tools: true, input: ["text"], output: ["text"] },
limit: { context: 128_000, output: 8_192 },
})
expect(yield* catalog.model.get(providerID, ModelV2.ID.make("router:support"))).toBeDefined()
}),
)
it.effect("stores a manually registered model access key", () =>
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
yield* integrations.connection.key({ integrationID, key: "model-access-key" })
expect((yield* (yield* Credential.Service).list(integrationID))[0]?.value).toEqual({
type: "key",
key: "model-access-key",
})
}),
)
})

View file

@ -1,4 +1,6 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
@ -41,6 +43,20 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
)
}
function eventually<A>(
effect: Effect.Effect<A>,
predicate: (value: A) => boolean,
remaining = 1000,
): Effect.Effect<A, Error> {
return Effect.gen(function* () {
const value = yield* effect
if (predicate(value)) return value
if (remaining === 0) return yield* Effect.fail(new Error("Timed out waiting for value"))
yield* Effect.promise(() => Bun.sleep(1))
return yield* eventually(effect, predicate, remaining - 1)
})
}
void mock.module("gitlab-ai-provider", () => ({
VERSION: "test-version",
createGitLab: (options: Record<string, unknown>) => {
@ -55,6 +71,155 @@ void mock.module("gitlab-ai-provider", () => ({
}))
describe("GitLabPlugin", () => {
it.effect("registers OAuth, PAT, and environment methods", () =>
Effect.gen(function* () {
yield* addPlugin()
expect((yield* (yield* Integration.Service).get(Integration.ID.make("gitlab")))?.methods).toEqual([
{
id: Integration.MethodID.make("oauth"),
type: "oauth",
label: "GitLab OAuth",
prompts: [
{
type: "text",
key: "instanceUrl",
message: "GitLab instance URL",
placeholder: "https://gitlab.com",
},
],
},
{
type: "key",
label: "GitLab Personal Access Token",
prompts: [
{
type: "text",
key: "instanceUrl",
message: "GitLab instance URL",
placeholder: "https://gitlab.com",
},
],
},
{ type: "env", names: ["GITLAB_TOKEN"] },
])
}),
)
it.effect("validates PATs and stores normalized instance URL metadata", () =>
Effect.gen(function* () {
yield* addPlugin()
const original = globalThis.fetch
const calls: [string, RequestInit | undefined][] = []
globalThis.fetch = Object.assign(
async (input: string | URL | Request, init?: RequestInit) => {
calls.push([String(input), init])
return new Response(JSON.stringify({ id: 1 }), { status: 200 })
},
{ preconnect: original.preconnect },
)
yield* Effect.addFinalizer(() => Effect.sync(() => (globalThis.fetch = original)))
const integrations = yield* Integration.Service
yield* integrations.connection.key({
integrationID: Integration.ID.make("gitlab"),
key: "glpat-test",
inputs: { instanceUrl: "https://gitlab.example/path/" },
})
expect(calls).toHaveLength(1)
expect(calls[0]?.[0]).toBe("https://gitlab.example/api/v4/user")
expect(calls[0]?.[1]?.headers).toEqual({ Authorization: "Bearer glpat-test" })
expect((yield* (yield* Credential.Service).list(Integration.ID.make("gitlab")))[0]?.value).toEqual({
type: "key",
key: "glpat-test",
metadata: { instanceUrl: "https://gitlab.example" },
})
}),
)
it.effect("rejects invalid PAT instance URLs before validation", () =>
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
const exit = yield* integrations.connection
.key({
integrationID: Integration.ID.make("gitlab"),
key: "glpat-test",
inputs: { instanceUrl: "file:///tmp/gitlab" },
})
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
expect(yield* (yield* Credential.Service).list(Integration.ID.make("gitlab"))).toHaveLength(0)
}),
)
it.effect("completes OAuth PKCE and refreshes with instance metadata", () =>
withEnv({ GITLAB_OAUTH_CLIENT_ID: "test-client" }, () =>
Effect.gen(function* () {
yield* addPlugin()
const original = globalThis.fetch
const tokenBodies: URLSearchParams[] = []
globalThis.fetch = Object.assign(
async (input: string | URL | Request, init?: RequestInit) => {
if (String(input).startsWith("http://127.0.0.1:8080/")) return original(input, init)
tokenBodies.push(new URLSearchParams(String(init?.body)))
return Response.json({
access_token: tokenBodies.length === 1 ? "access" : "refreshed-access",
refresh_token: tokenBodies.length === 1 ? "refresh" : "rotated-refresh",
expires_in: 1,
})
},
{ preconnect: original.preconnect },
)
yield* Effect.addFinalizer(() => Effect.sync(() => (globalThis.fetch = original)))
const integrations = yield* Integration.Service
const attempt = yield* integrations.connection.oauth({
integrationID: Integration.ID.make("gitlab"),
methodID: Integration.MethodID.make("oauth"),
inputs: { instanceUrl: "http://gitlab.example/path" },
})
const authorize = new URL(attempt.url)
expect(authorize.origin).toBe("http://gitlab.example")
expect(authorize.pathname).toBe("/oauth/authorize")
expect(authorize.searchParams.get("client_id")).toBe("test-client")
expect(authorize.searchParams.get("redirect_uri")).toBe("http://127.0.0.1:8080/callback")
expect(authorize.searchParams.get("code_challenge_method")).toBe("S256")
expect(authorize.searchParams.get("code_challenge")).toBeTruthy()
yield* Effect.promise(() =>
fetch(`http://127.0.0.1:8080/callback?code=test-code&state=${authorize.searchParams.get("state")}`),
)
yield* eventually(integrations.attempt.status(attempt.attemptID), (status) => status.status === "complete")
const saved = (yield* (yield* Credential.Service).list(Integration.ID.make("gitlab")))[0]
expect(saved?.value).toEqual({
type: "oauth",
methodID: Integration.MethodID.make("oauth"),
access: "access",
refresh: "refresh",
expires: expect.any(Number),
metadata: { instanceUrl: "http://gitlab.example" },
})
expect(tokenBodies[0]?.get("grant_type")).toBe("authorization_code")
expect(tokenBodies[0]?.get("code_verifier")).toBeTruthy()
if (saved?.value.type !== "oauth") throw new Error("Expected OAuth credential")
yield* (yield* Credential.Service).update(saved.id, { value: { ...saved.value, expires: 0 } })
const resolved = yield* integrations.connection.resolve({
type: "credential",
id: saved!.id,
label: saved!.label,
})
expect(resolved).toMatchObject({
type: "oauth",
access: "refreshed-access",
refresh: "rotated-refresh",
metadata: { instanceUrl: "http://gitlab.example" },
})
expect(tokenBodies[1]?.get("grant_type")).toBe("refresh_token")
expect(tokenBodies[1]?.get("refresh_token")).toBe("refresh")
}),
),
)
it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () =>
withEnv(
{

View file

@ -0,0 +1,66 @@
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { PoePlugin } from "@opencode-ai/core/plugin/provider/poe"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const integrationID = Integration.ID.make("poe")
const methodID = Integration.MethodID.make("browser")
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
yield* PoePlugin.effect(host)
})
describe("PoePlugin", () => {
it.effect("registers browser OAuth and manual API key methods", () =>
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
const integration = yield* integrations.get(integrationID)
expect(integration?.name).toBe("Poe")
expect(integration?.methods).toEqual([
{ id: methodID, type: "oauth", label: "Login with Poe (browser)" },
{ type: "key", label: "Manually enter API Key" },
])
}),
)
it.effect("stores manually entered Poe API keys", () =>
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
const credentials = yield* Credential.Service
yield* integrations.connection.key({ integrationID, key: "poe-test" })
expect((yield* credentials.list(integrationID))[0]?.value).toEqual({ type: "key", key: "poe-test" })
}),
)
it.effect("starts a PKCE browser authorization on an ephemeral loopback server", () =>
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} })
const url = new URL(attempt.url)
const redirect = new URL(url.searchParams.get("redirect_uri") ?? "")
expect(url.origin + url.pathname).toBe("https://poe.com/oauth/authorize")
expect(url.searchParams.get("response_type")).toBe("code")
expect(url.searchParams.get("client_id")).toBe("client_728290227fc048cc9262091a1ea197ea")
expect(url.searchParams.get("scope")).toBe("apikey:create")
expect(url.searchParams.get("code_challenge_method")).toBe("S256")
expect(url.searchParams.get("code_challenge")).toMatch(/^[A-Za-z0-9_-]{43}$/)
expect(redirect.hostname).toBe("127.0.0.1")
expect(redirect.pathname).toBe("/callback")
expect(Number(redirect.port)).toBeGreaterThan(0)
yield* integrations.attempt.cancel(attempt.attemptID)
}),
)
})

View file

@ -1,10 +1,11 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { Integration } from "@opencode-ai/core/integration"
import { describe, expect, it as bun_it } from "bun:test"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
import { SnowflakeCortexPlugin, cortexFetch, oauthScope } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
@ -41,6 +42,61 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
}
describe("SnowflakeCortexPlugin", () => {
it.effect("registers browser OAuth, key, and environment methods", () =>
Effect.gen(function* () {
yield* addPlugin()
expect((yield* (yield* Integration.Service).get(Integration.ID.make("snowflake-cortex")))?.methods).toEqual([
{
id: Integration.MethodID.make("snowflake-browser"),
type: "oauth",
label: "Login with Snowflake (External Browser)",
prompts: [
{
type: "text",
key: "account",
message: "Snowflake Account Identifier",
placeholder: "myorg-myaccount",
},
{
type: "text",
key: "role",
message: "Snowflake Role (optional)",
placeholder: "PUBLIC",
},
],
},
{ type: "key", label: "Paste PAT or bearer token manually" },
{ type: "env", names: ["SNOWFLAKE_CORTEX_TOKEN", "SNOWFLAKE_CORTEX_PAT"] },
])
}),
)
it.effect("uses account metadata to derive the Cortex endpoint", () =>
withEnv({ SNOWFLAKE_ACCOUNT: undefined }, () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { account: "https://myorg-myaccount.snowflakecomputing.com/", apiKey: "test-pat" },
})
expect(result.options.baseURL).toBe("https://myorg-myaccount.snowflakecomputing.com/api/v2/cortex/v1")
}),
),
)
it.effect("uses Snowflake-compatible OAuth scopes", () =>
Effect.sync(() => {
expect(oauthScope(undefined)).toBe("refresh_token")
expect(oauthScope("PUBLIC")).toBe("refresh_token session:role:PUBLIC")
expect(oauthScope("AUTH SNOWFLAKE")).toBe("refresh_token session:role-encoded:AUTH%20SNOWFLAKE")
}),
)
it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
Effect.sync(() => {
expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.snowflake-cortex")
@ -194,6 +250,7 @@ describe("cortexFetch", () => {
const body = JSON.parse(captured[0].body as string)
expect(body.max_completion_tokens).toBe(1024)
expect(body.max_tokens).toBeUndefined()
expect(new Headers(captured[0].headers).get("User-Agent")).toMatch(/^opencode\//)
})
bun_it("preserves body when max_tokens is absent", async () => {

View file

@ -1,4 +1,6 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
@ -16,7 +18,8 @@ const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* XAIPlugin.effect(host)
const integration = yield* Integration.Service
yield* XAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration))
})
function fakeSelectorSdk(calls: string[]) {
@ -33,6 +36,39 @@ function fakeSelectorSdk(calls: string[]) {
}
describe("XAIPlugin", () => {
it.effect("registers browser OAuth, device OAuth, and API key methods", () =>
Effect.gen(function* () {
yield* addPlugin()
const integration = yield* (yield* Integration.Service).get(Integration.ID.make("xai"))
expect(integration?.name).toBe("xAI")
expect(integration?.methods).toEqual([
{
id: Integration.MethodID.make("browser"),
type: "oauth",
label: "xAI Grok OAuth (SuperGrok Subscription)",
},
{
id: Integration.MethodID.make("device"),
type: "oauth",
label: "xAI Grok OAuth (Headless / Remote / VPS)",
},
{ type: "key", label: "Manually enter API Key" },
])
}),
)
it.effect("stores API keys through the registered key method", () =>
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
yield* integrations.connection.key({ integrationID: Integration.ID.make("xai"), key: "xai-test" })
expect((yield* (yield* Credential.Service).list(Integration.ID.make("xai")))[0]?.value).toEqual({
type: "key",
key: "xai-test",
})
}),
)
it.effect("creates an xAI SDK only for @ai-sdk/xai", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service