feat(opencode): native OpenAI and Anthropic passthroughs for Cloudflare AI Gateway (#42634)

Co-authored-by: Keefe Tang <keefe@pentagram.me>
This commit is contained in:
Aiden Cline
2026-08-16 23:53:20 -05:00
committed by GitHub
parent 1c965451b5
commit cba6b5f2f7
5 changed files with 265 additions and 105 deletions
@@ -61,16 +61,5 @@ export async function CloudflareAIGatewayAuthPlugin(_input: PluginInput): Promis
},
],
},
"chat.params": async (input, output) => {
if (input.model.providerID !== "cloudflare-ai-gateway") return
// The unified gateway routes through @ai-sdk/openai-compatible, which
// always emits max_tokens. OpenAI reasoning models (gpt-5.x, o-series)
// reject that field and require max_completion_tokens instead, and the
// compatible SDK has no way to rename it. Drop the cap so OpenAI falls
// back to the model's default output budget.
if (!input.model.api.id.toLowerCase().startsWith("openai/")) return
if (!input.model.capabilities.reasoning) return
output.maxOutputTokens = undefined
},
}
}
+37 -5
View File
@@ -800,9 +800,10 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
)
}
// Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility)
const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider"))
const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified"))
const { createOpenAI } = yield* Effect.promise(() => import("ai-gateway-provider/providers/openai"))
const { createAnthropic } = yield* Effect.promise(() => import("ai-gateway-provider/providers/anthropic"))
const metadata = iife(() => {
if (input.options?.metadata) return input.options.metadata
@@ -829,12 +830,24 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
apiKey: apiToken,
...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}),
})
const unified = createUnified({ apiKey: apiToken })
return {
autoload: true,
async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
// Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
// Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5").
// OpenAI and Anthropic ride their native passthrough routes so agents get the Responses
// and Messages APIs; new OpenAI models reject tools+reasoning_effort on chat completions.
// The passthrough wrappers inject a CF_TEMP_TOKEN sentinel that the gateway strips before
// dispatch, so upstream billing stays on the gateway (Unified Billing / stored BYOK).
if (modelID.startsWith("openai/")) return aigateway(createOpenAI()(modelID.slice("openai/".length)))
if (modelID.startsWith("anthropic/"))
return aigateway(createAnthropic()(modelID.slice("anthropic/".length)))
// Workers AI is the only first-party provider whose upstream is Cloudflare itself, so it is
// the only one that should receive the Cloudflare token as its upstream Authorization header.
// The Unified API addresses Workers AI both with the explicit "workers-ai/" prefix and as
// bare "@cf/..." ids. Third-party providers must not receive the token; they rely on the
// gateway's stored/BYOK keys instead.
const isWorkersAi = modelID.startsWith("workers-ai/") || modelID.startsWith("@cf/")
const unified = createUnified(isWorkersAi ? { apiKey: apiToken } : {})
return aigateway(unified(modelID))
},
options: {},
@@ -1209,6 +1222,17 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
return result
}
// Cloudflare AI Gateway routes OpenAI and Anthropic models through their native
// passthrough SDKs (Responses / Messages APIs). Resolving the native npm before
// variants are computed makes reasoning variants produce payloads the native
// SDKs understand (e.g. anthropic `effort` instead of compat `reasoningEffort`).
function cloudflareGatewayNpm(providerID: string, modelID: string) {
if (providerID !== "cloudflare-ai-gateway") return undefined
if (modelID.startsWith("openai/")) return "@ai-sdk/openai"
if (modelID.startsWith("anthropic/")) return "@ai-sdk/anthropic"
return undefined
}
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
const base: Model = {
id: ModelV2.ID.make(model.id),
@@ -1218,7 +1242,11 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
api: {
id: model.id,
url: model.provider?.api ?? provider.api ?? "",
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
npm:
cloudflareGatewayNpm(provider.id, model.id) ??
model.provider?.npm ??
provider.npm ??
"@ai-sdk/openai-compatible",
},
status: model.status ?? "active",
headers: {},
@@ -1440,6 +1468,9 @@ const layer = Layer.effect(
model.provider?.npm ??
provider.npm ??
existingModel?.api.npm ??
// Config-defined gateway models bypass fromModelsDevModel, so resolve the
// native passthrough npm here before falling back to the catalog default.
cloudflareGatewayNpm(providerID, apiID) ??
modelsDev[providerID]?.npm ??
"@ai-sdk/openai-compatible"
const name = iife(() => {
@@ -1619,6 +1650,7 @@ const layer = Layer.effect(
for (const [modelID, model] of Object.entries(provider.models)) {
model.api.id = model.api.id ?? model.id ?? modelID
if (
// These chat aliases are invalid for the special handling in the
// built-in providers below, but custom providers may support them.