package api import ( "database/sql" "time" "github.com/bigtux/ophion/internal/aiapm" "github.com/gofiber/fiber/v2" ) // AIAPMHandlers holds dependencies for AI APM route handlers type AIAPMHandlers struct { db *sql.DB collector *aiapm.Collector } // RegisterAIAPMRoutes registers all AI APM routes on the given router func RegisterAIAPMRoutes(api fiber.Router, db *sql.DB) *aiapm.Collector { collector := aiapm.NewCollector(db, 5000) h := &AIAPMHandlers{db: db, collector: collector} g := api.Group("/ai-apm") g.Post("/ingest", h.Ingest) g.Get("/summary", h.Summary) g.Get("/models", h.Models) g.Get("/vendors", h.Vendors) g.Get("/costs", h.Costs) g.Get("/calls", h.Calls) g.Get("/pricing", h.Pricing) return collector } // Ingest receives AI call records (single or batch) func (h *AIAPMHandlers) Ingest(c *fiber.Ctx) error { var req aiapm.IngestRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "invalid request body: " + err.Error()}) } count := 0 if req.Call != nil { h.collector.Collect(*req.Call) count = 1 } if len(req.Calls) > 0 { h.collector.CollectBatch(req.Calls) count += len(req.Calls) } if count == 0 { return c.Status(400).JSON(fiber.Map{"error": "no call records provided; use 'call' or 'calls' field"}) } return c.JSON(fiber.Map{"status": "accepted", "count": count}) } // parseFilter extracts common filter parameters from query string func parseFilter(c *fiber.Ctx) aiapm.AICallFilter { f := aiapm.AICallFilter{ ServiceName: c.Query("service"), ProjectID: c.Query("project"), Vendor: c.Query("vendor"), Model: c.Query("model"), Status: c.Query("status"), } if from := c.Query("from"); from != "" { if t, err := time.Parse(time.RFC3339, from); err == nil { f.From = t } } if to := c.Query("to"); to != "" { if t, err := time.Parse(time.RFC3339, to); err == nil { f.To = t } } if f.From.IsZero() { f.From = time.Now().Add(-24 * time.Hour) } if f.To.IsZero() { f.To = time.Now() } f.Limit = c.QueryInt("limit", 100) f.Offset = c.QueryInt("offset", 0) return f } // Summary returns aggregated usage statistics func (h *AIAPMHandlers) Summary(c *fiber.Ctx) error { filter := parseFilter(c) summary, err := aiapm.GetUsageSummary(h.db, filter) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(summary) } // Models returns per-model statistics func (h *AIAPMHandlers) Models(c *fiber.Ctx) error { filter := parseFilter(c) stats, err := aiapm.GetModelStats(h.db, filter) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"models": stats}) } // Vendors returns per-vendor statistics func (h *AIAPMHandlers) Vendors(c *fiber.Ctx) error { filter := parseFilter(c) stats, err := aiapm.GetVendorStats(h.db, filter) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"vendors": stats}) } // Costs returns cost timeseries data func (h *AIAPMHandlers) Costs(c *fiber.Ctx) error { filter := parseFilter(c) interval := c.Query("interval", "1d") points, err := aiapm.GetCostTimeseries(h.db, filter, interval) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"timeseries": points, "interval": interval}) } // Calls returns recent AI call records (paginated) func (h *AIAPMHandlers) Calls(c *fiber.Ctx) error { filter := parseFilter(c) calls, err := aiapm.QueryCalls(h.db, filter) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"calls": calls, "count": len(calls)}) } // Pricing returns the current pricing table func (h *AIAPMHandlers) Pricing(c *fiber.Ctx) error { return c.JSON(fiber.Map{"pricing": aiapm.GetPricingTable()}) }