Skip to content

Commit eaa272e

Browse files
fix: show clear error when Cloudflare provider env vars are missing (anomalyco#20399)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
1 parent 70b636a commit eaa272e

7 files changed

Lines changed: 138 additions & 9 deletions

File tree

packages/opencode/src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export namespace Auth {
2424
export class Api extends Schema.Class<Api>("ApiAuth")({
2525
type: Schema.Literal("api"),
2626
key: Schema.String,
27+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
2728
}) {}
2829

2930
export class WellKnown extends Schema.Class<WellKnown>("WellKnownAuth")({

packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,15 @@ export function createDialogProviderOptions() {
129129
}
130130
}
131131
if (method.type === "api") {
132-
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
132+
let metadata: Record<string, string> | undefined
133+
if (method.prompts?.length) {
134+
const value = await PromptsMethod({ dialog, prompts: method.prompts })
135+
if (!value) return
136+
metadata = value
137+
}
138+
return dialog.replace(() => (
139+
<ApiMethod providerID={provider.id} title={method.label} metadata={metadata} />
140+
))
133141
}
134142
},
135143
}
@@ -249,6 +257,7 @@ function CodeMethod(props: CodeMethodProps) {
249257
interface ApiMethodProps {
250258
providerID: string
251259
title: string
260+
metadata?: Record<string, string>
252261
}
253262
function ApiMethod(props: ApiMethodProps) {
254263
const dialog = useDialog()
@@ -293,6 +302,7 @@ function ApiMethod(props: ApiMethodProps) {
293302
auth: {
294303
type: "api",
295304
key: value,
305+
...(props.metadata ? { metadata: props.metadata } : {}),
296306
},
297307
})
298308
await sdk.client.instance.dispose()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
2+
3+
export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise<Hooks> {
4+
const prompts = [
5+
...(!process.env.CLOUDFLARE_ACCOUNT_ID
6+
? [
7+
{
8+
type: "text" as const,
9+
key: "accountId",
10+
message: "Enter your Cloudflare Account ID",
11+
placeholder: "e.g. 1234567890abcdef1234567890abcdef",
12+
},
13+
]
14+
: []),
15+
]
16+
17+
return {
18+
auth: {
19+
provider: "cloudflare-workers-ai",
20+
methods: [
21+
{
22+
type: "api",
23+
label: "API key",
24+
prompts,
25+
},
26+
],
27+
},
28+
}
29+
}
30+
31+
export async function CloudflareAIGatewayAuthPlugin(_input: PluginInput): Promise<Hooks> {
32+
const prompts = [
33+
...(!process.env.CLOUDFLARE_ACCOUNT_ID
34+
? [
35+
{
36+
type: "text" as const,
37+
key: "accountId",
38+
message: "Enter your Cloudflare Account ID",
39+
placeholder: "e.g. 1234567890abcdef1234567890abcdef",
40+
},
41+
]
42+
: []),
43+
...(!process.env.CLOUDFLARE_GATEWAY_ID
44+
? [
45+
{
46+
type: "text" as const,
47+
key: "gatewayId",
48+
message: "Enter your Cloudflare AI Gateway ID",
49+
placeholder: "e.g. my-gateway",
50+
},
51+
]
52+
: []),
53+
]
54+
55+
return {
56+
auth: {
57+
provider: "cloudflare-ai-gateway",
58+
methods: [
59+
{
60+
type: "api",
61+
label: "Gateway API token",
62+
prompts,
63+
},
64+
],
65+
},
66+
}
67+
}

packages/opencode/src/plugin/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { NamedError } from "@opencode-ai/util/error"
1010
import { CopilotAuthPlugin } from "./github-copilot/copilot"
1111
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
1212
import { PoeAuthPlugin } from "opencode-poe-auth"
13+
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
1314
import { Effect, Layer, ServiceMap, Stream } from "effect"
1415
import { InstanceState } from "@/effect/instance-state"
1516
import { makeRuntime } from "@/effect/run-service"
@@ -46,7 +47,14 @@ export namespace Plugin {
4647
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Plugin") {}
4748

4849
// Built-in plugins that are directly imported (not installed from npm)
49-
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin, PoeAuthPlugin]
50+
const INTERNAL_PLUGINS: PluginInstance[] = [
51+
CodexAuthPlugin,
52+
CopilotAuthPlugin,
53+
GitlabAuthPlugin,
54+
PoeAuthPlugin,
55+
CloudflareWorkersAuthPlugin,
56+
CloudflareAIGatewayAuthPlugin,
57+
]
5058

5159
function isServerPlugin(value: unknown): value is PluginInstance {
5260
return typeof value === "function"

packages/opencode/src/provider/provider.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -672,13 +672,26 @@ export namespace Provider {
672672
}
673673
}),
674674
"cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) {
675-
const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID")
676-
if (!accountId) return { autoload: false }
675+
// When baseURL is already configured (e.g. corporate config routing through a proxy/gateway),
676+
// skip the account ID check because the URL is already fully specified.
677+
if (input.options?.baseURL) return { autoload: false }
678+
679+
const auth = yield* dep.auth(input.id)
680+
const accountId =
681+
Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
682+
if (!accountId)
683+
return {
684+
autoload: false,
685+
async getModel() {
686+
throw new Error(
687+
"CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=<your-account-id>",
688+
)
689+
},
690+
}
677691

678692
const apiKey = yield* Effect.gen(function* () {
679693
const envToken = Env.get("CLOUDFLARE_API_KEY")
680694
if (envToken) return envToken
681-
const auth = yield* dep.auth(input.id)
682695
if (auth?.type === "api") return auth.key
683696
return undefined
684697
})
@@ -702,16 +715,34 @@ export namespace Provider {
702715
}
703716
}),
704717
"cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) {
705-
const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID")
706-
const gateway = Env.get("CLOUDFLARE_GATEWAY_ID")
718+
// When baseURL is already configured (e.g. corporate config), skip the ID checks.
719+
if (input.options?.baseURL) return { autoload: false }
707720

