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