Skip to content

feat(auth): add Anthropic OAuth setup-token login#926

Open
BallerIsLeet wants to merge 1 commit intosipeed:mainfrom
BallerIsLeet:anthropic-oauth
Open

feat(auth): add Anthropic OAuth setup-token login#926
BallerIsLeet wants to merge 1 commit intosipeed:mainfrom
BallerIsLeet:anthropic-oauth

Conversation

@BallerIsLeet
Copy link

Summary

  • Add support for Anthropic OAuth setup tokens (sk-ant-oat01-*) as an alternative to API keys via new --setup-token flag and interactive login menu
  • Integrate Anthropic usage endpoint to display 5-hour and 7-day utilization in auth status
  • Add streaming support for OAuth tokens (required by Anthropic API) and normalize model IDs (dots → hyphens) for API compatibility
  • Remove .env.example to avoid templating secrets

Test plan

  • Run picoclaw auth login -p anthropic and verify interactive menu shows setup token + API key options
  • Run picoclaw auth login -p anthropic --setup-token and verify it accepts valid sk-ant-oat01-* tokens
  • Verify invalid/short tokens are rejected with clear error messages
  • Run picoclaw auth status with an OAuth credential and verify usage percentages display
  • Verify chat works end-to-end with an OAuth setup token (streaming path)
  • Run existing tests: go test ./pkg/providers/anthropic/...

🤖 Generated with Claude Code

Add support for Anthropic's OAuth-based setup tokens (sk-ant-oat01-*)
as an alternative to API keys. This includes:

- New `--setup-token` flag on `auth login` command
- Interactive login menu for Anthropic (setup token vs API key)
- Setup token validation and credential storage with oauth auth method
- Usage endpoint integration to show 5h/7d utilization in `auth status`
- Streaming support for OAuth tokens (required by Anthropic API)
- Model ID normalization (dots to hyphens) for API compatibility
- Remove .env.example (secrets should not be templated)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@nikolasdehor nikolasdehor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(auth): add Anthropic OAuth setup-token login

Disclosure: I submitted PR #444 (Anthropic API key routing) and have hands-on experience with Anthropic OAuth/OAT token flows.

Overall this is a well-structured PR that adds a much-needed feature. The interactive menu, token validation, usage endpoint, and streaming path are all solid additions.

Security

  1. Token validation is correct -- the sk-ant-oat01- prefix check and minimum length (80 chars) are appropriate guards. Good that you do not log the token value anywhere.

  2. Usage endpoint auth headers look correct -- Bearer + anthropic-beta: oauth-2025-04-20 matches the Anthropic OAuth API surface.

  3. Good: .env.example removal -- removing the template that could lead to committed secrets is the right call.

Correctness

  1. Streaming requirement for OAuth tokens -- This is correct behavior. Anthropic OAuth tokens require the streaming endpoint. The chatStreaming implementation using Accumulate is clean. One note: if the stream returns zero content blocks (e.g., a refusal), does parseResponse(&msg) handle that gracefully? Worth a quick check.

  2. Model ID normalization (dots to hyphens) -- strings.ReplaceAll(model, ".", "-") is correct for the Anthropic API. Their API uses hyphens (claude-sonnet-4-6) while configs often use dots (claude-sonnet-4.6). The test update confirms this.

  3. anthropic-beta header on OAuth path -- Correctly added only when tokenSource != nil. Non-OAuth (API key) requests will not get the beta header, which is the right behavior.

Suggestions

  1. Hardcoded default model claude-sonnet-4.6 -- In authLoginAnthropicSetupToken(), the function hardcodes claude-sonnet-4.6 as the default model and sets AuthMethod: "oauth" on the first Anthropic model it finds in config. Consider: what if the user already has claude-opus-4-6 configured with an API key? This would overwrite their model choice. Maybe only set the default if no Anthropic model exists at all, rather than always overwriting Agents.Defaults.ModelName.

  2. body, _ := io.ReadAll(resp.Body) in usage.go -- The error from ReadAll is silently discarded. If the read fails, the subsequent status check will use an empty body, which is confusing. Consider handling the error.

  3. Status code check order in usage.go -- You check StatusForbidden before StatusOK, but the generic != StatusOK block would also catch 403. The explicit 403 message is nice for UX. Consider nesting it inside the != StatusOK block for clarity.

  4. Missing test coverage -- LoginSetupToken and FetchAnthropicUsage lack unit tests. The streaming path (chatStreaming) also lacks tests. Given the security sensitivity of the auth flow, tests for edge cases (empty input, wrong prefix, short token) would be valuable.

Minor

  1. The isAnthropicModel function is called but not shown in the diff -- assuming it exists elsewhere.

Good work overall. The core OAuth + streaming architecture is sound.

const supportedProvidersMsg = "supported providers: openai, anthropic, google-antigravity"

func authLoginCmd(provider string, useDeviceCode bool) error {
func authLoginCmd(provider string, useDeviceCode bool, setupToken bool) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Perhaps the var should be named useOauth instead of setupToken?

}

token := strings.TrimSpace(scanner.Text())
if token == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: with the validation below, this check is redundant, is that right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should be better named anthropic_usage.go to reflect its scope.

return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("anthropic-version", "2023-06-01")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two values should better be defined as const above, not hardcoded here.

opts = append(opts, option.WithAuthToken(tok))
opts = append(opts,
option.WithAuthToken(tok),
option.WithHeader("anthropic-beta", "oauth-2025-04-20"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic value flying around is not great.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether the removal of this file should be in scope for this PR.


scanner := bufio.NewScanner(os.Stdin)
choice := "1"
if scanner.Scan() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a loop here with validation logic inside so user will definition give either 1 or 2?

})
}

appCfg.Agents.Defaults.ModelName = "claude-sonnet-4.6"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this should be another magic value defined as const

@xiaket xiaket requested a review from yinwm February 28, 2026 21:21
Copy link
Collaborator

@xiaket xiaket left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general direction LGTM, but the number of minor issues makes it a RC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3 participants