-
-
Notifications
You must be signed in to change notification settings - Fork 70
fix(providers): fall back to configured models when upstream /models fails #266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,11 @@ package openai | |
| import ( | ||
| "context" | ||
| "io" | ||
| "log/slog" | ||
| "net/http" | ||
| "net/url" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "gomodel/internal/core" | ||
| "gomodel/internal/llmclient" | ||
|
|
@@ -15,24 +17,27 @@ import ( | |
| type RequestMutator func(*llmclient.Request) | ||
|
|
||
| type CompatibleProviderConfig struct { | ||
| ProviderName string | ||
| BaseURL string | ||
| SetHeaders func(*http.Request, string) | ||
| RequestMutator RequestMutator | ||
| ProviderName string | ||
| BaseURL string | ||
| SetHeaders func(*http.Request, string) | ||
| RequestMutator RequestMutator | ||
| ConfiguredModels []string | ||
| } | ||
|
|
||
| type CompatibleProvider struct { | ||
| client *llmclient.Client | ||
| apiKey string | ||
| providerName string | ||
| requestMutator RequestMutator | ||
| client *llmclient.Client | ||
| apiKey string | ||
| providerName string | ||
| requestMutator RequestMutator | ||
| configuredModels []string | ||
| } | ||
|
|
||
| func NewCompatibleProvider(apiKey string, opts providers.ProviderOptions, cfg CompatibleProviderConfig) *CompatibleProvider { | ||
| p := &CompatibleProvider{ | ||
| apiKey: apiKey, | ||
| providerName: cfg.ProviderName, | ||
| requestMutator: cfg.RequestMutator, | ||
| apiKey: apiKey, | ||
| providerName: cfg.ProviderName, | ||
| requestMutator: cfg.RequestMutator, | ||
| configuredModels: normalizeConfiguredModels(cfg.ConfiguredModels), | ||
| } | ||
| clientCfg := llmclient.Config{ | ||
| ProviderName: cfg.ProviderName, | ||
|
|
@@ -54,9 +59,10 @@ func NewCompatibleProviderWithHTTPClient(apiKey string, httpClient *http.Client, | |
| httpClient = http.DefaultClient | ||
| } | ||
| p := &CompatibleProvider{ | ||
| apiKey: apiKey, | ||
| providerName: cfg.ProviderName, | ||
| requestMutator: cfg.RequestMutator, | ||
| apiKey: apiKey, | ||
| providerName: cfg.ProviderName, | ||
| requestMutator: cfg.RequestMutator, | ||
| configuredModels: normalizeConfiguredModels(cfg.ConfiguredModels), | ||
| } | ||
| clientCfg := llmclient.DefaultConfig(cfg.ProviderName, cfg.BaseURL) | ||
| clientCfg.Hooks = hooks | ||
|
|
@@ -127,6 +133,53 @@ func (p *CompatibleProvider) StreamChatCompletion(ctx context.Context, req *core | |
| } | ||
|
|
||
| func (p *CompatibleProvider) ListModels(ctx context.Context) (*core.ModelsResponse, error) { | ||
| if len(p.configuredModels) == 0 { | ||
| return p.doListModels(ctx) | ||
| } | ||
|
|
||
| resp, err := p.doListModels(ctx) | ||
| if err != nil { | ||
| slog.Warn("openai-compatible upstream ListModels failed, using configured models fallback", | ||
| "provider", p.providerName, | ||
| "error", err, | ||
| "configured_models", len(p.configuredModels), | ||
| ) | ||
| } | ||
|
|
||
| byID := make(map[string]core.Model, len(p.configuredModels)) | ||
| if resp != nil { | ||
| for _, model := range resp.Data { | ||
| byID[strings.TrimSpace(model.ID)] = model | ||
| } | ||
| } | ||
|
|
||
| data := make([]core.Model, 0, len(p.configuredModels)) | ||
| for _, modelID := range p.configuredModels { | ||
| model, ok := byID[modelID] | ||
| if !ok { | ||
| model = core.Model{ | ||
| ID: modelID, | ||
| Object: "model", | ||
| OwnedBy: p.providerName, | ||
| } | ||
| } else { | ||
| if strings.TrimSpace(model.Object) == "" { | ||
| model.Object = "model" | ||
| } | ||
| if strings.TrimSpace(model.OwnedBy) == "" { | ||
| model.OwnedBy = p.providerName | ||
| } | ||
| } | ||
| data = append(data, model) | ||
| } | ||
|
|
||
| return &core.ModelsResponse{ | ||
| Object: "list", | ||
| Data: data, | ||
| }, nil | ||
|
Comment on lines
135
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When This is a subtle semantic shift for providers like If the intent is purely "fallback when upstream is unavailable," the success path could instead return the upstream response directly (unchanged), and only use the configured list when func (p *CompatibleProvider) ListModels(ctx context.Context) (*core.ModelsResponse, error) {
resp, err := p.doListModels(ctx)
if err == nil {
return resp, nil // upstream worked — return as-is
}
if len(p.configuredModels) == 0 {
return nil, err
}
// fallback to configured models ...
}If the allow-list behaviour is intentional (i.e. |
||
| } | ||
|
Comment on lines
135
to
+180
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On upstream success, configured models silently filter out any upstream model not listed in config. When The PR description frames Proposed fix: keep upstream-only models when the upstream call succeeded func (p *CompatibleProvider) ListModels(ctx context.Context) (*core.ModelsResponse, error) {
if len(p.configuredModels) == 0 {
return p.doListModels(ctx)
}
resp, err := p.doListModels(ctx)
if err != nil {
slog.Warn("openai-compatible upstream ListModels failed, using configured models fallback",
"provider", p.providerName,
"error", err,
"configured_models", len(p.configuredModels),
)
}
byID := make(map[string]core.Model, len(p.configuredModels))
if resp != nil {
for _, model := range resp.Data {
byID[strings.TrimSpace(model.ID)] = model
}
}
- data := make([]core.Model, 0, len(p.configuredModels))
- for _, modelID := range p.configuredModels {
- model, ok := byID[modelID]
- if !ok {
- model = core.Model{
- ID: modelID,
- Object: "model",
- OwnedBy: p.providerName,
- }
- } else {
- if strings.TrimSpace(model.Object) == "" {
- model.Object = "model"
- }
- if strings.TrimSpace(model.OwnedBy) == "" {
- model.OwnedBy = p.providerName
- }
- }
- data = append(data, model)
- }
+ seen := make(map[string]struct{}, len(p.configuredModels))
+ data := make([]core.Model, 0, len(p.configuredModels))
+ for _, modelID := range p.configuredModels {
+ model, ok := byID[modelID]
+ if !ok {
+ model = core.Model{ID: modelID, Object: "model", OwnedBy: p.providerName}
+ } else {
+ if strings.TrimSpace(model.Object) == "" {
+ model.Object = "model"
+ }
+ if strings.TrimSpace(model.OwnedBy) == "" {
+ model.OwnedBy = p.providerName
+ }
+ }
+ seen[modelID] = struct{}{}
+ data = append(data, model)
+ }
+ // Preserve upstream-only entries when upstream discovery succeeded.
+ if err == nil && resp != nil {
+ for _, model := range resp.Data {
+ if _, dup := seen[strings.TrimSpace(model.ID)]; dup {
+ continue
+ }
+ data = append(data, model)
+ }
+ }
return &core.ModelsResponse{
Object: "list",
Data: data,
}, nil
}🤖 Prompt for AI Agents |
||
|
|
||
| func (p *CompatibleProvider) doListModels(ctx context.Context) (*core.ModelsResponse, error) { | ||
| var resp core.ModelsResponse | ||
| err := p.Do(ctx, llmclient.Request{ | ||
| Method: http.MethodGet, | ||
|
|
@@ -492,3 +545,28 @@ func responseInputItemsEndpoint(id string, params core.ResponseInputItemsParams) | |
| } | ||
| return endpoint | ||
| } | ||
|
|
||
| // normalizeConfiguredModels deduplicates and trims model names. | ||
| func normalizeConfiguredModels(models []string) []string { | ||
| if len(models) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| seen := make(map[string]struct{}, len(models)) | ||
| normalized := make([]string, 0, len(models)) | ||
| for _, model := range models { | ||
| model = strings.TrimSpace(model) | ||
| if model == "" { | ||
| continue | ||
| } | ||
| if _, exists := seen[model]; exists { | ||
| continue | ||
| } | ||
| seen[model] = struct{}{} | ||
| normalized = append(normalized, model) | ||
| } | ||
| if len(normalized) == 0 { | ||
| return nil | ||
| } | ||
| return normalized | ||
| } | ||
|
Comment on lines
+549
to
+572
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider case-insensitive de-duplication, or document the case-sensitive contract.
🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline fallback loop in Azure's
ListModelstrims and skips empty entries, but does not deduplicate. If a user's configuration contains duplicate model IDs (e.g.["my-model", "my-model"]), the response will contain duplicate entries. ThenormalizeConfiguredModelshelper introduced in this PR handles exactly this — it trims, filters blanks, and deduplicates. For consistency, consider pre-normalizing the models inNewbefore assigning top.configuredModelsrather than repeating the logic inline.