Add Coder Free model route

f4bfff587cf6 · AtlantisPleb · · parent 05ac6988e346

Add Coder Free model route

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 464 · 2026-08-26T20:38:16.967215Z
built
2 modules in 157.0 s
deployed
live · 2 modules on 3 nodes · push→live —
deployed
needs_rolling_replace · 2 modules on 0 nodes · push→live —

Changed files

  • modified config/config.exs
  • modified lib/openagents/inference/models.ex
  • modified lib/openagents/inference/pricing.ex
  • modified lib/openagents_web/live/model_catalog_live.ex
  • modified test/openagents/inference/models_test.exs
  • modified test/openagents/inference/pricing_test.exs
  • modified test/openagents_web/controllers/inference_proxy_controller_test.exs
  • modified test/openagents_web/live/model_catalog_live_test.exs

Diff

8 files changed, +66 -41

config/config.exs modified +23 -7

@@ -160,7 +160,7 @@ config :openagents,

160 160
  # model this catalog does not admit and therefore has no rates. Every read
161 161
  # surface says `unpriced` rather than `$0.00`.
162 162
  #
163
  # This list is exactly two models, and a caller may spend nothing else. A
163
  # This list is exactly three models, and a caller may spend nothing else. A
164 164
  # model absent from it cannot be minted a grant (`OpenAgents.Inference.mint/1`
165 165
  # refuses the name) and cannot be called on a grant minted before it was
166 166
  # withdrawn (`OpenAgentsWeb.InferenceProxyController` refuses

@@ -186,12 +186,10 @@ config :openagents,

186 186
  # enough to hold a conversation. It led this list until GLM 5.3 Flash was
187 187
  # added.
188 188
  #
189
  # Both entries are on `:vercel_gateway`, and that is a single point of failure
190
  # this file no longer papers over. `gpt-5.6-luna` used to be the third entry
191
  # and the backup that answered when neither of the others could be served.
192
  # It is withdrawn from this list and nothing replaced it, so if the gateway
193
  # itself is unreachable this deployment serves no model at all: both lanes
194
  # report `unavailable` and neither is substituted for the other.
189
  # The first two entries are on `:vercel_gateway`. `openrouter/free` is the
190
  # independent Coder Free lane, and it remains available when the gateway is
191
  # unavailable. OpenRouter limits that router to free models, so its declared
192
  # per-token rates are zero even though the selected provider model can vary.
195 193
  #
196 194
  # `vercel_gateway_fallback_models` below is a different list and it still ends
197 195
  # with `openai/gpt-5.6-luna`, deliberately. That is the gateway's own ordered

@@ -256,6 +254,24 @@ config :openagents,

256 254
        output_per_million_tokens: 10_000_000,
257 255
        cached_input_per_million_tokens: 100_000
258 256
      }
257
    },
258
    %{
259
      # The Coder Free lane resolves this exact fallback after checking for a
260
      # deployment-specific preferred free model. OpenRouter selects a current
261
      # free model behind this router id, so callers cannot be promised one
262
      # vendor context window. Its route only selects zero-price models.
263
      id: "openrouter/free",
264
      provider: :openrouter,
265
      provider_model: "openrouter/free",
266
      context_window: 32_768,
267
      max_output: 8_192,
268
      pricing: %{
269
        id: "declared.openrouter-free.v1",
270
        source: :declared,
271
        input_per_million_tokens: 0,
272
        output_per_million_tokens: 0,
273
        cached_input_per_million_tokens: 0
274
      }
259 275
    }
260 276
  ],
261 277
  gemini_api_key: nil,
lib/openagents/inference/models.ex modified +3 -3

@@ -46,9 +46,9 @@ defmodule OpenAgents.Inference.Models do

46 46
  }
47 47
48 48
  @type pricing :: %{
49
          required(:input_per_million_tokens) => pos_integer(),
50
          required(:output_per_million_tokens) => pos_integer(),
51
          optional(:cached_input_per_million_tokens) => pos_integer()
49
          required(:input_per_million_tokens) => non_neg_integer(),
50
          required(:output_per_million_tokens) => non_neg_integer(),
51
          optional(:cached_input_per_million_tokens) => non_neg_integer()
52 52
        }
