From ef3f961fe9177b44453015585c34258070bd316c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 10 Aug 2026 17:33:31 -0500 Subject: [PATCH 1/3] fix(core): resolve Cloudflare account endpoints --- .../plugin/provider/cloudflare-workers-ai.ts | 58 +++++++++++++++---- .../provider-cloudflare-workers-ai.test.ts | 49 +++++++++++++++- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 0746a880c88b..3eb800c3506f 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -1,8 +1,10 @@ import os from "os" import { App } from "../../app" -import { Effect } from "effect" +import { Effect, Semaphore, Stream } from "effect" import { define } from "@opencode-ai/plugin/effect/plugin" import { Form } from "@opencode-ai/schema/form" +import { Bus } from "../../bus" +import { Integration } from "../../integration" import { Provider } from "../../provider" import { iife } from "../../util/iife" import { configuredSettings } from "./configured" @@ -12,9 +14,12 @@ const providerID = Provider.ID.make("cloudflare-workers-ai") export const CloudflareWorkersAIPlugin = define({ id: "opencode.provider.cloudflare-workers-ai", effect: Effect.fn(function* (ctx) { + const bus = yield* Bus.Service + const loading = Semaphore.makeUnsafe(1) + const loaded: { accountId?: string } = {} const configured = yield* configuredSettings(providerID) const form = iife(() => { - if (typeof configured?.baseURL === "string" || resolveAccountId(configured ?? {})) return + if (hasExplicitEndpoint(configured?.baseURL) || resolveAccountId(configured ?? {})) return return Form.Fields.make([ { type: "string", @@ -25,6 +30,14 @@ export const CloudflareWorkersAIPlugin = define({ }, ]) }) + const load = Effect.fn("CloudflareWorkersAIPlugin.load")(function* () { + const connection = yield* ctx.integration.connection.active(providerID) + const credential = connection + ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined))) + : undefined + loaded.accountId = + credential?.type === "key" ? stringOption(credential.configuration ?? {}, "accountId") : undefined + }) yield* ctx.integration.transform((draft) => { draft.method.update({ integrationID: providerID, @@ -35,15 +48,30 @@ export const CloudflareWorkersAIPlugin = define({ }, }) }) + yield* load() yield* ctx.catalog.transform((evt) => { const item = evt.provider.get(providerID) if (!item) return + const accountId = resolveAccountId(configured ?? {}, loaded.accountId) + if (!accountId) return evt.provider.update(item.provider.id, (provider) => { if (!Provider.isAISDK(provider.package)) return - if (typeof provider.settings?.baseURL === "string") return - const accountId = resolveAccountId(provider.settings ?? {}) - if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) } + const baseURL = provider.settings?.baseURL + if (hasExplicitEndpoint(baseURL)) return + provider.settings = { + ...provider.settings, + baseURL: typeof baseURL === "string" ? expandAccountId(baseURL, accountId) : workersEndpoint(accountId), + } }) + for (const model of item.models.values()) { + if (typeof model.settings?.baseURL !== "string") continue + evt.model.update(item.provider.id, model.id, (draft) => { + draft.settings = { + ...draft.settings, + baseURL: expandAccountId(draft.settings?.baseURL, accountId), + } + }) + } }) yield* ctx.aisdk.hook( "sdk", @@ -72,17 +100,27 @@ export const CloudflareWorkersAIPlugin = define({ evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id) }), ) + const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( + Stream.filter((event) => event.data.integrationID === Integration.ID.make(providerID)), + Stream.runForEach(refresh), + Effect.forkScoped({ startImmediately: true }), + ) }), }) -function resolveAccountId(options: Record) { - return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") +function resolveAccountId(options: Record, connected?: string) { + return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") ?? connected } function workersEndpoint(accountId: string) { return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1` } +function hasExplicitEndpoint(baseURL: unknown) { + return typeof baseURL === "string" && !baseURL.includes("${CLOUDFLARE_ACCOUNT_ID}") +} + function hasWorkersEndpoint(model: { readonly package?: string readonly settings?: Readonly> @@ -93,7 +131,7 @@ function hasWorkersEndpoint(model: { function sdkOptions(options: Record, app: App.Info) { return { ...options, - baseURL: expandAccountId(options.baseURL), + baseURL: expandAccountId(options.baseURL, resolveAccountId(options)), apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey, headers: { "User-Agent": `${App.useragent(app)} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`, @@ -103,9 +141,9 @@ function sdkOptions(options: Record, app: App.Info) { } } -function expandAccountId(baseURL: unknown) { +function expandAccountId(baseURL: unknown, accountId: string | undefined) { if (typeof baseURL !== "string") return baseURL - return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", process.env.CLOUDFLARE_ACCOUNT_ID ?? "${CLOUDFLARE_ACCOUNT_ID}") + return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", accountId ?? "${CLOUDFLARE_ACCOUNT_ID}") } function stringOption(options: Record, key: string) { diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 533ba82924af..1509dae4cacb 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -1,6 +1,8 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" -import { Effect } from "effect" +import { Effect, Fiber, Stream } from "effect" +import { TestClock } from "effect/testing" +import { Bus } from "@opencode-ai/core/bus" import { Catalog } from "@opencode-ai/core/catalog" import { Model } from "@opencode-ai/core/model" import { Plugin } from "@opencode-ai/core/plugin" @@ -206,6 +208,51 @@ describe("CloudflareWorkersAIPlugin", () => { ), ) + it.effect("reloads provider and model endpoints from a connected account ID", () => + withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined }, () => + Effect.gen(function* () { + const bus = yield* Bus.Service + const catalog = yield* Catalog.Service + const integrations = yield* Integration.Service + const providerID = Provider.ID.make("cloudflare-workers-ai") + yield* catalog.transform((draft) => { + draft.provider.update(providerID, (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.settings = { + baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", + } + }) + draft.model.update(providerID, Model.ID.make("@cf/model"), (model) => { + model.settings = { + baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", + } + }) + }) + yield* addPlugin() + + const updated = yield* bus + .subscribe(Catalog.Event.Updated) + .pipe(Stream.take(1), Stream.runHead, Effect.forkScoped({ startImmediately: true })) + yield* Effect.yieldNow + yield* integrations.connection.key({ + integrationID: Integration.ID.make(providerID), + key: "secret", + answer: { accountId: "connected-acct" }, + }) + yield* Effect.yieldNow + yield* TestClock.adjust(500) + yield* Fiber.join(updated) + + expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe( + "https://api.cloudflare.com/client/v4/accounts/connected-acct/ai/v1", + ) + expect(required(yield* catalog.model.get(providerID, Model.ID.make("@cf/model"))).settings?.baseURL).toBe( + "https://api.cloudflare.com/client/v4/accounts/connected-acct/ai/v1", + ) + }), + ), + ) + it.effect("uses env API key over auth or configured API key and keeps the Cloudflare User-Agent", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () => Effect.gen(function* () { From f0f72865fca328d68d859eb1d2654d9d6607632a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 10 Aug 2026 17:35:44 -0500 Subject: [PATCH 2/3] test(core): cover Cloudflare account lifecycle --- .../plugin/provider/cloudflare-workers-ai.ts | 3 +- .../provider-cloudflare-workers-ai.test.ts | 50 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 3eb800c3506f..c45c8a772bb4 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -65,10 +65,11 @@ export const CloudflareWorkersAIPlugin = define({ }) for (const model of item.models.values()) { if (typeof model.settings?.baseURL !== "string") continue + const modelAccountId = resolveAccountId(model.settings, accountId) evt.model.update(item.provider.id, model.id, (draft) => { draft.settings = { ...draft.settings, - baseURL: expandAccountId(draft.settings?.baseURL, accountId), + baseURL: expandAccountId(draft.settings?.baseURL, modelAccountId), } }) } diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 1509dae4cacb..fe555a094606 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -4,6 +4,7 @@ import { Effect, Fiber, Stream } from "effect" import { TestClock } from "effect/testing" import { Bus } from "@opencode-ai/core/bus" import { Catalog } from "@opencode-ai/core/catalog" +import { Credential } from "@opencode-ai/core/credential" import { Model } from "@opencode-ai/core/model" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" @@ -224,6 +225,7 @@ describe("CloudflareWorkersAIPlugin", () => { }) draft.model.update(providerID, Model.ID.make("@cf/model"), (model) => { model.settings = { + accountId: "model-acct", baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", } }) @@ -247,7 +249,53 @@ describe("CloudflareWorkersAIPlugin", () => { "https://api.cloudflare.com/client/v4/accounts/connected-acct/ai/v1", ) expect(required(yield* catalog.model.get(providerID, Model.ID.make("@cf/model"))).settings?.baseURL).toBe( - "https://api.cloudflare.com/client/v4/accounts/connected-acct/ai/v1", + "https://api.cloudflare.com/client/v4/accounts/model-acct/ai/v1", + ) + }), + ), + ) + + it.effect("loads a connected account at startup and restores the template after removal", () => + withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined }, () => + Effect.gen(function* () { + const bus = yield* Bus.Service + const catalog = yield* Catalog.Service + const credentials = yield* Credential.Service + const integrations = yield* Integration.Service + const providerID = Provider.ID.make("cloudflare-workers-ai") + yield* catalog.transform((draft) => + draft.provider.update(providerID, (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.settings = { + baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", + } + }), + ) + const credential = yield* credentials.create({ + integrationID: Integration.ID.make(providerID), + value: Credential.Key.make({ + type: "key", + key: "secret", + configuration: { accountId: "startup-acct" }, + }), + }) + yield* addPlugin() + + expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe( + "https://api.cloudflare.com/client/v4/accounts/startup-acct/ai/v1", + ) + + const updated = yield* bus + .subscribe(Catalog.Event.Updated) + .pipe(Stream.take(1), Stream.runHead, Effect.forkScoped({ startImmediately: true })) + yield* Effect.yieldNow + yield* integrations.connection.remove(credential.id) + yield* Effect.yieldNow + yield* TestClock.adjust(500) + yield* Fiber.join(updated) + + expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe( + "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", ) }), ), From d40518275abdee9655fe3a430cb0222e9e045fbe Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 10 Aug 2026 17:43:42 -0500 Subject: [PATCH 3/3] refactor(core): use native Cloudflare provider --- .../ai/src/providers/cloudflare-workers-ai.ts | 19 +++ .../provider/cloudflare-workers-ai.test.ts | 16 +++ .../plugin/provider/cloudflare-workers-ai.ts | 61 ++++----- .../provider-cloudflare-workers-ai.test.ts | 116 +++++------------- 4 files changed, 89 insertions(+), 123 deletions(-) create mode 100644 packages/ai/src/providers/cloudflare-workers-ai.ts create mode 100644 packages/ai/test/provider/cloudflare-workers-ai.test.ts diff --git a/packages/ai/src/providers/cloudflare-workers-ai.ts b/packages/ai/src/providers/cloudflare-workers-ai.ts new file mode 100644 index 000000000000..827010118bc3 --- /dev/null +++ b/packages/ai/src/providers/cloudflare-workers-ai.ts @@ -0,0 +1,19 @@ +import type { ProviderPackage } from "../provider-package" +import type { OpenAIProviderOptionsInput } from "./openai-options" +import { CloudflareWorkersAI } from "./cloudflare" + +export interface Settings extends ProviderPackage.Settings { + readonly accountId?: string + readonly apiKey?: string + readonly providerOptions?: OpenAIProviderOptionsInput +} + +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => + CloudflareWorkersAI.configure({ + ...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : { accountId: settings.accountId ?? "" }), + apiKey: settings.apiKey, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + providerOptions: settings.providerOptions, + }).model(modelID) diff --git a/packages/ai/test/provider/cloudflare-workers-ai.test.ts b/packages/ai/test/provider/cloudflare-workers-ai.test.ts new file mode 100644 index 000000000000..00fccc609fac --- /dev/null +++ b/packages/ai/test/provider/cloudflare-workers-ai.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test" +import { model } from "../../src/providers/cloudflare-workers-ai" + +describe("Cloudflare Workers AI provider package", () => { + test("derives the endpoint from accountId", () => { + const resolved = model("@cf/model", { accountId: "account", apiKey: "secret" }) + + expect(resolved.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/account/ai/v1") + }) + + test("preserves an explicit endpoint", () => { + const resolved = model("@cf/model", { baseURL: "https://proxy.example/v1", apiKey: "secret" }) + + expect(resolved.route.endpoint.baseURL).toBe("https://proxy.example/v1") + }) +}) diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index c45c8a772bb4..312cb962e705 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -1,22 +1,18 @@ import os from "os" import { App } from "../../app" -import { Effect, Semaphore, Stream } from "effect" +import { Effect } from "effect" import { define } from "@opencode-ai/plugin/effect/plugin" import { Form } from "@opencode-ai/schema/form" -import { Bus } from "../../bus" -import { Integration } from "../../integration" import { Provider } from "../../provider" import { iife } from "../../util/iife" import { configuredSettings } from "./configured" const providerID = Provider.ID.make("cloudflare-workers-ai") +const nativePackage = "@opencode-ai/ai/providers/cloudflare-workers-ai" export const CloudflareWorkersAIPlugin = define({ id: "opencode.provider.cloudflare-workers-ai", effect: Effect.fn(function* (ctx) { - const bus = yield* Bus.Service - const loading = Semaphore.makeUnsafe(1) - const loaded: { accountId?: string } = {} const configured = yield* configuredSettings(providerID) const form = iife(() => { if (hasExplicitEndpoint(configured?.baseURL) || resolveAccountId(configured ?? {})) return @@ -30,14 +26,6 @@ export const CloudflareWorkersAIPlugin = define({ }, ]) }) - const load = Effect.fn("CloudflareWorkersAIPlugin.load")(function* () { - const connection = yield* ctx.integration.connection.active(providerID) - const credential = connection - ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined))) - : undefined - loaded.accountId = - credential?.type === "key" ? stringOption(credential.configuration ?? {}, "accountId") : undefined - }) yield* ctx.integration.transform((draft) => { draft.method.update({ integrationID: providerID, @@ -48,29 +36,25 @@ export const CloudflareWorkersAIPlugin = define({ }, }) }) - yield* load() yield* ctx.catalog.transform((evt) => { const item = evt.provider.get(providerID) if (!item) return - const accountId = resolveAccountId(configured ?? {}, loaded.accountId) - if (!accountId) return + const compatible = + Provider.isAISDK(item.provider.package) && + Provider.packageName(item.provider.package) === "@ai-sdk/openai-compatible" evt.provider.update(item.provider.id, (provider) => { - if (!Provider.isAISDK(provider.package)) return - const baseURL = provider.settings?.baseURL - if (hasExplicitEndpoint(baseURL)) return - provider.settings = { - ...provider.settings, - baseURL: typeof baseURL === "string" ? expandAccountId(baseURL, accountId) : workersEndpoint(accountId), - } + if (!compatible) return + provider.package = nativePackage + provider.settings = nativeSettings(provider.settings) }) for (const model of item.models.values()) { - if (typeof model.settings?.baseURL !== "string") continue - const modelAccountId = resolveAccountId(model.settings, accountId) evt.model.update(item.provider.id, model.id, (draft) => { - draft.settings = { - ...draft.settings, - baseURL: expandAccountId(draft.settings?.baseURL, modelAccountId), - } + if (!draft.package && !compatible) return + if (draft.package === nativePackage) return + if (draft.package && !Provider.isAISDK(draft.package)) return + if (draft.package && Provider.packageName(draft.package) !== "@ai-sdk/openai-compatible") return + if (draft.package) draft.package = nativePackage + draft.settings = nativeSettings(draft.settings) }) } }) @@ -101,17 +85,11 @@ export const CloudflareWorkersAIPlugin = define({ evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id) }), ) - const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) - yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( - Stream.filter((event) => event.data.integrationID === Integration.ID.make(providerID)), - Stream.runForEach(refresh), - Effect.forkScoped({ startImmediately: true }), - ) }), }) -function resolveAccountId(options: Record, connected?: string) { - return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") ?? connected +function resolveAccountId(options: Record) { + return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") } function workersEndpoint(accountId: string) { @@ -122,6 +100,13 @@ function hasExplicitEndpoint(baseURL: unknown) { return typeof baseURL === "string" && !baseURL.includes("${CLOUDFLARE_ACCOUNT_ID}") } +function nativeSettings(settings: Record | undefined) { + const result = { ...settings } + if (process.env.CLOUDFLARE_ACCOUNT_ID) result.baseURL = workersEndpoint(process.env.CLOUDFLARE_ACCOUNT_ID) + else if (!hasExplicitEndpoint(result.baseURL)) delete result.baseURL + return result +} + function hasWorkersEndpoint(model: { readonly package?: string readonly settings?: Readonly> diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index fe555a094606..664d87cbc789 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -1,10 +1,9 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" -import { Effect, Fiber, Stream } from "effect" -import { TestClock } from "effect/testing" -import { Bus } from "@opencode-ai/core/bus" +import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" +import { ModelResolver } from "@opencode-ai/core/model-resolver" import { Model } from "@opencode-ai/core/model" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" @@ -19,7 +18,6 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* Plugin.Service - const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* CloudflareWorkersAIPlugin.effect(host) }) @@ -106,15 +104,13 @@ describe("CloudflareWorkersAIPlugin", () => { ), ) - it.effect("maps account ID to endpoint URL and creates an OpenAI-compatible SDK", () => + it.effect("maps the environment account ID to the native endpoint", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* Plugin.Service - const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = Provider.aisdk("test-provider") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") }), ) yield* addPlugin() @@ -122,21 +118,10 @@ describe("CloudflareWorkersAIPlugin", () => { (yield* (yield* Integration.Service).get(Integration.ID.make("cloudflare-workers-ai")))?.methods, ).toContainEqual({ type: "key", label: "API key" }) const provider = required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai"))) - const sdk = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), - modelID: Model.ID.make("@cf/model"), - package: provider.package, - settings: provider.settings, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "cloudflare-workers-ai", headers: { custom: "header" } }, - }) expect(provider).toMatchObject({ - package: "aisdk:test-provider", + package: "@opencode-ai/ai/providers/cloudflare-workers-ai", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" }, }) - expect(sdk.sdk).toBeDefined() }), ), ) @@ -196,30 +181,32 @@ describe("CloudflareWorkersAIPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = Provider.aisdk("test-provider") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, accountId: "configured-acct" } }), ) yield* addPlugin() expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({ - package: "aisdk:test-provider", - settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" }, + package: "@opencode-ai/ai/providers/cloudflare-workers-ai", + settings: { + accountId: "configured-acct", + baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1", + }, }) }), ), ) - it.effect("reloads provider and model endpoints from a connected account ID", () => + it.effect("passes the connected account ID to the native provider at runtime", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined }, () => Effect.gen(function* () { - const bus = yield* Bus.Service const catalog = yield* Catalog.Service - const integrations = yield* Integration.Service const providerID = Provider.ID.make("cloudflare-workers-ai") yield* catalog.transform((draft) => { draft.provider.update(providerID, (provider) => { provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { + accountId: "configured-acct", baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", } }) @@ -232,70 +219,29 @@ describe("CloudflareWorkersAIPlugin", () => { }) yield* addPlugin() - const updated = yield* bus - .subscribe(Catalog.Event.Updated) - .pipe(Stream.take(1), Stream.runHead, Effect.forkScoped({ startImmediately: true })) - yield* Effect.yieldNow - yield* integrations.connection.key({ - integrationID: Integration.ID.make(providerID), - key: "secret", - answer: { accountId: "connected-acct" }, - }) - yield* Effect.yieldNow - yield* TestClock.adjust(500) - yield* Fiber.join(updated) - - expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe( - "https://api.cloudflare.com/client/v4/accounts/connected-acct/ai/v1", - ) - expect(required(yield* catalog.model.get(providerID, Model.ID.make("@cf/model"))).settings?.baseURL).toBe( - "https://api.cloudflare.com/client/v4/accounts/model-acct/ai/v1", - ) - }), - ), - ) - - it.effect("loads a connected account at startup and restores the template after removal", () => - withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined }, () => - Effect.gen(function* () { - const bus = yield* Bus.Service - const catalog = yield* Catalog.Service - const credentials = yield* Credential.Service - const integrations = yield* Integration.Service - const providerID = Provider.ID.make("cloudflare-workers-ai") - yield* catalog.transform((draft) => - draft.provider.update(providerID, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") - provider.settings = { - baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", - } - }), - ) - const credential = yield* credentials.create({ - integrationID: Integration.ID.make(providerID), - value: Credential.Key.make({ + const selected = required(yield* catalog.model.get(providerID, Model.ID.make("@cf/model"))) + const { model } = yield* Effect.promise(() => import("@opencode-ai/ai/providers/cloudflare-workers-ai")) + const resolved = yield* ModelResolver.fromCatalogModel( + selected, + Credential.Key.make({ type: "key", key: "secret", - configuration: { accountId: "startup-acct" }, + configuration: { accountId: "connected-acct" }, }), - }) - yield* addPlugin() - - expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe( - "https://api.cloudflare.com/client/v4/accounts/startup-acct/ai/v1", + { loadPackage: () => Effect.succeed({ model }) }, ) - const updated = yield* bus - .subscribe(Catalog.Event.Updated) - .pipe(Stream.take(1), Stream.runHead, Effect.forkScoped({ startImmediately: true })) - yield* Effect.yieldNow - yield* integrations.connection.remove(credential.id) - yield* Effect.yieldNow - yield* TestClock.adjust(500) - yield* Fiber.join(updated) - - expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe( - "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", + expect(required(yield* catalog.provider.get(providerID))).toMatchObject({ + package: "@opencode-ai/ai/providers/cloudflare-workers-ai", + settings: { accountId: "configured-acct" }, + }) + expect(selected).toMatchObject({ + package: "@opencode-ai/ai/providers/cloudflare-workers-ai", + settings: { accountId: "model-acct" }, + }) + expect(selected.settings).not.toHaveProperty("baseURL") + expect(resolved.route.endpoint.baseURL).toBe( + "https://api.cloudflare.com/client/v4/accounts/connected-acct/ai/v1", ) }), ),