Compare commits

...

3 Commits

Author SHA1 Message Date
Aiden Cline d40518275a refactor(core): use native Cloudflare provider 2026-08-10 17:43:42 -05:00
Aiden Cline f0f72865fc test(core): cover Cloudflare account lifecycle 2026-08-10 17:35:44 -05:00
Aiden Cline ef3f961fe9 fix(core): resolve Cloudflare account endpoints 2026-08-10 17:33:31 -05:00
4 changed files with 128 additions and 28 deletions
@@ -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<Settings>["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)
@@ -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")
})
})
@@ -8,13 +8,14 @@ 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 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",
@@ -38,12 +39,24 @@ export const CloudflareWorkersAIPlugin = define({
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(providerID)
if (!item) 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
if (typeof provider.settings?.baseURL === "string") return
const accountId = resolveAccountId(provider.settings ?? {})
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
if (!compatible) return
provider.package = nativePackage
provider.settings = nativeSettings(provider.settings)
})
for (const model of item.models.values()) {
evt.model.update(item.provider.id, model.id, (draft) => {
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)
})
}
})
yield* ctx.aisdk.hook(
"sdk",
@@ -83,6 +96,17 @@ 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 nativeSettings(settings: Record<string, unknown> | 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<Record<string, unknown>>
@@ -93,7 +117,7 @@ function hasWorkersEndpoint(model: {
function sdkOptions(options: Record<string, any>, 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 +127,9 @@ function sdkOptions(options: Record<string, any>, 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<string, unknown>, key: string) {
@@ -2,6 +2,8 @@ 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 { 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"
@@ -16,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)
})
@@ -103,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()
@@ -119,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()
}),
),
)
@@ -193,19 +181,72 @@ 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("passes the connected account ID to the native provider at runtime", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined }, () =>
Effect.gen(function* () {
const catalog = yield* Catalog.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",
}
})
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",
}
})
})
yield* addPlugin()
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: "connected-acct" },
}),
{ loadPackage: () => Effect.succeed({ model }) },
)
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",
)
}),
),
)
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* () {