diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index f62b1f894cd..33b02131e64 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -1,55 +1,62 @@ import { Effect } from "effect" +import { isArrayNonEmpty } from "effect/Array" import { define } from "@opencode/plugin/effect/plugin" import { Form } from "@opencode/schema/form" import { Provider } from "../../provider.js" -import { iife } from "../../util/iife.js" import { configuredSettings } from "./configured.js" const providerID = Provider.ID.make("cloudflare-ai-gateway") +const accountIdField = Form.StringField.make({ + type: "string", + key: "accountId", + title: "Enter your Cloudflare Account ID", + placeholder: "e.g. 1234567890abcdef1234567890abcdef", + required: true, +}) + +const gatewayIdField = Form.StringField.make({ + type: "string", + key: "gatewayId", + title: "Enter your Cloudflare AI Gateway ID", + placeholder: "e.g. my-gateway", + required: true, +}) + export const CloudflareAIGatewayPlugin = define({ id: "opencode.provider.cloudflare.ai.gateway", effect: Effect.fn(function* (ctx) { + const accountId = process.env.CLOUDFLARE_ACCOUNT_ID + const gatewayId = process.env.CLOUDFLARE_GATEWAY_ID const configured = yield* configuredSettings(providerID) - const form = iife(() => { - if (typeof configured?.baseURL === "string") return - const accountId = process.env.CLOUDFLARE_ACCOUNT_ID || stringOption(configured ?? {}, "accountId") - const gatewayId = - process.env.CLOUDFLARE_GATEWAY_ID || - stringOption(configured ?? {}, "gatewayId") || - stringOption(configured ?? {}, "gateway") - if (accountId && gatewayId) return - const accountIdForm = Form.StringField.make({ - type: "string", - key: "accountId", - title: "Enter your Cloudflare Account ID", - placeholder: "e.g. 1234567890abcdef1234567890abcdef", - required: true, - }) - const gatewayIdForm = Form.StringField.make({ - type: "string", - key: "gatewayId", - title: "Enter your Cloudflare AI Gateway ID", - placeholder: "e.g. my-gateway", - required: true, - }) - if (accountId) return Form.Fields.make([gatewayIdForm]) - if (gatewayId) return Form.Fields.make([accountIdForm]) - return Form.Fields.make([accountIdForm, gatewayIdForm]) - }) + const fields = + typeof configured?.baseURL === "string" + ? [] + : [ + ...(accountId || typeof configured?.accountId === "string" ? [] : [accountIdField]), + ...(gatewayId || typeof configured?.gatewayId === "string" ? [] : [gatewayIdField]), + ] yield* ctx.integration.transform((editor) => { editor.method.update({ integrationID: providerID, method: { type: "key", label: "Gateway API token", - form, + form: isArrayNonEmpty(fields) ? Form.Fields.make(fields) : undefined, }, }) }) + yield* ctx.provider.transform((evt) => { + const item = evt.get(providerID) + if (!item || (!accountId && !gatewayId)) return + evt.update(item.provider.id, (provider) => { + if (typeof provider.settings?.baseURL === "string") return + provider.settings = { + ...(accountId ? { accountId } : {}), + ...(gatewayId ? { gatewayId } : {}), + ...provider.settings, + } + }) + }) }), }) - -function stringOption(options: Record, key: string) { - return typeof options[key] === "string" ? options[key] : undefined -} diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 1edfb10cd76..0efa0935443 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -50,7 +50,7 @@ export const CloudflareWorkersAIPlugin = define({ }) function resolveAccountId(options: Record) { - return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") + return stringOption(options, "accountId") ?? process.env.CLOUDFLARE_ACCOUNT_ID } function workersEndpoint(accountId: string) { diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index 1a122d2f6f9..bfb32dcdaec 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -4,11 +4,19 @@ import { Integration } from "@opencode/core/integration" import { Plugin } from "@opencode/core/plugin" import { PluginHost } from "@opencode/core/plugin/host" import { CloudflareAIGatewayPlugin } from "@opencode/core/plugin/provider/cloudflare-ai-gateway" +import { Provider } from "@opencode/core/provider" import { withEnv } from "../fixture/env" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) +const providerID = Provider.ID.make("cloudflare-ai-gateway") + +const seed = Effect.fn(function* (fn: (provider: Provider.MutableInfo) => void) { + const catalog = yield* Provider.Service + yield* catalog.transform((catalog) => catalog.update(providerID, fn)) + return catalog +}) const addPlugin = Effect.fn(function* () { const plugin = yield* Plugin.Service @@ -46,4 +54,55 @@ describe("CloudflareAIGatewayPlugin", () => { }), ), ) + + it.effect("requests only the gateway ID when the environment provides the account ID", () => + withEnv({ CLOUDFLARE_ACCOUNT_ID: "account", CLOUDFLARE_GATEWAY_ID: undefined }, () => + Effect.gen(function* () { + yield* addPlugin() + const integrations = yield* Integration.Service + expect((yield* integrations.get(Integration.ID.make("cloudflare-ai-gateway")))?.methods).toContainEqual({ + type: "key", + label: "Gateway API token", + form: [expect.objectContaining({ type: "string", key: "gatewayId", required: true })], + }) + }), + ), + ) + + it.effect("gives the provider its environment account and gateway IDs", () => + withEnv({ CLOUDFLARE_ACCOUNT_ID: "account", CLOUDFLARE_GATEWAY_ID: "gateway" }, () => + Effect.gen(function* () { + const catalog = yield* seed(() => {}) + yield* addPlugin() + expect((yield* catalog.get(providerID))?.settings).toEqual({ accountId: "account", gatewayId: "gateway" }) + }), + ), + ) + + it.effect("prefers configured IDs over environment IDs", () => + withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-account", CLOUDFLARE_GATEWAY_ID: "env-gateway" }, () => + Effect.gen(function* () { + const catalog = yield* seed((provider) => { + provider.settings = { accountId: "configured-account", gatewayId: "configured-gateway" } + }) + yield* addPlugin() + expect((yield* catalog.get(providerID))?.settings).toEqual({ + accountId: "configured-account", + gatewayId: "configured-gateway", + }) + }), + ), + ) + + it.effect("leaves a configured endpoint URL untouched", () => + withEnv({ CLOUDFLARE_ACCOUNT_ID: "account", CLOUDFLARE_GATEWAY_ID: "gateway" }, () => + Effect.gen(function* () { + const catalog = yield* seed((provider) => { + provider.settings = { baseURL: "https://proxy.example/v1" } + }) + yield* addPlugin() + expect((yield* catalog.get(providerID))?.settings).toEqual({ baseURL: "https://proxy.example/v1" }) + }), + ), + ) }) 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 178d62104ac..77dd75cff36 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -119,7 +119,7 @@ describe("CloudflareWorkersAIPlugin", () => { ), ) - it.effect("uses env account ID over configured account ID", () => + it.effect("uses configured account ID over env account ID", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () => Effect.gen(function* () { const catalog = yield* seed((provider) => { @@ -129,7 +129,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() expect(required(yield* catalog.get(providerID))).toMatchObject({ package: "aisdk:test-provider", - settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" }, + settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/configured-acct/ai/v1" }, }) }), ),