708-
if (!accountId || !gateway) return { autoload: false }
721+
const auth = yield* dep.auth(input.id)
722+
const accountId =
723+
Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
724+
const gateway =
725+
Env.get("CLOUDFLARE_GATEWAY_ID") || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
726+
727+
if (!accountId || !gateway) {
728+
const missing = [
729+
!accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined,
730+
!gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined,
731+
].filter((x): x is string => Boolean(x))
732+
return {
733+
autoload: false,
734+
async getModel() {
735+
throw new Error(
736+
`${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=<value>`).join(" && ")}`,
737+
)
738+
},
739+
}
740+
}
709741

710742
// Get API token from env or auth - required for authenticated gateways
711743
const apiToken = yield* Effect.gen(function* () {
712744
const envToken = Env.get("CLOUDFLARE_API_TOKEN") || Env.get("CF_AIG_TOKEN")
713745
if (envToken) return envToken
714-
const auth = yield* dep.auth(input.id)
715746
if (auth?.type === "api") return auth.key
716747
return undefined
717748
})

packages/sdk/js/src/v2/gen/types.gen.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,9 @@ export type OAuth = {
16391639
export type ApiAuth = {
16401640
type: "api"
16411641
key: string
1642+
metadata?: {
1643+
[key: string]: string
1644+
}
16421645
}
16431646

16441647
export type WellKnownAuth = {

packages/sdk/openapi.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11621,6 +11621,15 @@
1162111621
},
1162211622
"key": {
1162311623
"type": "string"
11624+
},
11625+
"metadata": {
11626+
"type": "object",
11627+
"propertyNames": {
11628+
"type": "string"
11629+
},
11630+
"additionalProperties": {
11631+
"type": "string"
11632+
}
1162411633
}
1162511634
},
1162611635
"required": ["type", "key"]

0 commit comments

Comments
 (0)