@charming_groot/providers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/auth/auth-resolver.d.ts +9 -0
  2. package/dist/auth/auth-resolver.d.ts.map +1 -0
  3. package/dist/auth/auth-resolver.js +200 -0
  4. package/dist/auth/auth-resolver.js.map +1 -0
  5. package/dist/auth/index.d.ts +2 -0
  6. package/dist/auth/index.d.ts.map +1 -0
  7. package/dist/auth/index.js +2 -0
  8. package/dist/auth/index.js.map +1 -0
  9. package/dist/base-provider.d.ts +10 -0
  10. package/dist/base-provider.d.ts.map +1 -0
  11. package/dist/base-provider.js +8 -0
  12. package/dist/base-provider.js.map +1 -0
  13. package/dist/circuit-breaker.d.ts +42 -0
  14. package/dist/circuit-breaker.d.ts.map +1 -0
  15. package/dist/circuit-breaker.js +116 -0
  16. package/dist/circuit-breaker.js.map +1 -0
  17. package/dist/claude-provider.d.ts +15 -0
  18. package/dist/claude-provider.d.ts.map +1 -0
  19. package/dist/claude-provider.js +171 -0
  20. package/dist/claude-provider.js.map +1 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +9 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/openai-provider.d.ts +16 -0
  26. package/dist/openai-provider.d.ts.map +1 -0
  27. package/dist/openai-provider.js +196 -0
  28. package/dist/openai-provider.js.map +1 -0
  29. package/dist/provider-factory.d.ts +17 -0
  30. package/dist/provider-factory.d.ts.map +1 -0
  31. package/dist/provider-factory.js +36 -0
  32. package/dist/provider-factory.js.map +1 -0
  33. package/dist/retry-provider.d.ts +25 -0
  34. package/dist/retry-provider.d.ts.map +1 -0
  35. package/dist/retry-provider.js +92 -0
  36. package/dist/retry-provider.js.map +1 -0
  37. package/dist/thinking-parser.d.ts +28 -0
  38. package/dist/thinking-parser.d.ts.map +1 -0
  39. package/dist/thinking-parser.js +40 -0
  40. package/dist/thinking-parser.js.map +1 -0
  41. package/package.json +34 -0
  42. package/src/auth/auth-resolver.ts +261 -0
  43. package/src/auth/index.ts +1 -0
  44. package/src/base-provider.ts +28 -0
  45. package/src/circuit-breaker.ts +157 -0
  46. package/src/claude-provider.ts +215 -0
  47. package/src/index.ts +13 -0
  48. package/src/openai-provider.ts +239 -0
  49. package/src/provider-factory.ts +48 -0
  50. package/src/retry-provider.ts +135 -0
  51. package/src/thinking-parser.ts +50 -0
  52. package/tests/auth-resolver.test.ts +204 -0
  53. package/tests/circuit-breaker.test.ts +220 -0
  54. package/tests/claude-provider.test.ts +35 -0
  55. package/tests/openai-provider.test.ts +35 -0
  56. package/tests/provider-factory.test.ts +73 -0
  57. package/tests/retry-provider-new.test.ts +166 -0
  58. package/tests/retry-provider.test.ts +118 -0
  59. package/tests/thinking-parser.test.ts +73 -0
  60. package/tsconfig.json +10 -0
  61. package/vitest.config.ts +15 -0
