mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 02:14:43 +00:00
feat(provider): add Requesty OpenAI-compatible provider (#1579)
Register Requesty (https://router.requesty.ai/v1) next to OpenRouter on the existing OpenAI-compatible transport: config and env vars (GOCLAW_REQUESTY_API_KEY, GOCLAW_REQUESTY_BASE_URL), secret masking, DB and in-memory registration, CLI setup, doctor, placeholder provider, OpenAPI enum, Web/Desktop provider lists and docs. The models list merges Requesty managed policies (GET /models/managed) with the key's catalog from GET /models.
This commit is contained in:
1 parent
b2b348a396
commit
32eb2328f3
20 files changed
+304
-4
No files matched your search
@@ -99,11 +99,13 @@ func runDoctor() {
|
||||
checkProvider("Anthropic (env)", cfg.Providers.Anthropic.APIKey)
|
||||
checkProvider("OpenAI (env)", cfg.Providers.OpenAI.APIKey)
|
||||
checkProvider("API Route (env)", cfg.Providers.APIRoute.APIKey)
|
||||
checkProvider("Requesty (env)", cfg.Providers.Requesty.APIKey)
|
||||
checkProvider("OpenRouter (env)", cfg.Providers.OpenRouter.APIKey)
|
||||
} else {
|
||||
checkProvider("Anthropic", cfg.Providers.Anthropic.APIKey)
|
||||
checkProvider("OpenAI", cfg.Providers.OpenAI.APIKey)
|
||||
checkProvider("API Route", cfg.Providers.APIRoute.APIKey)
|
||||
checkProvider("Requesty", cfg.Providers.Requesty.APIKey)
|
||||
checkProvider("OpenRouter", cfg.Providers.OpenRouter.APIKey)
|
||||
checkProvider("Gemini", cfg.Providers.Gemini.APIKey)
|
||||
checkProvider("Groq", cfg.Providers.Groq.APIKey)
|
||||
|
||||
@@ -65,6 +65,18 @@ func registerProviders(registry *providers.Registry, cfg *config.Config, modelRe
|
||||
slog.Info("registered provider", "name", "api_route")
|
||||
}
|
||||
|
||||
if cfg.Providers.Requesty.APIKey != "" {
|
||||
base := cfg.Providers.Requesty.APIBase
|
||||
if base == "" {
|
||||
base = store.RequestyDefaultAPIBase
|
||||
}
|
||||
prov := providers.NewOpenAIProvider("requesty", cfg.Providers.Requesty.APIKey, base, store.RequestyDefaultModel)
|
||||
prov.WithProviderType(store.ProviderRequesty)
|
||||
prov.WithSiteInfo("https://goclaw.sh", "GoClaw")
|
||||
registry.Register(prov)
|
||||
slog.Info("registered provider", "name", "requesty")
|
||||
}
|
||||
|
||||
if cfg.Providers.OpenRouter.APIKey != "" {
|
||||
orProv := providers.NewOpenAIProvider("openrouter", cfg.Providers.OpenRouter.APIKey, "https://openrouter.ai/api/v1", "anthropic/claude-sonnet-4-5-20250929")
|
||||
orProv.WithSiteInfo("https://goclaw.sh", "GoClaw")
|
||||
@@ -475,7 +487,7 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
|
||||
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, base, model)
|
||||
prov.WithProviderType(p.ProviderType)
|
||||
prov.WithThinkingEnabled(store.ParseThinkingEnabled(p.Settings))
|
||||
if p.ProviderType == store.ProviderOpenRouter {
|
||||
if p.ProviderType == store.ProviderOpenRouter || p.ProviderType == store.ProviderRequesty {
|
||||
prov.WithSiteInfo("https://goclaw.sh", "GoClaw")
|
||||
}
|
||||
registry.RegisterForTenant(p.TenantID, prov)
|
||||
@@ -496,6 +508,11 @@ func openAIProviderDefaults(providerType, apiBase string) (string, string) {
|
||||
apiBase = store.AtlasCloudDefaultAPIBase
|
||||
}
|
||||
return apiBase, store.AtlasCloudDefaultModel
|
||||
case store.ProviderRequesty:
|
||||
if apiBase == "" {
|
||||
apiBase = store.RequestyDefaultAPIBase
|
||||
}
|
||||
return apiBase, store.RequestyDefaultModel
|
||||
default:
|
||||
return apiBase, ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func TestRegisterProvidersRequestyDefaultsAndAuth(t *testing.T) {
|
||||
var gotPath, gotAuth, gotTitle string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
gotTitle = r.Header.Get("X-Title")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"choices": []map[string]any{{
|
||||
"message": map[string]string{"content": "ok"},
|
||||
"finish_reason": "stop",
|
||||
}},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
cfg := &config.Config{}
|
||||
cfg.Providers.Requesty.APIKey = "requesty-key"
|
||||
cfg.Providers.Requesty.APIBase = server.URL
|
||||
registry := providers.NewRegistry(nil)
|
||||
registerProviders(registry, cfg, providers.NewInMemoryRegistry())
|
||||
|
||||
p, err := registry.GetForTenant(providers.MasterTenantID, "requesty")
|
||||
if err != nil {
|
||||
t.Fatalf("GetForTenant() error = %v", err)
|
||||
}
|
||||
if p.DefaultModel() != store.RequestyDefaultModel {
|
||||
t.Fatalf("DefaultModel() = %q, want %q", p.DefaultModel(), store.RequestyDefaultModel)
|
||||
}
|
||||
if _, err := p.Chat(context.Background(), providers.ChatRequest{
|
||||
Messages: []providers.Message{{Role: "user", Content: "hi"}},
|
||||
}); err != nil {
|
||||
t.Fatalf("Chat() error = %v", err)
|
||||
}
|
||||
if gotPath != "/chat/completions" {
|
||||
t.Fatalf("request path = %q, want /chat/completions", gotPath)
|
||||
}
|
||||
if gotAuth != "Bearer requesty-key" {
|
||||
t.Fatalf("Authorization = %q, want Bearer requesty-key", gotAuth)
|
||||
}
|
||||
if gotTitle != "GoClaw" {
|
||||
t.Fatalf("X-Title = %q, want GoClaw", gotTitle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterProvidersFromDBUsesRequestyDefaults(t *testing.T) {
|
||||
tenantID := uuid.New()
|
||||
providerStore := gatewayProvidersStoreStub{
|
||||
providers: []store.LLMProviderData{{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
TenantID: tenantID,
|
||||
Name: "db-requesty",
|
||||
ProviderType: store.ProviderRequesty,
|
||||
APIKey: "requesty-key",
|
||||
Enabled: true,
|
||||
}},
|
||||
}
|
||||
|
||||
registry := providers.NewRegistry(nil)
|
||||
registerProvidersFromDB(registry, providerStore, nil, "", "", nil, &config.Config{}, providers.NewInMemoryRegistry())
|
||||
|
||||
assertProviderDefault(t, registry, tenantID, "db-requesty", store.RequestyDefaultModel, store.RequestyDefaultAPIBase)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ func testPostgresConnection(dsn string) error {
|
||||
var defaultPlaceholderProviders = []store.LLMProviderData{
|
||||
{Name: "aimlapi", DisplayName: "AI/ML API", ProviderType: store.ProviderAIMLAPI, APIBase: providers.AIMLAPIDefaultAPIBase, Enabled: false},
|
||||
{Name: "api_route", DisplayName: "API Route", ProviderType: store.ProviderAPIRoute, APIBase: store.APIRouteDefaultAPIBase, Enabled: false},
|
||||
{Name: "requesty", DisplayName: "Requesty", ProviderType: store.ProviderRequesty, APIBase: store.RequestyDefaultAPIBase, Enabled: false},
|
||||
{Name: "openrouter", DisplayName: "OpenRouter", ProviderType: store.ProviderOpenRouter, APIBase: "https://openrouter.ai/api/v1", Enabled: false},
|
||||
{Name: "synthetic", DisplayName: "Synthetic", ProviderType: store.ProviderOpenAICompat, APIBase: "https://api.synthetic.new/openai/v1", Enabled: false},
|
||||
{Name: "alicloud-api", DisplayName: "AliCloud API", ProviderType: store.ProviderDashScope, APIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", Enabled: false},
|
||||
|
||||
@@ -126,6 +126,7 @@ func runProvidersAdd() {
|
||||
{"OpenAI", "openai"},
|
||||
{"Atlas Cloud", "atlascloud"},
|
||||
{"API Route", "api_route"},
|
||||
{"Requesty", "requesty"},
|
||||
{"OpenRouter", "openrouter"},
|
||||
{"DashScope (Alibaba)", "dashscope"},
|
||||
{"OpenAI-compatible", "openai_compat"},
|
||||
@@ -153,7 +154,7 @@ func runProvidersAdd() {
|
||||
// Step 4: Base URL (pre-fill per type, editable)
|
||||
defaultURL := defaultBaseURL(providerType)
|
||||
baseURL := ""
|
||||
if providerType == "openai_compat" || providerType == "atlascloud" || providerType == "api_route" {
|
||||
if providerType == "openai_compat" || providerType == "atlascloud" || providerType == "api_route" || providerType == "requesty" {
|
||||
baseURL, err = promptString("Base URL", "e.g. https://api.example.com/v1", defaultURL)
|
||||
if err != nil {
|
||||
fmt.Println("Cancelled.")
|
||||
@@ -317,6 +318,8 @@ func defaultBaseURL(providerType string) string {
|
||||
return "https://api.atlascloud.ai/v1"
|
||||
case "api_route":
|
||||
return store.APIRouteDefaultAPIBase
|
||||
case "requesty":
|
||||
return store.RequestyDefaultAPIBase
|
||||
case "openrouter":
|
||||
return "https://openrouter.ai/api/v1"
|
||||
case "dashscope":
|
||||
|
||||
@@ -49,6 +49,7 @@ func addProvider() {
|
||||
{"OpenAI", "openai"},
|
||||
{"Atlas Cloud", "atlascloud"},
|
||||
{"API Route", "api_route"},
|
||||
{"Requesty", "requesty"},
|
||||
{"OpenRouter", "openrouter"},
|
||||
{"DashScope (Alibaba)", "dashscope"},
|
||||
{"OpenAI-compatible", "openai_compat"},
|
||||
@@ -70,12 +71,14 @@ func addProvider() {
|
||||
}
|
||||
|
||||
baseURL := ""
|
||||
if providerType == "openai_compat" || providerType == "atlascloud" || providerType == "api_route" {
|
||||
if providerType == "openai_compat" || providerType == "atlascloud" || providerType == "api_route" || providerType == "requesty" {
|
||||
defaultURL := ""
|
||||
if providerType == "atlascloud" {
|
||||
defaultURL = "https://api.atlascloud.ai/v1"
|
||||
} else if providerType == "api_route" {
|
||||
defaultURL = "https://global.api-route.com/v1"
|
||||
} else if providerType == "requesty" {
|
||||
defaultURL = "https://router.requesty.ai/v1"
|
||||
}
|
||||
baseURL, err = promptString("Base URL", "e.g. https://api.example.com/v1", defaultURL)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user