refactor: support explicit provider field in model list entries (#2609)

* refactor: support explicit model list providers

* fix(web): preserve explicit model providers

* fix(web): preserve legacy provider prefixes on model updates

fix(models): normalize explicit provider-prefixed ids

fix(api): preserve legacy model updates across providers

fix(agent): preserve config identity for explicit provider refs

* fix ci
This commit is contained in:
lxowalle
2026-04-22 11:28:47 +08:00
committed by GitHub
parent 3316ee6923
commit 77b0c43392
42 changed files with 1559 additions and 441 deletions
+41 -1
View File
@@ -6,10 +6,12 @@ import (
"io"
"net/http"
"strconv"
"strings"
"sync"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
)
// registerModelRoutes binds model list management endpoints to the ServeMux.
@@ -26,6 +28,7 @@ func (h *Handler) registerModelRoutes(mux *http.ServeMux) {
type modelResponse struct {
Index int `json:"index"`
ModelName string `json:"model_name"`
Provider string `json:"provider,omitempty"`
Model string `json:"model"`
APIBase string `json:"api_base,omitempty"`
APIKey string `json:"api_key"`
@@ -73,10 +76,12 @@ func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
models := make([]modelResponse, 0, len(cfg.ModelList))
for i, m := range cfg.ModelList {
provider, modelID := providers.ExtractProtocol(m)
models = append(models, modelResponse{
Index: i,
ModelName: m.ModelName,
Model: m.Model,
Provider: provider,
Model: modelID,
APIBase: m.APIBase,
APIKey: maskAPIKey(m.APIKey()),
Proxy: m.Proxy,
@@ -176,6 +181,12 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
}
defer r.Body.Close()
var rawFields map[string]json.RawMessage
if err = json.Unmarshal(body, &rawFields); err != nil {
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
return
}
type custom struct {
config.ModelConfig
APIKey string `json:"api_key"`
@@ -226,6 +237,35 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
} else if len(mc.CustomHeaders) == 0 {
mc.CustomHeaders = nil
}
// Preserve the existing Provider when the caller omits it. This keeps the
// update API backward-compatible for clients that haven't started sending
// the new field yet, while still allowing explicit clearing via "".
if _, ok := rawFields["provider"]; !ok {
mc.Provider = cfg.ModelList[idx].Provider
// Older clients still round-trip the legacy model field only. When the
// stored config encodes provider/model in Model and has no explicit
// Provider field yet, continue preserving that hidden provider prefix.
// This keeps provider-omitted updates backward-compatible even when an
// older client edits the visible model ID.
if strings.TrimSpace(cfg.ModelList[idx].Provider) == "" {
existingProtocol, existingModelID := providers.ExtractProtocol(cfg.ModelList[idx])
existingRawModel := strings.TrimSpace(cfg.ModelList[idx].Model)
incomingModel := strings.TrimSpace(mc.Model)
if existingRawModel != "" && existingRawModel != existingModelID && incomingModel != "" {
if incomingModel == existingModelID {
mc.Model = existingRawModel
} else if strings.Contains(incomingModel, "/") && !strings.Contains(existingModelID, "/") {
// Older clients never saw the hidden provider prefix for simple
// legacy entries such as "openai/gpt-4o". If they now send an
// explicit provider/model string, treat it as the caller's full
// intent instead of re-applying the old hidden prefix.
mc.Model = incomingModel
} else if !strings.HasPrefix(incomingModel, existingProtocol+"/") {
mc.Model = existingProtocol + "/" + incomingModel
}
}
}
}
cfg.ModelList[idx] = &mc.ModelConfig