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

View file

@ -10,7 +10,9 @@ import (
"context"
"time"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers/openai_compat"
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
)
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(
ctx context.Context,
messages []Message,
@ -52,6 +69,22 @@ func (p *HTTPProvider) Chat(
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 {
return ""
}