120 lines
3.2 KiB
Elixir
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
|