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.
121 lines
4.8 KiB
Go
121 lines
4.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// AdminModelRow is a catalog entry for the admin view (includes blocked /
|
|
// unavailable rows, with both discovered and override pricing).
|
|
type AdminModelRow struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ProviderName string `json:"provider_name"`
|
|
Slug string `json:"slug"`
|
|
DisplayName string `json:"display_name"`
|
|
Source string `json:"source"`
|
|
Available bool `json:"available"`
|
|
Enabled bool `json:"enabled"`
|
|
Blocked bool `json:"blocked"`
|
|
ContextLength *int `json:"context_length"`
|
|
ContextLengthOvr *int `json:"context_length_override"`
|
|
LastSeenAt *time.Time `json:"last_seen_at"`
|
|
DiscInputPerMTok *float64 `json:"disc_input_per_mtok"`
|
|
DiscOutputPerMTok *float64 `json:"disc_output_per_mtok"`
|
|
PriceInputPerMTok *float64 `json:"price_input_per_mtok"`
|
|
PriceOutputPerMTok *float64 `json:"price_output_per_mtok"`
|
|
PriceCachedPerMTok *float64 `json:"price_cached_per_mtok"`
|
|
RPMLimit *int `json:"rpm_limit"`
|
|
TPMLimit *int `json:"tpm_limit"`
|
|
FallbackModelID *uuid.UUID `json:"fallback_model_id"`
|
|
// FallbackAddress is the backup model's "<provider>/<slug>" for display.
|
|
FallbackAddress *string `json:"fallback_address"`
|
|
}
|
|
|
|
// ListModelsAdmin lists every catalog entry (no servability filter) for admin.
|
|
func (s *Store) ListModelsAdmin(ctx context.Context) ([]AdminModelRow, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT pm.id, p.name, pm.slug, COALESCE(pm.display_name,''), pm.source,
|
|
pm.available, pm.enabled, pm.blocked, pm.context_length, pm.context_length_override, pm.last_seen_at,
|
|
pm.disc_input_price_per_mtok, pm.disc_output_price_per_mtok,
|
|
pm.price_input_per_mtok, pm.price_output_per_mtok, pm.price_cached_per_mtok,
|
|
pm.rpm_limit, pm.tpm_limit,
|
|
pm.fallback_model_id, fp.name || '/' || fb.slug
|
|
FROM provider_models pm
|
|
JOIN providers p ON p.id = pm.provider_id
|
|
LEFT JOIN provider_models fb ON fb.id = pm.fallback_model_id
|
|
LEFT JOIN providers fp ON fp.id = fb.provider_id
|
|
ORDER BY p.name, pm.slug`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []AdminModelRow
|
|
for rows.Next() {
|
|
var m AdminModelRow
|
|
if err := rows.Scan(&m.ID, &m.ProviderName, &m.Slug, &m.DisplayName, &m.Source,
|
|
&m.Available, &m.Enabled, &m.Blocked, &m.ContextLength, &m.ContextLengthOvr, &m.LastSeenAt,
|
|
&m.DiscInputPerMTok, &m.DiscOutputPerMTok,
|
|
&m.PriceInputPerMTok, &m.PriceOutputPerMTok, &m.PriceCachedPerMTok,
|
|
&m.RPMLimit, &m.TPMLimit, &m.FallbackModelID, &m.FallbackAddress); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Patch is an optional column update. Set=false leaves the column unchanged.
|
|
// Set=true with a nil Value clears the column to NULL.
|
|
type Patch[T any] struct {
|
|
Set bool
|
|
Value *T
|
|
}
|
|
|
|
// ModelOverride carries admin override fields; unset patches are left unchanged.
|
|
type ModelOverride struct {
|
|
Enabled *bool
|
|
Blocked *bool
|
|
ParamOverrides []byte // jsonb; nil leaves unchanged
|
|
InputPrice Patch[float64]
|
|
OutputPrice Patch[float64]
|
|
CachedPrice Patch[float64]
|
|
RPMLimit Patch[int]
|
|
TPMLimit Patch[int]
|
|
// ContextLength overrides the provider-reported context window.
|
|
ContextLength Patch[int]
|
|
// FallbackModelID is the backup model served when this one fails.
|
|
FallbackModelID Patch[uuid.UUID]
|
|
}
|
|
|
|
func (s *Store) UpdateModelOverride(ctx context.Context, id uuid.UUID, o ModelOverride) error {
|
|
var params any = o.ParamOverrides
|
|
if len(o.ParamOverrides) == 0 {
|
|
params = nil
|
|
}
|
|
_, err := s.pool.Exec(ctx, `
|
|
UPDATE provider_models SET
|
|
enabled = COALESCE($2, enabled),
|
|
blocked = COALESCE($3, blocked),
|
|
param_overrides = COALESCE($4, param_overrides),
|
|
price_input_per_mtok = CASE WHEN $5::boolean THEN $6 ELSE price_input_per_mtok END,
|
|
price_output_per_mtok = CASE WHEN $7::boolean THEN $8 ELSE price_output_per_mtok END,
|
|
price_cached_per_mtok = CASE WHEN $9::boolean THEN $10 ELSE price_cached_per_mtok END,
|
|
rpm_limit = CASE WHEN $11::boolean THEN $12 ELSE rpm_limit END,
|
|
tpm_limit = CASE WHEN $13::boolean THEN $14 ELSE tpm_limit END,
|
|
context_length_override = CASE WHEN $15::boolean THEN $16 ELSE context_length_override END,
|
|
fallback_model_id = CASE WHEN $17::boolean THEN $18 ELSE fallback_model_id END
|
|
WHERE id = $1`,
|
|
id, o.Enabled, o.Blocked, params,
|
|
o.InputPrice.Set, o.InputPrice.Value,
|
|
o.OutputPrice.Set, o.OutputPrice.Value,
|
|
o.CachedPrice.Set, o.CachedPrice.Value,
|
|
o.RPMLimit.Set, o.RPMLimit.Value,
|
|
o.TPMLimit.Set, o.TPMLimit.Value,
|
|
o.ContextLength.Set, o.ContextLength.Value,
|
|
o.FallbackModelID.Set, o.FallbackModelID.Value,
|
|
)
|
|
return err
|
|
}
|