test/openagents/providers/open_router_test.exs

main at 58e6347eeb72 · 3 KB

defmodule OpenAgents.Providers.OpenRouterTest do
  use ExUnit.Case, async: true

  alias OpenAgents.Providers.{OpenRouter, Request}

  setup {Req.Test, :verify_on_exit!}

  defp request do
    %Request{
      model_id: "z-ai/glm-5.3-flash",
      instructions: "Remain OpenAgents.",
      input: [%{role: "user", content: "Say hello."}]
    }
  end

  defp collect(options) do
    parent = self()
    result = OpenRouter.stream(request(), &send(parent, {:event, &1}), options)
    {result, drain([])}
  end

  defp drain(events) do
    receive do
      {:event, event} -> drain([event | events])
    after
      0 -> Enum.reverse(events)
    end
  end

  test "sends the payload with the server's credential and emits the decoded stream" do
    test_process = self()

    Req.Test.expect(__MODULE__, fn conn ->
      {:ok, body, conn} = Plug.Conn.read_body(conn)
      send(test_process, {:outbound, Plug.Conn.get_req_header(conn, "authorization"), body})

      conn
      |> Plug.Conn.put_resp_content_type("text/event-stream")
      |> Plug.Conn.send_resp(200, sse_stream())
    end)

    assert {:ok, events} =
             collect(
               api_key: "sentinel-openrouter-key",
               request_options: [plug: {Req.Test, __MODULE__}]
             )

    assert_received {:outbound, ["Bearer sentinel-openrouter-key"], body}
    assert Jason.decode!(body)["model"] == "z-ai/glm-5.3-flash"

    assert events == [
             {:response_started, "gen-1"},
             {:text_delta, "Hello."},
             {:usage, %{"input_tokens" => 3, "output_tokens" => 1, "total_tokens" => 4}},
             {:response_completed, "gen-1"}
           ]
  end

  test "an HTTP failure is a bounded reason, not a stream" do
    Req.Test.expect(__MODULE__, fn conn -> Plug.Conn.send_resp(conn, 429, "slow down") end)

    assert {{:error, {:http_status, 429}}, []} =
             collect(
               api_key: "sentinel-openrouter-key",
               request_options: [plug: {Req.Test, __MODULE__}]
             )
  end

  test "a missing credential is refused before any request is made" do
    assert {{:error, :missing_api_key}, []} = collect([])
  end

  defp sse_stream do
    frames = [
      %{"id" => "gen-1", "choices" => [%{"delta" => %{"content" => "Hello."}}]},
      %{"id" => "gen-1", "choices" => [%{"delta" => %{}, "finish_reason" => "stop"}]},
      %{
        "id" => "gen-1",
        "choices" => [],
        "usage" => %{"prompt_tokens" => 3, "completion_tokens" => 1, "total_tokens" => 4}
      }
    ]

    Enum.map_join(frames, "", &("data: " <> Jason.encode!(&1) <> "\n\n")) <> "data: [DONE]\n\n"
  end

  describe "how many tokens the answer may take" do
    test "comes from the model's catalog entry, not a literal in this module" do
      # GLM 5.3 Flash is a reasoning model: its thinking is charged against this
      # allowance before a word of the answer is. Hardcoded at 4,096, a child
      # agent with a real task spent the whole budget reasoning and returned an
      # empty 200 after three minutes, which read as the proxy having failed.
      request = %Request{
        model_id: "z-ai/glm-5.3-flash",
        instructions: "Be brief.",
        input: [%{role: "user", content: "hello"}],
        max_output: 64_000
      }

      assert OpenRouter.request_payload(request)[:max_tokens] == 64_000
    end

    test "defaults to a figure a caller that names none still works on" do
      request = %Request{
        model_id: "z-ai/glm-5.3-flash",
        instructions: "Be brief.",
        input: [%{role: "user", content: "hello"}]
      }

      assert OpenRouter.request_payload(request)[:max_tokens] == 4_096
    end
  end
end