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.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user