import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { applyAuthProfileConfig, buildApiKeyCredential, ensureApiKeyFromOptionEnvOrPrompt, ensureAuthProfileStore, listProfilesForProvider, normalizeApiKeyInput, normalizeOptionalSecretInput, upsertAuthProfile, validateApiKeyInput, } from "openclaw/plugin-sdk/provider-auth"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { buildCloudflareAiGatewayCatalogProvider } from "./catalog-provider.js"; import { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "./models.js"; import { applyCloudflareAiGatewayConfig, buildCloudflareAiGatewayConfigPatch } from "./onboard.js"; const PROVIDER_ID = "cloudflare-ai-gateway"; const PROVIDER_ENV_VAR = "CLOUDFLARE_AI_GATEWAY_API_KEY"; const PROFILE_ID = "cloudflare-ai-gateway:default"; function readRequiredTextInput(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } async function resolveCloudflareGatewayMetadataInteractive(ctx: { accountId?: string; gatewayId?: string; prompter: { text: (params: { message: string; validate?: (value: unknown) => string | undefined; }) => Promise; }; }) { let accountId = normalizeOptionalString(ctx.accountId) ?? ""; let gatewayId = normalizeOptionalString(ctx.gatewayId) ?? ""; if (!accountId) { const value = await ctx.prompter.text({ message: "Enter Cloudflare Account ID", validate: (val) => (readRequiredTextInput(val) ? undefined : "Account ID is required"), }); accountId = readRequiredTextInput(value); } if (!gatewayId) { const value = await ctx.prompter.text({ message: "Enter Cloudflare AI Gateway ID", validate: (val) => (readRequiredTextInput(val) ? undefined : "Gateway ID is required"), }); gatewayId = readRequiredTextInput(value); } return { accountId, gatewayId }; } export default definePluginEntry({ id: PROVIDER_ID, name: "Cloudflare AI Gateway Provider", description: "Bundled Cloudflare AI Gateway provider plugin", register(api) { api.registerProvider({ id: PROVIDER_ID, label: "Cloudflare AI Gateway", docsPath: "/providers/cloudflare-ai-gateway", envVars: ["CLOUDFLARE_AI_GATEWAY_API_KEY"], auth: [ { id: "api-key", label: "Cloudflare AI Gateway", hint: "Account ID + Gateway ID + API key", kind: "api_key", wizard: { choiceId: "cloudflare-ai-gateway-api-key", choiceLabel: "Cloudflare AI Gateway", choiceHint: "Account ID + Gateway ID + API key", groupId: "cloudflare-ai-gateway", groupLabel: "Cloudflare AI Gateway", groupHint: "Account ID + Gateway ID + API key", }, run: async (ctx) => { const metadata = await resolveCloudflareGatewayMetadataInteractive({ accountId: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayAccountId), gatewayId: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayGatewayId), prompter: ctx.prompter, }); let capturedSecretInput: Parameters[1] = ""; let capturedCredential = false; let capturedMode: "plaintext" | "ref" | undefined; await ensureApiKeyFromOptionEnvOrPrompt({ token: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayApiKey), tokenProvider: "cloudflare-ai-gateway", secretInputMode: ctx.allowSecretRefPrompt === false ? (ctx.secretInputMode ?? "plaintext") : ctx.secretInputMode, config: ctx.config, expectedProviders: [PROVIDER_ID], provider: PROVIDER_ID, envLabel: PROVIDER_ENV_VAR, promptMessage: "Enter Cloudflare AI Gateway API key", normalize: normalizeApiKeyInput, validate: validateApiKeyInput, prompter: ctx.prompter, setCredential: async (apiKey, mode) => { capturedSecretInput = apiKey; capturedCredential = true; capturedMode = mode; }, }); if (!capturedCredential) { throw new Error("Missing Cloudflare AI Gateway API key."); } const credentialInput = capturedSecretInput ?? ""; return { profiles: [ { profileId: PROFILE_ID, credential: buildApiKeyCredential( PROVIDER_ID, credentialInput, { accountId: metadata.accountId, gatewayId: metadata.gatewayId, }, capturedMode ? { secretInputMode: capturedMode } : undefined, ), }, ], configPatch: buildCloudflareAiGatewayConfigPatch(metadata), defaultModel: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, }; }, runNonInteractive: async (ctx) => { const authStore = ensureAuthProfileStore(ctx.agentDir, { allowKeychainPrompt: false, }); const storedMetadata = authStore.profiles[PROFILE_ID]?.type === "api_key" ? { accountId: normalizeOptionalString( authStore.profiles[PROFILE_ID]?.metadata?.accountId, ), gatewayId: normalizeOptionalString( authStore.profiles[PROFILE_ID]?.metadata?.gatewayId, ), } : {}; const accountId = normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayAccountId) ?? storedMetadata.accountId; const gatewayId = normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayGatewayId) ?? storedMetadata.gatewayId; if (!accountId || !gatewayId) { ctx.runtime.error( "Cloudflare AI Gateway setup requires --cloudflare-ai-gateway-account-id and --cloudflare-ai-gateway-gateway-id.", ); ctx.runtime.exit(1); return null; } const resolved = await ctx.resolveApiKey({ provider: PROVIDER_ID, flagValue: normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayApiKey), flagName: "--cloudflare-ai-gateway-api-key", envVar: PROVIDER_ENV_VAR, }); if (!resolved) { return null; } if (resolved.source !== "profile") { const credential = ctx.toApiKeyCredential({ provider: PROVIDER_ID, resolved, metadata: { accountId, gatewayId }, }); if (!credential) { return null; } upsertAuthProfile({ profileId: PROFILE_ID, credential, agentDir: ctx.agentDir, }); } const next = applyAuthProfileConfig(ctx.config, { profileId: PROFILE_ID, provider: PROVIDER_ID, mode: "api_key", }); return applyCloudflareAiGatewayConfig(next, { accountId, gatewayId }); }, }, ], catalog: { order: "late", run: async (ctx) => { const authStore = ensureAuthProfileStore(ctx.agentDir, { allowKeychainPrompt: false, }); const envManagedApiKey = normalizeOptionalString(ctx.env[PROVIDER_ENV_VAR]) ? PROVIDER_ENV_VAR : undefined; for (const profileId of listProfilesForProvider(authStore, PROVIDER_ID)) { const provider = buildCloudflareAiGatewayCatalogProvider({ credential: authStore.profiles[profileId], envApiKey: envManagedApiKey, }); if (!provider) { continue; } return { provider, }; } return null; }, }, classifyFailoverReason: ({ errorMessage }) => /\bworkers?_ai\b.*\b(?:rate|limit|quota)\b/i.test(errorMessage) ? "rate_limit" : undefined, }); }, });