feat: enable per-provider SSE streaming via model_list stream field

Wire ModelConfig.Stream (*bool) through to openai_compat.WithStream
so each model_list entry can opt in to SSE streaming independently.

- Add NewHTTPProviderFromConfig that reads all ModelConfig fields
  including stream
- Implement StreamingProvider interface (CanStream/ChatStream) on
  HTTPProvider by delegating to openai_compat.Provider
- Switch factory_provider.go (openai, openai-compat, anthropic
  API-key paths) to use NewHTTPProviderFromConfig

Usage: set "stream": true in a model_list entry to enable streaming.
Default behavior (no stream field) is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-21 02:19:37 +09:00
parent 2548880ca7
commit 42f587c6bc
2 changed files with 36 additions and 21 deletions

View file

@ -87,13 +87,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
if apiBase == "" { if apiBase == "" {
apiBase = getDefaultAPIBase(protocol) apiBase = getDefaultAPIBase(protocol)
} }
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( return NewHTTPProviderFromConfig(cfg, apiBase), modelID, nil
cfg.APIKey,
apiBase,
cfg.Proxy,
cfg.MaxTokensField,
cfg.RequestTimeout,
), modelID, nil
case "azure", "azure-openai": case "azure", "azure-openai":
// Azure OpenAI uses deployment-based URLs, api-key header auth, // Azure OpenAI uses deployment-based URLs, api-key header auth,
@ -126,13 +120,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
if apiBase == "" { if apiBase == "" {
apiBase = getDefaultAPIBase(protocol) apiBase = getDefaultAPIBase(protocol)
} }
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( return NewHTTPProviderFromConfig(cfg, apiBase), modelID, nil
cfg.APIKey,
apiBase,
cfg.Proxy,
cfg.MaxTokensField,
cfg.RequestTimeout,
), modelID, nil
case "anthropic": case "anthropic":
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" { if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
@ -151,13 +139,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
if cfg.APIKey == "" { if cfg.APIKey == "" {
return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model) return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model)
} }
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( return NewHTTPProviderFromConfig(cfg, apiBase), modelID, nil
cfg.APIKey,
apiBase,
cfg.Proxy,
cfg.MaxTokensField,
cfg.RequestTimeout,
), modelID, nil
case "anthropic-messages": case "anthropic-messages":
// Anthropic Messages API with native format (HTTP-based, no SDK) // Anthropic Messages API with native format (HTTP-based, no SDK)

View file

@ -10,7 +10,9 @@ import (
"context" "context"
"time" "time"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers/openai_compat" "github.com/sipeed/picoclaw/pkg/providers/openai_compat"
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
) )
type HTTPProvider struct { type HTTPProvider struct {
@ -42,6 +44,21 @@ func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
} }
} }
// NewHTTPProviderFromConfig creates an HTTPProvider from a ModelConfig,
// honoring all optional fields including stream.
func NewHTTPProviderFromConfig(cfg *config.ModelConfig, apiBase string) *HTTPProvider {
opts := []openai_compat.Option{
openai_compat.WithMaxTokensField(cfg.MaxTokensField),
openai_compat.WithRequestTimeout(time.Duration(cfg.RequestTimeout) * time.Second),
}
if cfg.Stream != nil && *cfg.Stream {
opts = append(opts, openai_compat.WithStream(true))
}
return &HTTPProvider{
delegate: openai_compat.NewProvider(cfg.APIKey, apiBase, cfg.Proxy, opts...),
}
}
func (p *HTTPProvider) Chat( func (p *HTTPProvider) Chat(
ctx context.Context, ctx context.Context,
messages []Message, messages []Message,
@ -52,6 +69,22 @@ func (p *HTTPProvider) Chat(
return p.delegate.Chat(ctx, messages, tools, model, options) return p.delegate.Chat(ctx, messages, tools, model, options)
} }
// CanStream returns true when SSE streaming is enabled.
func (p *HTTPProvider) CanStream() bool {
return p.delegate.CanStream()
}
// ChatStream opens an SSE connection and returns a channel of StreamEvent.
func (p *HTTPProvider) ChatStream(
ctx context.Context,
messages []Message,
tools []ToolDefinition,
model string,
options map[string]any,
) (<-chan protocoltypes.StreamEvent, error) {
return p.delegate.ChatStream(ctx, messages, tools, model, options)
}
func (p *HTTPProvider) GetDefaultModel() string { func (p *HTTPProvider) GetDefaultModel() string {
return "" return ""
} }