Tell the caller which class of provider failure it hit

5b465f014aaa · AtlantisPleb · · parent aac22de2adb0

Tell the caller which class of provider failure it hit

A coder session that lost two turns in a row was told only "The model
provider failed. The call was not completed." twice. The reason had been
computed and logged on the server and then dropped: the caller got the
code `provider_failed` and nothing else, which reads as the product being
broken rather than as a call that ran out of context or hit a rate limit.

`OperationalLog.code/1` takes the reason's atom tag and bounds it to 64
characters, so it carries no provider text, no prompt, and no credential.
It is the same bounded word already written to the log. Send it as
`error.reason` beside the code.

The existing test still holds the boundary: the test provider fails with
`{:provider_failed, "test_failure"}`, and `test_failure` stays out of the
response body while the tag reaches it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012TRDRrfL1khQhQtNr3SRrA
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.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 311 · 2026-08-24T21:12:43.714905Z

Changed files

  • modified lib/openagents_web/controllers/inference_proxy_controller.ex
  • modified test/openagents_web/controllers/inference_proxy_controller_test.exs

Diff

2 files changed, +20 -3

lib/openagents_web/controllers/inference_proxy_controller.ex modified +13 -2

@@ -202,8 +202,9 @@ defmodule OpenAgentsWeb.InferenceProxyController do

202 202
        # sees a provider error, never raw provider detail.
203 203
        usage = usage_of(events)
204 204
        if usage != %{}, do: meter(grant, usage)
205
        Logger.warning("inference_proxy_failed code=#{OpenAgents.OperationalLog.code(reason)}")
206
        refuse(conn, :provider_failed)
205
        class = OpenAgents.OperationalLog.code(reason)
206
        Logger.warning("inference_proxy_failed code=#{class}")
207
        refuse(conn, {:provider_failed, class})
207 208
    end
208 209
  end
209 210

@@ -346,6 +347,16 @@ defmodule OpenAgentsWeb.InferenceProxyController do

346 347
     }}
347 348
  end
348 349
350
  # The failure class travels with the refusal. `OperationalLog.code/1` takes
351
  # only the reason's atom tag and bounds it to 64 characters, so it carries no
352
  # provider text, no prompt, and no credential — it is the same bounded word
353
  # the server logs. Withholding it left the caller with "the model provider
354
  # failed" and nothing to act on, which reads as the product being broken
355
  # rather than as a call that ran out of context or hit a rate limit.
356
  defp error_for({:provider_failed, class}) do
357
    {502, %{"code" => "provider_failed", "reason" => class}}
358
  end
359
349 360
  defp error_for(reason) do
350 361
    {status, code} = status_for(reason)
351 362
    {status, %{"code" => code}}
test/openagents_web/controllers/inference_proxy_controller_test.exs modified +7 -1

@@ -295,7 +295,13 @@ defmodule OpenAgentsWeb.InferenceProxyControllerTest do

295 295
    assert conn.status == 502
296 296
    body = Jason.decode!(conn.resp_body)
297 297
    assert body["error"]["code"] == "provider_failed"
298
    # No raw provider detail leaks.
298
299
    # The failure class travels with the refusal so a client can say more than
300
    # "something went wrong", but it is the reason's atom tag only.
301
    assert body["error"]["reason"] == "provider_failed"
302
303
    # No raw provider detail leaks. `OperationalLog.code/1` takes the tag and
304
    # drops the detail, which is what keeps the line above safe to send.
299 305
    refute conn.resp_body =~ "test_failure"
300 306
  end
301 307

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