@@ -0,0 +1,261 @@
1
+ import type { AuthConfig, ResolvedCredential } from '@charming_groot/core';
2
+ import { ProviderError } from '@charming_groot/core';
3
+
4
+ /**
5
+ * Synchronously extract a token/apiKey from an AuthConfig.
6
+ * For auth types that require async token exchange (OAuth client_credentials, Azure AD),
7
+ * the caller should use resolveAuth() first and pass the accessToken.
8
+ */
9
+ export function extractToken(auth: AuthConfig): string {
10
+ switch (auth.type) {
11
+ case 'no-auth':
12
+ return 'no-auth';
13
+ case 'api-key':
14
+ return auth.apiKey;
15
+ case 'oauth':
16
+ return auth.accessToken ?? '';
17
+ case 'azure-ad':
18
+ return auth.accessToken ?? '';
19
+ case 'aws-iam':
20
+ return auth.accessKeyId ?? '';
21
+ case 'gcp-service-account':
22
+ return auth.accessToken ?? '';
23
+ case 'credential-file':
24
+ return '';
25
+ }
26
+ }
27
+
28
+ export async function resolveAuth(auth: AuthConfig): Promise<ResolvedCredential> {
29
+ switch (auth.type) {
30
+ case 'no-auth':
31
+ return { type: 'no-auth', headers: {}, token: undefined };
32
+ case 'api-key':
33
+ return resolveApiKey(auth);
34
+ case 'oauth':
35
+ return resolveOAuth(auth);
36
+ case 'azure-ad':
37
+ return resolveAzureAd(auth);
38
+ case 'aws-iam':
39
+ return resolveAwsIam(auth);
40
+ case 'gcp-service-account':
41
+ return resolveGcp(auth);
42
+ case 'credential-file':
43
+ return resolveCredentialFile(auth);
44
+ default:
45
+ throw new ProviderError(`Unsupported auth type: ${(auth as AuthConfig).type}`);
46
+ }
47
+ }
48
+
49
+ function resolveApiKey(auth: { type: 'api-key'; apiKey: string }): ResolvedCredential {
50
+ return {
51
+ type: 'api-key',
52
+ headers: { Authorization: `Bearer ${auth.apiKey}` },
53
+ token: auth.apiKey,
54
+ };
55
+ }
56
+
57
+ async function resolveOAuth(auth: {
58
+ type: 'oauth';
59
+ clientId: string;
60
+ clientSecret: string;
61
+ tokenUrl: string;
62
+ scopes?: readonly string[];
63
+ accessToken?: string;
64
+ refreshToken?: string;
65
+ }): Promise<ResolvedCredential> {
66
+ // If we already have a valid access token, use it
67
+ if (auth.accessToken) {
68
+ return {
69
+ type: 'oauth',
70
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
71
+ token: auth.accessToken,
72
+ };
73
+ }
74
+
75
+ // Client credentials flow
76
+ const params = new URLSearchParams({
77
+ grant_type: auth.refreshToken ? 'refresh_token' : 'client_credentials',
78
+ client_id: auth.clientId,
79
+ client_secret: auth.clientSecret,
80
+ });
81
+
82
+ if (auth.refreshToken) {
83
+ params.set('refresh_token', auth.refreshToken);
84
+ }
85
+ if (auth.scopes && auth.scopes.length > 0) {
86
+ params.set('scope', auth.scopes.join(' '));
87
+ }
88
+
89
+ const response = await fetch(auth.tokenUrl, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
92
+ body: params.toString(),
93
+ });
94
+
95
+ if (!response.ok) {
96
+ const body = await response.text();
97
+ throw new ProviderError(`OAuth token request failed (${response.status}): ${body}`);
98
+ }
99
+
100
+ const data = await response.json() as { access_token: string; expires_in?: number };
101
+ const expiresAt = data.expires_in
102
+ ? new Date(Date.now() + data.expires_in * 1000)
103
+ : undefined;
104
+
105
+ return {
106
+ type: 'oauth',
107
+ headers: { Authorization: `Bearer ${data.access_token}` },
108
+ token: data.access_token,
109
+ expiresAt,
110
+ };
111
+ }
112
+
113
+ async function resolveAzureAd(auth: {
114
+ type: 'azure-ad';
115
+ tenantId: string;
116
+ clientId: string;
117
+ clientSecret?: string;
118
+ accessToken?: string;
119
+ }): Promise<ResolvedCredential> {
120
+ if (auth.accessToken) {
121
+ return {
122
+ type: 'azure-ad',
123
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
124
+ token: auth.accessToken,
125
+ };
126
+ }
127
+
128
+ if (!auth.clientSecret) {
129
+ throw new ProviderError('Azure AD auth requires either accessToken or clientSecret');
130
+ }
131
+
132
+ const tokenUrl = `https://login.microsoftonline.com/${auth.tenantId}/oauth2/v2.0/token`;
133
+ const params = new URLSearchParams({
134
+ grant_type: 'client_credentials',
135
+ client_id: auth.clientId,
136
+ client_secret: auth.clientSecret,
137
+ scope: 'https://cognitiveservices.azure.com/.default',
138
+ });
139
+
140
+ const response = await fetch(tokenUrl, {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
143
+ body: params.toString(),
144
+ });
145
+
146
+ if (!response.ok) {
147
+ const body = await response.text();
148
+ throw new ProviderError(`Azure AD token request failed (${response.status}): ${body}`);
149
+ }
150
+
151
+ const data = await response.json() as { access_token: string; expires_in?: number };
152
+ const expiresAt = data.expires_in
153
+ ? new Date(Date.now() + data.expires_in * 1000)
154
+ : undefined;
155
+
156
+ return {
157
+ type: 'azure-ad',
158
+ headers: {
159
+ Authorization: `Bearer ${data.access_token}`,
160
+ 'api-key': data.access_token,
161
+ },
162
+ token: data.access_token,
163
+ expiresAt,
164
+ };
165
+ }
166
+
167
+ async function resolveAwsIam(auth: {
168
+ type: 'aws-iam';
169
+ accessKeyId?: string;
170
+ secretAccessKey?: string;
171
+ sessionToken?: string;
172
+ region: string;
173
+ profile?: string;
174
+ }): Promise<ResolvedCredential> {
175
+ // If explicit credentials provided, use them directly
176
+ // Real AWS Bedrock signing would use AWS SDK's SigV4
177
+ // This provides the credential structure for consumers to use
178
+ const headers: Record<string, string> = {};
179
+
180
+ if (auth.accessKeyId && auth.secretAccessKey) {
181
+ headers['x-aws-access-key-id'] = auth.accessKeyId;
182
+ headers['x-aws-region'] = auth.region;
183
+ if (auth.sessionToken) {
184
+ headers['x-aws-session-token'] = auth.sessionToken;
185
+ }
186
+ }
187
+
188
+ return {
189
+ type: 'aws-iam',
190
+ headers,
191
+ token: auth.accessKeyId,
192
+ };
193
+ }
194
+
195
+ async function resolveGcp(auth: {
196
+ type: 'gcp-service-account';
197
+ projectId: string;
198
+ keyFilePath?: string;
199
+ accessToken?: string;
200
+ }): Promise<ResolvedCredential> {
201
+ if (auth.accessToken) {
202
+ return {
203
+ type: 'gcp-service-account',
204
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
205
+ token: auth.accessToken,
206
+ };
207
+ }
208
+
209
+ // When keyFilePath is provided, consumers should use Google Auth Library
210
+ // This returns a placeholder that signals ADC should be used
211
+ return {
212
+ type: 'gcp-service-account',
213
+ headers: {},
214
+ token: undefined,
215
+ };
216
+ }
217
+
218
+ async function resolveCredentialFile(auth: {
219
+ type: 'credential-file';
220
+ filePath: string;
221
+ profile?: string;
222
+ }): Promise<ResolvedCredential> {
223
+ const { readFile } = await import('node:fs/promises');
224
+ let content: string;
225
+
226
+ try {
227
+ content = await readFile(auth.filePath, 'utf-8');
228
+ } catch (error) {
229
+ throw new ProviderError(
230
+ `Failed to read credential file: ${auth.filePath}`,
231
+ error instanceof Error ? error : undefined
232
+ );
233
+ }
234
+
235
+ try {
236
+ const credentials = JSON.parse(content) as Record<string, Record<string, string>>;
237
+ const profile = auth.profile ?? 'default';
238
+ const entry = credentials[profile];
239
+
240
+ if (!entry) {
241
+ throw new ProviderError(`Profile '${profile}' not found in credential file`);
242
+ }
243
+
244
+ const apiKey = entry['api_key'] ?? entry['apiKey'] ?? entry['token'];
245
+ if (!apiKey) {
246
+ throw new ProviderError(`No api_key/apiKey/token found in profile '${profile}'`);
247
+ }
248
+
249
+ return {
250
+ type: 'credential-file',
251
+ headers: { Authorization: `Bearer ${apiKey}` },
252
+ token: apiKey,
253
+ };
254
+ } catch (error) {
255
+ if (error instanceof ProviderError) throw error;
256
+ throw new ProviderError(
257
+ `Failed to parse credential file: ${auth.filePath}`,
258
+ error instanceof Error ? error : undefined
259
+ );
260
+ }
261
+ }
@@ -0,0 +1 @@
1
+ export { resolveAuth, extractToken } from './auth-resolver.js';
@@ -0,0 +1,28 @@
1
+ import type {
2
+ ILlmProvider,
3
+ Message,
4
+ LlmResponse,
5
+ StreamEvent,
6
+ ToolDescription,
7
+ } from '@charming_groot/core';
8
+ import { createChildLogger } from '@charming_groot/core';
9
+ import type { AgentLogger } from '@charming_groot/core';
10
+
11
+ export abstract class BaseProvider implements ILlmProvider {
12
+ abstract readonly providerId: string;
13
+ protected readonly logger: AgentLogger;
14
+
15
+ constructor(loggerName: string) {
16
+ this.logger = createChildLogger(loggerName);
17
+ }
18
+
19
+ abstract chat(
20
+ messages: readonly Message[],
21
+ tools?: readonly ToolDescription[]
22
+ ): Promise<LlmResponse>;
23
+
24
+ abstract stream(
25
+ messages: readonly Message[],
26
+ tools?: readonly ToolDescription[]
27
+ ): AsyncIterable<StreamEvent>;
28
+ }
@@ -0,0 +1,157 @@
1
+ import type {
2
+ ILlmProvider,
3
+ Message,
4
+ LlmResponse,
5
+ StreamEvent,
6
+ ToolDescription,
7
+ AgentLogger,
8
+ } from '@charming_groot/core';
9
+ import { ProviderError, createChildLogger } from '@charming_groot/core';
10
+
11
+ export interface CircuitBreakerConfig {
12
+ /** Consecutive failures before opening the circuit (default: 5) */
13
+ failureThreshold?: number;
14
+ /** Consecutive successes in HALF_OPEN to close the circuit (default: 2) */
15
+ successThreshold?: number;
16
+ /** Ms to stay OPEN before allowing a probe request (default: 60_000) */
17
+ openTimeoutMs?: number;
18
+ }
19
+
20
+ type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
21
+
22
+ /**
23
+ * Circuit breaker wrapping an ILlmProvider.
24
+ *
25
+ * CLOSED → normal operation, counts failures
26
+ * OPEN → rejects immediately, waits openTimeoutMs then probes
27
+ * HALF_OPEN → allows one request; success → CLOSED, failure → OPEN
28
+ *
29
+ * Wrap inside RetryProvider for best results:
30
+ * createProvider() → RetryProvider → CircuitBreakerProvider
31
+ */
32
+ export class CircuitBreakerProvider implements ILlmProvider {
33
+ readonly providerId: string;
34
+
35
+ private readonly inner: ILlmProvider;
36
+ private readonly failureThreshold: number;
37
+ private readonly successThreshold: number;
38
+ private readonly openTimeoutMs: number;
39
+ private readonly logger: AgentLogger;
40
+
41
+ private state: State = 'CLOSED';
42
+ private failureCount = 0;
43
+ private successCount = 0;
44
+ private openedAt = 0;
45
+
46
+ constructor(provider: ILlmProvider, config?: CircuitBreakerConfig) {
47
+ this.inner = provider;
48
+ this.providerId = provider.providerId;
49
+ this.failureThreshold = config?.failureThreshold ?? 5;
50
+ this.successThreshold = config?.successThreshold ?? 2;
51
+ this.openTimeoutMs = config?.openTimeoutMs ?? 60_000;
52
+ this.logger = createChildLogger('circuit-breaker');
53
+ }
54
+
55
+ get currentState(): State {
56
+ return this.state;
57
+ }
58
+
59
+ async chat(
60
+ messages: readonly Message[],
61
+ tools?: readonly ToolDescription[]
62
+ ): Promise<LlmResponse> {
63
+ this.guardOrThrow();
64
+ try {
65
+ const result = await this.inner.chat(messages, tools);
66
+ this.onSuccess();
67
+ return result;
68
+ } catch (error) {
69
+ this.onFailure(error);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ async *stream(
75
+ messages: readonly Message[],
76
+ tools?: readonly ToolDescription[]
77
+ ): AsyncIterable<StreamEvent> {
78
+ this.guardOrThrow();
79
+ try {
80
+ yield* this.inner.stream(messages, tools);
81
+ this.onSuccess();
82
+ } catch (error) {
83
+ this.onFailure(error);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ // ── Internal state machine ───────────────────────────────────────────────
89
+
90
+ private guardOrThrow(): void {
91
+ if (this.state === 'CLOSED' || this.state === 'HALF_OPEN') return;
92
+
93
+ // OPEN: check if timeout has elapsed
94
+ const elapsed = Date.now() - this.openedAt;
95
+ if (elapsed >= this.openTimeoutMs) {
96
+ this.transitionTo('HALF_OPEN');
97
+ return; // allow this probe request through
98
+ }
99
+
100
+ const remaining = Math.ceil((this.openTimeoutMs - elapsed) / 1000);
101
+ throw new ProviderError(
102
+ `Circuit breaker OPEN for provider '${this.providerId}' — retry in ${remaining}s`
103
+ );
104
+ }
105
+
106
+ private onSuccess(): void {
107
+ if (this.state === 'HALF_OPEN') {
108
+ this.successCount++;
109
+ if (this.successCount >= this.successThreshold) {
110
+ this.transitionTo('CLOSED');
111
+ }
112
+ } else {
113
+ this.failureCount = 0; // reset on any success in CLOSED
114
+ }
115
+ }
116
+
117
+ private onFailure(error: unknown): void {
118
+ const msg = error instanceof Error ? error.message : String(error);
119
+ this.logger.warn({ state: this.state, error: msg }, 'Circuit breaker recorded failure');
120
+
121
+ if (this.state === 'HALF_OPEN') {
122
+ // Probe failed → back to OPEN
123
+ this.transitionTo('OPEN');
124
+ return;
125
+ }
126
+
127
+ this.failureCount++;
128
+ if (this.failureCount >= this.failureThreshold) {
129
+ this.transitionTo('OPEN');
130
+ }
131
+ }
132
+
133
+ private transitionTo(next: State): void {
134
+ const prev = this.state;
135
+ this.state = next;
136
+
137
+ if (next === 'OPEN') {
138
+ this.openedAt = Date.now();
139
+ this.successCount = 0;
140
+ this.logger.error(
141
+ { failureCount: this.failureCount, openTimeoutMs: this.openTimeoutMs },
142
+ `Circuit breaker OPENED for provider '${this.providerId}'`
143
+ );
144
+ } else if (next === 'HALF_OPEN') {
145
+ this.successCount = 0;
146
+ this.logger.warn({}, `Circuit breaker HALF_OPEN — probing provider '${this.providerId}'`);
147
+ } else {
148
+ this.failureCount = 0;
149
+ this.logger.info({}, `Circuit breaker CLOSED for provider '${this.providerId}'`);
150
+ }
151
+
152
+ if (prev !== next) {
153
+ // Allow external inspection of state transitions
154
+ this.logger.debug({ from: prev, to: next }, 'Circuit breaker state transition');
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,215 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import type {
3
+ Message,
4
+ LlmResponse,
5
+ StreamEvent,
6
+ ToolDescription,
7
+ ToolCall,
8
+ ProviderConfig,
9
+ } from '@charming_groot/core';
10
+ import { ProviderError } from '@charming_groot/core';
11
+ import { BaseProvider } from './base-provider.js';
12
+ import { extractToken } from './auth/auth-resolver.js';
13
+ import { extractThinkTag, estimateThinkingMs } from './thinking-parser.js';
14
+
15
+ interface AnthropicToolParam {
16
+ name: string;
17
+ description: string;
18
+ input_schema: {
19
+ type: 'object';
20
+ properties: Record<string, { type: string; description: string }>;
21
+ required: string[];
22
+ };
23
+ }
24
+
25
+ export class ClaudeProvider extends BaseProvider {
26
+ readonly providerId = 'claude';
27
+ private readonly client: Anthropic;
28
+ private readonly model: string;
29
+ private readonly maxTokens: number;
30
+
31
+ constructor(config: ProviderConfig) {
32
+ super('claude-provider');
33
+ const apiKey = extractToken(config.auth);
34
+ this.client = new Anthropic({ apiKey, baseURL: config.baseUrl });
35
+ this.model = config.model;
36
+ this.maxTokens = config.maxTokens;
37
+ }
38
+
39
+ async chat(
40
+ messages: readonly Message[],
41
+ tools?: readonly ToolDescription[]
42
+ ): Promise<LlmResponse> {
43
+ try {
44
+ const systemMsg = messages.find((m) => m.role === 'system');
45
+ const nonSystemMsgs = messages.filter((m) => m.role !== 'system');
46
+
47
+ const response = await this.client.messages.create({
48
+ model: this.model,
49
+ max_tokens: this.maxTokens,
50
+ system: systemMsg?.content,
51
+ messages: this.toAnthropicMessages(nonSystemMsgs),
52
+ tools: tools ? this.toAnthropicTools(tools) : undefined,
53
+ });
54
+
55
+ return this.parseResponse(response);
56
+ } catch (error) {
57
+ throw new ProviderError(
58
+ `Claude API error: ${error instanceof Error ? error.message : String(error)}`,
59
+ error instanceof Error ? error : undefined
60
+ );
61
+ }
62
+ }
63
+
64
+ async *stream(
65
+ messages: readonly Message[],
66
+ tools?: readonly ToolDescription[]
67
+ ): AsyncIterable<StreamEvent> {
68
+ try {
69
+ const systemMsg = messages.find((m) => m.role === 'system');
70
+ const nonSystemMsgs = messages.filter((m) => m.role !== 'system');
71
+
72
+ const stream = this.client.messages.stream({
73
+ model: this.model,
74
+ max_tokens: this.maxTokens,
75
+ system: systemMsg?.content,
76
+ messages: this.toAnthropicMessages(nonSystemMsgs),
77
+ tools: tools ? this.toAnthropicTools(tools) : undefined,
78
+ });
79
+
80
+ for await (const event of stream) {
81
+ if (event.type === 'content_block_delta') {
82
+ const delta = event.delta as { type: string; text?: string; partial_json?: string };
83
+ if (delta.type === 'text_delta' && delta.text) {
84
+ yield { type: 'text_delta', content: delta.text };
85
+ } else if (delta.type === 'input_json_delta' && delta.partial_json) {
86
+ yield { type: 'tool_call_delta', content: delta.partial_json };
87
+ }
88
+ } else if (event.type === 'content_block_start') {
89
+ const block = event.content_block as { type: string; id?: string; name?: string };
90
+ if (block.type === 'tool_use') {
91
+ yield {
92
+ type: 'tool_call_start',
93
+ toolCall: { id: block.id, name: block.name },
94
+ };
95
+ }
96
+ } else if (event.type === 'message_stop') {
97
+ const finalMessage = await stream.finalMessage();
98
+ yield { type: 'done', response: this.parseResponse(finalMessage) };
99
+ }
100
+ }
101
+ } catch (error) {
102
+ throw new ProviderError(
103
+ `Claude stream error: ${error instanceof Error ? error.message : String(error)}`,
104
+ error instanceof Error ? error : undefined
105
+ );
106
+ }
107
+ }
108
+
109
+ private toAnthropicMessages(
110
+ messages: readonly Message[]
111
+ ): Anthropic.MessageParam[] {
112
+ return messages.map((msg) => {
113
+ if (msg.toolResults && msg.toolResults.length > 0) {
114
+ return {
115
+ role: 'user' as const,
116
+ content: msg.toolResults.map((tr) => ({
117
+ type: 'tool_result' as const,
118
+ tool_use_id: tr.toolCallId,
119
+ content: tr.content,
120
+ })),
121
+ };
122
+ }
123
+
124
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
125
+ const content: Anthropic.ContentBlockParam[] = [];
126
+ if (msg.content) {
127
+ content.push({ type: 'text', text: msg.content });
128
+ }
129
+ for (const tc of msg.toolCalls) {
130
+ content.push({
131
+ type: 'tool_use',
132
+ id: tc.id,
133
+ name: tc.name,
134
+ input: JSON.parse(tc.arguments),
135
+ });
136
+ }
137
+ return { role: 'assistant' as const, content };
138
+ }
139
+
140
+ return {
141
+ role: msg.role as 'user' | 'assistant',
142
+ content: msg.content,
143
+ };
144
+ });
145
+ }
146
+
147
+ private toAnthropicTools(tools: readonly ToolDescription[]): AnthropicToolParam[] {
148
+ return tools.map((tool) => ({
149
+ name: tool.name,
150
+ description: tool.description,
151
+ input_schema: {
152
+ type: 'object' as const,
153
+ properties: Object.fromEntries(
154
+ tool.parameters.map((p) => [
155
+ p.name,
156
+ { type: p.type, description: p.description },
157
+ ])
158
+ ),
159
+ required: tool.parameters
160
+ .filter((p) => p.required)
161
+ .map((p) => p.name),
162
+ },
163
+ }));
164
+ }
165
+
166
+ private parseResponse(response: Anthropic.Message): LlmResponse {
167
+ let content = '';
168
+ const toolCalls: ToolCall[] = [];
169
+ let thinkingMs: number | undefined;
170
+
171
+ for (const block of response.content) {
172
+ if (block.type === 'thinking') {
173
+ // Anthropic extended thinking block — estimate duration from token count
174
+ // (no direct timing from API, but the block existing means thinking occurred)
175
+ const thinkingBlock = block as { type: 'thinking'; thinking: string };
176
+ thinkingMs = estimateThinkingMs(thinkingBlock.thinking);
177
+ } else if (block.type === 'text') {
178
+ content += block.text;
179
+ } else if (block.type === 'tool_use') {
180
+ toolCalls.push({
181
+ id: block.id,
182
+ name: block.name,
183
+ arguments: JSON.stringify(block.input),
184
+ });
185
+ }
186
+ }
187
+
188
+ // Fallback: parse <think>...</think> tags from text content (DeepSeek, etc.)
189
+ const parsed = extractThinkTag(content);
190
+ if (parsed.thinkContent) {
191
+ content = parsed.cleanContent;
192
+ if (!thinkingMs) {
193
+ thinkingMs = estimateThinkingMs(parsed.thinkContent);
194
+ }
195
+ }
196
+
197
+ const stopReason =
198
+ response.stop_reason === 'tool_use'
199
+ ? 'tool_use' as const
200
+ : response.stop_reason === 'max_tokens'
201
+ ? 'max_tokens' as const
202
+ : 'end_turn' as const;
203
+
204
+ return {
205
+ content,
206
+ stopReason,
207
+ toolCalls,
208
+ usage: {
209
+ inputTokens: response.usage.input_tokens,
210
+ outputTokens: response.usage.output_tokens,
211
+ thinkingMs,
212
+ },
213
+ };
214
+ }
215
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { BaseProvider } from './base-provider.js';
2
+ export { ClaudeProvider } from './claude-provider.js';
3
+ export { OpenAIProvider } from './openai-provider.js';
4
+ export {
5
+ createProvider,
6
+ registerProvider,
7
+ getProviderRegistry,
8
+ } from './provider-factory.js';
9
+ export { RetryProvider, type RetryConfig } from './retry-provider.js';
10
+ export { CircuitBreakerProvider, type CircuitBreakerConfig } from './circuit-breaker.js';
11
+ export { resolveAuth, extractToken } from './auth/index.js';
12
+ export { extractThinkTag, estimateThinkingMs } from './thinking-parser.js';
13
+ export type { ThinkTagResult } from './thinking-parser.js';