feat(providers): support unknown protocols as OpenAI-compatible fallback

Treat unknown protocol prefixes (e.g., zai-org/GLM-4.7) as
OpenAI-compatible HTTP providers instead of returning an error.
The full model string is passed to the API for unknown protocols.

Added tests for unknown protocol with API key, API base, and
missing auth scenarios.

Fixes #661

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vadim 2026-03-08 04:23:56 +03:00
parent 7ea7bb0717
commit dca32335ce
2 changed files with 51 additions and 4 deletions

View file

@ -10,6 +10,7 @@ import (
"strings"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
)
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
@ -53,8 +54,8 @@ func ExtractProtocol(model string) (protocol, modelID string) {
// CreateProviderFromConfig creates a provider based on the ModelConfig.
// It uses the protocol prefix in the Model field to determine which provider to create.
// Supported protocols: openai, litellm, anthropic, antigravity, claude-cli, codex-cli, github-copilot
// Returns the provider, the model ID (without protocol prefix), and any error.
// Supported protocols: openai, litellm, anthropic, antigravity, claude-cli, codex-cli, github-copilot, and any OpenAI-compatible HTTP provider.
// Returns the provider, the model ID (full model string for unknown protocols, otherwise without protocol prefix), and any error.
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
if cfg == nil {
return nil, "", fmt.Errorf("config is nil")
@ -169,7 +170,16 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
return provider, modelID, nil
default:
return nil, "", fmt.Errorf("unknown protocol %q in model %q", protocol, cfg.Model)
// Treat unknown protocols as OpenAI-compatible
logger.WarnF("unknown protocol, falling back to OpenAI-compatible HTTP provider", map[string]any{"protocol": protocol, "model": cfg.Model})
if cfg.APIKey == "" && cfg.APIBase == "" {
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
}
apiBase := cfg.APIBase
if apiBase == "" {
apiBase = getDefaultAPIBase("openai")
}
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(cfg.APIKey, apiBase, cfg.Proxy, cfg.MaxTokensField, cfg.RequestTimeout), cfg.Model, nil
}
}

View file

@ -254,9 +254,46 @@ func TestCreateProviderFromConfig_UnknownProtocol(t *testing.T) {
APIKey: "test-key",
}
provider, modelID, err := CreateProviderFromConfig(cfg)
if err != nil {
t.Fatalf("CreateProviderFromConfig() unexpected error: %v", err)
}
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider for unknown protocol with APIKey")
}
if modelID != cfg.Model {
t.Fatalf("CreateProviderFromConfig() expected modelID=%q, got %q", cfg.Model, modelID)
}
}
func TestCreateProviderFromConfig_UnknownProtocolWithAPIBase(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-unknown",
Model: "unknown-protocol/model",
APIBase: "http://localhost:8080/v1",
}
provider, modelID, err := CreateProviderFromConfig(cfg)
if err != nil {
t.Fatalf("CreateProviderFromConfig() unexpected error: %v", err)
}
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider")
}
if modelID != cfg.Model {
t.Fatalf("CreateProviderFromConfig() expected modelID=%q, got %q", cfg.Model, modelID)
}
}
func TestCreateProviderFromConfig_UnknownProtocolNoAuth(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-unknown",
Model: "unknown-protocol/model",
}
_, _, err := CreateProviderFromConfig(cfg)
if err == nil {
t.Fatal("CreateProviderFromConfig() expected error for unknown protocol")
t.Fatal("CreateProviderFromConfig() expected error for unknown protocol without api_key or api_base")
}
}