From d40518275abdee9655fe3a430cb0222e9e045fbe Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 10 Aug 2026 17:43:42 -0500 Subject: [PATCH] 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 0000000000..827010118b --- /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 0000000000..00fccc609f --- /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 c45c8a772b..312cb962e7 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 fe555a0946..664d87cbc7 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", ) }), ),