symbiont_ex/lib/symbiont/api.ex

120 lines
3.2 KiB
Elixir

defmodule Symbiont.API do
@moduledoc """
HTTP API for Symbiont — drop-in replacement for the Python FastAPI server.
Endpoints:
POST /task — Submit and execute a task immediately
POST /queue — Add a task to the persistent queue
GET /status — Health check + system overview
GET /ledger — Recent inference calls
GET /ledger/stats — Aggregate cost & usage
GET /health — Simple liveness probe
"""
use Plug.Router
plug(Plug.Logger)
plug(:match)
plug(Plug.Parsers, parsers: [:json], json_decoder: Jason)
plug(:dispatch)
# -- POST /task --
post "/task" do
task = conn.body_params["task"]
force_tier = conn.body_params["force_tier"]
if is_nil(task) or task == "" do
send_json(conn, 400, %{error: "missing 'task' field"})
else
opts = if force_tier, do: [force_tier: force_tier], else: []
case Symbiont.Router.route_and_execute(task, opts) do
{:ok, result} ->
response = %{
id: "task-#{System.system_time(:second)}",
task: task,
model: result.model,
result: result.result,
elapsed_seconds: result.elapsed_seconds,
input_tokens: result.input_tokens,
output_tokens: result.output_tokens,
estimated_cost_usd: result.estimated_cost_usd,
timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
}
send_json(conn, 200, response)
{:error, reason} ->
send_json(conn, 500, %{error: inspect(reason)})
end
end
end
# -- POST /queue --
post "/queue" do
task = conn.body_params["task"]
priority = conn.body_params["priority"] || "normal"
if is_nil(task) or task == "" do
send_json(conn, 400, %{error: "missing 'task' field"})
else
{:ok, task_id} = Symbiont.Queue.enqueue(task, priority)
queue_size = Symbiont.Queue.size()
send_json(conn, 200, %{
id: task_id,
status: "queued",
position: queue_size
})
end
end
# -- GET /status --
get "/status" do
ledger_stats = Symbiont.Ledger.stats()
queue_size = Symbiont.Queue.size()
last_heartbeat = Symbiont.Heartbeat.last_snapshot()
response = %{
status: "healthy",
runtime: "elixir/otp",
queue_size: queue_size,
last_heartbeat: last_heartbeat && last_heartbeat["timestamp"],
total_calls: ledger_stats["total_calls"],
total_cost_estimated_usd: ledger_stats["total_cost_estimated_usd"],
by_model: ledger_stats["by_model"]
}
send_json(conn, 200, response)
end
# -- GET /ledger --
get "/ledger" do
entries = Symbiont.Ledger.recent(50)
send_json(conn, 200, %{entries: entries, count: length(entries)})
end
# -- GET /ledger/stats --
get "/ledger/stats" do
stats = Symbiont.Ledger.stats()
send_json(conn, 200, stats)
end
# -- GET /health --
get "/health" do
send_json(conn, 200, %{status: "ok", runtime: "elixir/otp"})
end
# -- Fallback --
match _ do
send_json(conn, 404, %{error: "not found"})
end
# -- Helpers --
defp send_json(conn, status, body) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
end