cowork-os 0.3.21 → 0.3.25

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 (252) hide show
  1. package/README.md +372 -10
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
  48. package/dist/electron/electron/agent/daemon.js +189 -13
  49. package/dist/electron/electron/agent/executor.js +895 -78
  50. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  51. package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
  52. package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
  53. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  54. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  55. package/dist/electron/electron/agent/llm/index.js +13 -1
  56. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  57. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  58. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  59. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  60. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  61. package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
  62. package/dist/electron/electron/agent/llm/types.js +66 -1
  63. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  64. package/dist/electron/electron/agent/search/provider-factory.js +38 -2
  65. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  66. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  67. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  68. package/dist/electron/electron/agent/tools/file-tools.js +66 -3
  69. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  70. package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
  71. package/dist/electron/electron/agent/tools/image-tools.js +11 -1
  72. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  73. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  74. package/dist/electron/electron/agent/tools/registry.js +548 -10
  75. package/dist/electron/electron/agent/tools/search-tools.js +28 -10
  76. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  77. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  78. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  79. package/dist/electron/electron/agents/agent-dispatch.js +63 -0
  80. package/dist/electron/electron/database/repositories.js +19 -5
  81. package/dist/electron/electron/database/schema.js +8 -0
  82. package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
  83. package/dist/electron/electron/gateway/index.js +75 -1
  84. package/dist/electron/electron/gateway/router.js +209 -154
  85. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  86. package/dist/electron/electron/ipc/handlers.js +763 -267
  87. package/dist/electron/electron/main.js +63 -0
  88. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  89. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  90. package/dist/electron/electron/memory/MemoryService.js +2 -1
  91. package/dist/electron/electron/preload.js +78 -1
  92. package/dist/electron/electron/settings/appearance-manager.js +18 -1
  93. package/dist/electron/electron/settings/box-manager.js +54 -0
  94. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  95. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  96. package/dist/electron/electron/settings/notion-manager.js +56 -0
  97. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  98. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  99. package/dist/electron/electron/utils/box-api.js +153 -0
  100. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  101. package/dist/electron/electron/utils/env-migration.js +19 -0
  102. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  103. package/dist/electron/electron/utils/notion-api.js +103 -0
  104. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  105. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  106. package/dist/electron/electron/utils/validation.js +98 -3
  107. package/dist/electron/electron/utils/x-cli.js +1 -1
  108. package/dist/electron/shared/channelMessages.js +284 -3
  109. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  110. package/dist/electron/shared/types.js +90 -1
  111. package/package.json +14 -3
  112. package/resources/skills/nano-banana-pro.json +4 -4
  113. package/resources/skills/openai-image-gen.json +3 -3
  114. package/resources/skills/scripts/gen.py +163 -0
  115. package/resources/skills/scripts/generate_image.py +91 -0
  116. package/src/electron/agent/custom-skill-loader.ts +34 -1
  117. package/src/electron/agent/daemon.ts +210 -14
  118. package/src/electron/agent/executor.ts +1124 -85
  119. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  120. package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
  121. package/src/electron/agent/llm/bedrock-provider.ts +62 -9
  122. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  123. package/src/electron/agent/llm/groq-provider.ts +39 -0
  124. package/src/electron/agent/llm/index.ts +6 -0
  125. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  126. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  127. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  128. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  129. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  130. package/src/electron/agent/llm/provider-factory.ts +459 -6
  131. package/src/electron/agent/llm/types.ts +95 -1
  132. package/src/electron/agent/llm/xai-provider.ts +39 -0
  133. package/src/electron/agent/search/provider-factory.ts +43 -2
  134. package/src/electron/agent/tools/box-tools.ts +239 -0
  135. package/src/electron/agent/tools/builtin-settings.ts +36 -0
  136. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  137. package/src/electron/agent/tools/file-tools.ts +66 -3
  138. package/src/electron/agent/tools/gmail-tools.ts +240 -0
  139. package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
  140. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  141. package/src/electron/agent/tools/grep-tools.ts +97 -12
  142. package/src/electron/agent/tools/image-tools.ts +11 -1
  143. package/src/electron/agent/tools/notion-tools.ts +330 -0
  144. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  145. package/src/electron/agent/tools/registry.ts +794 -10
  146. package/src/electron/agent/tools/search-tools.ts +29 -11
  147. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  148. package/src/electron/agent/tools/shell-tools.ts +11 -3
  149. package/src/electron/agent/tools/x-tools.ts +1 -1
  150. package/src/electron/agents/agent-dispatch.ts +79 -0
  151. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  152. package/src/electron/database/repositories.ts +58 -6
  153. package/src/electron/database/schema.ts +8 -0
  154. package/src/electron/gateway/channels/discord.ts +4 -0
  155. package/src/electron/gateway/channels/google-chat.ts +3 -0
  156. package/src/electron/gateway/channels/line.ts +3 -0
  157. package/src/electron/gateway/channels/matrix-client.ts +15 -0
  158. package/src/electron/gateway/channels/matrix.ts +31 -0
  159. package/src/electron/gateway/channels/mattermost.ts +3 -0
  160. package/src/electron/gateway/channels/signal.ts +3 -0
  161. package/src/electron/gateway/channels/slack.ts +9 -4
  162. package/src/electron/gateway/channels/teams.ts +4 -0
  163. package/src/electron/gateway/channels/telegram.ts +2 -0
  164. package/src/electron/gateway/channels/twitch.ts +2 -0
  165. package/src/electron/gateway/channels/types.ts +8 -0
  166. package/src/electron/gateway/channels/whatsapp.ts +66 -0
  167. package/src/electron/gateway/index.ts +95 -2
  168. package/src/electron/gateway/router.ts +231 -161
  169. package/src/electron/gateway/security.ts +21 -9
  170. package/src/electron/ipc/canvas-handlers.ts +10 -0
  171. package/src/electron/ipc/handlers.ts +848 -292
  172. package/src/electron/main.ts +35 -0
  173. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  174. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  175. package/src/electron/memory/MemoryService.ts +7 -1
  176. package/src/electron/preload.ts +200 -5
  177. package/src/electron/settings/appearance-manager.ts +20 -2
  178. package/src/electron/settings/box-manager.ts +58 -0
  179. package/src/electron/settings/dropbox-manager.ts +58 -0
  180. package/src/electron/settings/google-workspace-manager.ts +59 -0
  181. package/src/electron/settings/notion-manager.ts +60 -0
  182. package/src/electron/settings/onedrive-manager.ts +58 -0
  183. package/src/electron/settings/sharepoint-manager.ts +58 -0
  184. package/src/electron/utils/box-api.ts +184 -0
  185. package/src/electron/utils/dropbox-api.ts +171 -0
  186. package/src/electron/utils/env-migration.ts +22 -0
  187. package/src/electron/utils/gmail-api.ts +121 -0
  188. package/src/electron/utils/google-calendar-api.ts +115 -0
  189. package/src/electron/utils/google-workspace-api.ts +228 -0
  190. package/src/electron/utils/google-workspace-auth.ts +109 -0
  191. package/src/electron/utils/google-workspace-oauth.ts +232 -0
  192. package/src/electron/utils/notion-api.ts +126 -0
  193. package/src/electron/utils/onedrive-api.ts +137 -0
  194. package/src/electron/utils/sharepoint-api.ts +132 -0
  195. package/src/electron/utils/validation.ts +128 -1
  196. package/src/electron/utils/x-cli.ts +1 -1
  197. package/src/renderer/App.tsx +119 -8
  198. package/src/renderer/components/ActivityFeedItem.tsx +34 -17
  199. package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
  200. package/src/renderer/components/AppearanceSettings.tsx +37 -2
  201. package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
  202. package/src/renderer/components/BoxSettings.tsx +203 -0
  203. package/src/renderer/components/BrowserView.tsx +101 -0
  204. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  205. package/src/renderer/components/CanvasPreview.tsx +68 -1
  206. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  207. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  208. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  209. package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
  210. package/src/renderer/components/DiscordSettings.tsx +18 -7
  211. package/src/renderer/components/DropboxSettings.tsx +202 -0
  212. package/src/renderer/components/EmailSettings.tsx +18 -7
  213. package/src/renderer/components/FileViewer.tsx +21 -13
  214. package/src/renderer/components/GoogleChatSettings.tsx +17 -7
  215. package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
  216. package/src/renderer/components/ImessageSettings.tsx +22 -11
  217. package/src/renderer/components/LineIcons.tsx +376 -0
  218. package/src/renderer/components/LineSettings.tsx +18 -7
  219. package/src/renderer/components/MCPSettings.tsx +56 -0
  220. package/src/renderer/components/MainContent.tsx +740 -76
  221. package/src/renderer/components/MatrixSettings.tsx +18 -7
  222. package/src/renderer/components/MattermostSettings.tsx +18 -7
  223. package/src/renderer/components/NodesSettings.tsx +58 -99
  224. package/src/renderer/components/NotificationPanel.tsx +25 -11
  225. package/src/renderer/components/NotionSettings.tsx +231 -0
  226. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  227. package/src/renderer/components/OnboardingModal.tsx +70 -1
  228. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  229. package/src/renderer/components/RightPanel.tsx +141 -28
  230. package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
  231. package/src/renderer/components/SearchSettings.tsx +118 -114
  232. package/src/renderer/components/Settings.tsx +1425 -651
  233. package/src/renderer/components/SharePointSettings.tsx +224 -0
  234. package/src/renderer/components/Sidebar.tsx +94 -19
  235. package/src/renderer/components/SignalSettings.tsx +18 -7
  236. package/src/renderer/components/SkillHubBrowser.tsx +144 -185
  237. package/src/renderer/components/SlackSettings.tsx +18 -7
  238. package/src/renderer/components/TaskQuickActions.tsx +11 -6
  239. package/src/renderer/components/TaskTimeline.tsx +58 -26
  240. package/src/renderer/components/TeamsSettings.tsx +18 -7
  241. package/src/renderer/components/TelegramSettings.tsx +18 -7
  242. package/src/renderer/components/ThemeIcon.tsx +16 -0
  243. package/src/renderer/components/TwitchSettings.tsx +18 -7
  244. package/src/renderer/components/VoiceSettings.tsx +30 -74
  245. package/src/renderer/components/WhatsAppSettings.tsx +48 -37
  246. package/src/renderer/components/WorkingStateHistory.tsx +7 -5
  247. package/src/renderer/components/WorkspaceSelector.tsx +42 -13
  248. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  249. package/src/renderer/styles/index.css +2333 -209
  250. package/src/shared/channelMessages.ts +367 -4
  251. package/src/shared/llm-provider-catalog.ts +217 -0
  252. package/src/shared/types.ts +251 -2