53 53
54 54
  @type t :: %{
lib/openagents/inference/pricing.ex modified +5 -6

@@ -2,8 +2,8 @@ defmodule OpenAgents.Inference.Pricing do

2 2
  @moduledoc """
3 3
  What a metered call cost, and on whose authority.
4 4
5
  Metering that prices some lanes at zero is worse than no metering: it reports
6
  a number, and the number is wrong. This deployment ran for a long time with
5
  Metering that prices an unknown lane at zero is worse than no metering: it
6
  reports a number, and the number is wrong. This deployment ran for a long time with
7 7
  `gpt-5.6-luna` admitted and its rates never entered, so a surface that read a
8 8
  missing cost as zero would have shown `$0.00` beside a session that spent
9 9
  real money — and shown it in the same typeface as a figure that was measured.

@@ -28,10 +28,9 @@ defmodule OpenAgents.Inference.Pricing do

28 28
29 29
  `unpriced` is not an error state and it is not a free lane. It is the
30 30
  deployment saying it does not know what a call cost, which is a different
31
  fact from the call having cost nothing, and the distinction survives all the
32
  way to the read surfaces. Turning it into a number is an owner action —
33
  entering real rates in `config :openagents, :model_catalog` — not something
34
  this module may guess at.
31
  fact from the call having cost nothing. A declared zero rate is a measured
32
  free lane; an absent rate remains unknown. The distinction survives all the
33
  way to the read surfaces.
35 34
36 35
  The `pricing_id` convention is `OpenAgents.Voice.Usage`'s, so
37 36
  `OpenAgents.DataRights.AtifExport` reads inference usage and voice usage with
lib/openagents_web/live/model_catalog_live.ex modified +2 -2

@@ -20,8 +20,8 @@ defmodule OpenAgentsWeb.ModelCatalogLive do

20 20
  `Unpriced`, never `$0.00` — the deployment not knowing what a call cost is a
21 21
  different fact from the call having cost nothing. And a rate this deployment
22 22
  wrote to make itself run is labelled `provisional`, so nobody reads a working
23
  figure as a price. Today that is every rate in the catalog, and the page says
24
  so at the top rather than leaving a reader to infer it column by column.
23
  figure as a price. The Coder Free lane declares zero rates because its router
24
  only selects free models; the other listed rates remain provisional.
25 25
26 26
  Availability is `Models.availability/1`, the same word the endpoint
27 27
  publishes, refreshed on a slow tick: a page about what a lane costs that
test/openagents/inference/models_test.exs modified +11 -3

@@ -48,7 +48,7 @@ defmodule OpenAgents.Inference.ModelsTest do

48 48
49 49
    assert ids == Enum.uniq(ids)
50 50
    assert Models.default_id() in ids
51
    assert ids == ["glm-5.3-flash", "gemini-3.7-flash"]
51
    assert ids == ["glm-5.3-flash", "gemini-3.7-flash", "openrouter/free"]
52 52
    assert Enum.map(Models.all(), & &1.id) == ids
53 53
    assert Models.fetch("attacker/gpt-9-ultra") == :error
54 54
    assert Models.fetch(nil) == :error

@@ -171,10 +171,18 @@ defmodule OpenAgents.Inference.ModelsTest do

171 171
      assert glm["pricing"]["basis"] == "provisional"
172 172
    end
173 173
174
    test "every published lane carries a pricing block, because every one is priced" do
174
    test "the free router declares its zero price" do
175
      free_router = Enum.find(Models.catalog(), &(&1["id"] == "openrouter/free"))
176
177
      assert free_router["pricing"]["id"] == "declared.openrouter-free.v1"
178
      assert free_router["pricing"]["input_per_million_tokens"] == 0
179
      assert free_router["pricing"]["output_per_million_tokens"] == 0
180
      assert free_router["pricing_basis"] == "declared"
181
    end
182
183
    test "each catalog lane carries a pricing block" do
175 184
      for entry <- Models.catalog() do
176 185
        assert Map.has_key?(entry, "pricing")
177
        assert entry["pricing_basis"] == "provisional"
178 186
      end
179 187
    end
180 188
test/openagents/inference/pricing_test.exs modified +8 -12

@@ -136,23 +136,19 @@ defmodule OpenAgents.Inference.PricingTest do

136 136
  end
137 137
138 138
  describe "the catalog this deployment actually ships" do
139
    test "no lane claims declared rates, because none has been given any" do
140
      # The moment an operator enters real rates and sets `source: :declared`,
141
      # this test fails and whoever did it has to say so here. That is the
142
      # point: turning a lane billable is a decision, not a config typo.
139
    test "only the free router claims declared zero rates" do
143 140
      bases = Enum.map(OpenAgents.Inference.Models.catalog(), & &1["pricing_basis"])
144 141
145
      assert "declared" not in bases
142
      free_router =
143
        Enum.find(OpenAgents.Inference.Models.catalog(), &(&1["id"] == "openrouter/free"))
144
145
      assert Enum.count(bases, &(&1 == "declared")) == 1
146 146
      assert "provisional" in bases
147
      assert free_router["pricing"]["input_per_million_tokens"] == 0
148
      assert free_router["pricing"]["output_per_million_tokens"] == 0
147 149
    end
148 150
149
    test "every shipped lane carries rates, so none of them reads as unpriced" do
150
      # This was not true while `gpt-5.6-luna` was admitted, and it is the one
151
      # thing that changed when it was withdrawn. `unpriced` did not stop being
152
      # reachable — a gateway fallback that answers with a model this catalog
153
      # does not admit still produces it, which is what
154
      # `OpenAgentsWeb.InferenceProxyFallbackTest` proves — but no model a
155
      # caller may select is in that state.
151
    test "every shipped lane carries rates, so none reads as unpriced" do
156 152
      bases = Enum.map(OpenAgents.Inference.Models.catalog(), & &1["pricing_basis"])
157 153
158 154
      assert "unpriced" not in bases
test/openagents_web/controllers/inference_proxy_controller_test.exs modified +5 -1

@@ -257,7 +257,11 @@ defmodule OpenAgentsWeb.InferenceProxyControllerTest do

257 257
258 258
    on_exit(fn -> Application.put_env(:openagents, :vercel_gateway_provider, previous) end)
259 259
260
    conn = post_chat(conn, token, %{"messages" => [%{"role" => "user", "content" => "hi"}]})
260
    conn =
261
      post_chat(conn, token, %{
262
        "model" => "glm-5.3-flash",
263
        "messages" => [%{"role" => "user", "content" => "hi"}]
264
      })
261 265
262 266
    assert conn.status == 503
263 267
    assert Jason.decode!(conn.resp_body)["error"]["code"] == "model_unavailable"
test/openagents_web/live/model_catalog_live_test.exs modified +9 -7

@@ -4,10 +4,9 @@ defmodule OpenAgentsWeb.ModelCatalogLiveTest do

4 4
  (#200, METER-001).
5 5
6 6
  The endpoint answered the CLI; nothing rendered the catalog for a human. What
7
  matters here is not that the page exists but that it carries the same absence
8
  the endpoint does: an unpriced lane shows the word rather than `$0.00`, and a
9
  deployment with no declared rates says so at the top instead of presenting a
10
  column of working figures as a price list.
7
  matters here is not that the page exists but that it carries the same pricing
8
  truth as the endpoint does: an unpriced lane shows the word rather than
9
  `$0.00`, while the declared Coder Free router shows its zero rate.
11 10
  """
12 11
  use OpenAgentsWeb.ConnCase, async: false
13 12

@@ -64,10 +63,13 @@ defmodule OpenAgentsWeb.ModelCatalogLiveTest do

64 63
             "placeholder.gemini-3.7-flash.v1"
65 64
  end
66 65
67
  test "the page says nothing is billable while no lane declares its rates", %{conn: conn} do
68
    {:ok, view, _html} = live(reader(conn, "model-catalog-not-billable"), ~p"/models")
66
  test "the declared free router is billable at zero", %{conn: conn} do
67
    {:ok, view, _html} = live(reader(conn, "model-catalog-free"), ~p"/models")
69 68
70
    assert has_element?(view, "#model-catalog-not-billable")
69
    refute has_element?(view, "#model-catalog-not-billable")
70
    assert view |> element(~s([id="model-basis-openrouter/free"])) |> render() =~ "declared"
71
    assert view |> element(~s([id="model-input-rate-openrouter/free"])) |> render() =~ "$0.00"
72
    assert view |> element(~s([id="model-output-rate-openrouter/free"])) |> render() =~ "$0.00"
71 73
  end
72 74
73 75
  test "the billable notice is derived from the catalog, not written into the page",

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