diff --git a/internal/provider/catalog/service.go b/internal/provider/catalog/service.go index 5df9bcdf..8a5e23fa 100644 --- a/internal/provider/catalog/service.go +++ b/internal/provider/catalog/service.go @@ -147,11 +147,7 @@ func (s *Service) loadCatalog(ctx context.Context, identity provider.ProviderIde } func (s *Service) discoverAndPersist(ctx context.Context, input provider.CatalogInput) ([]providertypes.ModelDescriptor, error) { - supported, err := s.supportsModelDiscovery(input.Identity.Driver) - if err != nil { - return nil, err - } - if !supported { + if !s.registry.Supports(input.Identity.Driver) { return nil, nil } @@ -190,8 +186,7 @@ func (s *Service) queueRefresh(input provider.CatalogInput) { return } - supported, err := s.supportsModelDiscovery(input.Identity.Driver) - if err != nil || !supported { + if !s.registry.Supports(input.Identity.Driver) { return } identity := input.Identity @@ -221,20 +216,6 @@ func (s *Service) queueRefresh(input provider.CatalogInput) { }() } -func (s *Service) supportsModelDiscovery(driverType string) (bool, error) { - driverType = strings.TrimSpace(driverType) - if !s.registry.Supports(driverType) { - return false, nil - } - - caps, err := s.registry.DriverTransportCapabilities(driverType) - if err != nil { - return false, err - } - - return caps.ModelDiscovery, nil -} - func shouldQueueRefresh(options queryOptions, snapshot catalogSnapshot, performedSyncRefresh bool) bool { if !options.queueRefresh { return false diff --git a/internal/provider/catalog/service_test.go b/internal/provider/catalog/service_test.go index 91d06765..473ee9e5 100644 --- a/internal/provider/catalog/service_test.go +++ b/internal/provider/catalog/service_test.go @@ -363,90 +363,6 @@ func TestListProviderModelsCachedUsesFreshCatalogWithoutDiscovery(t *testing.T) } } -func TestListProviderModelsSkipsDiscoveryWhenDriverDisablesModelDiscovery(t *testing.T) { - t.Setenv(testAPIKeyEnv, "") - - var resolveCalls int32 - var discoverCalls int32 - registry := newRegistryWithCapabilities( - t, - openaicompat.DriverName, - provider.DriverTransportCapabilities{ - Streaming: true, - ToolTransport: true, - ModelDiscovery: false, - ImageInputTransport: false, - }, - func(ctx context.Context, cfg provider.RuntimeConfig) ([]providertypes.ModelDescriptor, error) { - atomic.AddInt32(&discoverCalls, 1) - return []providertypes.ModelDescriptor{{ID: "server-model", Name: "Server Model"}}, nil - }, - ) - - service := NewService("", registry, newMemoryStore()) - input := openAIProviderSource() - input.ResolveDiscoveryConfig = func() (provider.RuntimeConfig, error) { - atomic.AddInt32(&resolveCalls, 1) - return provider.RuntimeConfig{}, errors.New("resolver should not run") - } - - models, err := service.ListProviderModels(context.Background(), input) - if err != nil { - t.Fatalf("ListProviderModels() error = %v", err) - } - if len(models) != 1 || models[0].ID != config.OpenAIDefaultModel { - t.Fatalf("expected builtin default model fallback, got %+v", models) - } - if atomic.LoadInt32(&resolveCalls) != 0 { - t.Fatalf("expected discovery config resolver to be skipped, got %d calls", resolveCalls) - } - if atomic.LoadInt32(&discoverCalls) != 0 { - t.Fatalf("expected discovery func to be skipped, got %d calls", discoverCalls) - } -} - -func TestListProviderModelsCustomProviderSkipsDiscoveryWhenDriverDisablesModelDiscovery(t *testing.T) { - t.Setenv(testAPIKeyEnv, "") - - var resolveCalls int32 - var discoverCalls int32 - registry := newRegistryWithCapabilities( - t, - openaicompat.DriverName, - provider.DriverTransportCapabilities{ - Streaming: true, - ToolTransport: true, - ModelDiscovery: false, - ImageInputTransport: false, - }, - func(ctx context.Context, cfg provider.RuntimeConfig) ([]providertypes.ModelDescriptor, error) { - atomic.AddInt32(&discoverCalls, 1) - return []providertypes.ModelDescriptor{{ID: "server-model", Name: "Server Model"}}, nil - }, - ) - - service := NewService("", registry, newMemoryStore()) - input := customGatewayProviderSource() - input.ResolveDiscoveryConfig = func() (provider.RuntimeConfig, error) { - atomic.AddInt32(&resolveCalls, 1) - return provider.RuntimeConfig{}, errors.New("resolver should not run") - } - - models, err := service.ListProviderModels(context.Background(), input) - if err != nil { - t.Fatalf("ListProviderModels() error = %v", err) - } - if len(models) != 0 { - t.Fatalf("expected custom provider without discovery support to stay empty, got %+v", models) - } - if atomic.LoadInt32(&resolveCalls) != 0 { - t.Fatalf("expected discovery config resolver to be skipped, got %d calls", resolveCalls) - } - if atomic.LoadInt32(&discoverCalls) != 0 { - t.Fatalf("expected discovery func to be skipped, got %d calls", discoverCalls) - } -} - func TestDiscoverAndPersistFailurePaths(t *testing.T) { t.Run("unsupported driver", func(t *testing.T) { service := NewService("", provider.NewRegistry(), newMemoryStore()) @@ -558,33 +474,55 @@ func TestQueueRefreshDeduplicatesInFlightRequests(t *testing.T) { t.Fatal("expected in-flight refresh marker to be cleared") } -func newRegistry(t *testing.T, name string, discover provider.DiscoveryFunc) *provider.Registry { - return newRegistryWithCapabilities( - t, - name, - provider.DriverTransportCapabilities{ - Streaming: true, - ToolTransport: true, - ModelDiscovery: true, - ImageInputTransport: false, - }, - discover, - ) +func TestQueueRefreshSkipsWhenDriverUnsupportedOrIdentityIncomplete(t *testing.T) { + t.Parallel() + + t.Run("unsupported driver", func(t *testing.T) { + t.Parallel() + + service := NewService("", provider.NewRegistry(), newMemoryStore()) + input := openAIProviderSource() + input.Identity.Driver = "missing" + + service.queueRefresh(input) + + service.refreshMu.Lock() + defer service.refreshMu.Unlock() + if len(service.inFlightByID) != 0 { + t.Fatalf("expected no refresh to be queued, got %+v", service.inFlightByID) + } + }) + + t.Run("incomplete identity", func(t *testing.T) { + t.Parallel() + + service := NewService("", newRegistry(t, openaicompat.DriverName, nil), newMemoryStore()) + input := openAIProviderSource() + input.Identity.BaseURL = "" + + service.queueRefresh(input) + + service.refreshMu.Lock() + defer service.refreshMu.Unlock() + if len(service.inFlightByID) != 0 { + t.Fatalf("expected no refresh to be queued, got %+v", service.inFlightByID) + } + }) } -func newRegistryWithCapabilities( - t *testing.T, - name string, - capabilities provider.DriverTransportCapabilities, - discover provider.DiscoveryFunc, -) *provider.Registry { +func newRegistry(t *testing.T, name string, discover provider.DiscoveryFunc) *provider.Registry { t.Helper() + if discover == nil { + discover = func(ctx context.Context, cfg provider.RuntimeConfig) ([]providertypes.ModelDescriptor, error) { + return nil, nil + } + } + registry := provider.NewRegistry() if err := registry.Register(provider.DriverDefinition{ - Name: name, - Discover: discover, - Capabilities: capabilities, + Name: name, + Discover: discover, Build: func(ctx context.Context, cfg provider.RuntimeConfig) (provider.Provider, error) { return catalogTestProvider{}, nil }, diff --git a/internal/provider/contracts.go b/internal/provider/contracts.go index 42fb33a8..a3d20a55 100644 --- a/internal/provider/contracts.go +++ b/internal/provider/contracts.go @@ -23,14 +23,6 @@ type Provider interface { Generate(ctx context.Context, req providertypes.GenerateRequest, events chan<- providertypes.StreamEvent) error } -// DriverTransportCapabilities 描述 driver 层本身是否能传输某类运行时能力。 -type DriverTransportCapabilities struct { - Streaming bool - ToolTransport bool - ModelDiscovery bool - ImageInputTransport bool -} - // CatalogInput 汇总 provider/catalog 查询、发现与缓存所需的最小输入。 type CatalogInput struct { Identity ProviderIdentity diff --git a/internal/provider/openaicompat/driver.go b/internal/provider/openaicompat/driver.go index ef6202c4..6c5b9e93 100644 --- a/internal/provider/openaicompat/driver.go +++ b/internal/provider/openaicompat/driver.go @@ -43,11 +43,5 @@ func driverDefinition(name string) provider.DriverDefinition { return p.DiscoverModels(ctx) }, ValidateCatalogIdentity: validateCatalogIdentity, - Capabilities: provider.DriverTransportCapabilities{ - Streaming: true, - ToolTransport: true, - ModelDiscovery: true, - ImageInputTransport: false, - }, } } diff --git a/internal/provider/registry.go b/internal/provider/registry.go index f89eddc2..57d3d54e 100644 --- a/internal/provider/registry.go +++ b/internal/provider/registry.go @@ -17,7 +17,6 @@ type DriverDefinition struct { Build Builder Discover DiscoveryFunc ValidateCatalogIdentity func(identity ProviderIdentity) error - Capabilities DriverTransportCapabilities } type Registry struct { @@ -43,6 +42,9 @@ func (r *Registry) Register(driver DriverDefinition) error { if driver.Build == nil { return fmt.Errorf("provider: driver %q build func is nil", driver.Name) } + if driver.Discover == nil { + return fmt.Errorf("provider: driver %q discover func is nil", driver.Name) + } if _, exists := r.drivers[driverType]; exists { return fmt.Errorf("%w: %s", ErrDriverAlreadyRegistered, driver.Name) } @@ -63,9 +65,6 @@ func (r *Registry) DiscoverModels(ctx context.Context, cfg RuntimeConfig) ([]pro if err != nil { return nil, err } - if driver.Discover == nil { - return nil, nil - } return driver.Discover(ctx, cfg) } @@ -74,15 +73,6 @@ func (r *Registry) Supports(driverType string) bool { return err == nil } -// DriverTransportCapabilities 返回指定 driver 的传输能力声明;driver 不存在时返回对应错误。 -func (r *Registry) DriverTransportCapabilities(driverType string) (DriverTransportCapabilities, error) { - driver, err := r.driver(driverType) - if err != nil { - return DriverTransportCapabilities{}, err - } - return driver.Capabilities, nil -} - // ValidateCatalogIdentity 在读取 catalog 快照或执行默认模型回退前执行无需密钥的静态校验,避免无效配置被误判为可用。 func (r *Registry) ValidateCatalogIdentity(identity ProviderIdentity) error { if r == nil { diff --git a/internal/provider/registry_test.go b/internal/provider/registry_test.go index dc471b7a..0d0f05ec 100644 --- a/internal/provider/registry_test.go +++ b/internal/provider/registry_test.go @@ -23,6 +23,9 @@ func stubDriver(driverType string) provider.DriverDefinition { Build: func(ctx context.Context, cfg provider.RuntimeConfig) (provider.Provider, error) { return stubProvider{}, nil }, + Discover: func(ctx context.Context, cfg provider.RuntimeConfig) ([]providertypes.ModelDescriptor, error) { + return nil, nil + }, } } @@ -111,30 +114,6 @@ func TestRegistryDiscoverModels(t *testing.T) { } }) - t.Run("driver without discovery function", func(t *testing.T) { - t.Parallel() - - registry := provider.NewRegistry() - driver := provider.DriverDefinition{ - Name: "test-driver", - Build: func(ctx context.Context, cfg provider.RuntimeConfig) (provider.Provider, error) { - return stubProvider{}, nil - }, - Discover: nil, - } - if err := registry.Register(driver); err != nil { - t.Fatalf("Register() error = %v", err) - } - - got, err := registry.DiscoverModels(context.Background(), provider.RuntimeConfig{Driver: "test-driver"}) - if err != nil { - t.Fatalf("DiscoverModels() error = %v", err) - } - if got != nil { - t.Fatalf("expected nil models, got %v", got) - } - }) - t.Run("unknown driver", func(t *testing.T) { t.Parallel() @@ -176,30 +155,6 @@ func TestRegistrySupports(t *testing.T) { } } -func TestRegistryDriverTransportCapabilities(t *testing.T) { - t.Parallel() - - registry := newTestRegistry(t) - got, err := registry.DriverTransportCapabilities("OPENAICOMPAT") - if err != nil { - t.Fatalf("DriverTransportCapabilities() error = %v", err) - } - if !got.Streaming { - t.Fatalf("expected openaicompat driver to support streaming, got %+v", got) - } - if !got.ToolTransport { - t.Fatalf("expected openaicompat driver to support tool transport, got %+v", got) - } - if !got.ModelDiscovery { - t.Fatalf("expected openaicompat driver to support model discovery, got %+v", got) - } - - _, err = registry.DriverTransportCapabilities("missing") - if !errors.Is(err, provider.ErrDriverNotFound) { - t.Fatalf("expected ErrDriverNotFound for missing driver, got %v", err) - } -} - func TestRegistryValidateCatalogIdentity(t *testing.T) { t.Parallel() @@ -277,6 +232,25 @@ func TestRegistryRegisterErrors(t *testing.T) { t.Fatalf("unexpected error: %v", err) } }) + + t.Run("nil discover function", func(t *testing.T) { + t.Parallel() + + registry := provider.NewRegistry() + err := registry.Register(provider.DriverDefinition{ + Name: "test", + Build: func(ctx context.Context, cfg provider.RuntimeConfig) (provider.Provider, error) { + return nil, nil + }, + Discover: nil, + }) + if err == nil { + t.Fatal("expected error for nil discover function") + } + if err.Error() != `provider: driver "test" discover func is nil` { + t.Fatalf("unexpected error: %v", err) + } + }) } func TestRegistryDriverWithNilMap(t *testing.T) { diff --git a/internal/runtime/compact_generator.go b/internal/runtime/compact_generator.go index ff063827..061e1f6b 100644 --- a/internal/runtime/compact_generator.go +++ b/internal/runtime/compact_generator.go @@ -42,9 +42,6 @@ func (g *compactSummaryGenerator) Generate(ctx context.Context, input contextcom strings.TrimSpace(g.providerConfig.APIKey) == "" { return "", errors.New("runtime: compact summary generator provider config is incomplete") } - if err := ensureDriverTransportCapabilities(g.providerFactory, g.providerConfig, true, false); err != nil { - return "", err - } prompt := agentcontext.BuildCompactPrompt(agentcontext.CompactPromptInput{ Mode: string(input.Mode), diff --git a/internal/runtime/compact_generator_test.go b/internal/runtime/compact_generator_test.go index 8d21c41c..6d327003 100644 --- a/internal/runtime/compact_generator_test.go +++ b/internal/runtime/compact_generator_test.go @@ -233,68 +233,3 @@ func TestCompactSummaryGeneratorMalformedStreamEventDoesNotDeadlock(t *testing.T t.Fatal("expected compact generation to fail instead of deadlocking on malformed stream event") } } - -func TestCompactSummaryGeneratorRejectsDriverWithoutStreaming(t *testing.T) { - t.Parallel() - - manager := newRuntimeConfigManager(t) - resolvedProvider, err := resolvedProviderForTests(manager.Get(), config.OpenAIName) - if err != nil { - t.Fatalf("resolve provider: %v", err) - } - - scripted := &scriptedProvider{ - streams: [][]providertypes.StreamEvent{ - {providertypes.NewTextDeltaStreamEvent("should not run")}, - }, - } - factory := &scriptedProviderFactory{ - provider: scripted, - capabilities: provider.DriverTransportCapabilities{ - Streaming: false, - ToolTransport: true, - }, - } - generator := newCompactSummaryGenerator(factory, resolvedProvider.ToRuntimeConfig(), "session-model") - - _, err = generator.Generate(context.Background(), contextcompact.SummaryInput{ - Mode: contextcompact.ModeManual, - Config: manager.Get().Context.Compact, - }) - if err == nil || !strings.Contains(err.Error(), "does not support streaming") { - t.Fatalf("expected streaming capability rejection, got %v", err) - } - if factory.calls != 0 { - t.Fatalf("expected provider build to be skipped, got %d", factory.calls) - } - if scripted.callCount != 0 { - t.Fatalf("expected provider Generate() to be skipped, got %d", scripted.callCount) - } -} - -func TestCompactSummaryGeneratorPropagatesDriverNotFoundFromCapabilities(t *testing.T) { - t.Parallel() - - manager := newRuntimeConfigManager(t) - resolvedProvider, err := resolvedProviderForTests(manager.Get(), config.OpenAIName) - if err != nil { - t.Fatalf("resolve provider: %v", err) - } - - factory := &scriptedProviderFactory{ - provider: &scriptedProvider{}, - capabilitiesErr: provider.ErrDriverNotFound, - } - generator := newCompactSummaryGenerator(factory, resolvedProvider.ToRuntimeConfig(), "session-model") - - _, err = generator.Generate(context.Background(), contextcompact.SummaryInput{ - Mode: contextcompact.ModeManual, - Config: manager.Get().Context.Compact, - }) - if !errors.Is(err, provider.ErrDriverNotFound) { - t.Fatalf("expected ErrDriverNotFound, got %v", err) - } - if factory.calls != 0 { - t.Fatalf("expected provider build to be skipped, got %d", factory.calls) - } -} diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 2a89957c..7b3ff365 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -130,7 +130,6 @@ type UserInput struct { type ProviderFactory interface { Build(ctx context.Context, cfg provider.RuntimeConfig) (provider.Provider, error) - DriverTransportCapabilities(driverType string) (provider.DriverTransportCapabilities, error) } type Service struct { @@ -689,31 +688,6 @@ func isRetryableProviderError(err error) bool { return pErr.Retryable } -// ensureDriverTransportCapabilities 校验当前 driver 是否满足指定运行场景的基础传输能力要求。 -func ensureDriverTransportCapabilities( - factory ProviderFactory, - cfg provider.RuntimeConfig, - requireStreaming bool, - requireToolTransport bool, -) error { - if factory == nil { - return errors.New("runtime: provider factory is nil") - } - - driverType := strings.TrimSpace(cfg.Driver) - caps, err := factory.DriverTransportCapabilities(driverType) - if err != nil { - return err - } - if requireStreaming && !caps.Streaming { - return fmt.Errorf("runtime: provider driver %q does not support streaming", driverType) - } - if requireToolTransport && !caps.ToolTransport { - return fmt.Errorf("runtime: provider driver %q does not support tool transport", driverType) - } - return nil -} - // callProviderWithRetry 在可重试的 ProviderError 上自动重试 provider.Generate() 调用。 // 每次重试都会重新创建 provider 实例、流式事件转发管道和累积器。 // 非可重试错误、context 取消、重试耗尽时直接返回错误。 @@ -748,10 +722,6 @@ func (s *Service) callProviderWithRetry( return nil, err } runtimeCfg := resolvedProvider.ToRuntimeConfig() - requireToolTransport := len(req.Tools) > 0 - if err := ensureDriverTransportCapabilities(s.providerFactory, runtimeCfg, true, requireToolTransport); err != nil { - return nil, err - } modelProvider, err := s.providerFactory.Build(ctx, runtimeCfg) if err != nil { diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go index acb72e97..2cf84928 100644 --- a/internal/runtime/runtime_test.go +++ b/internal/runtime/runtime_test.go @@ -172,12 +172,10 @@ func streamContainsMessageDone(events []providertypes.StreamEvent) bool { } type scriptedProviderFactory struct { - provider provider.Provider - calls int - configs []provider.RuntimeConfig - err error - capabilities provider.DriverTransportCapabilities - capabilitiesErr error + provider provider.Provider + calls int + configs []provider.RuntimeConfig + err error } func (f *scriptedProviderFactory) Build(ctx context.Context, cfg provider.RuntimeConfig) (provider.Provider, error) { @@ -189,19 +187,6 @@ func (f *scriptedProviderFactory) Build(ctx context.Context, cfg provider.Runtim return f.provider, nil } -func (f *scriptedProviderFactory) DriverTransportCapabilities(driverType string) (provider.DriverTransportCapabilities, error) { - if f.capabilitiesErr != nil { - return provider.DriverTransportCapabilities{}, f.capabilitiesErr - } - if f.capabilities == (provider.DriverTransportCapabilities{}) { - return provider.DriverTransportCapabilities{ - Streaming: true, - ToolTransport: true, - }, nil - } - return f.capabilities, nil -} - type stubTool struct { name string content string @@ -3116,115 +3101,6 @@ func TestCallProviderWithRetryReturnsCombinedForwardError(t *testing.T) { } } -func TestServiceRunRejectsDriverWithoutToolTransportWhenRequestExposesTools(t *testing.T) { - t.Parallel() - - manager := newRuntimeConfigManager(t) - store := newMemoryStore() - registry := tools.NewRegistry() - registry.Register(&stubTool{name: "filesystem_read_file", content: "default"}) - - scripted := &scriptedProvider{ - streams: [][]providertypes.StreamEvent{ - {providertypes.NewTextDeltaStreamEvent("should not run")}, - }, - } - factory := &scriptedProviderFactory{ - provider: scripted, - capabilities: provider.DriverTransportCapabilities{ - Streaming: true, - ToolTransport: false, - }, - } - - service := NewWithFactory(manager, registry, store, factory, &stubContextBuilder{}) - err := service.Run(context.Background(), UserInput{ - RunID: "run-driver-no-tools", - Content: "hello", - }) - if err == nil || !containsError(err, "does not support tool transport") { - t.Fatalf("expected tool transport capability error, got %v", err) - } - if factory.calls != 0 { - t.Fatalf("expected provider build to be skipped, got %d", factory.calls) - } - if scripted.callCount != 0 { - t.Fatalf("expected provider Generate() to be skipped, got %d", scripted.callCount) - } -} - -func TestServiceRunAllowsDriverWithoutToolTransportWhenRequestHasNoTools(t *testing.T) { - t.Parallel() - - manager := newRuntimeConfigManager(t) - store := newMemoryStore() - toolManager := &stubToolManager{} - - scripted := &scriptedProvider{ - streams: [][]providertypes.StreamEvent{ - {providertypes.NewTextDeltaStreamEvent("plain answer")}, - }, - } - factory := &scriptedProviderFactory{ - provider: scripted, - capabilities: provider.DriverTransportCapabilities{ - Streaming: true, - ToolTransport: false, - }, - } - - service := NewWithFactory(manager, toolManager, store, factory, &stubContextBuilder{}) - err := service.Run(context.Background(), UserInput{ - RunID: "run-driver-no-tool-transport-no-tools", - Content: "hello", - }) - if err != nil { - t.Fatalf("expected no-tools request to succeed, got %v", err) - } - if factory.calls != 1 { - t.Fatalf("expected provider build once, got %d", factory.calls) - } - if scripted.callCount != 1 { - t.Fatalf("expected provider Generate() once, got %d", scripted.callCount) - } - if len(scripted.requests) != 1 { - t.Fatalf("expected one provider request, got %d", len(scripted.requests)) - } - if len(scripted.requests[0].Tools) != 0 { - t.Fatalf("expected provider request without tools, got %+v", scripted.requests[0].Tools) - } - - events := collectRuntimeEvents(service.Events()) - assertEventSequence(t, events, []EventType{EventUserMessage, EventAgentChunk, EventAgentDone}) - assertNoEventType(t, events, EventError) -} - -func TestServiceRunPropagatesDriverNotFoundFromCapabilities(t *testing.T) { - t.Parallel() - - manager := newRuntimeConfigManager(t) - store := newMemoryStore() - registry := tools.NewRegistry() - registry.Register(&stubTool{name: "filesystem_read_file", content: "default"}) - - factory := &scriptedProviderFactory{ - provider: &scriptedProvider{}, - capabilitiesErr: fmt.Errorf("%w: missing", provider.ErrDriverNotFound), - } - - service := NewWithFactory(manager, registry, store, factory, &stubContextBuilder{}) - err := service.Run(context.Background(), UserInput{ - RunID: "run-driver-missing", - Content: "hello", - }) - if !errors.Is(err, provider.ErrDriverNotFound) { - t.Fatalf("expected ErrDriverNotFound, got %v", err) - } - if factory.calls != 0 { - t.Fatalf("expected provider build to be skipped, got %d", factory.calls) - } -} - func TestServiceRunPersistsAndRestoresTokenUsage(t *testing.T) { t.Parallel()