Skip to content

Commit 666066b

Browse files
fix(providers): refresh unavailable providers on request (#357)
* fix(providers): refresh unavailable providers on request * fix(providers): handle alias refresh and empty inventories * fix(gateway): surface refresh target resolution errors
1 parent 37eb40b commit 666066b

9 files changed

Lines changed: 974 additions & 17 deletions

internal/aliases/service.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,28 @@ func (s *Service) ResolveModel(requested core.RequestedModelSelector) (core.Mode
149149
return resolution.Resolved, changed, nil
150150
}
151151

152+
// ResolveRefreshTarget returns an alias target without consulting the current
153+
// catalog so callers can refresh an unavailable target provider before normal
154+
// alias resolution is retried.
155+
func (s *Service) ResolveRefreshTarget(requested core.RequestedModelSelector) (core.ModelSelector, bool, error) {
156+
if s == nil || requested.ExplicitProvider {
157+
return core.ModelSelector{}, false, nil
158+
}
159+
name := normalizeName(requested.Model)
160+
if name == "" {
161+
return core.ModelSelector{}, false, nil
162+
}
163+
alias, ok := s.Get(name)
164+
if !ok || !alias.Enabled {
165+
return core.ModelSelector{}, false, nil
166+
}
167+
target, err := alias.TargetSelector()
168+
if err != nil {
169+
return core.ModelSelector{}, false, err
170+
}
171+
return target, true, nil
172+
}
173+
152174
// Supports reports whether an alias currently resolves to a concrete model.
153175
func (s *Service) Supports(model string) bool {
154176
_, ok := s.resolveAlias(model)

internal/aliases/service_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,43 @@ func TestServiceResolveAndExposeModels(t *testing.T) {
128128
}
129129
}
130130

131+
func TestServiceResolveRefreshTargetDoesNotRequireCatalogSupport(t *testing.T) {
132+
service, err := NewService(newMemoryStore(Alias{
133+
Name: "smart",
134+
TargetModel: "qwen3:8b",
135+
TargetProvider: "ollama",
136+
Enabled: true,
137+
}), newTestCatalog())
138+
if err != nil {
139+
t.Fatalf("NewService() error = %v", err)
140+
}
141+
if err := service.Refresh(context.Background()); err != nil {
142+
t.Fatalf("Refresh() error = %v", err)
143+
}
144+
145+
selector, changed, err := service.ResolveModel(core.NewRequestedModelSelector("smart", ""))
146+
if err != nil {
147+
t.Fatalf("ResolveModel() error = %v", err)
148+
}
149+
if changed {
150+
t.Fatal("ResolveModel() changed = true, want false while target is absent from catalog")
151+
}
152+
if got := selector.QualifiedModel(); got != "smart" {
153+
t.Fatalf("ResolveModel() selector = %q, want smart", got)
154+
}
155+
156+
target, ok, err := service.ResolveRefreshTarget(core.NewRequestedModelSelector("smart", ""))
157+
if err != nil {
158+
t.Fatalf("ResolveRefreshTarget() error = %v", err)
159+
}
160+
if !ok {
161+
t.Fatal("ResolveRefreshTarget() ok = false, want true")
162+
}
163+
if got := target.QualifiedModel(); got != "ollama/qwen3:8b" {
164+
t.Fatalf("ResolveRefreshTarget() = %q, want ollama/qwen3:8b", got)
165+
}
166+
}
167+
131168
func TestServiceUpsertRejectsAliasChainAndAllowsMasking(t *testing.T) {
132169
catalog := newTestCatalog()
133170
catalog.add("gpt-4o", "openai", core.Model{ID: "gpt-4o", Object: "model"})

internal/gateway/request_model_resolution.go

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ type modelCountProvider interface {
1111
ModelCount() int
1212
}
1313

14+
type providerModelRefresher interface {
15+
RefreshProviderModels(ctx context.Context, providerSelector string) (int, error)
16+
}
17+
18+
type modelRefreshTargetResolver interface {
19+
ResolveRefreshTarget(requested core.RequestedModelSelector) (core.ModelSelector, bool, error)
20+
}
21+
1422
// ResolvedProviderName returns the configured provider instance name for a selector.
1523
func ResolvedProviderName(provider core.RoutableProvider, selector core.ModelSelector, fallback string) string {
1624
fallback = strings.TrimSpace(fallback)
@@ -64,8 +72,20 @@ func ResolveRequestModelWithAuthorizer(
6472
requested = core.NewRequestedModelSelector(requested.Model, requested.ProviderHint)
6573

6674
resolvedSelector, aliasApplied, err := ResolveExecutionSelector(provider, resolver, requested)
75+
refreshed := false
6776
if err != nil {
68-
return nil, core.NewInvalidRequestError(err.Error(), err)
77+
var refreshErr error
78+
refreshed, refreshErr = refreshProviderModelsForResolution(ctx, provider, resolver, requested, resolvedSelector)
79+
if refreshErr != nil {
80+
return nil, refreshErr
81+
}
82+
if !refreshed {
83+
return nil, core.NewInvalidRequestError(err.Error(), err)
84+
}
85+
resolvedSelector, aliasApplied, err = ResolveExecutionSelector(provider, resolver, requested)
86+
if err != nil {
87+
return nil, core.NewInvalidRequestError(err.Error(), err)
88+
}
6989
}
7090
if resolvedSelector == (core.ModelSelector{}) {
7191
resolvedSelector, err = requested.Normalize()
@@ -75,9 +95,41 @@ func ResolveRequestModelWithAuthorizer(
7595
}
7696

7797
resolvedModel := resolvedSelector.QualifiedModel()
98+
if counted, ok := provider.(modelCountProvider); ok && counted.ModelCount() == 0 {
99+
if !refreshed {
100+
var refreshErr error
101+
refreshed, refreshErr = refreshProviderModelsForResolution(ctx, provider, resolver, requested, resolvedSelector)
102+
if refreshErr != nil {
103+
return nil, refreshErr
104+
}
105+
if refreshed {
106+
resolvedSelector, aliasApplied, err = ResolveExecutionSelector(provider, resolver, requested)
107+
if err != nil {
108+
return nil, core.NewInvalidRequestError(err.Error(), err)
109+
}
110+
resolvedModel = resolvedSelector.QualifiedModel()
111+
}
112+
}
113+
}
78114
if counted, ok := provider.(modelCountProvider); ok && counted.ModelCount() == 0 {
79115
return nil, core.NewProviderError("", 0, "model registry not initialized", nil)
80116
}
117+
if !provider.Supports(resolvedModel) {
118+
if !refreshed {
119+
var refreshErr error
120+
refreshed, refreshErr = refreshProviderModelsForResolution(ctx, provider, resolver, requested, resolvedSelector)
121+
if refreshErr != nil {
122+
return nil, refreshErr
123+
}
124+
if refreshed {
125+
resolvedSelector, aliasApplied, err = ResolveExecutionSelector(provider, resolver, requested)
126+
if err != nil {
127+
return nil, core.NewInvalidRequestError(err.Error(), err)
128+
}
129+
resolvedModel = resolvedSelector.QualifiedModel()
130+
}
131+
}
132+
}
81133
if !provider.Supports(resolvedModel) {
82134
return nil, core.NewInvalidRequestError("unsupported model: "+resolvedModel, nil)
83135
}
@@ -96,6 +148,45 @@ func ResolveRequestModelWithAuthorizer(
96148
}, nil
97149
}
98150

151+
func refreshProviderModelsForResolution(
152+
ctx context.Context,
153+
provider core.RoutableProvider,
154+
resolver ModelResolver,
155+
requested core.RequestedModelSelector,
156+
resolvedSelector core.ModelSelector,
157+
) (bool, error) {
158+
refresher, ok := provider.(providerModelRefresher)
159+
if !ok {
160+
return false, nil
161+
}
162+
163+
providerSelector := strings.TrimSpace(resolvedSelector.Provider)
164+
if providerSelector == "" {
165+
if targetResolver, ok := resolver.(modelRefreshTargetResolver); ok {
166+
selector, ok, err := targetResolver.ResolveRefreshTarget(requested)
167+
if err != nil {
168+
return false, err
169+
}
170+
if ok {
171+
providerSelector = strings.TrimSpace(selector.Provider)
172+
}
173+
}
174+
}
175+
if providerSelector == "" {
176+
selector, err := requested.Normalize()
177+
if err != nil {
178+
return false, nil
179+
}
180+
providerSelector = strings.TrimSpace(selector.Provider)
181+
}
182+
if providerSelector == "" {
183+
return false, nil
184+
}
185+
186+
_, err := refresher.RefreshProviderModels(ctx, providerSelector)
187+
return true, err
188+
}
189+
99190
// ResolveExecutionSelector applies explicit and provider-owned selector resolution.
100191
func ResolveExecutionSelector(
101192
provider core.RoutableProvider,
@@ -119,12 +210,15 @@ func ResolveExecutionSelector(
119210
}
120211

121212
if providerResolver, ok := provider.(ModelResolver); ok {
122-
var providerChanged bool
123-
resolvedSelector, providerChanged, err = providerResolver.ResolveModel(requested)
213+
providerSelector, providerChanged, err := providerResolver.ResolveModel(requested)
124214
if err != nil {
215+
if resolvedSelector != (core.ModelSelector{}) {
216+
// Preserve alias targets so callers can refresh the concrete provider before retrying.
217+
return resolvedSelector, aliasApplied, err
218+
}
125219
return core.ModelSelector{}, false, err
126220
}
127-
return resolvedSelector, aliasApplied || providerChanged, nil
221+
return providerSelector, aliasApplied || providerChanged, nil
128222
}
129223

130224
if resolvedSelector != (core.ModelSelector{}) {

0 commit comments

Comments
 (0)