Changeset 0.10.4 (#60)
This commit is contained in:
29
CHANGELOG.md
29
CHANGELOG.md
@@ -2,6 +2,35 @@
|
|||||||
|
|
||||||
All notable changes to Chat Switchboard.
|
All notable changes to Chat Switchboard.
|
||||||
|
|
||||||
|
## [0.10.4] — 2026-02-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`model_type` field across the full pipeline.** Models now carry a type
|
||||||
|
classification (`chat`, `embedding`, `image`) sourced from provider APIs
|
||||||
|
at sync time — no hardcoded lists. Venice's `/v1/models` returns `type`
|
||||||
|
per model; OpenAI-compatible APIs pass through the field when present.
|
||||||
|
- New DB column: `model_catalog.model_type VARCHAR(20) DEFAULT 'chat'`
|
||||||
|
- Migration: `005_model_type.sql`
|
||||||
|
- Propagation: `providers.Model.Type` → `CatalogSyncEntry.ModelType` →
|
||||||
|
`CatalogEntry.ModelType` → `UserModel.ModelType` → frontend `model_type`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Admin role save didn't refresh UI.** `adminSaveRole()` showed "✓ Saved"
|
||||||
|
but never called `UI.loadAdminRoles()`, so dropdowns appeared stale after
|
||||||
|
save. Now reloads the roles panel after a successful save.
|
||||||
|
- **Role model dropdowns showed all models regardless of type.** Embedding
|
||||||
|
role showed chat models, utility role showed embedding models. Both admin
|
||||||
|
and user role UIs now filter the model dropdown by `model_type`:
|
||||||
|
- "embedding" role → only `model_type === 'embedding'` models
|
||||||
|
- "utility" role → only `model_type === 'chat'` models
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Venice provider now reads the `type` field from each model in the API
|
||||||
|
response and normalizes it (`text` → `chat`, `embedding` → `embedding`,
|
||||||
|
`image` → `image`).
|
||||||
|
- OpenAI provider wire type extended with optional `type` field for
|
||||||
|
OpenAI-compatible APIs that include it.
|
||||||
|
|
||||||
## [0.10.3] — 2026-02-24
|
## [0.10.3] — 2026-02-24
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
42
ROADMAP.md
42
ROADMAP.md
@@ -27,6 +27,8 @@ v0.10.2 Summarize & Continue (first utility role consumer)
|
|||||||
│
|
│
|
||||||
v0.10.3 Frontend Refactor (no features, code health)
|
v0.10.3 Frontend Refactor (no features, code health)
|
||||||
│
|
│
|
||||||
|
v0.10.4 Model Type Pipeline + Role Fixes
|
||||||
|
│
|
||||||
┌───────┴──────────────┐
|
┌───────┴──────────────┐
|
||||||
│ │
|
│ │
|
||||||
v0.11.0 Extension v0.12.0 File Handling
|
v0.11.0 Extension v0.12.0 File Handling
|
||||||
@@ -467,17 +469,35 @@ Vanilla JS, no modules, no build step, no function renaming.
|
|||||||
|
|
||||||
See [REFACTOR-0.10.3.md](REFACTOR-0.10.3.md) for full plan.
|
See [REFACTOR-0.10.3.md](REFACTOR-0.10.3.md) for full plan.
|
||||||
|
|
||||||
- [ ] Extract `ui-format.js` — markdown, code blocks, `esc()`, helpers
|
- [x] Extract `ui-format.js` — markdown, code blocks, `esc()`, helpers
|
||||||
- [ ] Extract `ui-settings.js` — settings tabs, teams, providers, user prefs
|
- [x] Extract `ui-settings.js` — settings tabs, teams, providers, user prefs
|
||||||
- [ ] Extract `ui-admin.js` — admin modal tabs, all admin rendering
|
- [x] Extract `ui-admin.js` — admin modal tabs, all admin rendering
|
||||||
- [ ] Extract `tokens.js` — context tracking, token estimation
|
- [x] Extract `tokens.js` — context tracking, token estimation
|
||||||
- [ ] Extract `notes.js` — notes panel, editor, multi-select
|
- [x] Extract `notes.js` — notes panel, editor, multi-select
|
||||||
- [ ] Extract `chat.js` — chat ops, send, regen, edit, branch, summarize
|
- [x] Extract `chat.js` — chat ops, send, regen, edit, branch, summarize
|
||||||
- [ ] Extract `settings-handlers.js` — settings save, provider CRUD, cmd palette
|
- [x] Extract `settings-handlers.js` — settings save, provider CRUD, cmd palette
|
||||||
- [ ] Extract `admin-handlers.js` — admin actions, presets, team management
|
- [x] Extract `admin-handlers.js` — admin actions, presets, team management
|
||||||
- [ ] Slim `app.js` to orchestrator — state, init, boot, auth, listener dispatch
|
- [x] Slim `app.js` to orchestrator — state, init, boot, auth, listener dispatch
|
||||||
- [ ] Update `sw.js` SHELL_FILES, `index.html` script tags
|
- [x] Update `sw.js` SHELL_FILES, `index.html` script tags
|
||||||
- [ ] All 159+ frontend tests pass, `node --check` on all files
|
- [x] All 159+ frontend tests pass, `node --check` on all files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.10.4 — Model Type Pipeline + Role Fixes ✅
|
||||||
|
|
||||||
|
Provider APIs report model types (chat, embedding, image) — now captured
|
||||||
|
end-to-end through sync → catalog → resolver → frontend. Role dropdowns
|
||||||
|
filter models by type instead of showing everything.
|
||||||
|
|
||||||
|
- [x] Migration `005_model_type.sql` — `model_catalog.model_type` column
|
||||||
|
- [x] `providers.Model.Type` — captured from provider wire response
|
||||||
|
- [x] Venice: reads `type` field, normalizes (`text`→`chat`, `embedding`→`embedding`)
|
||||||
|
- [x] OpenAI: wire struct extended with optional `type` for compatible APIs
|
||||||
|
- [x] `CatalogSyncEntry.ModelType` → `CatalogEntry.ModelType` → `UserModel.ModelType`
|
||||||
|
- [x] Frontend `model_type` mapped through `fetchModels()`
|
||||||
|
- [x] Admin role dropdowns: filter by `model_type` matching role
|
||||||
|
- [x] User role dropdowns: filter by `model_type` matching role
|
||||||
|
- [x] `adminSaveRole()` reloads UI after save (was showing "Saved" without refresh)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]m
|
|||||||
ID: entry.ModelID,
|
ID: entry.ModelID,
|
||||||
DisplayName: displayName(entry.DisplayName, entry.ModelID),
|
DisplayName: displayName(entry.DisplayName, entry.ModelID),
|
||||||
ModelID: entry.ModelID,
|
ModelID: entry.ModelID,
|
||||||
|
ModelType: entry.ModelType,
|
||||||
Source: "catalog",
|
Source: "catalog",
|
||||||
ProviderConfigID: entry.ProviderConfigID,
|
ProviderConfigID: entry.ProviderConfigID,
|
||||||
ConfigID: entry.ProviderConfigID,
|
ConfigID: entry.ProviderConfigID,
|
||||||
@@ -104,6 +105,7 @@ func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]m
|
|||||||
ID: entry.ModelID,
|
ID: entry.ModelID,
|
||||||
DisplayName: displayName(entry.DisplayName, entry.ModelID),
|
DisplayName: displayName(entry.DisplayName, entry.ModelID),
|
||||||
ModelID: entry.ModelID,
|
ModelID: entry.ModelID,
|
||||||
|
ModelType: entry.ModelType,
|
||||||
Source: "catalog",
|
Source: "catalog",
|
||||||
ProviderConfigID: prov.ID,
|
ProviderConfigID: prov.ID,
|
||||||
ConfigID: prov.ID,
|
ConfigID: prov.ID,
|
||||||
@@ -141,6 +143,7 @@ func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]m
|
|||||||
ID: entry.ModelID,
|
ID: entry.ModelID,
|
||||||
DisplayName: displayName(entry.DisplayName, entry.ModelID),
|
DisplayName: displayName(entry.DisplayName, entry.ModelID),
|
||||||
ModelID: entry.ModelID,
|
ModelID: entry.ModelID,
|
||||||
|
ModelType: entry.ModelType,
|
||||||
Source: "catalog",
|
Source: "catalog",
|
||||||
ProviderConfigID: prov.ID,
|
ProviderConfigID: prov.ID,
|
||||||
ConfigID: prov.ID,
|
ConfigID: prov.ID,
|
||||||
|
|||||||
14
server/database/migrations/005_model_type.sql
Normal file
14
server/database/migrations/005_model_type.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-- Migration 005: Add model_type to model_catalog
|
||||||
|
--
|
||||||
|
-- Providers like Venice return a "type" field per model (e.g. "text", "embedding", "image").
|
||||||
|
-- This column captures that classification so role dropdowns can filter appropriately:
|
||||||
|
-- - "embedding" role → only embedding models
|
||||||
|
-- - "utility" role → only chat/text models
|
||||||
|
--
|
||||||
|
-- Default is "chat" (the overwhelming majority of models). Provider sync will
|
||||||
|
-- populate from the wire response when available — NO hardcoding of model types.
|
||||||
|
|
||||||
|
ALTER TABLE model_catalog ADD COLUMN IF NOT EXISTS model_type VARCHAR(20) DEFAULT 'chat';
|
||||||
|
|
||||||
|
-- Index for role UI filtering (e.g. "show me all embedding models for this provider")
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
|
||||||
@@ -196,6 +196,19 @@ func TruncateAll(t *testing.T) {
|
|||||||
}'::jsonb)
|
}'::jsonb)
|
||||||
ON CONFLICT (key) DO NOTHING
|
ON CONFLICT (key) DO NOTHING
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// Re-seed platform_policies — also wiped by CASCADE from users truncation
|
||||||
|
// (platform_policies.updated_by REFERENCES users(id)).
|
||||||
|
DB.Exec(`
|
||||||
|
INSERT INTO platform_policies (key, value) VALUES
|
||||||
|
('allow_user_byok', 'false'),
|
||||||
|
('allow_user_personas', 'false'),
|
||||||
|
('allow_raw_model_access', 'true'),
|
||||||
|
('allow_registration', 'true'),
|
||||||
|
('default_user_active', 'false'),
|
||||||
|
('allow_team_providers', 'true')
|
||||||
|
ON CONFLICT (key) DO NOTHING
|
||||||
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeedTestUser creates a test user and returns the user ID.
|
// SeedTestUser creates a test user and returns the user ID.
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ func syncProviderModels(ctx context.Context, stores store.Stores, cfg *models.Pr
|
|||||||
syncEntries[i] = store.CatalogSyncEntry{
|
syncEntries[i] = store.CatalogSyncEntry{
|
||||||
ModelID: m.ID,
|
ModelID: m.ID,
|
||||||
DisplayName: m.Name,
|
DisplayName: m.Name,
|
||||||
|
ModelType: m.Type,
|
||||||
Capabilities: m.Capabilities,
|
Capabilities: m.Capabilities,
|
||||||
Pricing: m.Pricing,
|
Pricing: m.Pricing,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ type CatalogEntry struct {
|
|||||||
ProviderConfigID string `json:"provider_config_id" db:"provider_config_id"`
|
ProviderConfigID string `json:"provider_config_id" db:"provider_config_id"`
|
||||||
ModelID string `json:"model_id" db:"model_id"`
|
ModelID string `json:"model_id" db:"model_id"`
|
||||||
DisplayName string `json:"display_name,omitempty" db:"display_name"`
|
DisplayName string `json:"display_name,omitempty" db:"display_name"`
|
||||||
|
ModelType string `json:"model_type" db:"model_type"` // "chat", "embedding", "image", etc.
|
||||||
Capabilities ModelCapabilities `json:"capabilities" db:"capabilities"`
|
Capabilities ModelCapabilities `json:"capabilities" db:"capabilities"`
|
||||||
Pricing *ModelPricing `json:"pricing,omitempty" db:"pricing"`
|
Pricing *ModelPricing `json:"pricing,omitempty" db:"pricing"`
|
||||||
Visibility string `json:"visibility" db:"visibility"`
|
Visibility string `json:"visibility" db:"visibility"`
|
||||||
@@ -470,7 +471,8 @@ type UserModel struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
ModelID string `json:"model_id"`
|
ModelID string `json:"model_id"`
|
||||||
Source string `json:"source"` // "catalog", "persona", "live"
|
ModelType string `json:"model_type"` // "chat", "embedding", "image", etc.
|
||||||
|
Source string `json:"source"` // "catalog", "persona", "live"
|
||||||
|
|
||||||
ProviderConfigID string `json:"provider_config_id"`
|
ProviderConfigID string `json:"provider_config_id"`
|
||||||
ConfigID string `json:"config_id"` // Alias of ProviderConfigID for frontend compat
|
ConfigID string `json:"config_id"` // Alias of ProviderConfigID for frontend compat
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ func (p *OpenAIProvider) ListModels(ctx context.Context, cfg ProviderConfig) ([]
|
|||||||
|
|
||||||
out = append(out, Model{
|
out = append(out, Model{
|
||||||
ID: m.ID,
|
ID: m.ID,
|
||||||
|
Type: m.Type, // passthrough from API if present (empty → "chat" at sync time)
|
||||||
OwnedBy: m.OwnedBy,
|
OwnedBy: m.OwnedBy,
|
||||||
Capabilities: caps,
|
Capabilities: caps,
|
||||||
})
|
})
|
||||||
@@ -506,6 +507,7 @@ type openaiModelsResponse struct {
|
|||||||
Data []struct {
|
Data []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
OwnedBy string `json:"owned_by"`
|
OwnedBy string `json:"owned_by"`
|
||||||
|
Type string `json:"type,omitempty"` // Some APIs (e.g. Venice-compat) include model type
|
||||||
ContextLength int `json:"context_length,omitempty"` // Ollama, OpenRouter
|
ContextLength int `json:"context_length,omitempty"` // Ollama, OpenRouter
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ type EmbeddingResponse struct {
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"` // "chat", "embedding", "image" — from provider API
|
||||||
OwnedBy string `json:"owned_by,omitempty"`
|
OwnedBy string `json:"owned_by,omitempty"`
|
||||||
Capabilities models.ModelCapabilities `json:"capabilities"`
|
Capabilities models.ModelCapabilities `json:"capabilities"`
|
||||||
Pricing *models.ModelPricing `json:"pricing,omitempty"`
|
Pricing *models.ModelPricing `json:"pricing,omitempty"`
|
||||||
|
|||||||
@@ -99,9 +99,23 @@ func (p *VeniceProvider) ListModels(ctx context.Context, cfg ProviderConfig) ([]
|
|||||||
name = m.ID
|
name = m.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize Venice type → our model type
|
||||||
|
// Venice returns: "text", "embedding", "image", "code"
|
||||||
|
// We normalize to: "chat", "embedding", "image"
|
||||||
|
modelType := "chat"
|
||||||
|
switch m.Type {
|
||||||
|
case "embedding":
|
||||||
|
modelType = "embedding"
|
||||||
|
case "image":
|
||||||
|
modelType = "image"
|
||||||
|
default:
|
||||||
|
modelType = "chat" // "text", "code", etc. are all chat-capable
|
||||||
|
}
|
||||||
|
|
||||||
out = append(out, Model{
|
out = append(out, Model{
|
||||||
ID: m.ID,
|
ID: m.ID,
|
||||||
Name: name,
|
Name: name,
|
||||||
|
Type: modelType,
|
||||||
OwnedBy: "venice",
|
OwnedBy: "venice",
|
||||||
Capabilities: caps,
|
Capabilities: caps,
|
||||||
Pricing: pricing,
|
Pricing: pricing,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ type CatalogStore interface {
|
|||||||
type CatalogSyncEntry struct {
|
type CatalogSyncEntry struct {
|
||||||
ModelID string
|
ModelID string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
ModelType string // "chat", "embedding", "image" — from provider API
|
||||||
Capabilities models.ModelCapabilities
|
Capabilities models.ModelCapabilities
|
||||||
Pricing *models.ModelPricing
|
Pricing *models.ModelPricing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ type CatalogStore struct{}
|
|||||||
|
|
||||||
func NewCatalogStore() *CatalogStore { return &CatalogStore{} }
|
func NewCatalogStore() *CatalogStore { return &CatalogStore{} }
|
||||||
|
|
||||||
const catalogCols = `id, provider_config_id, model_id, display_name,
|
const catalogCols = `id, provider_config_id, model_id, display_name, model_type,
|
||||||
capabilities, pricing, visibility, last_synced_at, created_at, updated_at`
|
capabilities, pricing, visibility, last_synced_at, created_at, updated_at`
|
||||||
|
|
||||||
// catalogColsMC is catalogCols with mc. prefix for use in JOINs
|
// catalogColsMC is catalogCols with mc. prefix for use in JOINs
|
||||||
// where id/created_at/updated_at are ambiguous.
|
// where id/created_at/updated_at are ambiguous.
|
||||||
const catalogColsMC = `mc.id, mc.provider_config_id, mc.model_id, mc.display_name,
|
const catalogColsMC = `mc.id, mc.provider_config_id, mc.model_id, mc.display_name, mc.model_type,
|
||||||
mc.capabilities, mc.pricing, mc.visibility, mc.last_synced_at, mc.created_at, mc.updated_at`
|
mc.capabilities, mc.pricing, mc.visibility, mc.last_synced_at, mc.created_at, mc.updated_at`
|
||||||
|
|
||||||
// UpsertFromSync bulk-inserts or updates catalog entries from a provider API fetch.
|
// UpsertFromSync bulk-inserts or updates catalog entries from a provider API fetch.
|
||||||
@@ -31,6 +31,12 @@ func (s *CatalogStore) UpsertFromSync(ctx context.Context, providerConfigID stri
|
|||||||
capsJSON := ToJSON(e.Capabilities)
|
capsJSON := ToJSON(e.Capabilities)
|
||||||
pricingJSON := ToJSON(e.Pricing) // nil → "{}", always valid JSONB
|
pricingJSON := ToJSON(e.Pricing) // nil → "{}", always valid JSONB
|
||||||
|
|
||||||
|
// Normalize model type: empty → "chat" (the default)
|
||||||
|
modelType := e.ModelType
|
||||||
|
if modelType == "" {
|
||||||
|
modelType = "chat"
|
||||||
|
}
|
||||||
|
|
||||||
var existingID string
|
var existingID string
|
||||||
err := DB.QueryRowContext(ctx,
|
err := DB.QueryRowContext(ctx,
|
||||||
"SELECT id FROM model_catalog WHERE provider_config_id = $1 AND model_id = $2",
|
"SELECT id FROM model_catalog WHERE provider_config_id = $1 AND model_id = $2",
|
||||||
@@ -40,10 +46,10 @@ func (s *CatalogStore) UpsertFromSync(ctx context.Context, providerConfigID stri
|
|||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
// Insert new (disabled by default)
|
// Insert new (disabled by default)
|
||||||
_, err = DB.ExecContext(ctx, `
|
_, err = DB.ExecContext(ctx, `
|
||||||
INSERT INTO model_catalog (provider_config_id, model_id, display_name,
|
INSERT INTO model_catalog (provider_config_id, model_id, display_name, model_type,
|
||||||
capabilities, pricing, visibility, last_synced_at)
|
capabilities, pricing, visibility, last_synced_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, 'disabled', $6)`,
|
VALUES ($1, $2, $3, $4, $5, $6, 'disabled', $7)`,
|
||||||
providerConfigID, e.ModelID, e.DisplayName, capsJSON, pricingJSON, now)
|
providerConfigID, e.ModelID, e.DisplayName, modelType, capsJSON, pricingJSON, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return added, updated, fmt.Errorf("insert %s: %w", e.ModelID, err)
|
return added, updated, fmt.Errorf("insert %s: %w", e.ModelID, err)
|
||||||
}
|
}
|
||||||
@@ -51,10 +57,10 @@ func (s *CatalogStore) UpsertFromSync(ctx context.Context, providerConfigID stri
|
|||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
// Update existing (preserve visibility)
|
// Update existing (preserve visibility)
|
||||||
_, err = DB.ExecContext(ctx, `
|
_, err = DB.ExecContext(ctx, `
|
||||||
UPDATE model_catalog SET display_name = $1, capabilities = $2,
|
UPDATE model_catalog SET display_name = $1, model_type = $2, capabilities = $3,
|
||||||
pricing = $3, last_synced_at = $4
|
pricing = $4, last_synced_at = $5
|
||||||
WHERE id = $5`,
|
WHERE id = $6`,
|
||||||
e.DisplayName, capsJSON, pricingJSON, now, existingID)
|
e.DisplayName, modelType, capsJSON, pricingJSON, now, existingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return added, updated, fmt.Errorf("update %s: %w", e.ModelID, err)
|
return added, updated, fmt.Errorf("update %s: %w", e.ModelID, err)
|
||||||
}
|
}
|
||||||
@@ -190,7 +196,7 @@ func scanCatalogEntry(row *sql.Row) (*models.CatalogEntry, error) {
|
|||||||
var displayName sql.NullString
|
var displayName sql.NullString
|
||||||
var lastSynced sql.NullTime
|
var lastSynced sql.NullTime
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&e.ID, &e.ProviderConfigID, &e.ModelID, &displayName,
|
&e.ID, &e.ProviderConfigID, &e.ModelID, &displayName, &e.ModelType,
|
||||||
&capsJSON, &pricingJSON, &e.Visibility, &lastSynced,
|
&capsJSON, &pricingJSON, &e.Visibility, &lastSynced,
|
||||||
&e.CreatedAt, &e.UpdatedAt,
|
&e.CreatedAt, &e.UpdatedAt,
|
||||||
)
|
)
|
||||||
@@ -217,7 +223,7 @@ func scanCatalogEntries(rows *sql.Rows) ([]models.CatalogEntry, error) {
|
|||||||
var displayName sql.NullString
|
var displayName sql.NullString
|
||||||
var lastSynced sql.NullTime
|
var lastSynced sql.NullTime
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&e.ID, &e.ProviderConfigID, &e.ModelID, &displayName,
|
&e.ID, &e.ProviderConfigID, &e.ModelID, &displayName, &e.ModelType,
|
||||||
&capsJSON, &pricingJSON, &e.Visibility, &lastSynced,
|
&capsJSON, &pricingJSON, &e.Visibility, &lastSynced,
|
||||||
&e.CreatedAt, &e.UpdatedAt,
|
&e.CreatedAt, &e.UpdatedAt,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -204,7 +204,12 @@ async function adminRoleProviderChanged(role, slot) {
|
|||||||
modelSelect.innerHTML = '<option value="">— select model —</option>';
|
modelSelect.innerHTML = '<option value="">— select model —</option>';
|
||||||
if (!provId || !window._adminModelList) return;
|
if (!provId || !window._adminModelList) return;
|
||||||
|
|
||||||
const models = window._adminModelList.filter(m => m.provider_config_id === provId);
|
// Filter by provider AND model type matching the role
|
||||||
|
const typeForRole = role === 'embedding' ? 'embedding' : 'chat';
|
||||||
|
const models = window._adminModelList.filter(m =>
|
||||||
|
m.provider_config_id === provId &&
|
||||||
|
(m.model_type || 'chat') === typeForRole
|
||||||
|
);
|
||||||
models.forEach(m => {
|
models.forEach(m => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = m.model_id;
|
opt.value = m.model_id;
|
||||||
@@ -226,7 +231,8 @@ async function adminSaveRole(role) {
|
|||||||
fallback: getBinding('fallback')
|
fallback: getBinding('fallback')
|
||||||
});
|
});
|
||||||
if (status) { status.textContent = '✓ Saved'; status.style.color = 'var(--success)'; }
|
if (status) { status.textContent = '✓ Saved'; status.style.color = 'var(--success)'; }
|
||||||
setTimeout(() => { if (status) status.textContent = ''; }, 3000);
|
// Reload to reflect saved state in dropdowns
|
||||||
|
setTimeout(() => UI.loadAdminRoles(), 500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (status) { status.textContent = '✗ ' + e.message; status.style.color = 'var(--error)'; }
|
if (status) { status.textContent = '✗ ' + e.message; status.style.color = 'var(--error)'; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ async function fetchModels() {
|
|||||||
name: m.display_name || baseModelId,
|
name: m.display_name || baseModelId,
|
||||||
provider: m.provider_name || m.provider || '',
|
provider: m.provider_name || m.provider || '',
|
||||||
configId: m.config_id || m.provider_config_id || null,
|
configId: m.config_id || m.provider_config_id || null,
|
||||||
|
model_type: m.model_type || 'chat',
|
||||||
capabilities: resolveCapabilities(m.capabilities, baseModelId),
|
capabilities: resolveCapabilities(m.capabilities, baseModelId),
|
||||||
isPreset,
|
isPreset,
|
||||||
presetId: m.preset_id || m.persona_id || null,
|
presetId: m.preset_id || m.persona_id || null,
|
||||||
|
|||||||
@@ -220,9 +220,13 @@ async function userRoleProviderChanged(role) {
|
|||||||
modelSel.innerHTML = '<option value="">— select model —</option>';
|
modelSel.innerHTML = '<option value="">— select model —</option>';
|
||||||
if (!providerId) return;
|
if (!providerId) return;
|
||||||
|
|
||||||
// Load models for the selected provider (App.models uses configId, not provider_config_id)
|
// Filter by provider AND model type matching the role
|
||||||
|
const typeForRole = role === 'embedding' ? 'embedding' : 'chat';
|
||||||
const allModels = App.models || [];
|
const allModels = App.models || [];
|
||||||
const provModels = allModels.filter(m => m.configId === providerId);
|
const provModels = allModels.filter(m =>
|
||||||
|
m.configId === providerId &&
|
||||||
|
(m.model_type || 'chat') === typeForRole
|
||||||
|
);
|
||||||
provModels.forEach(m => {
|
provModels.forEach(m => {
|
||||||
modelSel.innerHTML += `<option value="${m.baseModelId}">${esc(m.name || m.baseModelId)}</option>`;
|
modelSel.innerHTML += `<option value="${m.baseModelId}">${esc(m.name || m.baseModelId)}</option>`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ Object.assign(UI, {
|
|||||||
const roleNames = ['utility', 'embedding'];
|
const roleNames = ['utility', 'embedding'];
|
||||||
el.innerHTML = roleNames.map(role => {
|
el.innerHTML = roleNames.map(role => {
|
||||||
const cfg = roles[role] || { primary: null, fallback: null };
|
const cfg = roles[role] || { primary: null, fallback: null };
|
||||||
|
const typeForRole = role === 'embedding' ? 'embedding' : 'chat';
|
||||||
|
const typeFilter = m => (m.model_type || 'chat') === typeForRole;
|
||||||
return `
|
return `
|
||||||
<div class="settings-section" style="margin-bottom:16px">
|
<div class="settings-section" style="margin-bottom:16px">
|
||||||
<h3 style="font-size:14px;margin:0 0 8px;text-transform:capitalize">${role}</h3>
|
<h3 style="font-size:14px;margin:0 0 8px;text-transform:capitalize">${role}</h3>
|
||||||
@@ -156,7 +158,7 @@ Object.assign(UI, {
|
|||||||
</select>
|
</select>
|
||||||
<select id="role-${role}-primary-model" style="min-width:200px">
|
<select id="role-${role}-primary-model" style="min-width:200px">
|
||||||
<option value="">— select model —</option>
|
<option value="">— select model —</option>
|
||||||
${cfg.primary ? modelList.filter(m => m.provider_config_id === cfg.primary.provider_config_id).map(m => `<option value="${m.model_id}" ${m.model_id === cfg.primary.model_id ? 'selected' : ''}>${esc(m.display_name || m.model_id)}</option>`).join('') : ''}
|
${cfg.primary ? modelList.filter(m => m.provider_config_id === cfg.primary.provider_config_id && typeFilter(m)).map(m => `<option value="${m.model_id}" ${m.model_id === cfg.primary.model_id ? 'selected' : ''}>${esc(m.display_name || m.model_id)}</option>`).join('') : ''}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="gap:8px;align-items:center;margin-top:6px">
|
<div class="form-row" style="gap:8px;align-items:center;margin-top:6px">
|
||||||
@@ -167,7 +169,7 @@ Object.assign(UI, {
|
|||||||
</select>
|
</select>
|
||||||
<select id="role-${role}-fallback-model" style="min-width:200px">
|
<select id="role-${role}-fallback-model" style="min-width:200px">
|
||||||
<option value="">— select model —</option>
|
<option value="">— select model —</option>
|
||||||
${cfg.fallback ? modelList.filter(m => m.provider_config_id === cfg.fallback.provider_config_id).map(m => `<option value="${m.model_id}" ${m.model_id === cfg.fallback.model_id ? 'selected' : ''}>${esc(m.display_name || m.model_id)}</option>`).join('') : ''}
|
${cfg.fallback ? modelList.filter(m => m.provider_config_id === cfg.fallback.provider_config_id && typeFilter(m)).map(m => `<option value="${m.model_id}" ${m.model_id === cfg.fallback.model_id ? 'selected' : ''}>${esc(m.display_name || m.model_id)}</option>`).join('') : ''}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="gap:8px;margin-top:8px">
|
<div class="form-row" style="gap:8px;margin-top:8px">
|
||||||
|
|||||||
@@ -391,6 +391,8 @@ Object.assign(UI, {
|
|||||||
const roleNames = ['utility', 'embedding'];
|
const roleNames = ['utility', 'embedding'];
|
||||||
el.innerHTML = roleNames.map(role => {
|
el.innerHTML = roleNames.map(role => {
|
||||||
const cfg = userRoles[role] || { primary: null };
|
const cfg = userRoles[role] || { primary: null };
|
||||||
|
const typeForRole = role === 'embedding' ? 'embedding' : 'chat';
|
||||||
|
const typeFilter = m => (m.model_type || 'chat') === typeForRole;
|
||||||
return `
|
return `
|
||||||
<div class="settings-section" style="margin-bottom:12px">
|
<div class="settings-section" style="margin-bottom:12px">
|
||||||
<h4 style="font-size:13px;margin:0 0 6px;text-transform:capitalize">${role}
|
<h4 style="font-size:13px;margin:0 0 6px;text-transform:capitalize">${role}
|
||||||
@@ -404,7 +406,7 @@ Object.assign(UI, {
|
|||||||
</select>
|
</select>
|
||||||
<select id="userRole-${role}-model" style="min-width:200px;font-size:12px">
|
<select id="userRole-${role}-model" style="min-width:200px;font-size:12px">
|
||||||
<option value="">— select model —</option>
|
<option value="">— select model —</option>
|
||||||
${cfg.primary ? personalModels.filter(m => m.configId === cfg.primary.provider_config_id).map(m => `<option value="${m.baseModelId}" ${m.baseModelId === cfg.primary.model_id ? 'selected' : ''}>${esc(m.name || m.baseModelId)}</option>`).join('') : ''}
|
${cfg.primary ? personalModels.filter(m => m.configId === cfg.primary.provider_config_id && typeFilter(m)).map(m => `<option value="${m.baseModelId}" ${m.baseModelId === cfg.primary.model_id ? 'selected' : ''}>${esc(m.name || m.baseModelId)}</option>`).join('') : ''}
|
||||||
</select>
|
</select>
|
||||||
<button class="btn-small" onclick="saveUserRole('${role}')" style="font-size:11px">Save</button>
|
<button class="btn-small" onclick="saveUserRole('${role}')" style="font-size:11px">Save</button>
|
||||||
${cfg.primary ? `<button class="btn-small btn-danger" onclick="clearUserRole('${role}')" style="font-size:11px" title="Remove override, use org default">Clear</button>` : ''}
|
${cfg.primary ? `<button class="btn-small btn-danger" onclick="clearUserRole('${role}')" style="font-size:11px" title="Remove override, use org default">Clear</button>` : ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user