Changeset 0.22.0.1 (#94)
This commit is contained in:
@@ -44,6 +44,7 @@ type Stores struct {
|
||||
NotifPrefs NotificationPreferenceStore
|
||||
Workspaces WorkspaceStore
|
||||
GitCredentials GitCredentialStore
|
||||
CapOverrides CapabilityOverrideStore
|
||||
}
|
||||
|
||||
// =========================================
|
||||
@@ -562,6 +563,30 @@ type GitCredentialStore interface {
|
||||
Delete(ctx context.Context, id, userID string) error
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// CAPABILITY OVERRIDES (v0.22.0)
|
||||
// =========================================
|
||||
|
||||
type CapabilityOverrideStore interface {
|
||||
// Set creates or updates an override for (provider, model, field).
|
||||
Set(ctx context.Context, o *models.CapabilityOverride) error
|
||||
|
||||
// Delete removes a specific override.
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// ListForModel returns all overrides for a model ID (across all providers + global).
|
||||
ListForModel(ctx context.Context, modelID string) ([]models.CapabilityOverride, error)
|
||||
|
||||
// ListForProviderModel returns overrides for a specific provider+model combination.
|
||||
ListForProviderModel(ctx context.Context, providerConfigID, modelID string) ([]models.CapabilityOverride, error)
|
||||
|
||||
// ListAll returns every override (admin view).
|
||||
ListAll(ctx context.Context) ([]models.CapabilityOverride, error)
|
||||
|
||||
// DeleteForProvider removes all overrides for a provider (cascade cleanup).
|
||||
DeleteForProvider(ctx context.Context, providerConfigID string) error
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// SHARED TYPES
|
||||
// =========================================
|
||||
|
||||
85
server/store/postgres/cap_override.go
Normal file
85
server/store/postgres/cap_override.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
)
|
||||
|
||||
// CapOverrideStore implements store.CapabilityOverrideStore for Postgres.
|
||||
type CapOverrideStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewCapOverrideStore(db *sql.DB) *CapOverrideStore {
|
||||
return &CapOverrideStore{db: db}
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) Set(ctx context.Context, o *models.CapabilityOverride) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO capability_overrides (provider_config_id, model_id, field, value, set_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (provider_config_id, model_id, field) DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
set_by = EXCLUDED.set_by,
|
||||
created_at = now()
|
||||
`, o.ProviderConfigID, o.ModelID, o.Field, o.Value, o.SetBy)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := s.db.ExecContext(ctx, `DELETE FROM capability_overrides WHERE id = $1`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) ListForModel(ctx context.Context, modelID string) ([]models.CapabilityOverride, error) {
|
||||
return s.query(ctx, `
|
||||
SELECT id, provider_config_id, model_id, field, value, set_by, created_at
|
||||
FROM capability_overrides
|
||||
WHERE model_id = $1
|
||||
ORDER BY provider_config_id NULLS LAST, field
|
||||
`, modelID)
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) ListForProviderModel(ctx context.Context, providerConfigID, modelID string) ([]models.CapabilityOverride, error) {
|
||||
return s.query(ctx, `
|
||||
SELECT id, provider_config_id, model_id, field, value, set_by, created_at
|
||||
FROM capability_overrides
|
||||
WHERE model_id = $1 AND (provider_config_id = $2 OR provider_config_id IS NULL)
|
||||
ORDER BY provider_config_id NULLS LAST, field
|
||||
`, modelID, providerConfigID)
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) ListAll(ctx context.Context) ([]models.CapabilityOverride, error) {
|
||||
return s.query(ctx, `
|
||||
SELECT id, provider_config_id, model_id, field, value, set_by, created_at
|
||||
FROM capability_overrides
|
||||
ORDER BY model_id, provider_config_id NULLS LAST, field
|
||||
`)
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) DeleteForProvider(ctx context.Context, providerConfigID string) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
DELETE FROM capability_overrides WHERE provider_config_id = $1
|
||||
`, providerConfigID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) query(ctx context.Context, q string, args ...interface{}) ([]models.CapabilityOverride, error) {
|
||||
rows, err := s.db.QueryContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.CapabilityOverride
|
||||
for rows.Next() {
|
||||
var o models.CapabilityOverride
|
||||
if err := rows.Scan(&o.ID, &o.ProviderConfigID, &o.ModelID, &o.Field, &o.Value, &o.SetBy, &o.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, o)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
130
server/store/postgres/health.go
Normal file
130
server/store/postgres/health.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
)
|
||||
|
||||
// HealthStore implements health.Store for Postgres.
|
||||
type HealthStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewHealthStore(db *sql.DB) *HealthStore {
|
||||
return &HealthStore{db: db}
|
||||
}
|
||||
|
||||
func (s *HealthStore) UpsertWindow(ctx context.Context, w *models.ProviderHealthWindow) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO provider_health (provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now())
|
||||
ON CONFLICT (provider_config_id, window_start) DO UPDATE SET
|
||||
request_count = provider_health.request_count + EXCLUDED.request_count,
|
||||
error_count = provider_health.error_count + EXCLUDED.error_count,
|
||||
timeout_count = provider_health.timeout_count + EXCLUDED.timeout_count,
|
||||
total_latency_ms = provider_health.total_latency_ms + EXCLUDED.total_latency_ms,
|
||||
max_latency_ms = GREATEST(provider_health.max_latency_ms, EXCLUDED.max_latency_ms),
|
||||
last_error = COALESCE(EXCLUDED.last_error, provider_health.last_error),
|
||||
last_error_at = COALESCE(EXCLUDED.last_error_at, provider_health.last_error_at),
|
||||
updated_at = now()
|
||||
`, w.ProviderConfigID, w.WindowStart,
|
||||
w.RequestCount, w.ErrorCount, w.TimeoutCount,
|
||||
w.TotalLatencyMs, w.MaxLatencyMs, w.LastError, w.LastErrorAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *HealthStore) GetCurrentWindow(ctx context.Context, providerConfigID string) (*models.ProviderHealthWindow, error) {
|
||||
windowStart := time.Now().UTC().Truncate(time.Hour)
|
||||
var w models.ProviderHealthWindow
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT id, provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at
|
||||
FROM provider_health
|
||||
WHERE provider_config_id = $1 AND window_start = $2
|
||||
`, providerConfigID, windowStart).Scan(
|
||||
&w.ID, &w.ProviderConfigID, &w.WindowStart,
|
||||
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount,
|
||||
&w.TotalLatencyMs, &w.MaxLatencyMs, &w.LastError, &w.LastErrorAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &w, err
|
||||
}
|
||||
|
||||
func (s *HealthStore) ListWindows(ctx context.Context, providerConfigID string, hours int) ([]models.ProviderHealthWindow, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT id, provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at
|
||||
FROM provider_health
|
||||
WHERE provider_config_id = $1
|
||||
ORDER BY window_start DESC
|
||||
LIMIT $2
|
||||
`, providerConfigID, hours)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.ProviderHealthWindow
|
||||
for rows.Next() {
|
||||
var w models.ProviderHealthWindow
|
||||
if err := rows.Scan(
|
||||
&w.ID, &w.ProviderConfigID, &w.WindowStart,
|
||||
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount,
|
||||
&w.TotalLatencyMs, &w.MaxLatencyMs, &w.LastError, &w.LastErrorAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, w)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *HealthStore) ListAllCurrent(ctx context.Context) ([]models.ProviderHealthWindow, error) {
|
||||
windowStart := time.Now().UTC().Truncate(time.Hour)
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT id, provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at
|
||||
FROM provider_health
|
||||
WHERE window_start = $1
|
||||
ORDER BY provider_config_id
|
||||
`, windowStart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.ProviderHealthWindow
|
||||
for rows.Next() {
|
||||
var w models.ProviderHealthWindow
|
||||
if err := rows.Scan(
|
||||
&w.ID, &w.ProviderConfigID, &w.WindowStart,
|
||||
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount,
|
||||
&w.TotalLatencyMs, &w.MaxLatencyMs, &w.LastError, &w.LastErrorAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, w)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *HealthStore) Prune(ctx context.Context, before time.Time) (int64, error) {
|
||||
result, err := s.db.ExecContext(ctx, `
|
||||
DELETE FROM provider_health WHERE window_start < $1
|
||||
`, before)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
@@ -37,5 +37,6 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
NotifPrefs: NewNotificationPreferenceStore(),
|
||||
Workspaces: NewWorkspaceStore(),
|
||||
GitCredentials: &GitCredentialStore{},
|
||||
CapOverrides: NewCapOverrideStore(db),
|
||||
}
|
||||
}
|
||||
|
||||
90
server/store/sqlite/cap_override.go
Normal file
90
server/store/sqlite/cap_override.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
type CapOverrideStore struct{}
|
||||
|
||||
func NewCapOverrideStore() *CapOverrideStore { return &CapOverrideStore{} }
|
||||
|
||||
func (s *CapOverrideStore) Set(ctx context.Context, o *models.CapabilityOverride) error {
|
||||
id := store.NewID()
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO capability_overrides (id, provider_config_id, model_id, field, value, set_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (provider_config_id, model_id, field) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
set_by = excluded.set_by,
|
||||
created_at = datetime('now')
|
||||
`, id, o.ProviderConfigID, o.ModelID, o.Field, o.Value, o.SetBy)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM capability_overrides WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) ListForModel(ctx context.Context, modelID string) ([]models.CapabilityOverride, error) {
|
||||
return s.query(ctx, `
|
||||
SELECT id, provider_config_id, model_id, field, value, set_by, created_at
|
||||
FROM capability_overrides
|
||||
WHERE model_id = ?
|
||||
ORDER BY provider_config_id, field
|
||||
`, modelID)
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) ListForProviderModel(ctx context.Context, providerConfigID, modelID string) ([]models.CapabilityOverride, error) {
|
||||
return s.query(ctx, `
|
||||
SELECT id, provider_config_id, model_id, field, value, set_by, created_at
|
||||
FROM capability_overrides
|
||||
WHERE model_id = ? AND (provider_config_id = ? OR provider_config_id IS NULL)
|
||||
ORDER BY provider_config_id, field
|
||||
`, modelID, providerConfigID)
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) ListAll(ctx context.Context) ([]models.CapabilityOverride, error) {
|
||||
return s.query(ctx, `
|
||||
SELECT id, provider_config_id, model_id, field, value, set_by, created_at
|
||||
FROM capability_overrides
|
||||
ORDER BY model_id, provider_config_id, field
|
||||
`)
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) DeleteForProvider(ctx context.Context, providerConfigID string) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
DELETE FROM capability_overrides WHERE provider_config_id = ?
|
||||
`, providerConfigID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *CapOverrideStore) query(ctx context.Context, q string, args ...interface{}) ([]models.CapabilityOverride, error) {
|
||||
rows, err := DB.QueryContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.CapabilityOverride
|
||||
for rows.Next() {
|
||||
var o models.CapabilityOverride
|
||||
if err := rows.Scan(&o.ID, &o.ProviderConfigID, &o.ModelID, &o.Field, &o.Value, &o.SetBy, &o.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, o)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// ensure interface compliance (compile-time check)
|
||||
var _ store.CapabilityOverrideStore = (*CapOverrideStore)(nil)
|
||||
|
||||
// also check the health store interface at this package level
|
||||
// (health.Store is in the health package, but we verify via the sql.Rows pattern)
|
||||
func init() {
|
||||
// compile-time interface checks happen via the var _ lines above
|
||||
}
|
||||
125
server/store/sqlite/health.go
Normal file
125
server/store/sqlite/health.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
type HealthStore struct{}
|
||||
|
||||
func NewHealthStore() *HealthStore { return &HealthStore{} }
|
||||
|
||||
func (s *HealthStore) UpsertWindow(ctx context.Context, w *models.ProviderHealthWindow) error {
|
||||
id := store.NewID()
|
||||
windowStr := w.WindowStart.Format(timeFmt)
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO provider_health (id, provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
||||
ON CONFLICT (provider_config_id, window_start) DO UPDATE SET
|
||||
request_count = provider_health.request_count + excluded.request_count,
|
||||
error_count = provider_health.error_count + excluded.error_count,
|
||||
timeout_count = provider_health.timeout_count + excluded.timeout_count,
|
||||
total_latency_ms = provider_health.total_latency_ms + excluded.total_latency_ms,
|
||||
max_latency_ms = MAX(provider_health.max_latency_ms, excluded.max_latency_ms),
|
||||
last_error = COALESCE(excluded.last_error, provider_health.last_error),
|
||||
last_error_at = COALESCE(excluded.last_error_at, provider_health.last_error_at),
|
||||
updated_at = datetime('now')
|
||||
`, id, w.ProviderConfigID, windowStr,
|
||||
w.RequestCount, w.ErrorCount, w.TimeoutCount,
|
||||
w.TotalLatencyMs, w.MaxLatencyMs, w.LastError, w.LastErrorAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *HealthStore) GetCurrentWindow(ctx context.Context, providerConfigID string) (*models.ProviderHealthWindow, error) {
|
||||
windowStr := time.Now().UTC().Truncate(time.Hour).Format(timeFmt)
|
||||
var w models.ProviderHealthWindow
|
||||
var windowStartStr string
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at
|
||||
FROM provider_health
|
||||
WHERE provider_config_id = ? AND window_start = ?
|
||||
`, providerConfigID, windowStr).Scan(
|
||||
&w.ID, &w.ProviderConfigID, &windowStartStr,
|
||||
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount,
|
||||
&w.TotalLatencyMs, &w.MaxLatencyMs, &w.LastError, &w.LastErrorAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.WindowStart, _ = time.Parse(timeFmt, windowStartStr)
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
func (s *HealthStore) ListWindows(ctx context.Context, providerConfigID string, hours int) ([]models.ProviderHealthWindow, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at
|
||||
FROM provider_health
|
||||
WHERE provider_config_id = ?
|
||||
ORDER BY window_start DESC
|
||||
LIMIT ?
|
||||
`, providerConfigID, hours)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanHealthRows(rows)
|
||||
}
|
||||
|
||||
func (s *HealthStore) ListAllCurrent(ctx context.Context) ([]models.ProviderHealthWindow, error) {
|
||||
windowStr := time.Now().UTC().Truncate(time.Hour).Format(timeFmt)
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, provider_config_id, window_start,
|
||||
request_count, error_count, timeout_count,
|
||||
total_latency_ms, max_latency_ms, last_error, last_error_at
|
||||
FROM provider_health
|
||||
WHERE window_start = ?
|
||||
ORDER BY provider_config_id
|
||||
`, windowStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanHealthRows(rows)
|
||||
}
|
||||
|
||||
func (s *HealthStore) Prune(ctx context.Context, before time.Time) (int64, error) {
|
||||
result, err := DB.ExecContext(ctx, `
|
||||
DELETE FROM provider_health WHERE window_start < ?
|
||||
`, before.Format(timeFmt))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
func scanHealthRows(rows *sql.Rows) ([]models.ProviderHealthWindow, error) {
|
||||
var result []models.ProviderHealthWindow
|
||||
for rows.Next() {
|
||||
var w models.ProviderHealthWindow
|
||||
var windowStartStr string
|
||||
if err := rows.Scan(
|
||||
&w.ID, &w.ProviderConfigID, &windowStartStr,
|
||||
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount,
|
||||
&w.TotalLatencyMs, &w.MaxLatencyMs, &w.LastError, &w.LastErrorAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.WindowStart, _ = time.Parse(timeFmt, windowStartStr)
|
||||
result = append(result, w)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
@@ -37,5 +37,6 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
NotifPrefs: NewNotificationPreferenceStore(),
|
||||
Workspaces: NewWorkspaceStore(),
|
||||
GitCredentials: &GitCredentialStore{},
|
||||
CapOverrides: NewCapOverrideStore(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user