test/openagents/chat/open_router/responses_stream_decoder_test.exs

58e6347eeb72 · 3 KB

defmodule OpenAgents.Chat.OpenRouter.ResponsesStreamDecoderTest do
  use ExUnit.Case, async: true

  alias OpenAgents.Chat.OpenRouter.ResponsesStreamDecoder

  test "assembles completed tool output from item events and argument deltas" do
    reasoning = %{
      "type" => "reasoning",
      "id" => "rs_test",
      "status" => "completed",
      "summary" => [%{"type" => "summary_text", "text" => "Use the demo tool."}],
      "encrypted_content" => "encrypted-reasoning"
    }

    function_call = %{
      "type" => "function_call",
      "id" => "fc_test",
      "call_id" => "call_test",
      "name" => "get_demo_time",
      "status" => "completed"
    }

    stream =
      frame(%{
        "type" => "response.output_item.done",
        "output_index" => 0,
        "item" => reasoning
      }) <>
        frame(%{
          "type" => "response.output_item.added",
          "output_index" => 1,
          "item" => Map.put(function_call, "arguments", "")
        }) <>
        frame(%{
          "type" => "response.function_call_arguments.delta",
          "item_id" => "fc_test",
          "delta" => "{"
        }) <>
        frame(%{
          "type" => "response.function_call_arguments.delta",
          "item_id" => "fc_test",
          "delta" => "}"
        }) <>
        frame(%{
          "type" => "response.output_item.done",
          "output_index" => 1,
          "item" => function_call
        }) <>
        frame(%{
          "type" => "response.completed",
          "response" => %{
            "id" => "resp_test",
            "object" => "response",
            "status" => "completed",
            "model" => "z-ai/glm-5.3-flash"
          }
        })

    assert {:ok, state, []} = ResponsesStreamDecoder.feed(ResponsesStreamDecoder.new(), stream)

    expected_call = Map.put(function_call, "arguments", "{}")

    assert {:ok,
            %{
              "output" => [^reasoning, ^expected_call],
              "reasoning_items" => [^reasoning],
              "tool_calls" => [
                %{
                  "id" => "fc_test",
                  "call_id" => "call_test",
                  "name" => "get_demo_time",
                  "arguments" => "{}"
                }
              ]
            }} = ResponsesStreamDecoder.finish(state)
  end

  test "returns structured failure and incomplete response details" do
    failed =
      frame(%{
        "type" => "response.failed",
        "error_type" => "authentication",
        "response" => %{
          "id" => "resp_failed",
          "error" => %{"code" => "server_error", "message" => "Invalid credentials"}
        }
      })

    assert {:error, {:provider_error, "authentication", "Invalid credentials"}} =
             ResponsesStreamDecoder.feed(ResponsesStreamDecoder.new(), failed)

    incomplete =
      frame(%{
        "type" => "response.incomplete",
        "response" => %{
          "id" => "resp_incomplete",
          "incomplete_details" => %{"reason" => "max_output_tokens"}
        }
      })

    assert {:error, {:provider_error, "max_output_tokens", nil}} =
             ResponsesStreamDecoder.feed(ResponsesStreamDecoder.new(), incomplete)
  end

  defp frame(value), do: "data: " <> Jason.encode!(value) <> "\n\n"
end