mcp-rubber-duck 1.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 (184) hide show
  1. package/.dockerignore +19 -0
  2. package/.env.desktop.example +145 -0
  3. package/.env.example +45 -0
  4. package/.env.pi.example +106 -0
  5. package/.env.template +165 -0
  6. package/.eslintrc.json +40 -0
  7. package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
  9. package/.github/ISSUE_TEMPLATE/question.md +67 -0
  10. package/.github/pull_request_template.md +111 -0
  11. package/.github/workflows/docker-build.yml +138 -0
  12. package/.github/workflows/release.yml +182 -0
  13. package/.github/workflows/security.yml +141 -0
  14. package/.github/workflows/semantic-release.yml +89 -0
  15. package/.prettierrc +10 -0
  16. package/.releaserc.json +66 -0
  17. package/CHANGELOG.md +95 -0
  18. package/CONTRIBUTING.md +242 -0
  19. package/Dockerfile +62 -0
  20. package/LICENSE +21 -0
  21. package/README.md +803 -0
  22. package/audit-ci.json +8 -0
  23. package/config/claude_desktop.json +14 -0
  24. package/config/config.example.json +91 -0
  25. package/dist/config/config.d.ts +51 -0
  26. package/dist/config/config.d.ts.map +1 -0
  27. package/dist/config/config.js +301 -0
  28. package/dist/config/config.js.map +1 -0
  29. package/dist/config/types.d.ts +356 -0
  30. package/dist/config/types.d.ts.map +1 -0
  31. package/dist/config/types.js +41 -0
  32. package/dist/config/types.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +109 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/providers/duck-provider-enhanced.d.ts +29 -0
  38. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
  39. package/dist/providers/duck-provider-enhanced.js +230 -0
  40. package/dist/providers/duck-provider-enhanced.js.map +1 -0
  41. package/dist/providers/enhanced-manager.d.ts +54 -0
  42. package/dist/providers/enhanced-manager.d.ts.map +1 -0
  43. package/dist/providers/enhanced-manager.js +217 -0
  44. package/dist/providers/enhanced-manager.js.map +1 -0
  45. package/dist/providers/manager.d.ts +28 -0
  46. package/dist/providers/manager.d.ts.map +1 -0
  47. package/dist/providers/manager.js +204 -0
  48. package/dist/providers/manager.js.map +1 -0
  49. package/dist/providers/provider.d.ts +29 -0
  50. package/dist/providers/provider.d.ts.map +1 -0
  51. package/dist/providers/provider.js +179 -0
  52. package/dist/providers/provider.js.map +1 -0
  53. package/dist/providers/types.d.ts +69 -0
  54. package/dist/providers/types.d.ts.map +1 -0
  55. package/dist/providers/types.js +2 -0
  56. package/dist/providers/types.js.map +1 -0
  57. package/dist/server.d.ts +24 -0
  58. package/dist/server.d.ts.map +1 -0
  59. package/dist/server.js +501 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/services/approval.d.ts +44 -0
  62. package/dist/services/approval.d.ts.map +1 -0
  63. package/dist/services/approval.js +159 -0
  64. package/dist/services/approval.js.map +1 -0
  65. package/dist/services/cache.d.ts +21 -0
  66. package/dist/services/cache.d.ts.map +1 -0
  67. package/dist/services/cache.js +63 -0
  68. package/dist/services/cache.js.map +1 -0
  69. package/dist/services/conversation.d.ts +24 -0
  70. package/dist/services/conversation.d.ts.map +1 -0
  71. package/dist/services/conversation.js +108 -0
  72. package/dist/services/conversation.js.map +1 -0
  73. package/dist/services/function-bridge.d.ts +41 -0
  74. package/dist/services/function-bridge.d.ts.map +1 -0
  75. package/dist/services/function-bridge.js +259 -0
  76. package/dist/services/function-bridge.js.map +1 -0
  77. package/dist/services/health.d.ts +17 -0
  78. package/dist/services/health.d.ts.map +1 -0
  79. package/dist/services/health.js +77 -0
  80. package/dist/services/health.js.map +1 -0
  81. package/dist/services/mcp-client-manager.d.ts +49 -0
  82. package/dist/services/mcp-client-manager.d.ts.map +1 -0
  83. package/dist/services/mcp-client-manager.js +279 -0
  84. package/dist/services/mcp-client-manager.js.map +1 -0
  85. package/dist/tools/approve-mcp-request.d.ts +9 -0
  86. package/dist/tools/approve-mcp-request.d.ts.map +1 -0
  87. package/dist/tools/approve-mcp-request.js +111 -0
  88. package/dist/tools/approve-mcp-request.js.map +1 -0
  89. package/dist/tools/ask-duck.d.ts +9 -0
  90. package/dist/tools/ask-duck.d.ts.map +1 -0
  91. package/dist/tools/ask-duck.js +43 -0
  92. package/dist/tools/ask-duck.js.map +1 -0
  93. package/dist/tools/chat-duck.d.ts +9 -0
  94. package/dist/tools/chat-duck.d.ts.map +1 -0
  95. package/dist/tools/chat-duck.js +57 -0
  96. package/dist/tools/chat-duck.js.map +1 -0
  97. package/dist/tools/clear-conversations.d.ts +8 -0
  98. package/dist/tools/clear-conversations.d.ts.map +1 -0
  99. package/dist/tools/clear-conversations.js +17 -0
  100. package/dist/tools/clear-conversations.js.map +1 -0
  101. package/dist/tools/compare-ducks.d.ts +8 -0
  102. package/dist/tools/compare-ducks.d.ts.map +1 -0
  103. package/dist/tools/compare-ducks.js +49 -0
  104. package/dist/tools/compare-ducks.js.map +1 -0
  105. package/dist/tools/duck-council.d.ts +8 -0
  106. package/dist/tools/duck-council.d.ts.map +1 -0
  107. package/dist/tools/duck-council.js +69 -0
  108. package/dist/tools/duck-council.js.map +1 -0
  109. package/dist/tools/get-pending-approvals.d.ts +15 -0
  110. package/dist/tools/get-pending-approvals.d.ts.map +1 -0
  111. package/dist/tools/get-pending-approvals.js +74 -0
  112. package/dist/tools/get-pending-approvals.js.map +1 -0
  113. package/dist/tools/list-ducks.d.ts +9 -0
  114. package/dist/tools/list-ducks.d.ts.map +1 -0
  115. package/dist/tools/list-ducks.js +47 -0
  116. package/dist/tools/list-ducks.js.map +1 -0
  117. package/dist/tools/list-models.d.ts +8 -0
  118. package/dist/tools/list-models.d.ts.map +1 -0
  119. package/dist/tools/list-models.js +72 -0
  120. package/dist/tools/list-models.js.map +1 -0
  121. package/dist/tools/mcp-status.d.ts +17 -0
  122. package/dist/tools/mcp-status.d.ts.map +1 -0
  123. package/dist/tools/mcp-status.js +100 -0
  124. package/dist/tools/mcp-status.js.map +1 -0
  125. package/dist/utils/ascii-art.d.ts +19 -0
  126. package/dist/utils/ascii-art.d.ts.map +1 -0
  127. package/dist/utils/ascii-art.js +73 -0
  128. package/dist/utils/ascii-art.js.map +1 -0
  129. package/dist/utils/logger.d.ts +3 -0
  130. package/dist/utils/logger.d.ts.map +1 -0
  131. package/dist/utils/logger.js +86 -0
  132. package/dist/utils/logger.js.map +1 -0
  133. package/dist/utils/safe-logger.d.ts +23 -0
  134. package/dist/utils/safe-logger.d.ts.map +1 -0
  135. package/dist/utils/safe-logger.js +145 -0
  136. package/dist/utils/safe-logger.js.map +1 -0
  137. package/docker-compose.yml +161 -0
  138. package/jest.config.js +26 -0
  139. package/package.json +65 -0
  140. package/scripts/build-multiarch.sh +290 -0
  141. package/scripts/deploy-raspbian.sh +410 -0
  142. package/scripts/deploy.sh +322 -0
  143. package/scripts/gh-deploy.sh +343 -0
  144. package/scripts/setup-docker-raspbian.sh +530 -0
  145. package/server.json +8 -0
  146. package/src/config/config.ts +357 -0
  147. package/src/config/types.ts +89 -0
  148. package/src/index.ts +114 -0
  149. package/src/providers/duck-provider-enhanced.ts +294 -0
  150. package/src/providers/enhanced-manager.ts +290 -0
  151. package/src/providers/manager.ts +257 -0
  152. package/src/providers/provider.ts +207 -0
  153. package/src/providers/types.ts +78 -0
  154. package/src/server.ts +603 -0
  155. package/src/services/approval.ts +225 -0
  156. package/src/services/cache.ts +79 -0
  157. package/src/services/conversation.ts +146 -0
  158. package/src/services/function-bridge.ts +329 -0
  159. package/src/services/health.ts +107 -0
  160. package/src/services/mcp-client-manager.ts +362 -0
  161. package/src/tools/approve-mcp-request.ts +126 -0
  162. package/src/tools/ask-duck.ts +74 -0
  163. package/src/tools/chat-duck.ts +82 -0
  164. package/src/tools/clear-conversations.ts +24 -0
  165. package/src/tools/compare-ducks.ts +67 -0
  166. package/src/tools/duck-council.ts +88 -0
  167. package/src/tools/get-pending-approvals.ts +90 -0
  168. package/src/tools/list-ducks.ts +65 -0
  169. package/src/tools/list-models.ts +101 -0
  170. package/src/tools/mcp-status.ts +117 -0
  171. package/src/utils/ascii-art.ts +85 -0
  172. package/src/utils/logger.ts +116 -0
  173. package/src/utils/safe-logger.ts +165 -0
  174. package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
  175. package/systemd/mcp-rubber-duck.service +58 -0
  176. package/test-functionality.js +147 -0
  177. package/test-mcp-interface.js +221 -0
  178. package/tests/ascii-art.test.ts +36 -0
  179. package/tests/config.test.ts +239 -0
  180. package/tests/conversation.test.ts +308 -0
  181. package/tests/mcp-bridge.test.ts +291 -0
  182. package/tests/providers.test.ts +269 -0
  183. package/tests/tools/clear-conversations.test.ts +163 -0
  184. package/tsconfig.json +26 -0
