|
1
|
+ |
defmodule OpenAgents.Providers.VercelGateway do
|
|
2
|
+ |
@moduledoc """
|
|
3
|
+ |
Vercel AI Gateway adapter: one endpoint in front of many providers.
|
|
4
|
+ |
|
|
5
|
+ |
This is how Gemini reaches Google's own hardware, and the reason it exists is
|
|
6
|
+ |
money. The credits this account holds are Google's, so a Gemini call has to
|
|
7
|
+ |
land on Vertex to spend them. Two routes reach Vertex:
|
|
8
|
+ |
|
|
9
|
+ |
1. Directly, with a Google identity. That was written and it worked, and it
|
|
10
|
+ |
cost an OAuth token minter, a service-account JWT signer, a project-scoped
|
|
11
|
+ |
`locations/global` path, and a workaround for Gemini 3 refusing any
|
|
12
|
+ |
transcript whose `functionCall` parts do not carry back the
|
|
13
|
+ |
`thoughtSignature` it produced — a 400, not a warning, with nowhere in an
|
|
14
|
+ |
OpenAI-shaped transcript to put a 360-character signature.
|
|
15
|
+ |
2. Through this gateway, which is OpenAI chat completions — the shape the
|
|
16
|
+ |
OpenRouter adapter already speaks — and which does that translation
|
|
17
|
+ |
itself.
|
|
18
|
+ |
|
|
19
|
+ |
The gateway's own reply settles it. With BYOK Vertex credentials configured
|
|
20
|
+ |
it answers `"credentialType":"byok"`, `"resolvedProvider":"vertex"`, and
|
|
21
|
+ |
`"cost":"0"`: the call ran on Google's hardware against the credits, and the
|
|
22
|
+ |
gateway charged nothing to put it there.
|
|
23
|
+ |
|
|
24
|
+ |
`providerOptions.gateway.only` pins the provider, because the same slug is
|
|
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.
|
|
27
|
+ |
|
|
28
|
+ |
The wire format is OpenRouter's, so the request building and the stream
|
|
29
|
+ |
decoding are OpenRouter's too. What differs is the endpoint, the credential,
|
|
30
|
+ |
and the pin.
|
|
31
|
+ |
"""
|
|
32
|
+ |
|
|
33
|
+ |
@behaviour OpenAgents.Providers.Provider
|
|
34
|
+ |
|
|
35
|
+ |
alias OpenAgents.Providers.{OpenRouter, Request}
|
|
36
|
+ |
|
|
37
|
+ |
@endpoint "https://ai-gateway.vercel.sh/v1/chat/completions"
|
|
38
|
+ |
|
|
39
|
+ |
@impl true
|
|
40
|
+ |
def id, do: "vercel_gateway.chat_completions"
|
|
41
|
+ |
|
|
42
|
+ |
@impl true
|
|
43
|
+ |
def capabilities, do: [:text, :tool_calls, :usage]
|
|
44
|
+ |
|
|
45
|
+ |
@impl true
|
|
46
|
+ |
def configured? do
|
|
47
|
+ |
match?({:ok, _key}, OpenAgents.RuntimeConfig.fetch_secret(:vercel_gateway_api_key))
|
|
48
|
+ |
end
|
|
49
|
+ |
|
|
50
|
+ |
@impl true
|
|
51
|
+ |
def stream(%Request{} = request, on_event) when is_function(on_event, 1) do
|
|
52
|
+ |
stream(request, on_event, [])
|
|
53
|
+ |
end
|
|
54
|
+ |
|
|
55
|
+ |
@doc false
|
|
56
|
+ |
def stream(%Request{} = request, on_event, options)
|
|
57
|
+ |
when is_function(on_event, 1) and is_list(options) do
|
|
58
|
+ |
with {:ok, api_key} <- fetch_api_key(options),
|
|
59
|
+ |
{:ok, response} <- OpenRouter.post(api_key, request, gateway_options(options)) do
|
|
60
|
+ |
OpenRouter.consume(response, on_event)
|
|
61
|
+ |
end
|
|
62
|
+ |
end
|
|
63
|
+ |
|
|
64
|
+ |
defp fetch_api_key(options) do
|
|
65
|
+ |
case Keyword.fetch(options, :api_key) do
|
|
66
|
+ |
{:ok, key} when is_binary(key) and byte_size(key) > 0 ->
|
|
67
|
+ |
{:ok, key}
|
|
68
|
+ |
|
|
69
|
+ |
_not_supplied ->
|
|
70
|
+ |
case OpenAgents.RuntimeConfig.fetch_secret(:vercel_gateway_api_key) do
|
|
71
|
+ |
{:ok, key} -> {:ok, key}
|
|
72
|
+ |
{:error, :not_configured} -> {:error, :missing_api_key}
|
|
73
|
+ |
end
|
|
74
|
+ |
end
|
|
75
|
+ |
end
|
|
76
|
+ |
|
|
77
|
+ |
defp gateway_options(options) do
|
|
78
|
+ |
options
|
|
79
|
+ |
|> Keyword.put(:endpoint, @endpoint)
|
|
80
|
+ |
|> Keyword.put(:payload_extra, payload_extra())
|
|
81
|
+ |
end
|
|
82
|
+ |
|
|
83
|
+ |
@doc false
|
|
84
|
+ |
def payload_extra do
|
|
85
|
+ |
case Application.get_env(:openagents, :vercel_gateway_providers) do
|
|
86
|
+ |
[_first | _rest] = providers -> %{providerOptions: %{gateway: %{only: providers}}}
|
|
87
|
+ |
_unset -> %{}
|
|
88
|
+ |
end
|
|
89
|
+ |
end
|
|
90
|
+ |
end
|