Skip to content

Commit 31bd6c3

Browse files
Add OAuth token refresh support for Anthropic API
- Add OAuthTokenManager class for loading, checking expiration, and refreshing tokens - Add OAuthCredential and AuthProfilesStore models for auth-profiles.json format - Add OAuthTokenHandler DelegatingHandler for automatic token refresh on 401 - Update AnthropicProvider to support OAuth authentication - Add auth_mode, auth_profiles_path, auth_profile_id config options - Add unit tests for OAuthTokenManager Usage: [providers.anthropic] auth_mode = "oauth" auth_profile_id = "anthropic:default" auth_profiles_path = "~/.openclaw/agents/main/agent/auth-profiles.json"
1 parent 2e03de3 commit 31bd6c3

11 files changed

Lines changed: 981 additions & 15 deletions

File tree

config.example.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ api_key = "sk-..." # Your OpenAI API key
2929
default_model = "gpt-4o"
3030

3131
[providers.anthropic]
32-
api_key = "sk-ant-..." # Your Anthropic API key
32+
api_key = "sk-ant-..." # Your Anthropic API key (used when auth_mode is "api_key")
3333
# base_url = "https://api.anthropic.com"
3434
default_model = "claude-sonnet-4-20250514"
3535

36+
# OAuth authentication (alternative to api_key)
37+
# auth_mode = "oauth" # Set to "oauth" to use OAuth tokens from auth-profiles.json
38+
# auth_profile_id = "anthropic:default" # Profile ID in auth-profiles.json
39+
# auth_profiles_path = "~/.openclaw/agents/main/agent/auth-profiles.json"
40+
3641
[providers.openrouter]
3742
api_key = "sk-or-..." # Your OpenRouter API key
3843
default_model = "anthropic/claude-sonnet-4-20250514"

src/ClawSharp.Cli/Commands/AgentCommand.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,26 @@ namespace ClawSharp.Cli.Commands;
99

1010
public class AgentCommand : Command
1111
{
12+
private static readonly Option<string?> MessageOption = new("-m", "--message") { Description = "Send a single message" };
13+
private static readonly Option<string?> ProviderOption = new("-p", "--provider") { Description = "LLM provider to use" };
14+
private static readonly Option<string?> ModelOption = new("--model") { Description = "Model to use" };
15+
1216
public AgentCommand() : base("agent", "Chat with the AI agent")
1317
{
14-
SetAction(_ => ExecuteAsync().GetAwaiter().GetResult());
18+
Add(MessageOption);
19+
Add(ProviderOption);
20+
Add(ModelOption);
21+
SetAction(ctx =>
22+
{
23+
var message = ctx.GetValue(MessageOption);
24+
var provider = ctx.GetValue(ProviderOption);
25+
var model = ctx.GetValue(ModelOption);
26+
return ExecuteAsync(message, provider, model);
27+
});
1528
}
1629

17-
private static async Task ExecuteAsync()
30+
private static async Task ExecuteAsync(string? message, string? provider, string? model)
1831
{
19-
// For now, just use defaults - proper arg parsing can be added later
20-
string? message = null;
21-
string? provider = null;
22-
string? model = null;
2332

2433
// Load configuration
2534
var config = ConfigLoader.LoadConfig();
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ClawSharp.Core.Auth;
4+
5+
/// <summary>
6+
/// Represents an OAuth credential with access token, refresh token, and expiration.
7+
/// </summary>
8+
public class OAuthCredential
9+
{
10+
/// <summary>
11+
/// Credential type: "oauth", "token", or "api_key".
12+
/// </summary>
13+
[JsonPropertyName("type")]
14+
public string Type { get; set; } = "oauth";
15+
16+
/// <summary>
17+
/// Provider name (e.g., "anthropic", "minimax-portal").
18+
/// </summary>
19+
[JsonPropertyName("provider")]
20+
public string Provider { get; set; } = "";
21+
22+
/// <summary>
23+
/// The OAuth access token.
24+
/// </summary>
25+
[JsonPropertyName("access")]
26+
public string? Access { get; set; }
27+
28+
/// <summary>
29+
/// The OAuth refresh token.
30+
/// </summary>
31+
[JsonPropertyName("refresh")]
32+
public string? Refresh { get; set; }
33+
34+
/// <summary>
35+
/// Unix timestamp (milliseconds) when the token expires.
36+
/// </summary>
37+
[JsonPropertyName("expires")]
38+
public long Expires { get; set; }
39+
40+
/// <summary>
41+
/// Optional email associated with the OAuth account.
42+
/// </summary>
43+
[JsonPropertyName("email")]
44+
public string? Email { get; set; }
45+
46+
/// <summary>
47+
/// Optional enterprise URL.
48+
/// </summary>
49+
[JsonPropertyName("enterpriseUrl")]
50+
public string? EnterpriseUrl { get; set; }
51+
52+
/// <summary>
53+
/// Optional project ID.
54+
/// </summary>
55+
[JsonPropertyName("projectId")]
56+
public string? ProjectId { get; set; }
57+
58+
/// <summary>
59+
/// Optional account ID.
60+
/// </summary>
61+
[JsonPropertyName("accountId")]
62+
public string? AccountId { get; set; }
63+
}
64+
65+
/// <summary>
66+
/// Represents the auth-profiles.json file structure.
67+
/// </summary>
68+
public class AuthProfilesStore
69+
{
70+
[JsonPropertyName("version")]
71+
public int Version { get; set; }
72+
73+
[JsonPropertyName("profiles")]
74+
public Dictionary<string, OAuthCredential> Profiles { get; set; } = new();
75+
76+
[JsonPropertyName("lastGood")]
77+
public Dictionary<string, string>? LastGood { get; set; }
78+
79+
[JsonPropertyName("usageStats")]
80+
public Dictionary<string, ProfileUsageStats>? UsageStats { get; set; }
81+
}
82+
83+
/// <summary>
84+
/// Usage statistics for an auth profile.
85+
/// </summary>
86+
public class ProfileUsageStats
87+
{
88+
[JsonPropertyName("lastUsed")]
89+
public long LastUsed { get; set; }
90+
91+
[JsonPropertyName("errorCount")]
92+
public int ErrorCount { get; set; }
93+
94+
[JsonPropertyName("lastFailureAt")]
95+
public long? LastFailureAt { get; set; }
96+
}

0 commit comments

Comments
 (0)