Add Vercel AI Gateway model fallback list.

5895a271a566 · AtlantisPleb · · parent 32ef279cd25f

Add Vercel AI Gateway model fallback list.

Switches the gateway pin from `only: ["vertex"]` to `order: ["vertex"]`
so the configured fallback models can be served by other providers.
Adds `vercel_gateway_fallback_models` with the ordered list:
zai/glm-5.3, zai/glm-5.2, and openai/gpt-5.6-luna.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 370 · 2026-08-25T12:29:31.477618Z

Changed files

  • modified INVARIANTS.md
  • modified config/config.exs
  • modified config/test.exs
  • modified lib/openagents/providers/vercel_gateway.ex
  • modified test/openagents/providers/vercel_gateway_test.exs

Diff

5 files changed, +62 -20

INVARIANTS.md modified +4 -4

@@ -1184,10 +1184,10 @@ A third lane, `OpenAgents.Providers.VercelGateway`, reaches models through

1184 1184
Vercel's AI Gateway rather than a vendor directly. It speaks the same chat
1185 1185
completions the OpenRouter adapter does, so it reuses that adapter's request
1186 1186
building and stream decoding and differs only in endpoint, credential, and the
1187
provider it pins. The pin is the point: Gemini is served there by both `google`
1188
and `vertex`, and only `vertex` spends this account's Google credits, so
1189
`vercel_gateway_providers` names the provider set and an unpinned fallback
1190
cannot quietly spend money instead.
1187
provider it tries first. Gemini is served there by both `google` and `vertex`,
1188
and `vertex` is where this account's Google credits are spent, so
1189
`vercel_gateway_providers` names the provider order and `vercel_gateway_fallback_models`
1190
names the model fallback list Vercel tries if the primary model fails.
1191 1191
1192 1192
Evidence: `OpenAgents.Providers.ProviderEvent`, `OpenAgents.Providers.OpenAI`,
1193 1193
`OpenAgents.Providers.OpenAI.StreamDecoderTest`,
config/config.exs modified +9 -4

@@ -194,11 +194,16 @@ config :openagents,

194 194
  ],
195 195
  gemini_api_key: nil,
196 196
  vercel_gateway_api_key: nil,
197
  # Pin the gateway to Vertex for every model it serves here. The same Gemini
198
  # slug is also served by `google` — the Generative Language endpoint, which
199
  # is not where this account's credits are — and a silent fallback there would
200
  # spend money beside a balance already paid for.
197
  # Try Vertex first for every model it serves here. The same Gemini slug is
198
  # also served by `google` — the Generative Language endpoint, which is not
199
  # where this account's credits are — but the fallback models are not on
200
  # Vertex, so Vercel may try other providers for them.
201 201
  vercel_gateway_providers: ["vertex"],
202
  vercel_gateway_fallback_models: [
203
    "zai/glm-5.3",
204
    "zai/glm-5.2",
205
    "openai/gpt-5.6-luna"
206
  ],
202 207
  gemini_model: "gemini-3.7-flash",
203 208
  box_api_key: nil,