@@ -0,0 +1,257 @@
1
+ import { DuckProvider } from './provider.js';
2
+ import { ConfigManager } from '../config/config.js';
3
+ import { ProviderHealth, DuckResponse } from '../config/types.js';
4
+ import { ChatOptions, ModelInfo } from './types.js';
5
+ import { logger } from '../utils/logger.js';
6
+ import { getRandomDuckMessage } from '../utils/ascii-art.js';
7
+
8
+ export class ProviderManager {
9
+ private providers: Map<string, DuckProvider> = new Map();
10
+ private healthStatus: Map<string, ProviderHealth> = new Map();
11
+ protected configManager: ConfigManager;
12
+ private defaultProvider?: string;
13
+
14
+ constructor(configManager: ConfigManager) {
15
+ this.configManager = configManager;
16
+ this.initializeProviders();
17
+ }
18
+
19
+ private initializeProviders() {
20
+ const config = this.configManager.getConfig();
21
+ const allProviders = config.providers;
22
+
23
+ for (const [name, providerConfig] of Object.entries(allProviders)) {
24
+ try {
25
+ const provider = new DuckProvider(name, providerConfig.nickname, {
26
+ apiKey: providerConfig.api_key,
27
+ baseURL: providerConfig.base_url,
28
+ model: providerConfig.default_model,
29
+ availableModels: providerConfig.models,
30
+ temperature: providerConfig.temperature,
31
+ timeout: providerConfig.timeout,
32
+ maxRetries: providerConfig.max_retries,
33
+ systemPrompt: providerConfig.system_prompt,
34
+ });
35
+
36
+ this.providers.set(name, provider);
37
+ logger.info(`Initialized provider: ${name} (${providerConfig.nickname})`);
38
+ } catch (error) {
39
+ logger.error(`Failed to initialize provider ${name}:`, error);
40
+ }
41
+ }
42
+
43
+ this.defaultProvider = config.default_provider;
44
+
45
+ if (this.providers.size === 0) {
46
+ throw new Error('No providers could be initialized');
47
+ }
48
+ }
49
+
50
+ async checkHealth(providerName?: string): Promise<ProviderHealth[]> {
51
+ const results: ProviderHealth[] = [];
52
+ const providersToCheck = providerName
53
+ ? [this.providers.get(providerName)].filter(Boolean)
54
+ : Array.from(this.providers.values());
55
+
56
+ for (const provider of providersToCheck) {
57
+ if (!provider) continue;
58
+
59
+ const startTime = Date.now();
60
+ try {
61
+ const healthy = await provider.healthCheck();
62
+ const health: ProviderHealth = {
63
+ provider: provider.name,
64
+ healthy,
65
+ latency: Date.now() - startTime,
66
+ lastCheck: new Date(),
67
+ };
68
+
69
+ this.healthStatus.set(provider.name, health);
70
+ results.push(health);
71
+ } catch (error: unknown) {
72
+ const health: ProviderHealth = {
73
+ provider: provider.name,
74
+ healthy: false,
75
+ lastCheck: new Date(),
76
+ error: error instanceof Error ? error.message : String(error),
77
+ };
78
+
79
+ this.healthStatus.set(provider.name, health);
80
+ results.push(health);
81
+ }
82
+ }
83
+
84
+ return results;
85
+ }
86
+
87
+ async askDuck(
88
+ providerName: string | undefined,
89
+ prompt: string,
90
+ options?: Partial<ChatOptions>
91
+ ): Promise<DuckResponse> {
92
+ const provider = this.getProvider(providerName);
93
+ const startTime = Date.now();
94
+
95
+ try {
96
+ const response = await provider.chat({
97
+ messages: [{ role: 'user', content: prompt, timestamp: new Date() }],
98
+ ...options,
99
+ });
100
+
101
+ return {
102
+ provider: provider.name,
103
+ nickname: provider.nickname,
104
+ model: response.model,
105
+ content: response.content,
106
+ usage: response.usage ? {
107
+ prompt_tokens: response.usage.promptTokens,
108
+ completion_tokens: response.usage.completionTokens,
109
+ total_tokens: response.usage.totalTokens,
110
+ promptTokens: response.usage.promptTokens,
111
+ completionTokens: response.usage.completionTokens,
112
+ totalTokens: response.usage.totalTokens,
113
+ } : undefined,
114
+ latency: Date.now() - startTime,
115
+ cached: false,
116
+ };
117
+ } catch (error: unknown) {
118
+ // Try failover if enabled
119
+ if (this.configManager.getConfig().enable_failover && providerName === undefined) {
120
+ const errorMessage = error instanceof Error ? error.message : String(error);
121
+ logger.warn(`Primary provider failed, attempting failover: ${errorMessage}`);
122
+ return this.askWithFailover(prompt, options, provider.name);
123
+ }
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ async compareDucks(
129
+ prompt: string,
130
+ providerNames?: string[],
131
+ options?: Partial<ChatOptions>
132
+ ): Promise<DuckResponse[]> {
133
+ const providersToUse = providerNames
134
+ ? providerNames.map(name => this.providers.get(name)).filter(Boolean)
135
+ : Array.from(this.providers.values());
136
+
137
+ if (providersToUse.length === 0) {
138
+ throw new Error('No valid providers specified');
139
+ }
140
+
141
+ const promises = providersToUse.map(provider =>
142
+ provider ? this.askDuck(provider.name, prompt, options).catch(error => ({
143
+ provider: provider.name,
144
+ nickname: provider.nickname,
145
+ model: '',
146
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`,
147
+ latency: 0,
148
+ cached: false,
149
+ })) : Promise.resolve({
150
+ provider: 'unknown',
151
+ nickname: 'Unknown',
152
+ model: '',
153
+ content: 'Error: Invalid provider',
154
+ latency: 0,
155
+ cached: false,
156
+ })
157
+ );
158
+
159
+ return Promise.all(promises);
160
+ }
161
+
162
+ async duckCouncil(
163
+ prompt: string,
164
+ options?: Partial<ChatOptions>
165
+ ): Promise<DuckResponse[]> {
166
+ return this.compareDucks(prompt, undefined, options);
167
+ }
168
+
169
+ private async askWithFailover(
170
+ prompt: string,
171
+ options: Partial<ChatOptions> | undefined,
172
+ failedProvider: string
173
+ ): Promise<DuckResponse> {
174
+ const availableProviders = Array.from(this.providers.keys()).filter(
175
+ name => name !== failedProvider
176
+ );
177
+
178
+ for (const providerName of availableProviders) {
179
+ try {
180
+ logger.info(`${getRandomDuckMessage('failover')} Trying ${providerName}...`);
181
+ return await this.askDuck(providerName, prompt, options);
182
+ } catch (error) {
183
+ logger.warn(`Failover to ${providerName} failed:`, error);
184
+ continue;
185
+ }
186
+ }
187
+
188
+ throw new Error('All ducks have flown away! No providers available.');
189
+ }
190
+
191
+ getProvider(name?: string): DuckProvider {
192
+ const providerName = name || this.defaultProvider;
193
+
194
+ if (!providerName) {
195
+ throw new Error('No provider specified and no default provider configured');
196
+ }
197
+
198
+ const provider = this.providers.get(providerName);
199
+
200
+ if (!provider) {
201
+ throw new Error(`Duck "${providerName}" not found in the pond`);
202
+ }
203
+
204
+ return provider;
205
+ }
206
+
207
+ getAllProviders(): Array<{ name: string; info: ReturnType<DuckProvider['getInfo']>; health?: ProviderHealth }> {
208
+ return Array.from(this.providers.entries()).map(([name, provider]) => ({
209
+ name,
210
+ info: provider.getInfo(),
211
+ health: this.healthStatus.get(name),
212
+ }));
213
+ }
214
+
215
+ getProviderNames(): string[] {
216
+ return Array.from(this.providers.keys());
217
+ }
218
+
219
+ async getAvailableModels(providerName: string): Promise<ModelInfo[]> {
220
+ const provider = this.providers.get(providerName);
221
+ if (!provider) {
222
+ throw new Error(`Provider ${providerName} not found`);
223
+ }
224
+ return provider.listModels();
225
+ }
226
+
227
+ async getAllModels(): Promise<Map<string, ModelInfo[]>> {
228
+ const allModels = new Map<string, ModelInfo[]>();
229
+
230
+ for (const [name, provider] of this.providers) {
231
+ try {
232
+ const models = await provider.listModels();
233
+ allModels.set(name, models);
234
+ } catch (error) {
235
+ logger.error(`Failed to get models for ${name}:`, error);
236
+ allModels.set(name, []);
237
+ }
238
+ }
239
+
240
+ return allModels;
241
+ }
242
+
243
+ validateModel(providerName: string, modelId: string): boolean {
244
+ const provider = this.providers.get(providerName);
245
+ if (!provider) {
246
+ return false;
247
+ }
248
+
249
+ const info = provider.getInfo();
250
+ if (info.availableModels) {
251
+ return info.availableModels.includes(modelId);
252
+ }
253
+
254
+ // If no models list, accept any model (let the API validate)
255
+ return true;
256
+ }
257
+ }
@@ -0,0 +1,207 @@
1
+ import OpenAI from 'openai';
2
+ import { ChatOptions, ChatResponse, ProviderOptions, StreamChunk, ModelInfo, OpenAIChatParams, OpenAIChatResponse, OpenAIMessage } from './types.js';
3
+ import { ConversationMessage } from '../config/types.js';
4
+ import { logger } from '../utils/logger.js';
5
+
6
+ export class DuckProvider {
7
+ protected client: OpenAI;
8
+ protected options: ProviderOptions;
9
+ public name: string;
10
+ public nickname: string;
11
+
12
+ constructor(name: string, nickname: string, options: ProviderOptions) {
13
+ this.name = name;
14
+ this.nickname = nickname;
15
+ this.options = options;
16
+
17
+ this.client = new OpenAI({
18
+ apiKey: options.apiKey || 'not-needed',
19
+ baseURL: options.baseURL,
20
+ timeout: options.timeout || 300000,
21
+ maxRetries: options.maxRetries || 3,
22
+ });
23
+ }
24
+
25
+ protected supportsTemperature(model: string): boolean {
26
+ // Reasoning models don't support temperature parameter
27
+ return !model.startsWith('o1') &&
28
+ !model.includes('o1-') &&
29
+ !model.startsWith('o3') &&
30
+ !model.includes('o3-') &&
31
+ !model.startsWith('gpt-5') &&
32
+ !model.includes('gpt-5');
33
+ }
34
+
35
+ async chat(options: ChatOptions): Promise<ChatResponse> {
36
+ try {
37
+ const messages = this.prepareMessages(options.messages, options.systemPrompt);
38
+ const modelToUse = options.model || this.options.model;
39
+
40
+ const baseParams: Partial<OpenAIChatParams> = {
41
+ model: modelToUse,
42
+ messages: messages as OpenAIMessage[],
43
+ stream: false,
44
+ };
45
+
46
+ // Only add temperature if the model supports it
47
+ if (this.supportsTemperature(modelToUse)) {
48
+ baseParams.temperature = options.temperature ?? this.options.temperature ?? 0.7;
49
+ }
50
+
51
+ const response = await this.createChatCompletion(baseParams);
52
+ const choice = response.choices[0];
53
+
54
+ return {
55
+ content: choice.message?.content || '',
56
+ usage: response.usage ? {
57
+ promptTokens: response.usage.prompt_tokens,
58
+ completionTokens: response.usage.completion_tokens,
59
+ totalTokens: response.usage.total_tokens,
60
+ } : undefined,
61
+ model: modelToUse, // Return the requested model, not the resolved one
62
+ finishReason: choice.finish_reason || undefined,
63
+ };
64
+ } catch (error: unknown) {
65
+ logger.error(`Provider ${this.name} chat error:`, error);
66
+ const errorMessage = error instanceof Error ? error.message : String(error);
67
+ throw new Error(`Duck ${this.nickname} couldn't respond: ${errorMessage}`);
68
+ }
69
+ }
70
+
71
+ protected async createChatCompletion(baseParams: Partial<OpenAIChatParams>): Promise<OpenAIChatResponse> {
72
+ const params = { ...baseParams } as OpenAIChatParams;
73
+ return await this.client.chat.completions.create(params);
74
+ }
75
+
76
+ async *chatStream(options: ChatOptions): AsyncGenerator<StreamChunk> {
77
+ try {
78
+ const messages = this.prepareMessages(options.messages, options.systemPrompt);
79
+ const modelToUse = options.model || this.options.model;
80
+
81
+ const streamParams = {
82
+ model: modelToUse,
83
+ messages: messages as OpenAIMessage[],
84
+ stream: true as const,
85
+ ...(this.supportsTemperature(modelToUse) && {
86
+ temperature: options.temperature ?? this.options.temperature ?? 0.7
87
+ })
88
+ };
89
+
90
+ const stream = await this.client.chat.completions.create(streamParams);
91
+
92
+ for await (const chunk of stream) {
93
+ const content = chunk.choices[0]?.delta?.content || '';
94
+ const done = chunk.choices[0]?.finish_reason !== null;
95
+
96
+ yield { content, done };
97
+ }
98
+ } catch (error: unknown) {
99
+ logger.error(`Provider ${this.name} stream error:`, error);
100
+ const errorMessage = error instanceof Error ? error.message : String(error);
101
+ throw new Error(`Duck ${this.nickname} stream failed: ${errorMessage}`);
102
+ }
103
+ }
104
+
105
+ async healthCheck(): Promise<boolean> {
106
+ try {
107
+ const baseParams: Partial<OpenAIChatParams> = {
108
+ model: this.options.model,
109
+ messages: [{ role: 'user', content: 'Say "healthy"' }] as OpenAIMessage[],
110
+ stream: false,
111
+ };
112
+
113
+ // Only add temperature if the model supports it
114
+ if (this.supportsTemperature(this.options.model)) {
115
+ baseParams.temperature = 0.5;
116
+ }
117
+
118
+ // Health check without token limits
119
+ const response = await this.createChatCompletion(baseParams);
120
+
121
+ const content = response.choices[0]?.message?.content;
122
+ const hasContent = !!content;
123
+
124
+ if (!hasContent) {
125
+ logger.warn(`Health check for ${this.name}: No content in response`, {
126
+ response: JSON.stringify(response, null, 2)
127
+ });
128
+ } else {
129
+ logger.debug(`Health check for ${this.name} succeeded with response: ${content}`);
130
+ }
131
+
132
+ return hasContent;
133
+ } catch (error) {
134
+ logger.warn(`Health check failed for ${this.name}:`, error);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ protected prepareMessages(
140
+ messages: ConversationMessage[],
141
+ systemPrompt?: string
142
+ ): Array<{ role: string; content: string }> {
143
+ const prepared: Array<{ role: string; content: string }> = [];
144
+
145
+ // Add system prompt if provided
146
+ const prompt = systemPrompt || this.options.systemPrompt;
147
+ if (prompt) {
148
+ prepared.push({ role: 'system', content: prompt });
149
+ }
150
+
151
+ // Add conversation messages
152
+ for (const msg of messages) {
153
+ prepared.push({
154
+ role: msg.role,
155
+ content: msg.content,
156
+ });
157
+ }
158
+
159
+ return prepared;
160
+ }
161
+
162
+ async listModels(): Promise<ModelInfo[]> {
163
+ try {
164
+ // Try to fetch models from the API
165
+ const response = await this.client.models.list();
166
+ const models: ModelInfo[] = [];
167
+
168
+ for await (const model of response) {
169
+ models.push({
170
+ id: model.id,
171
+ created: model.created,
172
+ owned_by: model.owned_by,
173
+ object: model.object,
174
+ });
175
+ }
176
+
177
+ logger.debug(`Fetched ${models.length} models from ${this.name}`);
178
+ return models;
179
+ } catch (error: unknown) {
180
+ const errorMessage = error instanceof Error ? error.message : String(error);
181
+ logger.warn(`Failed to fetch models from ${this.name}: ${errorMessage}`);
182
+ // Fall back to configured models
183
+ if (this.options.availableModels && this.options.availableModels.length > 0) {
184
+ return this.options.availableModels.map(id => ({
185
+ id,
186
+ description: 'Configured model (not fetched from API)',
187
+ }));
188
+ }
189
+ // Last fallback: return just the default model
190
+ return [{
191
+ id: this.options.model,
192
+ description: 'Default configured model',
193
+ }];
194
+ }
195
+ }
196
+
197
+ getInfo() {
198
+ return {
199
+ name: this.name,
200
+ nickname: this.nickname,
201
+ model: this.options.model,
202
+ availableModels: this.options.availableModels,
203
+ baseURL: this.options.baseURL,
204
+ hasApiKey: !!this.options.apiKey,
205
+ };
206
+ }
207
+ }
@@ -0,0 +1,78 @@
1
+ import { ConversationMessage } from '../config/types.js';
2
+ import { FunctionDefinition } from '../services/function-bridge.js';
3
+ import type {
4
+ ChatCompletion,
5
+ ChatCompletionMessageParam,
6
+ ChatCompletionMessageToolCall,
7
+ ChatCompletionCreateParamsNonStreaming
8
+ } from 'openai/resources/chat/completions';
9
+
10
+ export interface ModelInfo {
11
+ id: string;
12
+ created?: number;
13
+ owned_by?: string;
14
+ object?: string;
15
+ context_window?: number;
16
+ description?: string;
17
+ }
18
+
19
+ export interface ProviderOptions {
20
+ apiKey?: string;
21
+ baseURL: string;
22
+ model: string;
23
+ availableModels?: string[];
24
+ temperature?: number;
25
+ timeout?: number;
26
+ maxRetries?: number;
27
+ systemPrompt?: string;
28
+ }
29
+
30
+ export interface ToolCall {
31
+ id: string;
32
+ type: 'function';
33
+ function: {
34
+ name: string;
35
+ arguments: string; // JSON string
36
+ };
37
+ }
38
+
39
+ export interface ChatOptions {
40
+ messages: ConversationMessage[];
41
+ model?: string;
42
+ stream?: boolean;
43
+ temperature?: number;
44
+ systemPrompt?: string;
45
+ tools?: FunctionDefinition[];
46
+ toolChoice?: 'auto' | 'none' | { type: 'function'; function: { name: string } };
47
+ }
48
+
49
+ export interface ChatResponse {
50
+ content: string;
51
+ usage?: {
52
+ promptTokens: number;
53
+ completionTokens: number;
54
+ totalTokens: number;
55
+ };
56
+ model: string;
57
+ finishReason?: string;
58
+ toolCalls?: ToolCall[];
59
+ }
60
+
61
+ export interface StreamChunk {
62
+ content: string;
63
+ done: boolean;
64
+ }
65
+
66
+ // OpenAI API parameter types
67
+ export type OpenAIChatParams = ChatCompletionCreateParamsNonStreaming;
68
+ export type OpenAIMessage = ChatCompletionMessageParam;
69
+ export type OpenAIChatResponse = ChatCompletion;
70
+ export type OpenAIToolCall = ChatCompletionMessageToolCall;
71
+
72
+ // Enhanced response type with MCP functionality
73
+ export interface MCPResult {
74
+ id: string;
75
+ success: boolean;
76
+ data?: unknown;
77
+ error?: string;
78
+ }