Files
finance-duck/internal/classification/models.go
T
Lars Nolden ec99434002 Route requests only with parameters ZDR endpoints declare, and list them
The gpt-5.6 family's zero-data-retention endpoints declare
max_completion_tokens, so sending max_tokens under require_parameters
excluded every ZDR route and returned HTTP 404 for the whole family.
The cap is retired: the strict schema, the finish_reason check and the
64 KiB read cap already bound the response.

The model fields now offer the provider's public ZDR catalog filtered
by the exact conditions completions are routed under (live endpoint,
strict structured outputs), fetched server-side, cached for an hour,
and served at GET /api/models; the inputs stay free text so an unlisted
model remains usable when the catalog is unreachable.
2026-09-12 22:33:34 +02:00

116 lines
3.9 KiB
Go

package classification
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"slices"
"strings"
"time"
)
// VerifiedModel is one OpenRouter model that currently satisfies every routing
// control this app sends fail-closed: at least one live zero-data-retention
// endpoint that supports strict structured outputs. Anything outside this list
// is routed to zero providers and fails with HTTP 404.
type VerifiedModel struct {
ID string `json:"id"`
Name string `json:"name"`
}
// endpointBase validates and returns the provider API root shared by every
// provider request. Redirect and scheme rules exist because prompts contain
// payee text; they must never travel to an endpoint with different policies.
func (c *Client) endpointBase() (string, error) {
base := strings.TrimRight(c.BaseURL, "/")
if base == "" {
base = "https://openrouter.ai/api/v1"
}
endpoint, err := url.Parse(base)
if err != nil || endpoint.Host == "" || endpoint.User != nil || endpoint.RawQuery != "" || endpoint.Fragment != "" {
return "", errors.New("invalid AI endpoint")
}
if endpoint.Scheme != "https" && !(endpoint.Scheme == "http" && (endpoint.Hostname() == "localhost" || endpoint.Hostname() == "127.0.0.1" || endpoint.Hostname() == "::1")) {
return "", errors.New("AI endpoint must use HTTPS")
}
return base, nil
}
func (c *Client) httpClient() http.Client {
client := http.Client{Timeout: 45 * time.Second}
if c.HTTPClient != nil {
client = *c.HTTPClient
if client.Timeout == 0 {
client.Timeout = 45 * time.Second
}
}
// Redirects could send sensitive prompts to endpoints with different policies.
client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
return client
}
// VerifiedModels queries the provider's public zero-data-retention catalog and
// keeps only models with at least one live endpoint supporting strict
// structured outputs — the exact conditions completions are routed under. The
// catalog is public: no credential is attached to the request.
func (c *Client) VerifiedModels(ctx context.Context) ([]VerifiedModel, error) {
base, err := c.endpointBase()
if err != nil {
return nil, err
}
client := c.httpClient()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/endpoints/zdr", nil)
if err != nil {
return nil, errors.New("cannot create model catalog request")
}
resp, err := client.Do(req)
if err != nil {
if cause := requestContextError(ctx, err); cause != nil {
return nil, cause
}
return nil, errors.New("model catalog request failed")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("model catalog is unavailable")
}
var catalog struct {
Data []struct {
ModelID string `json:"model_id"`
ModelName string `json:"model_name"`
Status int `json:"status"`
SupportedParameters []string `json:"supported_parameters"`
} `json:"data"`
}
const maxCatalog = 16 << 20
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxCatalog+1))
if err != nil || len(raw) > maxCatalog {
return nil, errors.New("invalid model catalog response")
}
if json.Unmarshal(raw, &catalog) != nil {
return nil, errors.New("invalid model catalog response")
}
names := map[string]string{}
for _, endpoint := range catalog.Data {
if endpoint.ModelID == "" || endpoint.Status < 0 {
continue
}
if !slices.Contains(endpoint.SupportedParameters, "structured_outputs") ||
!slices.Contains(endpoint.SupportedParameters, "response_format") {
continue
}
if _, ok := names[endpoint.ModelID]; !ok {
names[endpoint.ModelID] = endpoint.ModelName
}
}
models := make([]VerifiedModel, 0, len(names))
for id, name := range names {
models = append(models, VerifiedModel{ID: id, Name: name})
}
slices.SortFunc(models, func(a, b VerifiedModel) int { return strings.Compare(a.ID, b.ID) })
return models, nil
}