204 209
  box_api: [
config/test.exs modified +1

@@ -57,6 +57,7 @@ config :openagents,

57 57
config :openagents, :provider, OpenAgents.Providers.Test
58 58
config :openagents, :openrouter_provider, OpenAgents.Providers.Test
59 59
config :openagents, :vercel_gateway_provider, OpenAgents.Providers.Test
60
config :openagents, :vercel_gateway_fallback_models, []
60 61
config :openagents, :voice_call_provider, OpenAgents.Voice.TestCallProvider
61 62
config :openagents, :voice_sideband_provider, OpenAgents.Voice.TestSidebandProvider
62 63
lib/openagents/providers/vercel_gateway.ex modified +19 -5

@@ -21,9 +21,10 @@ defmodule OpenAgents.Providers.VercelGateway do

21 21
  `"cost":"0"`: the call ran on Google's hardware against the credits, and the
22 22
  gateway charged nothing to put it there.
23 23
24
  `providerOptions.gateway.only` pins the provider, because the same slug is
24
  `providerOptions.gateway.order` tries Vertex first, because the same slug is
25 25
  also served by `google` — the Generative Language endpoint, which is not
26
  where the credits are. Without the pin a fallback would quietly spend money.
26
  where the credits are. `providerOptions.gateway.models` lists the fallback
27
  models Vercel tries if the primary model fails.
27 28
28 29
  The wire format is OpenRouter's, so the request building and the stream
29 30
  decoding are OpenRouter's too. What differs is the endpoint, the credential,

@@ -82,9 +83,22 @@ defmodule OpenAgents.Providers.VercelGateway do

82 83
83 84
  @doc false
84 85
  def payload_extra do
85
    case Application.get_env(:openagents, :vercel_gateway_providers) do
86
      [_first | _rest] = providers -> %{providerOptions: %{gateway: %{only: providers}}}
87
      _unset -> %{}
86
    providers = Application.get_env(:openagents, :vercel_gateway_providers, [])
87
    fallbacks = Application.get_env(:openagents, :vercel_gateway_fallback_models, [])
88
89
    gateway =
90
      %{}
91
      |> maybe_put(:order, providers)
92
      |> maybe_put(:models, fallbacks)
93
94
    if map_size(gateway) > 0 do
95
      %{providerOptions: %{gateway: gateway}}
96
    else
97
      %{}
88 98
    end
89 99
  end
100
101
  defp maybe_put(map, _key, nil), do: map
102
  defp maybe_put(map, _key, []), do: map
103
  defp maybe_put(map, key, [_ | _] = values), do: Map.put(map, key, values)
90 104
end
test/openagents/providers/vercel_gateway_test.exs modified +29 -7

@@ -3,24 +3,46 @@ defmodule OpenAgents.Providers.VercelGatewayTest do

3 3
4 4
  alias OpenAgents.Providers.VercelGateway
5 5
6
  describe "pinning the provider" do
7
    test "sends the gateway the provider set this deployment intends" do
6
  describe "provider routing" do
7
    test "sends the gateway the provider order this deployment intends" do
8 8
      # The same Gemini slug is served by `google` — the Generative Language
9 9
      # endpoint — and by `vertex`, which is where this account's credits are.
10
      # Without the pin, a fallback spends money beside a balance already paid
11
      # for, and nothing in the response would say so.
12
      assert VercelGateway.payload_extra() == %{providerOptions: %{gateway: %{only: ["vertex"]}}}
10
      # `order` tries Vertex first and lets Vercel move to other providers
11
      # for the configured fallback models.
12
      assert VercelGateway.payload_extra() == %{providerOptions: %{gateway: %{order: ["vertex"]}}}
13 13
    end
14 14
15
    test "sends no pin at all when none is configured, rather than an empty one" do
15
    test "sends no provider or model options when none are configured" do
16 16
      previous = Application.get_env(:openagents, :vercel_gateway_providers)
17 17
      Application.put_env(:openagents, :vercel_gateway_providers, [])
18 18
      on_exit(fn -> Application.put_env(:openagents, :vercel_gateway_providers, previous) end)
19 19
20
      # An empty `only` would be a pin to nothing, which the gateway is entitled
20
      # An empty `order` would be a pin to nothing, which the gateway is entitled
21 21
      # to read as "no provider may serve this".
22 22
      assert VercelGateway.payload_extra() == %{}
23 23
    end
24
25
    test "sends the fallback model list when configured" do
26
      previous = Application.get_env(:openagents, :vercel_gateway_fallback_models)
27
28
      Application.put_env(:openagents, :vercel_gateway_fallback_models, [
29
        "zai/glm-5.3",
30
        "openai/gpt-5.6-luna"
31
      ])
32
33
      on_exit(fn ->
34
        Application.put_env(:openagents, :vercel_gateway_fallback_models, previous)
35
      end)
36
37
      assert VercelGateway.payload_extra() == %{
38
               providerOptions: %{
39
                 gateway: %{
40
                   order: ["vertex"],
41
                   models: ["zai/glm-5.3", "openai/gpt-5.6-luna"]
42
                 }
43
               }
44
             }
45
    end
24 46
  end
25 47
26 48
  describe "the credential" do

This page updates live while a promote is in flight · changelog