From eaa272ef7f034137746d2ed5d13383d9ef20ca8d Mon Sep 17 00:00:00 2001 From: MC Date: Mon, 6 Apr 2026 01:26:04 -0400 Subject: [PATCH] fix: show clear error when Cloudflare provider env vars are missing (#20399) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/opencode/src/auth/index.ts | 1 + .../cli/cmd/tui/component/dialog-provider.tsx | 12 +++- packages/opencode/src/plugin/cloudflare.ts | 67 +++++++++++++++++++ packages/opencode/src/plugin/index.ts | 10 ++- packages/opencode/src/provider/provider.ts | 45 +++++++++++-- packages/sdk/js/src/v2/gen/types.gen.ts | 3 + packages/sdk/openapi.json | 9 +++ 7 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 packages/opencode/src/plugin/cloudflare.ts diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index b6d340cc8d..2a9fb6c19e 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -24,6 +24,7 @@ export namespace Auth { export class Api extends Schema.Class("ApiAuth")({ type: Schema.Literal("api"), key: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), }) {} export class WellKnown extends Schema.Class("WellKnownAuth")({ diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 8add73dd6e..cb7abb8227 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -129,7 +129,15 @@ export function createDialogProviderOptions() { } } if (method.type === "api") { - return dialog.replace(() => ) + let metadata: Record | undefined + if (method.prompts?.length) { + const value = await PromptsMethod({ dialog, prompts: method.prompts }) + if (!value) return + metadata = value + } + return dialog.replace(() => ( + + )) } }, } @@ -249,6 +257,7 @@ function CodeMethod(props: CodeMethodProps) { interface ApiMethodProps { providerID: string title: string + metadata?: Record } function ApiMethod(props: ApiMethodProps) { const dialog = useDialog() @@ -293,6 +302,7 @@ function ApiMethod(props: ApiMethodProps) { auth: { type: "api", key: value, + ...(props.metadata ? { metadata: props.metadata } : {}), }, }) await sdk.client.instance.dispose() diff --git a/packages/opencode/src/plugin/cloudflare.ts b/packages/opencode/src/plugin/cloudflare.ts new file mode 100644 index 0000000000..e20a488a36 --- /dev/null +++ b/packages/opencode/src/plugin/cloudflare.ts @@ -0,0 +1,67 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin" + +export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise { + const prompts = [ + ...(!process.env.CLOUDFLARE_ACCOUNT_ID + ? [ + { + type: "text" as const, + key: "accountId", + message: "Enter your Cloudflare Account ID", + placeholder: "e.g. 1234567890abcdef1234567890abcdef", + }, + ] + : []), + ] + + return { + auth: { + provider: "cloudflare-workers-ai", + methods: [ + { + type: "api", + label: "API key", + prompts, + }, + ], + }, + } +} + +export async function CloudflareAIGatewayAuthPlugin(_input: PluginInput): Promise { + const prompts = [ + ...(!process.env.CLOUDFLARE_ACCOUNT_ID + ? [ + { + type: "text" as const, + key: "accountId", + message: "Enter your Cloudflare Account ID", + placeholder: "e.g. 1234567890abcdef1234567890abcdef", + }, + ] + : []), + ...(!process.env.CLOUDFLARE_GATEWAY_ID + ? [ + { + type: "text" as const, + key: "gatewayId", + message: "Enter your Cloudflare AI Gateway ID", + placeholder: "e.g. my-gateway", + }, + ] + : []), + ] + + return { + auth: { + provider: "cloudflare-ai-gateway", + methods: [ + { + type: "api", + label: "Gateway API token", + prompts, + }, + ], + }, + } +} diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index fb60fa096e..df69c8eba7 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -10,6 +10,7 @@ import { NamedError } from "@opencode-ai/util/error" import { CopilotAuthPlugin } from "./github-copilot/copilot" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" +import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" import { Effect, Layer, ServiceMap, Stream } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" @@ -46,7 +47,14 @@ export namespace Plugin { export class Service extends ServiceMap.Service()("@opencode/Plugin") {} // Built-in plugins that are directly imported (not installed from npm) - const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin, PoeAuthPlugin] + const INTERNAL_PLUGINS: PluginInstance[] = [ + CodexAuthPlugin, + CopilotAuthPlugin, + GitlabAuthPlugin, + PoeAuthPlugin, + CloudflareWorkersAuthPlugin, + CloudflareAIGatewayAuthPlugin, + ] function isServerPlugin(value: unknown): value is PluginInstance { return typeof value === "function" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 924d13312a..9ca49bf8f1 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -672,13 +672,26 @@ export namespace Provider { } }), "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) { - const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID") - if (!accountId) return { autoload: false } + // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway), + // skip the account ID check because the URL is already fully specified. + if (input.options?.baseURL) return { autoload: false } + + const auth = yield* dep.auth(input.id) + const accountId = + Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined) + if (!accountId) + return { + autoload: false, + async getModel() { + throw new Error( + "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=", + ) + }, + } const apiKey = yield* Effect.gen(function* () { const envToken = Env.get("CLOUDFLARE_API_KEY") if (envToken) return envToken - const auth = yield* dep.auth(input.id) if (auth?.type === "api") return auth.key return undefined }) @@ -702,16 +715,34 @@ export namespace Provider { } }), "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) { - const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID") - const gateway = Env.get("CLOUDFLARE_GATEWAY_ID") + // When baseURL is already configured (e.g. corporate config), skip the ID checks. + if (input.options?.baseURL) return { autoload: false } - if (!accountId || !gateway) return { autoload: false } + const auth = yield* dep.auth(input.id) + const accountId = + Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined) + const gateway = + Env.get("CLOUDFLARE_GATEWAY_ID") || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined) + + if (!accountId || !gateway) { + const missing = [ + !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined, + !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined, + ].filter((x): x is string => Boolean(x)) + return { + autoload: false, + async getModel() { + throw new Error( + `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=`).join(" && ")}`, + ) + }, + } + } // Get API token from env or auth - required for authenticated gateways const apiToken = yield* Effect.gen(function* () { const envToken = Env.get("CLOUDFLARE_API_TOKEN") || Env.get("CF_AIG_TOKEN") if (envToken) return envToken - const auth = yield* dep.auth(input.id) if (auth?.type === "api") return auth.key return undefined }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 72e549e485..548ab8363e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1639,6 +1639,9 @@ export type OAuth = { export type ApiAuth = { type: "api" key: string + metadata?: { + [key: string]: string + } } export type WellKnownAuth = { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1aa4010e7a..e21c48e89a 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11621,6 +11621,15 @@ }, "key": { "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } } }, "required": ["type", "key"]