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 <aidenpcline@gmail.com>pull/20180/head^2
parent
70b636a360
commit
eaa272ef7f
|
|
@ -24,6 +24,7 @@ export namespace Auth {
|
|||
export class Api extends Schema.Class<Api>("ApiAuth")({
|
||||
type: Schema.Literal("api"),
|
||||
key: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}) {}
|
||||
|
||||
export class WellKnown extends Schema.Class<WellKnown>("WellKnownAuth")({
|
||||
|
|
|
|||
|
|
@ -129,7 +129,15 @@ export function createDialogProviderOptions() {
|
|||
}
|
||||
}
|
||||
if (method.type === "api") {
|
||||
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
||||
let metadata: Record<string, string> | undefined
|
||||
if (method.prompts?.length) {
|
||||
const value = await PromptsMethod({ dialog, prompts: method.prompts })
|
||||
if (!value) return
|
||||
metadata = value
|
||||
}
|
||||
return dialog.replace(() => (
|
||||
<ApiMethod providerID={provider.id} title={method.label} metadata={metadata} />
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -249,6 +257,7 @@ function CodeMethod(props: CodeMethodProps) {
|
|||
interface ApiMethodProps {
|
||||
providerID: string
|
||||
title: string
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise<Hooks> {
|
||||
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<Hooks> {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Service, Interface>()("@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"
|
||||
|
|
|
|||
|
|
@ -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=<your-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}=<value>`).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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1639,6 +1639,9 @@ export type OAuth = {
|
|||
export type ApiAuth = {
|
||||
type: "api"
|
||||
key: string
|
||||
metadata?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type WellKnownAuth = {
|
||||
|
|
|
|||
|
|
@ -11621,6 +11621,15 @@
|
|||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["type", "key"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue