@auxiora/providers 1.0.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 (88) hide show
  1. package/LICENSE +191 -0
  2. package/dist/anthropic.d.ts +82 -0
  3. package/dist/anthropic.d.ts.map +1 -0
  4. package/dist/anthropic.js +618 -0
  5. package/dist/anthropic.js.map +1 -0
  6. package/dist/claude-code-tools.d.ts +29 -0
  7. package/dist/claude-code-tools.d.ts.map +1 -0
  8. package/dist/claude-code-tools.js +221 -0
  9. package/dist/claude-code-tools.js.map +1 -0
  10. package/dist/claude-oauth.d.ts +86 -0
  11. package/dist/claude-oauth.d.ts.map +1 -0
  12. package/dist/claude-oauth.js +318 -0
  13. package/dist/claude-oauth.js.map +1 -0
  14. package/dist/cohere.d.ts +18 -0
  15. package/dist/cohere.d.ts.map +1 -0
  16. package/dist/cohere.js +163 -0
  17. package/dist/cohere.js.map +1 -0
  18. package/dist/deepseek.d.ts +18 -0
  19. package/dist/deepseek.d.ts.map +1 -0
  20. package/dist/deepseek.js +164 -0
  21. package/dist/deepseek.js.map +1 -0
  22. package/dist/factory.d.ts +19 -0
  23. package/dist/factory.d.ts.map +1 -0
  24. package/dist/factory.js +108 -0
  25. package/dist/factory.js.map +1 -0
  26. package/dist/google.d.ts +18 -0
  27. package/dist/google.d.ts.map +1 -0
  28. package/dist/google.js +141 -0
  29. package/dist/google.js.map +1 -0
  30. package/dist/groq.d.ts +18 -0
  31. package/dist/groq.d.ts.map +1 -0
  32. package/dist/groq.js +186 -0
  33. package/dist/groq.js.map +1 -0
  34. package/dist/index.d.ts +15 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +14 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/ollama.d.ts +18 -0
  39. package/dist/ollama.d.ts.map +1 -0
  40. package/dist/ollama.js +141 -0
  41. package/dist/ollama.js.map +1 -0
  42. package/dist/openai-compatible.d.ts +20 -0
  43. package/dist/openai-compatible.d.ts.map +1 -0
  44. package/dist/openai-compatible.js +112 -0
  45. package/dist/openai-compatible.js.map +1 -0
  46. package/dist/openai.d.ts +20 -0
  47. package/dist/openai.d.ts.map +1 -0
  48. package/dist/openai.js +259 -0
  49. package/dist/openai.js.map +1 -0
  50. package/dist/replicate.d.ts +20 -0
  51. package/dist/replicate.d.ts.map +1 -0
  52. package/dist/replicate.js +186 -0
  53. package/dist/replicate.js.map +1 -0
  54. package/dist/thinking-levels.d.ts +16 -0
  55. package/dist/thinking-levels.d.ts.map +1 -0
  56. package/dist/thinking-levels.js +34 -0
  57. package/dist/thinking-levels.js.map +1 -0
  58. package/dist/types.d.ts +157 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/xai.d.ts +18 -0
  63. package/dist/xai.d.ts.map +1 -0
  64. package/dist/xai.js +164 -0
  65. package/dist/xai.js.map +1 -0
  66. package/package.json +30 -0
  67. package/src/anthropic.ts +691 -0
  68. package/src/claude-code-tools.ts +233 -0
  69. package/src/claude-oauth.ts +410 -0
  70. package/src/cohere.ts +242 -0
  71. package/src/deepseek.ts +241 -0
  72. package/src/factory.ts +142 -0
  73. package/src/google.ts +176 -0
  74. package/src/groq.ts +263 -0
  75. package/src/index.ts +44 -0
  76. package/src/ollama.ts +194 -0
  77. package/src/openai-compatible.ts +154 -0
  78. package/src/openai.ts +307 -0
  79. package/src/replicate.ts +247 -0
  80. package/src/thinking-levels.ts +37 -0
  81. package/src/types.ts +171 -0
  82. package/src/xai.ts +241 -0
  83. package/tests/adapters.test.ts +185 -0
  84. package/tests/claude-oauth.test.ts +45 -0
  85. package/tests/new-providers.test.ts +732 -0
  86. package/tests/thinking-levels.test.ts +82 -0
  87. package/tsconfig.json +8 -0
  88. package/tsconfig.tsbuildinfo +1 -0
package/src/cohere.ts ADDED
@@ -0,0 +1,242 @@
1
+ import type {
2
+ Provider,
3
+ ProviderMetadata,
4
+ ChatMessage,
5
+ CompletionOptions,
6
+ CompletionResult,
7
+ StreamChunk,
8
+ } from './types.js';
9
+
10
+ const DEFAULT_MODEL = 'command-r-plus';
11
+ const DEFAULT_MAX_TOKENS = 4096;
12
+ const BASE_URL = 'https://api.cohere.com/v2';
13
+
14
+ export interface CohereProviderOptions {
15
+ apiKey: string;
16
+ model?: string;
17
+ maxTokens?: number;
18
+ }
19
+
20
+ interface CohereChatMessage {
21
+ role: 'user' | 'assistant' | 'system';
22
+ content: string;
23
+ }
24
+
25
+ interface CohereUsage {
26
+ billed_units?: {
27
+ input_tokens?: number;
28
+ output_tokens?: number;
29
+ };
30
+ tokens?: {
31
+ input_tokens?: number;
32
+ output_tokens?: number;
33
+ };
34
+ }
35
+
36
+ interface CohereChatResponse {
37
+ message: {
38
+ role: string;
39
+ content: Array<{ type: string; text: string }>;
40
+ };
41
+ finish_reason: string;
42
+ usage: CohereUsage;
43
+ }
44
+
45
+ interface CohereStreamEvent {
46
+ type: string;
47
+ delta?: {
48
+ message?: {
49
+ content?: {
50
+ text?: string;
51
+ };
52
+ };
53
+ };
54
+ usage?: CohereUsage;
55
+ }
56
+
57
+ export class CohereProvider implements Provider {
58
+ name = 'cohere';
59
+ metadata: ProviderMetadata = {
60
+ name: 'cohere',
61
+ displayName: 'Cohere',
62
+ models: {
63
+ 'command-r-plus': {
64
+ maxContextTokens: 128000,
65
+ supportsVision: false,
66
+ supportsTools: true,
67
+ supportsStreaming: true,
68
+ supportsImageGen: false,
69
+ costPer1kInput: 0.0025,
70
+ costPer1kOutput: 0.01,
71
+ strengths: ['reasoning', 'rag', 'multilingual'],
72
+ isLocal: false,
73
+ },
74
+ 'command-r': {
75
+ maxContextTokens: 128000,
76
+ supportsVision: false,
77
+ supportsTools: true,
78
+ supportsStreaming: true,
79
+ supportsImageGen: false,
80
+ costPer1kInput: 0.00015,
81
+ costPer1kOutput: 0.0006,
82
+ strengths: ['fast', 'rag', 'multilingual'],
83
+ isLocal: false,
84
+ },
85
+ },
86
+ isAvailable: async () => {
87
+ try {
88
+ const response = await fetch(`${BASE_URL}/models`, {
89
+ headers: { Authorization: `Bearer ${this.apiKey}` },
90
+ });
91
+ return response.ok;
92
+ } catch {
93
+ return false;
94
+ }
95
+ },
96
+ };
97
+
98
+ private apiKey: string;
99
+ private defaultModel: string;
100
+ private defaultMaxTokens: number;
101
+
102
+ constructor(options: CohereProviderOptions) {
103
+ this.apiKey = options.apiKey;
104
+ this.defaultModel = options.model || DEFAULT_MODEL;
105
+ this.defaultMaxTokens = options.maxTokens || DEFAULT_MAX_TOKENS;
106
+ }
107
+
108
+ async complete(
109
+ messages: ChatMessage[],
110
+ options?: CompletionOptions,
111
+ ): Promise<CompletionResult> {
112
+ const model = options?.model || this.defaultModel;
113
+ const cohereMessages = this.prepareMessages(messages, options);
114
+
115
+ const response = await fetch(`${BASE_URL}/chat`, {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ Authorization: `Bearer ${this.apiKey}`,
120
+ },
121
+ body: JSON.stringify({
122
+ model,
123
+ messages: cohereMessages,
124
+ max_tokens: options?.maxTokens || this.defaultMaxTokens,
125
+ temperature: options?.temperature,
126
+ }),
127
+ });
128
+
129
+ if (!response.ok) {
130
+ throw new Error(`Cohere API error: ${response.status} ${response.statusText}`);
131
+ }
132
+
133
+ const data = (await response.json()) as CohereChatResponse;
134
+ const text = data.message?.content?.[0]?.text || '';
135
+ const usage = data.usage;
136
+
137
+ return {
138
+ content: text,
139
+ usage: {
140
+ inputTokens: usage?.tokens?.input_tokens ?? usage?.billed_units?.input_tokens ?? 0,
141
+ outputTokens: usage?.tokens?.output_tokens ?? usage?.billed_units?.output_tokens ?? 0,
142
+ },
143
+ model,
144
+ finishReason: data.finish_reason || 'unknown',
145
+ };
146
+ }
147
+
148
+ async *stream(
149
+ messages: ChatMessage[],
150
+ options?: CompletionOptions,
151
+ ): AsyncGenerator<StreamChunk, void, unknown> {
152
+ const model = options?.model || this.defaultModel;
153
+ const cohereMessages = this.prepareMessages(messages, options);
154
+
155
+ try {
156
+ const response = await fetch(`${BASE_URL}/chat`, {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ Authorization: `Bearer ${this.apiKey}`,
161
+ },
162
+ body: JSON.stringify({
163
+ model,
164
+ messages: cohereMessages,
165
+ max_tokens: options?.maxTokens || this.defaultMaxTokens,
166
+ temperature: options?.temperature,
167
+ stream: true,
168
+ }),
169
+ });
170
+
171
+ if (!response.ok) {
172
+ throw new Error(`Cohere API error: ${response.status} ${response.statusText}`);
173
+ }
174
+
175
+ const reader = response.body?.getReader();
176
+ if (!reader) {
177
+ throw new Error('No response body');
178
+ }
179
+
180
+ const decoder = new TextDecoder();
181
+ let buffer = '';
182
+ let inputTokens = 0;
183
+ let outputTokens = 0;
184
+
185
+ while (true) {
186
+ const { done, value } = await reader.read();
187
+ if (done) break;
188
+
189
+ buffer += decoder.decode(value, { stream: true });
190
+ const lines = buffer.split('\n');
191
+ buffer = lines.pop() || '';
192
+
193
+ for (const line of lines) {
194
+ const trimmed = line.trim();
195
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
196
+ const data = trimmed.slice(6);
197
+ if (data === '[DONE]') continue;
198
+
199
+ const event = JSON.parse(data) as CohereStreamEvent;
200
+
201
+ if (event.type === 'content-delta' && event.delta?.message?.content?.text) {
202
+ yield { type: 'text', content: event.delta.message.content.text };
203
+ }
204
+
205
+ if (event.usage) {
206
+ inputTokens = event.usage.tokens?.input_tokens ?? event.usage.billed_units?.input_tokens ?? 0;
207
+ outputTokens = event.usage.tokens?.output_tokens ?? event.usage.billed_units?.output_tokens ?? 0;
208
+ }
209
+
210
+ if (event.type === 'message-end') {
211
+ yield {
212
+ type: 'done',
213
+ usage: { inputTokens, outputTokens },
214
+ };
215
+ }
216
+ }
217
+ }
218
+ } catch (error) {
219
+ yield {
220
+ type: 'error',
221
+ error: error instanceof Error ? error.message : 'Unknown error',
222
+ };
223
+ }
224
+ }
225
+
226
+ private prepareMessages(
227
+ messages: ChatMessage[],
228
+ options?: CompletionOptions,
229
+ ): CohereChatMessage[] {
230
+ const cohereMessages: CohereChatMessage[] = [];
231
+
232
+ if (options?.systemPrompt) {
233
+ cohereMessages.push({ role: 'system', content: options.systemPrompt });
234
+ }
235
+
236
+ for (const msg of messages) {
237
+ cohereMessages.push({ role: msg.role, content: msg.content });
238
+ }
239
+
240
+ return cohereMessages;
241
+ }
242
+ }
@@ -0,0 +1,241 @@
1
+ import type {
2
+ Provider,
3
+ ProviderMetadata,
4
+ ChatMessage,
5
+ CompletionOptions,
6
+ CompletionResult,
7
+ StreamChunk,
8
+ } from './types.js';
9
+
10
+ const DEFAULT_MODEL = 'deepseek-chat';
11
+ const DEFAULT_MAX_TOKENS = 4096;
12
+ const BASE_URL = 'https://api.deepseek.com';
13
+
14
+ export interface DeepSeekProviderOptions {
15
+ apiKey: string;
16
+ model?: string;
17
+ maxTokens?: number;
18
+ }
19
+
20
+ interface DeepSeekChatMessage {
21
+ role: string;
22
+ content: string;
23
+ }
24
+
25
+ interface DeepSeekChoice {
26
+ message: { role: string; content: string };
27
+ finish_reason: string;
28
+ }
29
+
30
+ interface DeepSeekUsage {
31
+ prompt_tokens: number;
32
+ completion_tokens: number;
33
+ }
34
+
35
+ interface DeepSeekChatResponse {
36
+ choices: DeepSeekChoice[];
37
+ usage: DeepSeekUsage;
38
+ model: string;
39
+ }
40
+
41
+ interface DeepSeekStreamDelta {
42
+ content?: string;
43
+ }
44
+
45
+ interface DeepSeekStreamChoice {
46
+ delta: DeepSeekStreamDelta;
47
+ finish_reason: string | null;
48
+ }
49
+
50
+ interface DeepSeekStreamChunk {
51
+ choices: DeepSeekStreamChoice[];
52
+ usage?: DeepSeekUsage;
53
+ }
54
+
55
+ export class DeepSeekProvider implements Provider {
56
+ name = 'deepseek';
57
+ metadata: ProviderMetadata = {
58
+ name: 'deepseek',
59
+ displayName: 'DeepSeek',
60
+ models: {
61
+ 'deepseek-chat': {
62
+ maxContextTokens: 64000,
63
+ supportsVision: false,
64
+ supportsTools: true,
65
+ supportsStreaming: true,
66
+ supportsImageGen: false,
67
+ costPer1kInput: 0.00014,
68
+ costPer1kOutput: 0.00028,
69
+ strengths: ['reasoning', 'code', 'low-cost'],
70
+ isLocal: false,
71
+ },
72
+ 'deepseek-reasoner': {
73
+ maxContextTokens: 64000,
74
+ supportsVision: false,
75
+ supportsTools: true,
76
+ supportsStreaming: true,
77
+ supportsImageGen: false,
78
+ costPer1kInput: 0.00055,
79
+ costPer1kOutput: 0.0022,
80
+ strengths: ['reasoning', 'code', 'math'],
81
+ isLocal: false,
82
+ },
83
+ },
84
+ isAvailable: async () => {
85
+ try {
86
+ const response = await fetch(`${BASE_URL}/models`, {
87
+ headers: { Authorization: `Bearer ${this.apiKey}` },
88
+ });
89
+ return response.ok;
90
+ } catch {
91
+ return false;
92
+ }
93
+ },
94
+ };
95
+
96
+ private apiKey: string;
97
+ private defaultModel: string;
98
+ private defaultMaxTokens: number;
99
+
100
+ constructor(options: DeepSeekProviderOptions) {
101
+ this.apiKey = options.apiKey;
102
+ this.defaultModel = options.model || DEFAULT_MODEL;
103
+ this.defaultMaxTokens = options.maxTokens || DEFAULT_MAX_TOKENS;
104
+ }
105
+
106
+ async complete(
107
+ messages: ChatMessage[],
108
+ options?: CompletionOptions,
109
+ ): Promise<CompletionResult> {
110
+ const model = options?.model || this.defaultModel;
111
+ const dsMessages = this.prepareMessages(messages, options);
112
+
113
+ const response = await fetch(`${BASE_URL}/chat/completions`, {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ Authorization: `Bearer ${this.apiKey}`,
118
+ },
119
+ body: JSON.stringify({
120
+ model,
121
+ max_tokens: options?.maxTokens || this.defaultMaxTokens,
122
+ messages: dsMessages,
123
+ temperature: options?.temperature,
124
+ }),
125
+ });
126
+
127
+ if (!response.ok) {
128
+ throw new Error(`DeepSeek API error: ${response.status} ${response.statusText}`);
129
+ }
130
+
131
+ const data = (await response.json()) as DeepSeekChatResponse;
132
+ const choice = data.choices[0];
133
+
134
+ return {
135
+ content: choice?.message?.content || '',
136
+ usage: {
137
+ inputTokens: data.usage?.prompt_tokens || 0,
138
+ outputTokens: data.usage?.completion_tokens || 0,
139
+ },
140
+ model: data.model,
141
+ finishReason: choice?.finish_reason || 'unknown',
142
+ };
143
+ }
144
+
145
+ async *stream(
146
+ messages: ChatMessage[],
147
+ options?: CompletionOptions,
148
+ ): AsyncGenerator<StreamChunk, void, unknown> {
149
+ const model = options?.model || this.defaultModel;
150
+ const dsMessages = this.prepareMessages(messages, options);
151
+
152
+ try {
153
+ const response = await fetch(`${BASE_URL}/chat/completions`, {
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json',
157
+ Authorization: `Bearer ${this.apiKey}`,
158
+ },
159
+ body: JSON.stringify({
160
+ model,
161
+ max_tokens: options?.maxTokens || this.defaultMaxTokens,
162
+ messages: dsMessages,
163
+ temperature: options?.temperature,
164
+ stream: true,
165
+ stream_options: { include_usage: true },
166
+ }),
167
+ });
168
+
169
+ if (!response.ok) {
170
+ throw new Error(`DeepSeek API error: ${response.status} ${response.statusText}`);
171
+ }
172
+
173
+ const reader = response.body?.getReader();
174
+ if (!reader) {
175
+ throw new Error('No response body');
176
+ }
177
+
178
+ const decoder = new TextDecoder();
179
+ let buffer = '';
180
+ let inputTokens = 0;
181
+ let outputTokens = 0;
182
+
183
+ while (true) {
184
+ const { done, value } = await reader.read();
185
+ if (done) break;
186
+
187
+ buffer += decoder.decode(value, { stream: true });
188
+ const lines = buffer.split('\n');
189
+ buffer = lines.pop() || '';
190
+
191
+ for (const line of lines) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
194
+ const data = trimmed.slice(6);
195
+ if (data === '[DONE]') continue;
196
+
197
+ const chunk = JSON.parse(data) as DeepSeekStreamChunk;
198
+
199
+ const delta = chunk.choices[0]?.delta;
200
+ if (delta?.content) {
201
+ yield { type: 'text', content: delta.content };
202
+ }
203
+
204
+ if (chunk.usage) {
205
+ inputTokens = chunk.usage.prompt_tokens || 0;
206
+ outputTokens = chunk.usage.completion_tokens || 0;
207
+ }
208
+
209
+ if (chunk.choices[0]?.finish_reason) {
210
+ yield {
211
+ type: 'done',
212
+ usage: { inputTokens, outputTokens },
213
+ };
214
+ }
215
+ }
216
+ }
217
+ } catch (error) {
218
+ yield {
219
+ type: 'error',
220
+ error: error instanceof Error ? error.message : 'Unknown error',
221
+ };
222
+ }
223
+ }
224
+
225
+ private prepareMessages(
226
+ messages: ChatMessage[],
227
+ options?: CompletionOptions,
228
+ ): DeepSeekChatMessage[] {
229
+ const dsMessages: DeepSeekChatMessage[] = [];
230
+
231
+ if (options?.systemPrompt) {
232
+ dsMessages.push({ role: 'system', content: options.systemPrompt });
233
+ }
234
+
235
+ for (const msg of messages) {
236
+ dsMessages.push({ role: msg.role, content: msg.content });
237
+ }
238
+
239
+ return dsMessages;
240
+ }
241
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { AnthropicProvider, type AnthropicProviderOptions } from './anthropic.js';
2
+ import { OpenAIProvider, type OpenAIProviderOptions } from './openai.js';
3
+ import { GoogleProvider, type GoogleProviderOptions } from './google.js';
4
+ import { OllamaProvider, type OllamaProviderOptions } from './ollama.js';
5
+ import { OpenAICompatibleProvider, type OpenAICompatibleProviderOptions } from './openai-compatible.js';
6
+ import { GroqProvider, type GroqProviderOptions } from './groq.js';
7
+ import { ReplicateProvider, type ReplicateProviderOptions } from './replicate.js';
8
+ import { DeepSeekProvider, type DeepSeekProviderOptions } from './deepseek.js';
9
+ import { CohereProvider, type CohereProviderOptions } from './cohere.js';
10
+ import { XAIProvider, type XAIProviderOptions } from './xai.js';
11
+ import { readClaudeCliCredentials } from './claude-oauth.js';
12
+ import { getLogger } from '@auxiora/logger';
13
+ import type { Provider, ProviderConfig } from './types.js';
14
+
15
+ const logger = getLogger('providers');
16
+
17
+ export type ProviderName = string;
18
+
19
+ export interface ProviderFactoryOptions {
20
+ primary: ProviderName;
21
+ fallback?: ProviderName;
22
+ config: ProviderConfig;
23
+ }
24
+
25
+ export class ProviderFactory {
26
+ private providers: Map<ProviderName, Provider> = new Map();
27
+ private primary: ProviderName;
28
+ private fallback?: ProviderName;
29
+
30
+ constructor(options: ProviderFactoryOptions) {
31
+ this.primary = options.primary;
32
+ this.fallback = options.fallback;
33
+
34
+ // Initialize configured providers
35
+ const anthropicConfig = options.config.anthropic;
36
+ const hasAnthropicCredentials =
37
+ anthropicConfig?.apiKey ||
38
+ anthropicConfig?.oauthToken ||
39
+ (anthropicConfig?.useCliCredentials !== false && readClaudeCliCredentials() !== null);
40
+
41
+ if (hasAnthropicCredentials && anthropicConfig) {
42
+ this.providers.set('anthropic', new AnthropicProvider(anthropicConfig));
43
+ }
44
+
45
+ if (options.config.openai?.apiKey) {
46
+ this.providers.set('openai', new OpenAIProvider(options.config.openai));
47
+ }
48
+
49
+ if (options.config.google?.apiKey) {
50
+ this.providers.set('google', new GoogleProvider(options.config.google));
51
+ }
52
+
53
+ if (options.config.ollama) {
54
+ this.providers.set('ollama', new OllamaProvider(options.config.ollama));
55
+ }
56
+
57
+ if (options.config.openaiCompatible?.baseUrl) {
58
+ const compat = options.config.openaiCompatible;
59
+ this.providers.set(
60
+ compat.name || 'openai-compatible',
61
+ new OpenAICompatibleProvider({
62
+ baseUrl: compat.baseUrl,
63
+ apiKey: compat.apiKey,
64
+ model: compat.model || '',
65
+ maxTokens: compat.maxTokens,
66
+ name: compat.name,
67
+ }),
68
+ );
69
+ }
70
+
71
+ if (options.config.groq?.apiKey) {
72
+ this.providers.set('groq', new GroqProvider(options.config.groq));
73
+ }
74
+
75
+ if (options.config.replicate?.apiToken) {
76
+ this.providers.set('replicate', new ReplicateProvider(options.config.replicate));
77
+ }
78
+
79
+ if (options.config.deepseek?.apiKey) {
80
+ this.providers.set('deepseek', new DeepSeekProvider(options.config.deepseek));
81
+ }
82
+
83
+ if (options.config.cohere?.apiKey) {
84
+ this.providers.set('cohere', new CohereProvider(options.config.cohere));
85
+ }
86
+
87
+ if (options.config.xai?.apiKey) {
88
+ this.providers.set('xai', new XAIProvider(options.config.xai));
89
+ }
90
+
91
+ // Auto-fallback: if configured primary isn't available, use first registered provider
92
+ if (this.providers.size > 0 && !this.providers.has(this.primary)) {
93
+ const firstAvailable = this.providers.keys().next().value as string;
94
+ logger.warn(`Provider '${this.primary}' not configured, falling back to '${firstAvailable}'`);
95
+ this.primary = firstAvailable;
96
+ }
97
+ }
98
+
99
+ getProvider(name?: ProviderName): Provider {
100
+ const providerName = name || this.primary;
101
+ const provider = this.providers.get(providerName);
102
+
103
+ if (!provider) {
104
+ throw new Error(`Provider not configured: ${providerName}`);
105
+ }
106
+
107
+ return provider;
108
+ }
109
+
110
+ getPrimaryProvider(): Provider {
111
+ return this.getProvider(this.primary);
112
+ }
113
+
114
+ getFallbackProvider(): Provider | null {
115
+ if (!this.fallback) return null;
116
+
117
+ try {
118
+ return this.getProvider(this.fallback);
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ async withFallback<T>(
125
+ fn: (provider: Provider) => Promise<T>
126
+ ): Promise<T> {
127
+ try {
128
+ return await fn(this.getPrimaryProvider());
129
+ } catch (error) {
130
+ const fallback = this.getFallbackProvider();
131
+ if (fallback) {
132
+ logger.warn('Primary provider failed, using fallback', { error: error instanceof Error ? error : new Error(String(error)) });
133
+ return await fn(fallback);
134
+ }
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ listAvailable(): ProviderName[] {
140
+ return Array.from(this.providers.keys());
141
+ }
142
+ }