All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Any model can name another model as its backup. The gateway tries the backup when the model is offline, rate limited, or its upstream fails before the client has received anything. The response reports the requested model, so the client sees no switch. The request log records each failed attempt and the address that served the request, and the cost uses the backup's prices.
139 lines
4.6 KiB
Go
139 lines
4.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// DiscoveredModel is one entry from a provider's model listing, captured by the
|
|
// catalog syncer. Pointer fields are NULL when the provider does not report them.
|
|
type DiscoveredModel struct {
|
|
Slug string
|
|
DisplayName string
|
|
ContextLength *int
|
|
InputPricePerMTok *float64
|
|
OutputPricePerMTok *float64
|
|
// Capabilities are the modalities the provider reports for the model
|
|
// ("chat", "images", ...). Empty means the column default applies.
|
|
Capabilities []string
|
|
Raw []byte
|
|
}
|
|
|
|
// UpsertDiscovered inserts/updates discovered models for a provider in one
|
|
// transaction, then marks any previously-discovered model not seen in this sync
|
|
// (older last_seen_at) as unavailable. Admin override columns are never touched.
|
|
func (s *Store) UpsertDiscovered(ctx context.Context, providerID uuid.UUID, syncedAt time.Time, models []DiscoveredModel) error {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx) //nolint:errcheck // no-op after Commit
|
|
|
|
for _, m := range models {
|
|
raw := m.Raw
|
|
if raw == nil {
|
|
raw = []byte("{}")
|
|
}
|
|
caps := m.Capabilities
|
|
if len(caps) == 0 {
|
|
caps = []string{"chat"}
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO provider_models
|
|
(provider_id, slug, display_name, context_length,
|
|
disc_input_price_per_mtok, disc_output_price_per_mtok,
|
|
source, available, last_seen_at, raw, capabilities)
|
|
VALUES ($1, $2, $3, $4, $5, $6, 'discovered', true, $7, $8, $9)
|
|
ON CONFLICT (provider_id, slug) DO UPDATE SET
|
|
display_name = EXCLUDED.display_name,
|
|
context_length = EXCLUDED.context_length,
|
|
disc_input_price_per_mtok = EXCLUDED.disc_input_price_per_mtok,
|
|
disc_output_price_per_mtok = EXCLUDED.disc_output_price_per_mtok,
|
|
available = true,
|
|
last_seen_at = EXCLUDED.last_seen_at,
|
|
raw = EXCLUDED.raw,
|
|
capabilities = EXCLUDED.capabilities`,
|
|
providerID, m.Slug, nullString(m.DisplayName), m.ContextLength,
|
|
m.InputPricePerMTok, m.OutputPricePerMTok, syncedAt, raw, caps,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE provider_models SET available = false
|
|
WHERE provider_id = $1 AND source = 'discovered'
|
|
AND (last_seen_at IS NULL OR last_seen_at < $2)`,
|
|
providerID, syncedAt,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
// CatalogRow is a catalog entry joined with its provider, with effective pricing
|
|
// resolved (override else discovered else 0).
|
|
type CatalogRow struct {
|
|
ID uuid.UUID // provider_models.id
|
|
CreatedAt time.Time
|
|
ProviderID uuid.UUID
|
|
ProviderName string
|
|
ProviderType string
|
|
Slug string
|
|
DisplayName string
|
|
Capabilities []string
|
|
ContextLength *int
|
|
Enabled bool
|
|
Blocked bool
|
|
Available bool
|
|
ParamOverrides []byte
|
|
InputPricePerMTok float64
|
|
OutputPricePerMTok float64
|
|
CachedPricePerMTok float64
|
|
RPMLimit *int
|
|
TPMLimit *int
|
|
// FallbackModelID is the admin-configured backup model, when set.
|
|
FallbackModelID *uuid.UUID
|
|
}
|
|
|
|
// ListCatalog returns every catalog entry belonging to an enabled provider.
|
|
// The context length is the admin override when set, else the discovered
|
|
// value.
|
|
func (s *Store) ListCatalog(ctx context.Context) ([]CatalogRow, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT pm.id, pm.created_at, pm.provider_id, p.name, p.type, pm.slug,
|
|
COALESCE(pm.display_name, ''),
|
|
pm.capabilities, COALESCE(pm.context_length_override, pm.context_length),
|
|
pm.enabled, pm.blocked, pm.available, pm.param_overrides,
|
|
COALESCE(pm.price_input_per_mtok, pm.disc_input_price_per_mtok, 0)::double precision,
|
|
COALESCE(pm.price_output_per_mtok, pm.disc_output_price_per_mtok, 0)::double precision,
|
|
COALESCE(pm.price_cached_per_mtok, 0)::double precision,
|
|
pm.rpm_limit, pm.tpm_limit, pm.fallback_model_id
|
|
FROM provider_models pm
|
|
JOIN providers p ON p.id = pm.provider_id
|
|
WHERE p.enabled = true`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []CatalogRow
|
|
for rows.Next() {
|
|
var r CatalogRow
|
|
if err := rows.Scan(
|
|
&r.ID, &r.CreatedAt, &r.ProviderID, &r.ProviderName, &r.ProviderType, &r.Slug,
|
|
&r.DisplayName, &r.Capabilities, &r.ContextLength,
|
|
&r.Enabled, &r.Blocked, &r.Available, &r.ParamOverrides,
|
|
&r.InputPricePerMTok, &r.OutputPricePerMTok, &r.CachedPricePerMTok,
|
|
&r.RPMLimit, &r.TPMLimit, &r.FallbackModelID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|