feat(providers): add SDK-based providers for subscription OAuth login

Add ClaudeProvider (anthropic-sdk-go) and CodexProvider (openai-go) that
use the correct subscription endpoints and API formats:

- CodexProvider: chatgpt.com/backend-api/codex/responses (Responses API)
  with OAuth Bearer auth and Chatgpt-Account-Id header
- ClaudeProvider: api.anthropic.com/v1/messages (Messages API) with
  Authorization: Bearer token auth

Update CreateProvider() routing to use new SDK-based providers when
auth_method is "oauth" or "token", removing the stopgap that sent
subscription tokens to pay-per-token endpoints.

Closes #18

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Cory LaNou
2026-02-11 13:27:59 -06:00
parent 5efe8a2020
commit fbad753b2a
7 changed files with 960 additions and 58 deletions

View File

@@ -20,11 +20,9 @@ import (
)
type HTTPProvider struct {
apiKey string
apiBase string
httpClient *http.Client
tokenSource func() (string, error)
accountID string
apiKey string
apiBase string
httpClient *http.Client
}
func NewHTTPProvider(apiKey, apiBase string) *HTTPProvider {
@@ -76,16 +74,7 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
}
req.Header.Set("Content-Type", "application/json")
if p.tokenSource != nil {
token, err := p.tokenSource()
if err != nil {
return nil, fmt.Errorf("failed to get auth token: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
if p.accountID != "" {
req.Header.Set("Chatgpt-Account-Id", p.accountID)
}
} else if p.apiKey != "" {
if p.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+p.apiKey)
}
@@ -181,45 +170,26 @@ func (p *HTTPProvider) GetDefaultModel() string {
return ""
}
func createOAuthTokenSource(provider string) func() (string, error) {
return func() (string, error) {
cred, err := auth.GetCredential(provider)
if err != nil {
return "", fmt.Errorf("loading auth credentials: %w", err)
}
if cred == nil {
return "", fmt.Errorf("no OAuth credentials for %s. Run: picoclaw auth login --provider %s", provider, provider)
}
if cred.AuthMethod == "oauth" && cred.NeedsRefresh() && cred.RefreshToken != "" {
oauthCfg := auth.OpenAIOAuthConfig()
refreshed, err := auth.RefreshAccessToken(cred, oauthCfg)
if err != nil {
return "", fmt.Errorf("refreshing token: %w", err)
}
if err := auth.SetCredential(provider, refreshed); err != nil {
return "", fmt.Errorf("saving refreshed token: %w", err)
}
return refreshed.AccessToken, nil
}
return cred.AccessToken, nil
}
}
func createAuthProvider(providerName string, apiBase string) (LLMProvider, error) {
cred, err := auth.GetCredential(providerName)
func createClaudeAuthProvider() (LLMProvider, error) {
cred, err := auth.GetCredential("anthropic")
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", providerName, providerName)
return nil, fmt.Errorf("no credentials for anthropic. Run: picoclaw auth login --provider anthropic")
}
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
}
p := NewHTTPProvider(cred.AccessToken, apiBase)
p.tokenSource = createOAuthTokenSource(providerName)
p.accountID = cred.AccountID
return p, nil
func createCodexAuthProvider() (LLMProvider, error) {
cred, err := auth.GetCredential("openai")
if err != nil {
return nil, fmt.Errorf("loading auth credentials: %w", err)
}
if cred == nil {
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
}
return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
}
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
@@ -240,11 +210,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
case (strings.Contains(lowerModel, "claude") || strings.HasPrefix(model, "anthropic/")) && (cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != ""):
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
ab := cfg.Providers.Anthropic.APIBase
if ab == "" {
ab = "https://api.anthropic.com/v1"
}
return createAuthProvider("anthropic", ab)
return createClaudeAuthProvider()
}
apiKey = cfg.Providers.Anthropic.APIKey
apiBase = cfg.Providers.Anthropic.APIBase
@@ -254,11 +220,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
ab := cfg.Providers.OpenAI.APIBase
if ab == "" {
ab = "https://api.openai.com/v1"
}
return createAuthProvider("openai", ab)
return createCodexAuthProvider()
}
apiKey = cfg.Providers.OpenAI.APIKey
apiBase = cfg.Providers.OpenAI.APIBase