feat(auth): generalize token login to all OpenAI-compatible HTTP providers
Add `picoclaw auth login --provider <name>` support for any OpenAI-compatible HTTP provider (openrouter, groq, deepseek, mistral, gemini, novita, etc.) rather than only OAuth-based providers. Changes: - `pkg/providers/factory_provider.go`: replace `createOpenRouterAuthProvider` with generic `createHTTPTokenAuthProvider(protocol, cfg)`; all HTTP provider cases now check `auth_method: "token"` and read credentials from auth store - `cmd/picoclaw/internal/auth/helpers.go`: replace openrouter-specific login path with generic `authLoginHTTPTokenProvider(provider)` backed by an `httpTokenProviders` map; logout handler generalized via `isProviderModel`; `supportedProvidersMsg` now lists all supported providers dynamically - `pkg/auth/token.go`: add openrouter.ai/keys display hint - `cmd/picoclaw/internal/auth/login.go`: update --provider flag description Any provider in `httpTokenProviders` now works with: picoclaw auth login --provider <name> Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
60d7ec20a5
commit
39a30e43ac
4 changed files with 114 additions and 4 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -17,10 +18,32 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
supportedProvidersMsg = "supported providers: openai, anthropic, google-antigravity"
|
||||
defaultAnthropicModel = "claude-sonnet-4.6"
|
||||
)
|
||||
|
||||
// httpTokenProviders lists all OpenAI-compatible HTTP providers that support
|
||||
// `picoclaw auth login --provider <name>` with API-key (token) auth.
|
||||
var httpTokenProviders = map[string]bool{
|
||||
"openrouter": true, "groq": true, "deepseek": true, "mistral": true,
|
||||
"gemini": true, "nvidia": true, "ollama": true, "moonshot": true,
|
||||
"zhipu": true, "novita": true, "cerebras": true, "minimax": true,
|
||||
"vivgrid": true, "volcengine": true, "vllm": true, "litellm": true,
|
||||
"qwen": true, "qwen-intl": true, "qwen-international": true, "dashscope-intl": true,
|
||||
"qwen-us": true, "dashscope-us": true, "avian": true, "longcat": true,
|
||||
"modelscope": true, "shengsuanyun": true, "mimo": true,
|
||||
"coding-plan": true, "alibaba-coding": true, "qwen-coding": true,
|
||||
}
|
||||
|
||||
func supportedProvidersMsg() string {
|
||||
static := []string{"openai", "anthropic", "google-antigravity"}
|
||||
dynamic := make([]string, 0, len(httpTokenProviders))
|
||||
for p := range httpTokenProviders {
|
||||
dynamic = append(dynamic, p)
|
||||
}
|
||||
sort.Strings(dynamic)
|
||||
return "supported providers: " + strings.Join(append(static, dynamic...), ", ")
|
||||
}
|
||||
|
||||
func authLoginCmd(provider string, useDeviceCode bool, useOauth bool) error {
|
||||
switch provider {
|
||||
case "openai":
|
||||
|
|
@ -30,7 +53,10 @@ func authLoginCmd(provider string, useDeviceCode bool, useOauth bool) error {
|
|||
case "google-antigravity", "antigravity":
|
||||
return authLoginGoogleAntigravity()
|
||||
default:
|
||||
return fmt.Errorf("unsupported provider: %s (%s)", provider, supportedProvidersMsg)
|
||||
if httpTokenProviders[provider] {
|
||||
return authLoginHTTPTokenProvider(provider)
|
||||
}
|
||||
return fmt.Errorf("unsupported provider: %s (%s)", provider, supportedProvidersMsg())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +379,10 @@ func authLogoutCmd(provider string) error {
|
|||
if isAntigravityModel(appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
default:
|
||||
if isProviderModel(provider, appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
config.SaveConfig(internal.GetConfigPath(), appCfg)
|
||||
|
|
@ -484,6 +514,45 @@ func authModelsCmd() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func authLoginHTTPTokenProvider(provider string) error {
|
||||
cred, err := auth.LoginPasteToken(provider, os.Stdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
if err = auth.SetCredential(provider, cred); err != nil {
|
||||
return fmt.Errorf("failed to save credentials: %w", err)
|
||||
}
|
||||
|
||||
appCfg, err := internal.LoadConfig()
|
||||
if err == nil {
|
||||
updated := 0
|
||||
for i := range appCfg.ModelList {
|
||||
if isProviderModel(provider, appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = "token"
|
||||
updated++
|
||||
}
|
||||
}
|
||||
if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
|
||||
return fmt.Errorf("could not update config: %w", err)
|
||||
}
|
||||
if updated > 0 {
|
||||
fmt.Printf("Updated %d %s model(s) to use token auth.\n", updated, provider)
|
||||
} else {
|
||||
fmt.Printf("No %s models found in config. Add models with protocol %s/ to use this credential.\n", provider, provider)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Token saved for %s!\n", provider)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isProviderModel reports whether a model string belongs to the given provider protocol.
|
||||
func isProviderModel(protocol, model string) bool {
|
||||
return model == protocol || strings.HasPrefix(model, protocol+"/")
|
||||
}
|
||||
|
||||
// isAntigravityModel checks if a model string belongs to antigravity provider
|
||||
func isAntigravityModel(model string) bool {
|
||||
return model == "antigravity" ||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ func newLoginCommand() *cobra.Command {
|
|||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&provider, "provider", "p", "", "Provider to login with (openai, anthropic)")
|
||||
cmd.Flags().StringVarP(&provider, "provider", "p", "", "Provider to login with (openai, anthropic, google-antigravity, openrouter, groq, deepseek, mistral, …)")
|
||||
cmd.Flags().BoolVar(&useDeviceCode, "device-code", false, "Use device code flow (for headless environments)")
|
||||
cmd.Flags().BoolVar(
|
||||
&useOauth, "setup-token", false,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ func providerDisplayName(provider string) string {
|
|||
return "console.anthropic.com"
|
||||
case "openai":
|
||||
return "platform.openai.com"
|
||||
case "openrouter":
|
||||
return "openrouter.ai/keys"
|
||||
default:
|
||||
return provider
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,30 @@ func createClaudeAuthProvider() (LLMProvider, error) {
|
|||
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
||||
}
|
||||
|
||||
// createHTTPTokenAuthProvider creates an OpenAI-compatible HTTP provider using credentials
|
||||
// from the auth store. Used for any provider configured with auth_method: "token".
|
||||
func createHTTPTokenAuthProvider(protocol string, cfg *config.ModelConfig) (LLMProvider, error) {
|
||||
cred, err := getCredential(protocol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
||||
}
|
||||
if cred == nil {
|
||||
return nil, fmt.Errorf("no credentials for %s. Run: picoclaw auth login --provider %s", protocol, protocol)
|
||||
}
|
||||
apiBase := cfg.APIBase
|
||||
if apiBase == "" {
|
||||
apiBase = getDefaultAPIBase(protocol)
|
||||
}
|
||||
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||
cred.AccessToken,
|
||||
apiBase,
|
||||
cfg.Proxy,
|
||||
cfg.MaxTokensField,
|
||||
cfg.RequestTimeout,
|
||||
cfg.ExtraBody,
|
||||
), nil
|
||||
}
|
||||
|
||||
// createCodexAuthProvider creates a Codex provider using OAuth credentials from auth store.
|
||||
func createCodexAuthProvider() (LLMProvider, error) {
|
||||
cred, err := getCredential("openai")
|
||||
|
|
@ -159,7 +183,15 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
|||
"vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
|
||||
"qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita",
|
||||
"coding-plan", "alibaba-coding", "qwen-coding", "mimo":
|
||||
// All other OpenAI-compatible HTTP providers
|
||||
// OpenAI-compatible HTTP providers. If auth_method is "token", credentials are
|
||||
// read from the auth store (set via `picoclaw auth login --provider <name>`).
|
||||
if cfg.AuthMethod == "token" {
|
||||
provider, err := createHTTPTokenAuthProvider(protocol, cfg)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return provider, modelID, nil
|
||||
}
|
||||
if cfg.APIKey() == "" && cfg.APIBase == "" {
|
||||
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
||||
}
|
||||
|
|
@ -178,6 +210,13 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
|||
|
||||
case "minimax":
|
||||
// Minimax requires reasoning_split: true in the request body
|
||||
if cfg.AuthMethod == "token" {
|
||||
provider, err := createHTTPTokenAuthProvider(protocol, cfg)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return provider, modelID, nil
|
||||
}
|
||||
if cfg.APIKey() == "" && cfg.APIBase == "" {
|
||||
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue