feat: add subscription OAuth option to Anthropic onboarding
This commit is contained in:
parent
d6e609055a
commit
c31af80182
1 changed files with 104 additions and 13 deletions
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,7 +23,9 @@ import (
|
||||||
//go:embed workspace
|
//go:embed workspace
|
||||||
var embeddedFiles embed.FS
|
var embeddedFiles embed.FS
|
||||||
|
|
||||||
// providerChoice holds the details for a user-selected provider
|
// providerChoice holds the details for a user-selected provider.
|
||||||
|
// If onboardFunc is set, it handles the entire provider setup (auth + config)
|
||||||
|
// and needsAPIKey/keyPrompt/validateFunc are ignored.
|
||||||
type providerChoice struct {
|
type providerChoice struct {
|
||||||
name string
|
name string
|
||||||
modelName string
|
modelName string
|
||||||
|
|
@ -30,6 +33,7 @@ type providerChoice struct {
|
||||||
keyPrompt string
|
keyPrompt string
|
||||||
validateURL string
|
validateURL string
|
||||||
validateFunc func(apiKey string) *http.Request
|
validateFunc func(apiKey string) *http.Request
|
||||||
|
onboardFunc func(reader *bufio.Reader, cfg *config.Config) (verified bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
var providerChoices = []providerChoice{
|
var providerChoices = []providerChoice{
|
||||||
|
|
@ -52,15 +56,7 @@ var providerChoices = []providerChoice{
|
||||||
{
|
{
|
||||||
name: "Anthropic",
|
name: "Anthropic",
|
||||||
modelName: "claude-sonnet-4.6",
|
modelName: "claude-sonnet-4.6",
|
||||||
needsAPIKey: true,
|
onboardFunc: onboardAnthropic,
|
||||||
keyPrompt: "Enter your Anthropic API key: ",
|
|
||||||
validateURL: "https://api.anthropic.com/v1/models",
|
|
||||||
validateFunc: func(apiKey string) *http.Request {
|
|
||||||
req, _ := http.NewRequest("GET", "https://api.anthropic.com/v1/models", nil)
|
|
||||||
req.Header.Set("x-api-key", apiKey)
|
|
||||||
req.Header.Set("anthropic-version", "2023-06-01")
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OpenAI",
|
name: "OpenAI",
|
||||||
|
|
@ -88,6 +84,97 @@ var providerChoices = []providerChoice{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onboardAnthropic handles Anthropic provider setup during onboarding,
|
||||||
|
// offering Claude Max/Pro subscription (OAuth) or manual API key entry.
|
||||||
|
func onboardAnthropic(reader *bufio.Reader, cfg *config.Config) (verified bool) {
|
||||||
|
fmt.Println("\nHow would you like to authenticate with Anthropic?")
|
||||||
|
fmt.Println(" 1. Claude Max/Pro subscription (OAuth — free inference) [default]")
|
||||||
|
fmt.Println(" 2. API key (pay-per-use)")
|
||||||
|
fmt.Print("\nEnter choice [1]: ")
|
||||||
|
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input == "" {
|
||||||
|
input = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
return onboardAnthropicOAuth(cfg)
|
||||||
|
case "2":
|
||||||
|
return onboardAnthropicAPIKey(reader, cfg)
|
||||||
|
default:
|
||||||
|
fmt.Printf("Unknown choice %q, using subscription login.\n", input)
|
||||||
|
return onboardAnthropicOAuth(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// onboardAnthropicOAuth performs the Claude Max/Pro OAuth flow during onboarding.
|
||||||
|
func onboardAnthropicOAuth(cfg *config.Config) bool {
|
||||||
|
cred, err := auth.LoginAnthropicOAuth(auth.AnthropicOAuthMax)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" [!] OAuth login failed: %v\n", err)
|
||||||
|
fmt.Println(" You can try again later with: picoclaw auth login --provider anthropic")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.SetCredential("anthropic", cred); err != nil {
|
||||||
|
fmt.Printf(" [!] Failed to save credentials: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config to use OAuth
|
||||||
|
for i := range cfg.ModelList {
|
||||||
|
if isAnthropicModel(cfg.ModelList[i].Model) {
|
||||||
|
cfg.ModelList[i].AuthMethod = "oauth"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" [\u2713] Anthropic login successful")
|
||||||
|
if cred.Email != "" {
|
||||||
|
fmt.Printf(" (%s)", cred.Email)
|
||||||
|
}
|
||||||
|
if cred.SubscriptionType != "" {
|
||||||
|
fmt.Printf(" [%s]", cred.SubscriptionType)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// onboardAnthropicAPIKey collects and validates an Anthropic API key during onboarding.
|
||||||
|
func onboardAnthropicAPIKey(reader *bufio.Reader, cfg *config.Config) bool {
|
||||||
|
fmt.Print("Enter your Anthropic API key: ")
|
||||||
|
apiKey, _ := reader.ReadString('\n')
|
||||||
|
apiKey = strings.TrimSpace(apiKey)
|
||||||
|
|
||||||
|
if apiKey == "" {
|
||||||
|
fmt.Println(" [!] No API key provided")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the API key on the matching model entry
|
||||||
|
for i := range cfg.ModelList {
|
||||||
|
if cfg.ModelList[i].ModelName == "claude-sonnet-4.6" {
|
||||||
|
cfg.ModelList[i].APIKey = apiKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the key
|
||||||
|
choice := providerChoice{
|
||||||
|
name: "Anthropic",
|
||||||
|
validateFunc: func(key string) *http.Request {
|
||||||
|
req, _ := http.NewRequest("GET", "https://api.anthropic.com/v1/models", nil)
|
||||||
|
req.Header.Set("x-api-key", key)
|
||||||
|
req.Header.Set("anthropic-version", "2023-06-01")
|
||||||
|
return req
|
||||||
|
},
|
||||||
|
}
|
||||||
|
verifyAPIKey(choice, apiKey)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func onboard() {
|
func onboard() {
|
||||||
for _, arg := range os.Args[2:] {
|
for _, arg := range os.Args[2:] {
|
||||||
switch arg {
|
switch arg {
|
||||||
|
|
@ -122,7 +209,7 @@ func onboard() {
|
||||||
fmt.Println("Choose your AI provider:")
|
fmt.Println("Choose your AI provider:")
|
||||||
fmt.Println(" 1. Ollama (local, free — no API key needed) [default]")
|
fmt.Println(" 1. Ollama (local, free — no API key needed) [default]")
|
||||||
fmt.Println(" 2. OpenRouter (100+ models, one API key)")
|
fmt.Println(" 2. OpenRouter (100+ models, one API key)")
|
||||||
fmt.Println(" 3. Anthropic (Claude)")
|
fmt.Println(" 3. Anthropic (Claude — subscription or API key)")
|
||||||
fmt.Println(" 4. OpenAI (GPT)")
|
fmt.Println(" 4. OpenAI (GPT)")
|
||||||
fmt.Println(" 5. DeepSeek")
|
fmt.Println(" 5. DeepSeek")
|
||||||
fmt.Println(" 6. Skip — I'll configure manually")
|
fmt.Println(" 6. Skip — I'll configure manually")
|
||||||
|
|
@ -159,11 +246,15 @@ func onboard() {
|
||||||
choiceIdx = 0
|
choiceIdx = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var customOnboard bool
|
||||||
if choiceIdx >= 0 {
|
if choiceIdx >= 0 {
|
||||||
choice := providerChoices[choiceIdx]
|
choice := providerChoices[choiceIdx]
|
||||||
cfg.Agents.Defaults.Model = choice.modelName
|
cfg.Agents.Defaults.Model = choice.modelName
|
||||||
|
|
||||||
if choice.needsAPIKey {
|
if choice.onboardFunc != nil {
|
||||||
|
customOnboard = true
|
||||||
|
choice.onboardFunc(reader, cfg)
|
||||||
|
} else if choice.needsAPIKey {
|
||||||
fmt.Print(choice.keyPrompt)
|
fmt.Print(choice.keyPrompt)
|
||||||
apiKey, _ = reader.ReadString('\n')
|
apiKey, _ = reader.ReadString('\n')
|
||||||
apiKey = strings.TrimSpace(apiKey)
|
apiKey = strings.TrimSpace(apiKey)
|
||||||
|
|
@ -193,7 +284,7 @@ func onboard() {
|
||||||
fmt.Printf(" [\u2713] Config written to %s\n", configPath)
|
fmt.Printf(" [\u2713] Config written to %s\n", configPath)
|
||||||
fmt.Printf(" [\u2713] Workspace initialized\n")
|
fmt.Printf(" [\u2713] Workspace initialized\n")
|
||||||
|
|
||||||
if choiceIdx >= 0 {
|
if choiceIdx >= 0 && !customOnboard {
|
||||||
choice := providerChoices[choiceIdx]
|
choice := providerChoices[choiceIdx]
|
||||||
if choiceIdx == 0 {
|
if choiceIdx == 0 {
|
||||||
// Ollama: check if running
|
// Ollama: check if running
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue