summaryrefslogtreecommitdiff
path: root/internal/adminapi/model_page.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/adminapi/model_page.go276
1 files changed, 276 insertions, 0 deletions
diff --git a/internal/adminapi/model_page.go b/internal/adminapi/model_page.go
new file mode 100644
index 0000000..5a94564
--- /dev/null
+++ b/internal/adminapi/model_page.go
@@ -0,0 +1,276 @@
+package adminapi
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "aigw/internal/controlplane"
+)
+
+type publicModelExample struct {
+ Name string
+ Endpoint string
+ Code string
+}
+
+type publicModelPageData struct {
+ Model controlplane.PublicModel
+ Title string
+ Description string
+ CanonicalURL string
+ CatalogURL string
+ SignInURL string
+ RegistrationURL string
+ RegistrationEnabled bool
+ CSSURL string
+ HealthLabel string
+ HealthClass string
+ InputPrice string
+ OutputPrice string
+ CacheReadPrice string
+ ContextWindow string
+ MaxOutputTokens string
+ ReleasedAt string
+ Tags []string
+ Examples []publicModelExample
+}
+
+var publicModelTemplate = template.Must(template.New("public-model").Parse(`<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="description" content="{{.Description}}">
+ <meta property="og:type" content="website">
+ <meta property="og:title" content="{{.Title}}">
+ <meta property="og:description" content="{{.Description}}">
+ <meta property="og:url" content="{{.CanonicalURL}}">
+ <link rel="canonical" href="{{.CanonicalURL}}">
+ <title>{{.Title}}</title>
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23102a3a'/%3E%3Ctext x='16' y='22' text-anchor='middle' font-family='Arial' font-weight='700' font-size='18' fill='%23b8eef7'%3EA%3C/text%3E%3C/svg%3E">
+ <link rel="stylesheet" href="{{.CSSURL}}">
+</head>
+<body>
+ <header class="catalog-header">
+ <a class="catalog-brand" href="{{.CatalogURL}}" aria-label="AIGW model catalog"><span>A</span><strong>AIGW</strong><small>MODEL CATALOG</small></a>
+ <nav aria-label="Account access"><a class="button secondary" href="{{.SignInURL}}">Sign in</a>{{if .RegistrationEnabled}}<a class="button primary" href="{{.RegistrationURL}}">Create account</a>{{end}}</nav>
+ </header>
+ <main class="model-page">
+ <a class="back-link" href="{{.CatalogURL}}">Back to models</a>
+ <header class="model-page-header">
+ <div><span class="eyebrow">{{if .Model.OwnedBy}}{{.Model.OwnedBy}}{{else}}INDEPENDENT{{end}}</span><h1>{{.Model.DisplayName}}</h1><code>{{.Model.PublicID}}</code></div>
+ <span class="health {{.HealthClass}}">{{.HealthLabel}}</span>
+ </header>
+ <p class="model-page-description">{{.Description}}</p>
+ <div class="detail-tags">{{range .Tags}}<span>{{.}}</span>{{end}}</div>
+
+ <div class="model-page-layout">
+ <section class="model-specs" aria-labelledby="model-specs-heading">
+ <h2 id="model-specs-heading">Model details</h2>
+ <dl class="detail-grid">
+ <div><dt>Input price</dt><dd>{{.InputPrice}} / 1M tokens</dd></div>
+ <div><dt>Output price</dt><dd>{{.OutputPrice}} / 1M tokens</dd></div>
+ <div><dt>Cached input</dt><dd>{{.CacheReadPrice}} / 1M tokens</dd></div>
+ <div><dt>Context window</dt><dd>{{.ContextWindow}} tokens</dd></div>
+ <div><dt>Max output</dt><dd>{{.MaxOutputTokens}} tokens</dd></div>
+ <div><dt>Regions</dt><dd>{{if .Model.Regions}}{{range $index,$region := .Model.Regions}}{{if $index}}, {{end}}{{$region}}{{end}}{{else}}Global{{end}}</dd></div>
+ <div><dt>Released</dt><dd>{{.ReleasedAt}}</dd></div>
+ <div><dt>Lifecycle</dt><dd>{{.Model.Lifecycle}}</dd></div>
+ </dl>
+ </section>
+ <aside class="model-start">
+ <span class="eyebrow">API ACCESS</span>
+ <h2>Start building</h2>
+ <code>{{.Model.PublicID}}</code>
+ <div class="model-start-actions"><a class="button secondary" href="{{.SignInURL}}">Sign in</a>{{if .RegistrationEnabled}}<a class="button primary" href="{{.RegistrationURL}}">Create account</a>{{end}}</div>
+ </aside>
+ </div>
+
+ <section class="code-examples" aria-labelledby="examples-heading">
+ <div><span class="eyebrow">SUPPORTED APIS</span><h2 id="examples-heading">Code examples</h2></div>
+ <div class="code-example-grid">{{range .Examples}}<article class="code-example"><header><strong>{{.Name}}</strong><code>{{.Endpoint}}</code></header><pre><code>{{.Code}}</code></pre></article>{{end}}</div>
+ </section>
+ </main>
+</body>
+</html>`))
+
+func (a *API) findPublicModel(ctx context.Context, wanted string) (controlplane.PublicModel, bool, error) {
+ models, err := a.store.ListPublicModels(ctx)
+ if err != nil {
+ return controlplane.PublicModel{}, false, err
+ }
+ a.addPublicModelHealth(models)
+ for _, item := range models {
+ if item.PublicID == wanted {
+ return item, true, nil
+ }
+ }
+ return controlplane.PublicModel{}, false, nil
+}
+
+func (a *API) publicModelPage(w http.ResponseWriter, r *http.Request) {
+ wanted := strings.Trim(strings.TrimSpace(r.PathValue("id")), "/")
+ model, found, err := a.findPublicModel(r.Context(), wanted)
+ if err != nil {
+ a.logger.Error("public_model_page_failed", "error", err)
+ http.Error(w, "Model catalog unavailable", http.StatusServiceUnavailable)
+ return
+ }
+ if !found {
+ http.NotFound(w, r)
+ return
+ }
+ data := buildPublicModelPageData(model, a.publicURL, a.inferencePublicURL, a.prefix, a.registrationEnabled)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "public, max-age=30, stale-while-revalidate=120")
+ if err := publicModelTemplate.Execute(w, data); err != nil {
+ a.logger.Error("render_public_model_page_failed", "model", model.PublicID, "error", err)
+ }
+}
+
+func buildPublicModelPageData(model controlplane.PublicModel, publicURL, inferenceBase, prefix string, registrationEnabled bool) publicModelPageData {
+ canonicalBase := strings.TrimRight(publicURL, "/")
+ pathBase := strings.TrimRight(prefix, "/")
+ if pathBase == "" {
+ pathBase = "/admin"
+ }
+ if canonicalBase == "" {
+ canonicalBase = pathBase
+ }
+ pathID := escapeModelPath(model.PublicID)
+ canonical := canonicalBase + "/models/" + pathID
+ description := strings.TrimSpace(model.Description)
+ if description == "" {
+ description = fmt.Sprintf("Use %s through the AIGW unified API.", model.PublicID)
+ }
+ tags := append([]string(nil), model.Capabilities...)
+ tags = append(tags, model.InputModalities...)
+ for _, wireAPI := range model.SupportedWireAPIs {
+ tags = append(tags, protocolLabel(wireAPI))
+ }
+ healthLabel, healthClass := publicHealthLabel(model)
+ return publicModelPageData{
+ Model: model, Title: model.DisplayName + " API, pricing, and context | AIGW", Description: description,
+ CanonicalURL: canonical, CatalogURL: pathBase + "/models", SignInURL: pathBase + "/",
+ RegistrationURL: pathBase + "/?auth=register&model=" + url.QueryEscape(model.PublicID), RegistrationEnabled: registrationEnabled,
+ CSSURL: pathBase + "/models.css", HealthLabel: healthLabel, HealthClass: healthClass,
+ InputPrice: formatMicros(model.InputPriceMicrosPerMillion, model.PriceCurrency),
+ OutputPrice: formatMicros(model.OutputPriceMicrosPerMillion, model.PriceCurrency),
+ CacheReadPrice: formatMicros(model.CacheReadPriceMicrosPerMillion, model.PriceCurrency),
+ ContextWindow: formatInteger(model.ContextWindow), MaxOutputTokens: formatInteger(model.MaxOutputTokens),
+ ReleasedAt: formatPublicDate(model.ReleasedAt), Tags: uniquePageStrings(tags),
+ Examples: publicModelExamples(model, inferenceBase),
+ }
+}
+
+func publicModelExamples(model controlplane.PublicModel, inferenceBase string) []publicModelExample {
+ base := strings.TrimRight(inferenceBase, "/")
+ if base == "" {
+ base = "https://api.example.com"
+ }
+ modelJSON, _ := json.Marshal(model.PublicID)
+ result := make([]publicModelExample, 0, len(model.SupportedWireAPIs))
+ for _, wireAPI := range model.SupportedWireAPIs {
+ var endpoint, code string
+ switch wireAPI {
+ case "chat_completions":
+ endpoint = "/v1/chat/completions"
+ code = fmt.Sprintf("curl %s%s \\\n -H 'Authorization: Bearer $AIGW_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"model\":%s,\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}'", base, endpoint, modelJSON)
+ case "responses":
+ endpoint = "/v1/responses"
+ code = fmt.Sprintf("curl %s%s \\\n -H 'Authorization: Bearer $AIGW_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"model\":%s,\"input\":\"Hello\"}'", base, endpoint, modelJSON)
+ case "embeddings":
+ endpoint = "/v1/embeddings"
+ code = fmt.Sprintf("curl %s%s \\\n -H 'Authorization: Bearer $AIGW_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"model\":%s,\"input\":[\"Text to embed\"]}'", base, endpoint, modelJSON)
+ case "messages":
+ endpoint = "/anthropic/v1/messages"
+ code = fmt.Sprintf("curl %s%s \\\n -H 'x-api-key: $AIGW_API_KEY' \\\n -H 'anthropic-version: 2023-06-01' \\\n -H 'Content-Type: application/json' \\\n -d '{\"model\":%s,\"max_tokens\":256,\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}'", base, endpoint, modelJSON)
+ default:
+ continue
+ }
+ result = append(result, publicModelExample{Name: protocolLabel(wireAPI), Endpoint: endpoint, Code: code})
+ }
+ return result
+}
+
+func escapeModelPath(publicID string) string {
+ parts := strings.Split(strings.Trim(publicID, "/"), "/")
+ for index := range parts {
+ parts[index] = url.PathEscape(parts[index])
+ }
+ return strings.Join(parts, "/")
+}
+
+func publicHealthLabel(model controlplane.PublicModel) (string, string) {
+ switch model.HealthStatus {
+ case "unavailable":
+ return "Unavailable", "unavailable"
+ case "degraded":
+ return fmt.Sprintf("%d/%d routes", model.AvailableProviderCount, model.ProviderCount), "degraded"
+ default:
+ return "Available", "available"
+ }
+}
+
+func protocolLabel(value string) string {
+ switch value {
+ case "chat_completions":
+ return "Chat Completions"
+ case "responses":
+ return "Responses"
+ case "embeddings":
+ return "Embeddings"
+ case "messages":
+ return "Anthropic Messages"
+ default:
+ return value
+ }
+}
+
+func formatMicros(value int64, currency string) string {
+ amount := strconv.FormatFloat(float64(value)/1_000_000, 'f', 6, 64)
+ amount = strings.TrimRight(strings.TrimRight(amount, "0"), ".")
+ if amount == "" {
+ amount = "0"
+ }
+ return strings.ToUpper(currency) + " " + amount
+}
+
+func formatInteger(value int64) string {
+ raw := strconv.FormatInt(value, 10)
+ for index := len(raw) - 3; index > 0; index -= 3 {
+ raw = raw[:index] + "," + raw[index:]
+ }
+ return raw
+}
+
+func formatPublicDate(value *time.Time) string {
+ if value == nil {
+ return "Not published"
+ }
+ return value.UTC().Format("2 Jan 2006")
+}
+
+func uniquePageStrings(values []string) []string {
+ seen := make(map[string]struct{}, len(values))
+ result := make([]string, 0, len(values))
+ for _, value := range values {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ continue
+ }
+ if _, exists := seen[value]; exists {
+ continue
+ }
+ seen[value] = struct{}{}
+ result = append(result, value)
+ }
+ return result
+}