mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-05-25 16:00:35 +00:00
feat(provider): add gpt4free openai-compatible provider (#2909)
This commit is contained in:
@@ -191,7 +191,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
}
|
||||
return finalizeProviderFromConfig(provider, modelID, cfg)
|
||||
|
||||
case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "nvidia", "venice",
|
||||
case "litellm", "lmstudio", "gpt4free", "openrouter", "groq", "zhipu", "nvidia", "venice",
|
||||
"ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
|
||||
"vivgrid", "volcengine", "vllm", "qwen-portal", "qwen-intl", "qwen-us", "mistral",
|
||||
"avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo":
|
||||
|
||||
@@ -210,6 +210,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
|
||||
{"deepseek", "deepseek"},
|
||||
{"ollama", "ollama"},
|
||||
{"lmstudio", "lmstudio"},
|
||||
{"gpt4free", "gpt4free"},
|
||||
{"longcat", "longcat"},
|
||||
{"modelscope", "modelscope"},
|
||||
{"mimo", "mimo"},
|
||||
@@ -248,6 +249,15 @@ func TestGetDefaultAPIBase_LMStudio(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultAPIBase_GPT4Free(t *testing.T) {
|
||||
if got := getDefaultAPIBase("gpt4free"); got != "http://localhost:1337/v1" {
|
||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "gpt4free", got, "http://localhost:1337/v1")
|
||||
}
|
||||
if got := getDefaultAPIBase("g4f"); got != "http://localhost:1337/v1" {
|
||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "g4f", got, "http://localhost:1337/v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultAPIBase_Venice(t *testing.T) {
|
||||
if got := getDefaultAPIBase("venice"); got != "https://api.venice.ai/api/v1" {
|
||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "venice", got, "https://api.venice.ai/api/v1")
|
||||
@@ -330,6 +340,13 @@ func TestCreateProviderFromConfig_LocalProviders(t *testing.T) {
|
||||
apiKey: "",
|
||||
wantModelID: "Qwen/Qwen3-8B",
|
||||
},
|
||||
{
|
||||
name: "GPT4Free without API key",
|
||||
modelName: "test-gpt4free",
|
||||
model: "gpt4free/gpt-4o-mini",
|
||||
apiKey: "",
|
||||
wantModelID: "gpt-4o-mini",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -1010,6 +1027,19 @@ func TestModelProviderOptions(t *testing.T) {
|
||||
} else if !option.EmptyAPIKeyAllowed {
|
||||
t.Fatal("lmstudio should allow empty API keys")
|
||||
}
|
||||
if option, ok := seen["gpt4free"]; !ok {
|
||||
t.Fatal("gpt4free option missing")
|
||||
} else {
|
||||
if option.DefaultAPIBase != "http://localhost:1337/v1" {
|
||||
t.Fatalf("gpt4free default_api_base = %q, want %q", option.DefaultAPIBase, "http://localhost:1337/v1")
|
||||
}
|
||||
if !option.EmptyAPIKeyAllowed {
|
||||
t.Fatal("gpt4free should allow empty API keys")
|
||||
}
|
||||
if !option.SupportsFetch {
|
||||
t.Fatal("gpt4free should support upstream model listing")
|
||||
}
|
||||
}
|
||||
if option, ok := seen["siliconflow"]; !ok {
|
||||
t.Fatal("siliconflow option missing")
|
||||
} else if option.DefaultAPIBase != "https://api.siliconflow.cn/v1" {
|
||||
|
||||
@@ -76,6 +76,7 @@ func TestNormalizeProvider(t *testing.T) {
|
||||
{"claudecli", "claude-cli"},
|
||||
{"codexcli", "codex-cli"},
|
||||
{"copilot", "github-copilot"},
|
||||
{"g4f", "gpt4free"},
|
||||
// Alibaba Coding Plan aliases
|
||||
{"alibaba-coding", "alibaba-coding"},
|
||||
{"coding-plan", "alibaba-coding"},
|
||||
|
||||
@@ -361,6 +361,20 @@ var modelProviderOptionsByName = map[string]ModelProviderOption{
|
||||
Priority: 48,
|
||||
httpAPI: true,
|
||||
},
|
||||
"gpt4free": {
|
||||
ID: "gpt4free",
|
||||
DisplayName: "GPT4Free",
|
||||
Domain: "g4f.dev",
|
||||
DefaultAPIBase: "http://localhost:1337/v1",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Local: true,
|
||||
Priority: 47.5,
|
||||
Aliases: []string{"g4f"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"elevenlabs": {
|
||||
ID: "elevenlabs",
|
||||
DisplayName: "ElevenLabs ASR",
|
||||
|
||||
@@ -218,7 +218,7 @@ func runLocalModelProbe(m *config.ModelConfig) bool {
|
||||
switch protocol {
|
||||
case "ollama":
|
||||
return probeOllamaModelFunc(apiBase, modelID)
|
||||
case "vllm", "lmstudio":
|
||||
case "vllm", "lmstudio", "gpt4free":
|
||||
return probeOpenAICompatibleModelFunc(apiBase, modelID, m.APIKey())
|
||||
case "github-copilot":
|
||||
return probeTCPServiceFunc(apiBase)
|
||||
@@ -563,16 +563,29 @@ func probeOpenAICompatibleModel(apiBase, modelID, apiKey string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
var respEnvelope struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := getJSON(strings.TrimRight(strings.TrimSpace(apiBase), "/")+"/models", &resp, apiKey); err != nil {
|
||||
fetchURL := strings.TrimRight(strings.TrimSpace(apiBase), "/") + "/models"
|
||||
if err := getJSON(fetchURL, &respEnvelope, apiKey); err == nil {
|
||||
for _, model := range respEnvelope.Data {
|
||||
if strings.EqualFold(strings.TrimSpace(model.ID), modelID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, model := range resp.Data {
|
||||
var respArray []struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := getJSON(fetchURL, &respArray, apiKey); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, model := range respArray {
|
||||
if strings.EqualFold(strings.TrimSpace(model.ID), modelID) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
@@ -39,6 +40,21 @@ func TestProbeLocalModelAvailability_OpenAICompatibleIncludesAPIKey(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeOpenAICompatibleModel_SupportsBareArrayModels(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/models" {
|
||||
t.Fatalf("path = %q, want %q", r.URL.Path, "/v1/models")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `[{"id":"gpt-4o-mini"},{"id":"deepseek-r1"}]`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if !probeOpenAICompatibleModel(srv.URL+"/v1", "gpt-4o-mini", "") {
|
||||
t.Fatal("probeOpenAICompatibleModel() = false, want true for bare array /models response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiresRuntimeProbe_LMStudio(t *testing.T) {
|
||||
if !requiresRuntimeProbe(&config.ModelConfig{
|
||||
Model: "lmstudio/openai/gpt-oss-20b",
|
||||
@@ -54,6 +70,21 @@ func TestRequiresRuntimeProbe_LMStudio(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiresRuntimeProbe_GPT4Free(t *testing.T) {
|
||||
if !requiresRuntimeProbe(&config.ModelConfig{
|
||||
Model: "gpt4free/gpt-4o-mini",
|
||||
}) {
|
||||
t.Fatal("requiresRuntimeProbe(gpt4free with default base) = false, want true")
|
||||
}
|
||||
|
||||
if requiresRuntimeProbe(&config.ModelConfig{
|
||||
Model: "gpt4free/gpt-4o-mini",
|
||||
APIBase: "https://g4f.space/v1",
|
||||
}) {
|
||||
t.Fatal("requiresRuntimeProbe(gpt4free with remote base) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelProbeAPIBase_LMStudioDefault(t *testing.T) {
|
||||
got := modelProbeAPIBase(&config.ModelConfig{Model: "lmstudio/openai/gpt-oss-20b"})
|
||||
if got != "http://localhost:1234/v1" {
|
||||
@@ -61,6 +92,13 @@ func TestModelProbeAPIBase_LMStudioDefault(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelProbeAPIBase_GPT4FreeDefault(t *testing.T) {
|
||||
got := modelProbeAPIBase(&config.ModelConfig{Model: "gpt4free/gpt-4o-mini"})
|
||||
if got != "http://localhost:1337/v1" {
|
||||
t.Fatalf("modelProbeAPIBase(gpt4free) = %q, want %q", got, "http://localhost:1337/v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeLocalModelAvailability_LMStudioUsesOpenAICompatibleProbe(t *testing.T) {
|
||||
originalProbe := probeOpenAICompatibleModelFunc
|
||||
defer func() { probeOpenAICompatibleModelFunc = originalProbe }()
|
||||
@@ -89,6 +127,34 @@ func TestProbeLocalModelAvailability_LMStudioUsesOpenAICompatibleProbe(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeLocalModelAvailability_GPT4FreeUsesOpenAICompatibleProbe(t *testing.T) {
|
||||
originalProbe := probeOpenAICompatibleModelFunc
|
||||
defer func() { probeOpenAICompatibleModelFunc = originalProbe }()
|
||||
|
||||
called := false
|
||||
probeOpenAICompatibleModelFunc = func(apiBase, modelID, apiKey string) bool {
|
||||
called = true
|
||||
if apiBase != "http://localhost:1337/v1" {
|
||||
t.Fatalf("apiBase = %q, want %q", apiBase, "http://localhost:1337/v1")
|
||||
}
|
||||
if modelID != "gpt-4o-mini" {
|
||||
t.Fatalf("modelID = %q, want %q", modelID, "gpt-4o-mini")
|
||||
}
|
||||
if apiKey != "" {
|
||||
t.Fatalf("apiKey = %q, want empty", apiKey)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
model := &config.ModelConfig{Model: "gpt4free/gpt-4o-mini"}
|
||||
if !probeLocalModelAvailability(model) {
|
||||
t.Fatal("probeLocalModelAvailability(gpt4free) = false, want true")
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("probeOpenAICompatibleModelFunc was not called for gpt4free")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelProbeCacheKey_DifferentAPIKeysProduceDifferentKeys(t *testing.T) {
|
||||
base := &config.ModelConfig{
|
||||
ModelName: "local-vllm",
|
||||
|
||||
@@ -1030,7 +1030,7 @@ func probeModelConnectivity(m *config.ModelConfig) bool {
|
||||
switch protocol {
|
||||
case "ollama":
|
||||
return probeOllamaModel(apiBase, modelID)
|
||||
case "vllm", "lmstudio":
|
||||
case "vllm", "lmstudio", "gpt4free":
|
||||
return probeOpenAICompatibleModel(apiBase, modelID, m.APIKey())
|
||||
case "github-copilot":
|
||||
return probeTCPService(apiBase)
|
||||
|
||||
@@ -1939,6 +1939,19 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
|
||||
} else if !option.EmptyAPIKeyAllowed {
|
||||
t.Fatal("lmstudio should allow empty api keys")
|
||||
}
|
||||
if option, ok := optionsByID["gpt4free"]; !ok {
|
||||
t.Fatal("gpt4free provider option missing")
|
||||
} else {
|
||||
if option.DefaultAPIBase != "http://localhost:1337/v1" {
|
||||
t.Fatalf("gpt4free default_api_base = %q, want %q", option.DefaultAPIBase, "http://localhost:1337/v1")
|
||||
}
|
||||
if !option.EmptyAPIKeyAllowed {
|
||||
t.Fatal("gpt4free should allow empty api keys")
|
||||
}
|
||||
if !option.SupportsFetch {
|
||||
t.Fatal("gpt4free provider option should report supports_fetch")
|
||||
}
|
||||
}
|
||||
if option, ok := optionsByID["siliconflow"]; !ok {
|
||||
t.Fatal("siliconflow provider option missing")
|
||||
} else if option.DefaultAPIBase != "https://api.siliconflow.cn/v1" {
|
||||
|
||||
Reference in New Issue
Block a user