Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
name: "cloudflare-ai-gateway",
baseURL: `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`,
apiKey: apiToken,
headers: { "cf-aig-gateway-id": gateway },
headers: aiGatewayRestHeaders(gateway, opts.headers["User-Agent"], opts),
})(modelID)
},
options: {},
Expand Down Expand Up @@ -1250,6 +1250,33 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
// 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`).
/**
* Per-request AI Gateway controls for the catalog REST API. That endpoint cannot
* take the aigateway wrapper's options bag, so the gateway evaluates these
* cf-aig-* request headers instead.
*/
export function aiGatewayRestHeaders(
gateway: string,
userAgent: string,
control: {
metadata?: unknown
cacheTtl?: number | string
cacheKey?: string
skipCache?: boolean
collectLog?: boolean
},
) {
return {
"cf-aig-gateway-id": gateway,
"User-Agent": userAgent,
...(control.metadata !== undefined ? { "cf-aig-metadata": JSON.stringify(control.metadata) } : {}),
...(control.cacheTtl !== undefined ? { "cf-aig-cache-ttl": String(control.cacheTtl) } : {}),
...(control.cacheKey !== undefined ? { "cf-aig-cache-key": String(control.cacheKey) } : {}),
...(control.skipCache !== undefined ? { "cf-aig-skip-cache": String(control.skipCache) } : {}),
...(control.collectLog !== undefined ? { "cf-aig-collect-log": String(control.collectLog) } : {}),
}
}

function cloudflareGatewayNpm(providerID: string, modelID: string) {
if (providerID !== "cloudflare-ai-gateway") return undefined
if (modelID.startsWith("openai/")) return "@ai-sdk/openai"
Expand Down
43 changes: 43 additions & 0 deletions packages/opencode/test/provider/cf-ai-gateway-rest-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test"
import { aiGatewayRestHeaders } from "@/provider/provider"

describe("aiGatewayRestHeaders", () => {
test("maps every gateway control option to its cf-aig-* request header", () => {
const headers = aiGatewayRestHeaders("my-gateway", "opencode/test", {
metadata: { team: "core", cost_center: 7 },
cacheTtl: 300,
cacheKey: "prompt-v1",
skipCache: true,
collectLog: false,
})

expect(headers).toEqual({
"cf-aig-gateway-id": "my-gateway",
"User-Agent": "opencode/test",
"cf-aig-metadata": JSON.stringify({ team: "core", cost_center: 7 }),
"cf-aig-cache-ttl": "300",
"cf-aig-cache-key": "prompt-v1",
"cf-aig-skip-cache": "true",
"cf-aig-collect-log": "false",
})
})

test("omits headers for options that are not set", () => {
const headers = aiGatewayRestHeaders("my-gateway", "opencode/test", {})

expect(headers).toEqual({
"cf-aig-gateway-id": "my-gateway",
"User-Agent": "opencode/test",
})
})

test("keeps explicitly false boolean controls on the wire while omitting unset ones", () => {
const headers = aiGatewayRestHeaders("g", "ua", { skipCache: false })

// skipCache: false is meaningful to the gateway (explicitly re-enable caching),
// so it must serialize rather than disappear.
expect(headers["cf-aig-skip-cache"]).toBe("false")
expect(headers).not.toHaveProperty("cf-aig-cache-key")
expect(headers).not.toHaveProperty("cf-aig-metadata")
})
})
Loading