Packages

A spec-compliant Open Responses server for Elixir and Phoenix, with pluggable provider adapters and first-class streaming.

Current section

Files

Jump to
open_responses lib open_responses_web live compliance_live.ex
Raw

lib/open_responses_web/live/compliance_live.ex

defmodule OpenResponsesWeb.ComplianceLive do
@moduledoc """
Interactive compliance tester matching https://www.openresponses.org/compliance
Point it at any running Open Responses server, enter an API key and model,
and run the official test suite against the live endpoint.
"""
use OpenResponsesWeb, :live_view
@test_cases [
%{id: "basic-text-response", name: "Basic Text Response",
description: "Non-streaming response with a simple user message."},
%{id: "streaming-response", name: "Streaming Response",
description: "SSE stream delivers events and ends with response.completed."},
%{id: "system-prompt", name: "System Prompt",
description: "System prompt is included in the input and respected."},
%{id: "tool-calling", name: "Tool Calling",
description: "Model emits a function_call output item when given a matching tool."},
%{id: "multi-turn", name: "Multi-turn Conversation",
description: "Model recalls context supplied earlier in the same input."},
%{id: "previous-response-id", name: "previous_response_id",
description: "Second request recalls context from the first via previous_response_id."}
]
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
base_url = OpenResponsesWeb.Endpoint.url() <> "/v1"
{:ok,
assign(socket,
base_url: base_url,
api_key: "",
model: "gpt-4o-mini",
results: %{},
running: false,
running_id: nil,
test_cases: @test_cases
)}
end
@impl Phoenix.LiveView
def handle_event("update_field", %{"field" => field, "value" => value}, socket) do
{:noreply, assign(socket, String.to_existing_atom(field), value)}
end
def handle_event("run_all", _params, socket) do
send(self(), :run_next)
{:noreply, assign(socket, running: true, results: %{}, running_id: nil)}
end
def handle_event("run_one", %{"id" => id}, socket) do
send(self(), {:run_one, id})
{:noreply, assign(socket, running: true, running_id: id)}
end
@impl Phoenix.LiveView
def handle_info(:run_next, socket) do
pending =
@test_cases
|> Enum.map(& &1.id)
|> Enum.reject(&Map.has_key?(socket.assigns.results, &1))
case pending do
[] ->
{:noreply, assign(socket, running: false, running_id: nil)}
[next | _] ->
result = execute_test(next, socket.assigns)
results = Map.put(socket.assigns.results, next, result)
send(self(), :run_next)
{:noreply, assign(socket, results: results, running_id: next)}
end
end
def handle_info({:run_one, id}, socket) do
result = execute_test(id, socket.assigns)
results = Map.put(socket.assigns.results, id, result)
{:noreply, assign(socket, results: results, running: false, running_id: nil)}
end
defp execute_test(id, assigns) do
config = %{base_url: assigns.base_url, api_key: assigns.api_key, model: assigns.model}
try do
response =
if id == "previous-response-id" do
run_multi_step(config)
else
post_request(config, build_request(id, assigns.model))
end
case response do
{:ok, body} ->
case validate_response(id, body) do
{:pass, _} -> %{status: :pass, response: body}
{:fail, reason} -> %{status: :fail, reason: reason, response: body}
end
{:error, reason} ->
%{status: :error, reason: inspect(reason)}
end
rescue
e -> %{status: :error, reason: Exception.message(e)}
end
end
defp build_request("basic-text-response", model) do
%{
model: model,
input: [%{type: "message", role: "user", content: "Say hello in exactly 3 words."}],
stream: false
}
end
defp build_request("streaming-response", model) do
%{
model: model,
input: [%{type: "message", role: "user", content: "Count from 1 to 5."}],
stream: true
}
end
defp build_request("system-prompt", model) do
%{
model: model,
input: [
%{type: "message", role: "system", content: "You are a pirate. Always respond in pirate speak."},
%{type: "message", role: "user", content: "Say hello."}
],
stream: false
}
end
defp build_request("tool-calling", model) do
%{
model: model,
input: [%{type: "message", role: "user", content: "What's the weather like in San Francisco?"}],
tools: [
%{
type: "function",
name: "get_weather",
description: "Get the current weather for a location",
parameters: %{
type: "object",
properties: %{location: %{type: "string", description: "City and state"}},
required: ["location"]
}
}
],
stream: false
}
end
defp build_request("multi-turn", model) do
%{
model: model,
input: [
%{type: "message", role: "user", content: "My name is Alice."},
%{type: "message", role: "assistant", content: "Hello Alice! Nice to meet you."},
%{type: "message", role: "user", content: "What is my name?"}
],
stream: false
}
end
defp validate_response("streaming-response", result) do
events = result["events"] || []
last = List.last(events) || %{}
cond do
events == [] -> {:fail, "No SSE events received"}
last["type"] != "response.completed" -> {:fail, "Last event was #{inspect(last["type"])}, expected response.completed"}
true -> {:pass, nil}
end
end
defp validate_response("tool-calling", response) do
function_calls = Enum.filter(response["output"] || [], &(&1["type"] == "function_call"))
cond do
response["status"] != "completed" -> {:fail, "Expected status \"completed\", got #{inspect(response["status"])}"}
function_calls == [] -> {:fail, "Expected at least one function_call output item"}
true -> {:pass, nil}
end
end
defp validate_response("multi-turn", response) do
text = extract_text(response)
cond do
response["status"] != "completed" -> {:fail, "Expected status \"completed\", got #{inspect(response["status"])}"}
not String.contains?(text, "alice") -> {:fail, "Model did not recall the name Alice from conversation history"}
true -> {:pass, nil}
end
end
defp validate_response("previous-response-id", response) do
text = extract_text(response)
cond do
response["status"] != "completed" -> {:fail, "Expected status \"completed\", got #{inspect(response["status"])}"}
not String.contains?(text, "zephyr") -> {:fail, "Model did not recall the secret word from the previous response"}
true -> {:pass, nil}
end
end
defp validate_response(_id, response) do
cond do
response["status"] != "completed" -> {:fail, "Expected status \"completed\", got #{inspect(response["status"])}"}
not is_list(response["output"]) or response["output"] == [] -> {:fail, "Expected at least one output item"}
true -> {:pass, nil}
end
end
defp extract_text(response) do
(response["output"] || [])
|> Enum.filter(&(&1["type"] == "message"))
|> Enum.flat_map(fn item ->
content = item["content"] || []
if is_list(content), do: Enum.map(content, & &1["text"]), else: [content]
end)
|> Enum.join("")
|> String.downcase()
end
defp run_multi_step(config) do
with {:ok, first} <- post_request(config, %{
model: config.model,
input: [%{type: "message", role: "user", content: "My secret word is Zephyr."}],
stream: false
}),
id when is_binary(id) <- first["id"] do
post_request(config, %{
model: config.model,
previous_response_id: id,
input: [%{type: "message", role: "user", content: "What was my secret word?"}],
stream: false
})
else
_ -> {:error, "First request did not return a valid id"}
end
end
defp post_request(%{base_url: base_url, api_key: api_key}, body) do
headers = if api_key != "", do: [{"authorization", "Bearer #{api_key}"}], else: []
url = String.trim_trailing(base_url, "/") <> "/responses"
case Req.post(url, json: body, headers: headers, receive_timeout: 60_000) do
{:ok, %{status: 200, body: body}} when is_map(body) ->
{:ok, body}
{:ok, %{status: 200, body: body}} ->
case Jason.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, _} -> {:ok, parse_sse_response(body)}
end
{:ok, %{status: status, body: body}} ->
{:error, "HTTP #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
defp parse_sse_response(body) when is_binary(body) do
events =
body
|> String.split("\n\n")
|> Enum.flat_map(fn chunk ->
lines = String.split(chunk, "\n")
event_type =
Enum.find_value(lines, fn line ->
case String.split(line, "event: ", parts: 2) do
[_, type] -> String.trim(type)
_ -> nil
end
end)
data_json =
Enum.find_value(lines, fn line ->
case String.split(line, "data: ", parts: 2) do
[_, "[DONE]"] -> nil
[_, json] -> json
_ -> nil
end
end)
case data_json && Jason.decode(data_json) do
{:ok, event} ->
event =
if event_type && !Map.has_key?(event, "type"),
do: Map.put(event, "type", event_type),
else: event
[event]
_ ->
[]
end
end)
%{"events" => events}
end
@impl Phoenix.LiveView
def render(assigns) do
~H"""
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
.compliance-root {
font-family: 'IBM Plex Sans', sans-serif;
background: #0a0a0a;
min-height: 100vh;
padding: 2.5rem 1.5rem;
color: #e4e4e7;
}
.compliance-header { margin-bottom: 2.5rem; }
.compliance-header h1 {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.5rem;
font-weight: 600;
color: #f4f4f5;
letter-spacing: -0.02em;
margin-bottom: 0.4rem;
}
.compliance-header p {
font-size: 0.875rem;
color: #71717a;
line-height: 1.6;
}
.compliance-header a { color: #a1a1aa; text-decoration: underline; }
.compliance-header a:hover { color: #e4e4e7; }
.config-card {
background: #111111;
border: 1px solid #27272a;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.config-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 640px) {
.config-grid { grid-template-columns: 1fr 1fr 1fr; }
}
.field-label {
display: block;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #52525b;
margin-bottom: 0.4rem;
}
.field-input {
width: 100%;
background: #1c1c1e;
border: 1px solid #3f3f46;
border-radius: 5px;
padding: 0.5rem 0.75rem;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8125rem;
color: #e4e4e7;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
box-sizing: border-box;
}
.field-input::placeholder { color: #3f3f46; }
.field-input:focus {
border-color: #52525b;
box-shadow: 0 0 0 3px rgba(82,82,91,0.2);
}
.actions-row {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1.25rem;
}
.btn-run-all {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8125rem;
font-weight: 500;
background: #18181b;
color: #e4e4e7;
border: 1px solid #3f3f46;
border-radius: 5px;
padding: 0.5rem 1.25rem;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
white-space: nowrap;
}
.btn-run-all:hover:not(:disabled) {
background: #27272a;
border-color: #52525b;
}
.btn-run-all:disabled { opacity: 0.4; cursor: not-allowed; }
.score-badge {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: #52525b;
}
.score-badge .score-pass { color: #22c55e; }
.test-list { display: flex; flex-direction: column; gap: 0.5rem; }
.test-row {
background: #111111;
border: 1px solid #27272a;
border-radius: 6px;
display: flex;
align-items: stretch;
overflow: hidden;
transition: border-color 0.2s;
}
.test-row.state-idle { border-color: #27272a; }
.test-row.state-running { border-color: #854d0e; }
.test-row.state-pass { border-color: #166534; }
.test-row.state-fail { border-color: #991b1b; }
.test-row.state-error { border-color: #92400e; }
.test-indicator {
width: 3px;
flex-shrink: 0;
background: #27272a;
transition: background 0.2s;
}
.state-running .test-indicator { background: #f59e0b; }
.state-pass .test-indicator { background: #22c55e; }
.state-fail .test-indicator { background: #ef4444; }
.state-error .test-indicator { background: #f97316; }
.test-body {
flex: 1;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 0.875rem 1rem;
}
.test-icon {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.875rem;
width: 1.25rem;
flex-shrink: 0;
margin-top: 1px;
text-align: center;
color: #52525b;
transition: color 0.2s;
}
.state-running .test-icon { color: #f59e0b; }
.state-pass .test-icon { color: #22c55e; }
.state-fail .test-icon { color: #ef4444; }
.state-error .test-icon { color: #f97316; }
.test-content { flex: 1; }
.test-name {
font-family: 'IBM Plex Sans', sans-serif;
font-size: 0.875rem;
font-weight: 500;
color: #e4e4e7;
}
.test-desc {
font-size: 0.75rem;
color: #52525b;
margin-top: 0.125rem;
line-height: 1.5;
}
.test-reason {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
margin-top: 0.375rem;
padding: 0.375rem 0.5rem;
border-radius: 3px;
line-height: 1.5;
}
.state-fail .test-reason { color: #fca5a5; background: rgba(239,68,68,0.08); }
.state-error .test-reason { color: #fdba74; background: rgba(249,115,22,0.08); }
.btn-run-one {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
color: #52525b;
background: none;
border: 1px solid #27272a;
border-radius: 4px;
padding: 0.25rem 0.625rem;
cursor: pointer;
white-space: nowrap;
align-self: flex-start;
transition: color 0.15s, border-color 0.15s;
flex-shrink: 0;
}
.btn-run-one:hover:not(:disabled) { color: #a1a1aa; border-color: #52525b; }
.btn-run-one:disabled { opacity: 0.3; cursor: not-allowed; }
</style>
<div class="compliance-root">
<div style="max-width: 48rem; margin: 0 auto;">
<div class="compliance-header">
<h1>compliance tester</h1>
<p>
Test any Open Responses server against the official specification.
Mirrors <a href="https://www.openresponses.org/compliance" target="_blank">openresponses.org/compliance</a>.
</p>
</div>
<div class="config-card">
<div class="config-grid">
<div>
<label class="field-label">Base URL</label>
<input
type="text"
value={@base_url}
phx-blur="update_field"
phx-value-field="base_url"
class="field-input"
/>
</div>
<div>
<label class="field-label">API Key</label>
<input
type="password"
value={@api_key}
phx-blur="update_field"
phx-value-field="api_key"
placeholder="sk-… optional for local"
class="field-input"
/>
</div>
<div>
<label class="field-label">Model</label>
<input
type="text"
value={@model}
phx-blur="update_field"
phx-value-field="model"
placeholder="gpt-4o-mini"
class="field-input"
/>
</div>
</div>
<div class="actions-row">
<button
phx-click="run_all"
disabled={@running}
class="btn-run-all"
>
<%= if @running, do: "running…", else: "run all tests" %>
</button>
<%= if map_size(@results) > 0 do %>
<span class="score-badge">
<span class="score-pass"><%= Enum.count(@results, fn {_, r} -> r.status == :pass end) %></span>
/ <%= map_size(@results) %> passed
</span>
<% end %>
</div>
</div>
<div class="test-list">
<%= for test <- @test_cases do %>
<% result = Map.get(@results, test.id) %>
<% is_running = @running_id == test.id %>
<div class={["test-row", row_state_class(result, is_running)]}>
<div class="test-indicator"></div>
<div class="test-body">
<div class="test-icon"><%= result_icon(result, is_running) %></div>
<div class="test-content">
<div class="test-name"><%= test.name %></div>
<div class="test-desc"><%= test.description %></div>
<%= if result && result.status in [:fail, :error] do %>
<div class="test-reason"><%= result.reason %></div>
<% end %>
</div>
<button
phx-click="run_one"
phx-value-id={test.id}
disabled={@running}
class="btn-run-one"
>
run
</button>
</div>
</div>
<% end %>
</div>
</div>
</div>
"""
end
defp row_state_class(nil, true), do: "state-running"
defp row_state_class(nil, false), do: "state-idle"
defp row_state_class(%{status: :pass}, _), do: "state-pass"
defp row_state_class(%{status: :fail}, _), do: "state-fail"
defp row_state_class(%{status: :error}, _), do: "state-error"
defp result_icon(nil, true), do: "›"
defp result_icon(nil, false), do: "·"
defp result_icon(%{status: :pass}, _), do: "✓"
defp result_icon(%{status: :fail}, _), do: "✗"
defp result_icon(%{status: :error}, _), do: "!"
end