@@ -0,0 +1,214 @@
1
+ import {
2
+ LLMProvider,
3
+ LLMProviderType,
4
+ LLMRequest,
5
+ LLMResponse,
6
+ LLMContent,
7
+ LLMMessage,
8
+ LLMTool,
9
+ } from './types';
10
+
11
+ const ANTHROPIC_VERSION = '2023-06-01';
12
+
13
+ function joinUrl(baseUrl: string, path: string): string {
14
+ const trimmedBase = baseUrl.replace(/\/+$/, '');
15
+ const trimmedPath = path.startsWith('/') ? path : `/${path}`;
16
+ return `${trimmedBase}${trimmedPath}`;
17
+ }
18
+
19
+ export interface AnthropicCompatibleProviderOptions {
20
+ type: LLMProviderType;
21
+ providerName: string;
22
+ apiKey: string;
23
+ baseUrl: string;
24
+ defaultModel: string;
25
+ }
26
+
27
+ export class AnthropicCompatibleProvider implements LLMProvider {
28
+ readonly type: LLMProviderType;
29
+ private apiKey: string;
30
+ private baseUrl: string;
31
+ private defaultModel: string;
32
+ private providerName: string;
33
+
34
+ constructor(options: AnthropicCompatibleProviderOptions) {
35
+ this.type = options.type;
36
+ this.apiKey = options.apiKey;
37
+ this.baseUrl = options.baseUrl;
38
+ this.defaultModel = options.defaultModel;
39
+ this.providerName = options.providerName;
40
+ }
41
+
42
+ async createMessage(request: LLMRequest): Promise<LLMResponse> {
43
+ const messages = this.convertMessages(request.messages);
44
+ const tools = request.tools ? this.convertTools(request.tools) : undefined;
45
+ const model = request.model || this.defaultModel;
46
+
47
+ try {
48
+ console.log(`[${this.providerName}] Calling API with model: ${model}`);
49
+ const response = await fetch(joinUrl(this.baseUrl, '/messages'), {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'x-api-key': this.apiKey,
54
+ 'anthropic-version': ANTHROPIC_VERSION,
55
+ },
56
+ body: JSON.stringify({
57
+ model,
58
+ max_tokens: request.maxTokens,
59
+ system: request.system,
60
+ messages,
61
+ ...(tools && { tools }),
62
+ }),
63
+ signal: request.signal,
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
68
+ throw new Error(
69
+ `${this.providerName} API error: ${response.status} ${response.statusText}` +
70
+ (errorData.error?.message ? ` - ${errorData.error.message}` : '')
71
+ );
72
+ }
73
+
74
+ const data = await response.json() as any;
75
+ return this.convertResponse(data);
76
+ } catch (error: any) {
77
+ if (error.name === 'AbortError' || error.message?.includes('aborted')) {
78
+ console.log(`[${this.providerName}] Request aborted`);
79
+ throw new Error('Request cancelled');
80
+ }
81
+
82
+ console.error(`[${this.providerName}] API error:`, {
83
+ message: error.message,
84
+ status: error.status,
85
+ });
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ async testConnection(): Promise<{ success: boolean; error?: string }> {
91
+ try {
92
+ const response = await fetch(joinUrl(this.baseUrl, '/messages'), {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'x-api-key': this.apiKey,
97
+ 'anthropic-version': ANTHROPIC_VERSION,
98
+ },
99
+ body: JSON.stringify({
100
+ model: this.defaultModel,
101
+ max_tokens: 10,
102
+ messages: [{ role: 'user', content: 'Hi' }],
103
+ }),
104
+ });
105
+
106
+ if (!response.ok) {
107
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
108
+ return {
109
+ success: false,
110
+ error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
111
+ };
112
+ }
113
+
114
+ return { success: true };
115
+ } catch (error: any) {
116
+ return {
117
+ success: false,
118
+ error: error.message || `Failed to connect to ${this.providerName} API`,
119
+ };
120
+ }
121
+ }
122
+
123
+ private convertMessages(messages: LLMMessage[]): Array<{ role: string; content: any }> {
124
+ return messages.map((msg) => {
125
+ if (typeof msg.content === 'string') {
126
+ return {
127
+ role: msg.role,
128
+ content: msg.content,
129
+ };
130
+ }
131
+
132
+ const content = msg.content.map((item) => {
133
+ if (item.type === 'tool_result') {
134
+ return {
135
+ type: 'tool_result' as const,
136
+ tool_use_id: item.tool_use_id,
137
+ content: item.content,
138
+ ...(item.is_error && { is_error: true }),
139
+ };
140
+ }
141
+ if (item.type === 'tool_use') {
142
+ return {
143
+ type: 'tool_use' as const,
144
+ id: item.id,
145
+ name: item.name,
146
+ input: item.input,
147
+ };
148
+ }
149
+ return {
150
+ type: 'text' as const,
151
+ text: item.text,
152
+ };
153
+ });
154
+
155
+ return {
156
+ role: msg.role,
157
+ content,
158
+ };
159
+ });
160
+ }
161
+
162
+ private convertTools(tools: LLMTool[]): Array<{ name: string; description: string; input_schema: any }> {
163
+ return tools.map((tool) => ({
164
+ name: tool.name,
165
+ description: tool.description,
166
+ input_schema: tool.input_schema,
167
+ }));
168
+ }
169
+
170
+ private convertResponse(response: any): LLMResponse {
171
+ const content: LLMContent[] = (response.content || [])
172
+ .filter((block: any) => block.type === 'text' || block.type === 'tool_use')
173
+ .map((block: any) => {
174
+ if (block.type === 'tool_use') {
175
+ return {
176
+ type: 'tool_use' as const,
177
+ id: block.id,
178
+ name: block.name,
179
+ input: block.input as Record<string, any>,
180
+ };
181
+ }
182
+ return {
183
+ type: 'text' as const,
184
+ text: block.text || '',
185
+ };
186
+ });
187
+
188
+ return {
189
+ content: content.length > 0 ? content : [{ type: 'text', text: '' }],
190
+ stopReason: this.mapStopReason(response.stop_reason),
191
+ usage: response.usage
192
+ ? {
193
+ inputTokens: response.usage.input_tokens || 0,
194
+ outputTokens: response.usage.output_tokens || 0,
195
+ }
196
+ : undefined,
197
+ };
198
+ }
199
+
200
+ private mapStopReason(reason?: string): LLMResponse['stopReason'] {
201
+ switch (reason) {
202
+ case 'end_turn':
203
+ return 'end_turn';
204
+ case 'tool_use':
205
+ return 'tool_use';
206
+ case 'max_tokens':
207
+ return 'max_tokens';
208
+ case 'stop_sequence':
209
+ return 'stop_sequence';
210
+ default:
211
+ return 'end_turn';
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,388 @@
1
+ import {
2
+ LLMProvider,
3
+ LLMProviderConfig,
4
+ LLMRequest,
5
+ LLMResponse,
6
+ LLMMessage,
7
+ LLMTool,
8
+ LLMContent,
9
+ LLMToolResult,
10
+ LLMToolUse,
11
+ LLMTextContent,
12
+ } from './types';
13
+ import {
14
+ toOpenAICompatibleMessages,
15
+ toOpenAICompatibleTools,
16
+ fromOpenAICompatibleResponse,
17
+ } from './openai-compatible';
18
+
19
+ const DEFAULT_AZURE_API_VERSION = '2024-02-15-preview';
20
+
21
+ const isToolResult = (item: LLMContent | LLMToolResult): item is LLMToolResult =>
22
+ item?.type === 'tool_result';
23
+ const isToolUse = (item: LLMContent | LLMToolResult): item is LLMToolUse =>
24
+ item?.type === 'tool_use';
25
+ const isTextContent = (item: LLMContent | LLMToolResult): item is LLMTextContent =>
26
+ item?.type === 'text';
27
+
28
+ export class AzureOpenAIProvider implements LLMProvider {
29
+ readonly type = 'azure' as const;
30
+ private apiKey: string;
31
+ private endpoint: string;
32
+ private deployment: string;
33
+ private apiVersion: string;
34
+
35
+ constructor(config: LLMProviderConfig) {
36
+ const apiKey = config.azureApiKey?.trim();
37
+ const endpoint = config.azureEndpoint?.trim();
38
+ const deployment = config.azureDeployment?.trim();
39
+
40
+ if (!apiKey) {
41
+ throw new Error('Azure OpenAI API key is required. Configure it in Settings.');
42
+ }
43
+ if (!endpoint) {
44
+ throw new Error('Azure OpenAI endpoint is required. Configure it in Settings.');
45
+ }
46
+ if (!deployment) {
47
+ throw new Error('Azure OpenAI deployment name is required. Configure it in Settings.');
48
+ }
49
+
50
+ this.apiKey = apiKey;
51
+ this.endpoint = endpoint.replace(/\/+$/, '');
52
+ this.deployment = deployment;
53
+ this.apiVersion = config.azureApiVersion?.trim() || DEFAULT_AZURE_API_VERSION;
54
+ }
55
+
56
+ private getChatCompletionsUrl(): string {
57
+ const deployment = encodeURIComponent(this.deployment);
58
+ const apiVersion = encodeURIComponent(this.apiVersion);
59
+ return `${this.endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;
60
+ }
61
+
62
+ private getResponsesUrl(): string {
63
+ return `${this.endpoint}/openai/v1/responses`;
64
+ }
65
+
66
+ private isMaxTokensUnsupported(errorData: any): boolean {
67
+ const message = errorData?.error?.message || '';
68
+ return /max_tokens/i.test(message) && /max_completion_tokens/i.test(message);
69
+ }
70
+
71
+ private isChatCompletionUnsupported(errorData: any): boolean {
72
+ const message = errorData?.error?.message || '';
73
+ return /chatcompletion/i.test(message) && /(does not work|not supported|unsupported)/i.test(message);
74
+ }
75
+
76
+ private buildChatCompletionsBody(request: LLMRequest, useMaxCompletionTokens: boolean): Record<string, any> {
77
+ const messages = toOpenAICompatibleMessages(request.messages, request.system);
78
+ const tools = request.tools ? toOpenAICompatibleTools(request.tools) : undefined;
79
+ const tokenField = useMaxCompletionTokens ? 'max_completion_tokens' : 'max_tokens';
80
+
81
+ return {
82
+ model: request.model || this.deployment,
83
+ messages,
84
+ [tokenField]: request.maxTokens,
85
+ ...(tools && tools.length > 0 && { tools, tool_choice: 'auto' }),
86
+ };
87
+ }
88
+
89
+ private buildResponsesInput(messages: LLMMessage[]): any[] {
90
+ const input: any[] = [];
91
+
92
+ for (const msg of messages) {
93
+ if (typeof msg.content === 'string') {
94
+ input.push({
95
+ type: 'message',
96
+ role: msg.role,
97
+ content: [
98
+ {
99
+ type: msg.role === 'assistant' ? 'output_text' : 'input_text',
100
+ text: msg.content,
101
+ },
102
+ ],
103
+ });
104
+ continue;
105
+ }
106
+
107
+ if (!Array.isArray(msg.content)) {
108
+ continue;
109
+ }
110
+
111
+ for (const item of msg.content) {
112
+ if (isToolResult(item)) {
113
+ input.push({
114
+ type: 'function_call_output',
115
+ call_id: item.tool_use_id,
116
+ output: typeof item.content === 'string' ? item.content : JSON.stringify(item.content ?? ''),
117
+ });
118
+ }
119
+ }
120
+
121
+ const textBlocks = msg.content.filter(isTextContent);
122
+ if (textBlocks.length > 0) {
123
+ input.push({
124
+ type: 'message',
125
+ role: msg.role,
126
+ content: textBlocks.map((block) => ({
127
+ type: msg.role === 'assistant' ? 'output_text' : 'input_text',
128
+ text: block.text,
129
+ })),
130
+ });
131
+ }
132
+
133
+ if (msg.role === 'assistant') {
134
+ const toolUses = msg.content.filter(isToolUse);
135
+ for (const toolUse of toolUses) {
136
+ input.push({
137
+ type: 'function_call',
138
+ call_id: toolUse.id,
139
+ name: toolUse.name,
140
+ arguments: JSON.stringify(toolUse.input ?? {}),
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ return input;
147
+ }
148
+
149
+ private toResponsesTools(tools: LLMTool[]): Array<{ type: 'function'; name: string; description: string; parameters: any }> {
150
+ return tools.map((tool) => ({
151
+ type: 'function' as const,
152
+ name: tool.name,
153
+ description: tool.description,
154
+ parameters: this.sanitizeSchemaForResponses(tool.input_schema),
155
+ }));
156
+ }
157
+
158
+ private sanitizeSchemaForResponses(schema: any): any {
159
+ if (!schema || typeof schema !== 'object') {
160
+ return schema;
161
+ }
162
+
163
+ const result: any = Array.isArray(schema) ? [...schema] : { ...schema };
164
+
165
+ if (result.properties && typeof result.properties === 'object') {
166
+ const sanitizedProperties: Record<string, any> = {};
167
+ for (const [key, value] of Object.entries(result.properties)) {
168
+ sanitizedProperties[key] = this.sanitizeSchemaForResponses(value);
169
+ }
170
+ result.properties = sanitizedProperties;
171
+ }
172
+
173
+ if (result.items) {
174
+ result.items = this.sanitizeSchemaForResponses(result.items);
175
+ }
176
+
177
+ if (result.type === 'array' && !result.items) {
178
+ result.items = { type: 'string' };
179
+ }
180
+
181
+ return result;
182
+ }
183
+
184
+ private buildResponsesBody(request: LLMRequest): Record<string, any> {
185
+ const input = this.buildResponsesInput(request.messages);
186
+ const tools = request.tools ? this.toResponsesTools(request.tools) : undefined;
187
+ return {
188
+ model: request.model || this.deployment,
189
+ input,
190
+ ...(request.system ? { instructions: request.system } : {}),
191
+ max_output_tokens: request.maxTokens,
192
+ ...(tools && tools.length > 0 && { tools, tool_choice: 'auto' }),
193
+ };
194
+ }
195
+
196
+ private async sendRequest(url: string, body: Record<string, any>, signal?: AbortSignal): Promise<Response> {
197
+ return fetch(url, {
198
+ method: 'POST',
199
+ headers: {
200
+ 'Content-Type': 'application/json',
201
+ 'api-key': this.apiKey,
202
+ },
203
+ body: JSON.stringify(body),
204
+ signal,
205
+ });
206
+ }
207
+
208
+ private parseFunctionCallArguments(value: any): Record<string, any> {
209
+ if (!value) return {};
210
+ if (typeof value === 'object') return value;
211
+ if (typeof value !== 'string') return {};
212
+ try {
213
+ return JSON.parse(value);
214
+ } catch {
215
+ return {};
216
+ }
217
+ }
218
+
219
+ private fromResponsesApiResponse(response: any): LLMResponse {
220
+ const content: LLMContent[] = [];
221
+ let sawToolCall = false;
222
+
223
+ if (Array.isArray(response?.output)) {
224
+ response.output.forEach((item: any, index: number) => {
225
+ if (item.type === 'message') {
226
+ const blocks = Array.isArray(item.content) ? item.content : [];
227
+ for (const block of blocks) {
228
+ if (block.type === 'output_text' && typeof block.text === 'string') {
229
+ content.push({ type: 'text', text: block.text });
230
+ }
231
+ }
232
+ } else if (item.type === 'function_call') {
233
+ sawToolCall = true;
234
+ const id = item.call_id || item.id || `call_${index}`;
235
+ content.push({
236
+ type: 'tool_use',
237
+ id,
238
+ name: item.name,
239
+ input: this.parseFunctionCallArguments(item.arguments),
240
+ });
241
+ }
242
+ });
243
+ }
244
+
245
+ if (content.length === 0 && typeof response?.output_text === 'string') {
246
+ content.push({ type: 'text', text: response.output_text });
247
+ }
248
+
249
+ if (content.length === 0) {
250
+ content.push({ type: 'text', text: '' });
251
+ }
252
+
253
+ return {
254
+ content,
255
+ stopReason: sawToolCall ? 'tool_use' : 'end_turn',
256
+ usage: response?.usage
257
+ ? {
258
+ inputTokens: response.usage.input_tokens ?? 0,
259
+ outputTokens: response.usage.output_tokens ?? 0,
260
+ }
261
+ : undefined,
262
+ };
263
+ }
264
+
265
+ async createMessage(request: LLMRequest): Promise<LLMResponse> {
266
+ try {
267
+ const chatUrl = this.getChatCompletionsUrl();
268
+ const responsesUrl = this.getResponsesUrl();
269
+
270
+ const runResponses = async (): Promise<LLMResponse> => {
271
+ const response = await this.sendRequest(responsesUrl, this.buildResponsesBody(request), request.signal);
272
+ if (!response.ok) {
273
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
274
+ throw new Error(
275
+ `Azure OpenAI API error: ${response.status} ${response.statusText}` +
276
+ (errorData.error?.message ? ` - ${errorData.error.message}` : '')
277
+ );
278
+ }
279
+ const data = await response.json() as any;
280
+ return this.fromResponsesApiResponse(data);
281
+ };
282
+
283
+ let response = await this.sendRequest(chatUrl, this.buildChatCompletionsBody(request, false), request.signal);
284
+ if (!response.ok) {
285
+ let errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
286
+ if (this.isChatCompletionUnsupported(errorData)) {
287
+ return await runResponses();
288
+ }
289
+ if (this.isMaxTokensUnsupported(errorData)) {
290
+ response = await this.sendRequest(chatUrl, this.buildChatCompletionsBody(request, true), request.signal);
291
+ if (response.ok) {
292
+ const data = await response.json() as any;
293
+ return fromOpenAICompatibleResponse(data);
294
+ }
295
+ errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
296
+ if (this.isChatCompletionUnsupported(errorData)) {
297
+ return await runResponses();
298
+ }
299
+ }
300
+ throw new Error(
301
+ `Azure OpenAI API error: ${response.status} ${response.statusText}` +
302
+ (errorData.error?.message ? ` - ${errorData.error.message}` : '')
303
+ );
304
+ }
305
+
306
+ const data = await response.json() as any;
307
+ return fromOpenAICompatibleResponse(data);
308
+ } catch (error: any) {
309
+ if (error.name === 'AbortError' || error.message?.includes('aborted')) {
310
+ console.log('[Azure OpenAI] Request aborted');
311
+ throw new Error('Request cancelled');
312
+ }
313
+
314
+ console.error('[Azure OpenAI] API error:', {
315
+ message: error.message,
316
+ });
317
+ throw error;
318
+ }
319
+ }
320
+
321
+ async testConnection(): Promise<{ success: boolean; error?: string }> {
322
+ const testMaxTokens = 16;
323
+ try {
324
+ const chatUrl = this.getChatCompletionsUrl();
325
+ const responsesUrl = this.getResponsesUrl();
326
+
327
+ const runResponses = async (): Promise<{ success: boolean; error?: string }> => {
328
+ const response = await this.sendRequest(responsesUrl, {
329
+ model: this.deployment,
330
+ input: [
331
+ {
332
+ type: 'message',
333
+ role: 'user',
334
+ content: [{ type: 'input_text', text: 'Hi' }],
335
+ },
336
+ ],
337
+ max_output_tokens: testMaxTokens,
338
+ });
339
+ if (!response.ok) {
340
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
341
+ return {
342
+ success: false,
343
+ error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
344
+ };
345
+ }
346
+ return { success: true };
347
+ };
348
+
349
+ let response = await this.sendRequest(chatUrl, {
350
+ model: this.deployment,
351
+ messages: [{ role: 'user', content: 'Hi' }],
352
+ max_tokens: testMaxTokens,
353
+ });
354
+
355
+ if (!response.ok) {
356
+ let errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
357
+ if (this.isChatCompletionUnsupported(errorData)) {
358
+ return await runResponses();
359
+ }
360
+ if (this.isMaxTokensUnsupported(errorData)) {
361
+ response = await this.sendRequest(chatUrl, {
362
+ model: this.deployment,
363
+ messages: [{ role: 'user', content: 'Hi' }],
364
+ max_completion_tokens: testMaxTokens,
365
+ });
366
+ if (response.ok) {
367
+ return { success: true };
368
+ }
369
+ errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
370
+ if (this.isChatCompletionUnsupported(errorData)) {
371
+ return await runResponses();
372
+ }
373
+ }
374
+ return {
375
+ success: false,
376
+ error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
377
+ };
378
+ }
379
+
380
+ return { success: true };
381
+ } catch (error: any) {
382
+ return {
383
+ success: false,
384
+ error: error.message || 'Failed to connect to Azure OpenAI',
385
+ };
386
+ }
387
+ }
388
+ }