Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e57913f
feat(ai): add native Vercel AI Gateway language models
rekram1-node Oct 2, 2026
121800c
refactor(ai): simplify Vercel AI Gateway provider
rekram1-node Oct 2, 2026
8e9e03b
test(ai): record Gateway with current models
rekram1-node Oct 2, 2026
bf2faf3
fix(ai): support overlapping Responses reasoning items
rekram1-node Oct 2, 2026
cce4cf1
refactor(ai): use one Gateway Responses route
rekram1-node Oct 2, 2026
f5c8f5d
fix(ai): restore Responses reasoning overlap check
rekram1-node Oct 5, 2026
fcd6c71
feat(ai): route Gateway models by reasoning support
rekram1-node Oct 5, 2026
349335f
refactor(ai): untrace Gateway protocol wrapper and skip redundant work
rekram1-node Oct 5, 2026
8208b05
refactor(ai): clean up Gateway protocol state and request preparation
rekram1-node Oct 5, 2026
d989673
refactor(ai): inline Gateway options and simplify per-API preparation
rekram1-node Oct 5, 2026
109c2bf
feat(ai): use inline cache policy for Gateway Messages and trim README
rekram1-node Oct 5, 2026
0c0a092
refactor(ai): inline Gateway metadata read in step
rekram1-node Oct 5, 2026
2fe9389
refactor(ai): loosen Gateway option and wire metadata schemas
rekram1-node Oct 5, 2026
928650e
refactor(ai): keep Gateway request options optional-only
rekram1-node Oct 5, 2026
668ee8b
refactor(ai): reuse inner protocol event schema in Gateway wrapper
rekram1-node Oct 5, 2026
0f0b7b0
refactor(ai): use Effect Schema for Gateway event metadata
rekram1-node Oct 5, 2026
a93753f
docs(ai): document Vercel AI Gateway options
rekram1-node Oct 5, 2026
34c64a9
refactor(ai): inline Gateway request preparation into switch(api)
rekram1-node Oct 5, 2026
254d7e8
refactor(ai): flatten Gateway request preparation
rekram1-node Oct 5, 2026
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
2 changes: 1 addition & 1 deletion packages/ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,7 @@ const gateway = CloudflareAIGateway.configure({
}).model("workers-ai/@cf/meta/llama-3.1-8b-instruct")
```

Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazon Bedrock, Azure OpenAI, Baseten, Cerebras, Cohere, Cloudflare AI Gateway, Cloudflare Workers AI, DeepInfra, DeepSeek, Fireworks, Groq, Mistral, OpenRouter, TogetherAI, and xAI. Z.ai currently exposes image generation. Generic Chat Completions, Responses, and Anthropic Messages-compatible entrypoints support custom endpoints.
Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazon Bedrock, Azure OpenAI, Baseten, Cerebras, Cohere, Cloudflare AI Gateway, Cloudflare Workers AI, DeepInfra, DeepSeek, Fireworks, Groq, Mistral, OpenRouter, TogetherAI, Vercel AI Gateway, and xAI. Z.ai currently exposes image generation. Generic Chat Completions, Responses, and Anthropic Messages-compatible entrypoints support custom endpoints.

Each named provider owns its module, endpoint, authentication, and route setup. Providers with the same wire format compose the shared protocol directly:

Expand Down
21 changes: 12 additions & 9 deletions packages/ai/src/cache-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,22 @@ const RESPECTS_INLINE_HINTS = new Set([
"meta-messages",
"minimax-messages",
"moonshot-messages",
"vercel-ai-gateway-messages",
"zai-coding-messages",
"bedrock-converse",
"openrouter",
"digitalocean",
])

// OpenRouter upstreams other than Anthropic and Alibaba Qwen cache without breakpoints. Gemini uses only the last
// breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs more than none. Qwen ignores
// breakpoints on tool definitions and caches tools with the system prompt.
// OpenRouter and Vercel AI Gateway upstreams other than Anthropic and Alibaba Qwen cache without breakpoints.
// Gemini uses only the last breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs
// more than none. Qwen ignores breakpoints on tool definitions and caches tools with the system prompt.
const QWEN: CachePolicyObject = { system: true, messages: { tail: 1 } }
const openRouterPolicy = (modelID: string): CachePolicyObject => {
const gatewayPolicy = (modelID: string): CachePolicyObject => {
// `~anthropic/claude-sonnet-latest` style IDs are OpenRouter aliases for the latest model in a family.
const id = modelID.replace(/^~/, "")
if (id.startsWith("anthropic/")) return AUTO
if (id.startsWith("qwen/")) return QWEN
if (id.startsWith("qwen/") || id.startsWith("alibaba/qwen")) return QWEN
return NONE
}

Expand Down Expand Up @@ -170,11 +171,13 @@ const countHints = (request: LLMRequest) =>
)

export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
const route = request.model.route.id
if (!RESPECTS_INLINE_HINTS.has(route)) return request
const policy =
request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")
? openRouterPolicy(request.model.id)
: request.model.route.id === "alibaba-chat" && (request.cache === undefined || request.cache === "auto")
(route === "openrouter" || route === "vercel-ai-gateway-messages") &&
(request.cache === undefined || request.cache === "auto")
? gatewayPolicy(request.model.id)
: route === "alibaba-chat" && (request.cache === undefined || request.cache === "auto")
? request.model.id.toLowerCase().startsWith("qwen")
? QWEN
: NONE
Expand Down
90 changes: 90 additions & 0 deletions packages/ai/src/protocols/utils/gateway-protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Effect, Option, Schema } from "effect"
import { Protocol } from "../../route/protocol.js"
import { LLMEvent, mergeJsonRecords, type AIError, type LLMRequest } from "../../schema/index.js"
import { JsonObject, lenient } from "../shared.js"

interface ParserState<Inner> {
readonly inner: Inner
readonly gateway?: Record<string, unknown>
}

const GatewayHolder = Schema.Struct({
provider_metadata: lenient(
Schema.Struct({
gateway: lenient(JsonObject),
}),
),
})

const GatewayEvent = Schema.Struct({
...GatewayHolder.fields,
response: lenient(GatewayHolder),
choices: lenient(
Schema.Array(
Schema.Struct({
delta: lenient(GatewayHolder),
}),
),
),
})
const decodeGatewayEvent = Schema.decodeUnknownOption(GatewayEvent)

function attachGatewayMetadata(
events: ReadonlyArray<LLMEvent>,
gateway: Record<string, unknown> | undefined,
): ReadonlyArray<LLMEvent> {
if (!gateway || !events.some(LLMEvent.is.finish)) return events
return events.map((event) =>
LLMEvent.is.finish(event) ? { ...event, providerMetadata: { ...event.providerMetadata, gateway } } : event,
)
}

export function gatewayProtocol<Body, Event, State>(
protocol: Protocol<Body, string, Event, State>,
input: {
readonly id: string
readonly prepare: (
request: LLMRequest,
) => Effect.Effect<{ readonly request: LLMRequest; readonly body: Record<string, unknown> }, AIError>
},
) {
const initial = (request: LLMRequest): ParserState<State> => ({
inner: protocol.stream.initial(request),
})
const onHalt = protocol.stream.onHalt

return Protocol.make({
id: input.id,
body: {
schema: JsonObject,
from: Effect.fnUntraced(function* (request: LLMRequest) {
const prepared = yield* input.prepare(request)
const body = yield* protocol.body.from(prepared.request)
return { ...body, ...prepared.body }
}),
},
supportsEffortUpdates: protocol.supportsEffortUpdates,
sanitizer: protocol.sanitizer,
stream: {
event: protocol.stream.event,
initial,
step: Effect.fnUntraced(function* (state: ParserState<State>, event: Event) {
const decoded = Option.getOrUndefined(decodeGatewayEvent(event))
const gateway = decoded
? mergeJsonRecords(
state.gateway,
decoded.provider_metadata?.gateway,
decoded.response?.provider_metadata?.gateway,
...(decoded.choices ?? []).map((choice) => choice.delta?.provider_metadata?.gateway),
)
: state.gateway
const [inner, events] = yield* protocol.stream.step(state.inner, event)
return [{ inner, gateway }, attachGatewayMetadata(events, gateway)] as const
}),
terminal: protocol.stream.terminal,
onHalt: onHalt
? (state) => onHalt(state.inner).pipe(Effect.map((events) => attachGatewayMetadata(events, state.gateway)))
: undefined,
},
})
}
Loading
Loading