cowork-os 0.3.21 → 0.3.23

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 (170) hide show
  1. package/README.md +293 -6
  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/daemon.js +25 -0
  48. package/dist/electron/electron/agent/executor.js +181 -26
  49. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  50. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  51. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  52. package/dist/electron/electron/agent/llm/index.js +11 -1
  53. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  54. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  55. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  56. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  57. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  58. package/dist/electron/electron/agent/llm/provider-factory.js +318 -4
  59. package/dist/electron/electron/agent/llm/types.js +66 -1
  60. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  61. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  62. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  63. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  64. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  65. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  66. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  67. package/dist/electron/electron/agent/tools/registry.js +541 -0
  68. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  69. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  70. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  71. package/dist/electron/electron/gateway/index.js +1 -0
  72. package/dist/electron/electron/gateway/router.js +123 -143
  73. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  74. package/dist/electron/electron/ipc/handlers.js +627 -158
  75. package/dist/electron/electron/main.js +63 -0
  76. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  77. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  78. package/dist/electron/electron/memory/MemoryService.js +1 -1
  79. package/dist/electron/electron/preload.js +74 -1
  80. package/dist/electron/electron/settings/box-manager.js +54 -0
  81. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  82. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  83. package/dist/electron/electron/settings/notion-manager.js +56 -0
  84. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  85. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  86. package/dist/electron/electron/utils/box-api.js +153 -0
  87. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  88. package/dist/electron/electron/utils/env-migration.js +19 -0
  89. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  90. package/dist/electron/electron/utils/notion-api.js +103 -0
  91. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  92. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  93. package/dist/electron/electron/utils/validation.js +82 -3
  94. package/dist/electron/electron/utils/x-cli.js +1 -1
  95. package/dist/electron/shared/channelMessages.js +284 -3
  96. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  97. package/dist/electron/shared/types.js +88 -1
  98. package/package.json +12 -2
  99. package/src/electron/agent/executor.ts +205 -28
  100. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  101. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  102. package/src/electron/agent/llm/groq-provider.ts +39 -0
  103. package/src/electron/agent/llm/index.ts +5 -0
  104. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  105. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  106. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  107. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  108. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  109. package/src/electron/agent/llm/provider-factory.ts +414 -6
  110. package/src/electron/agent/llm/types.ts +90 -1
  111. package/src/electron/agent/llm/xai-provider.ts +39 -0
  112. package/src/electron/agent/tools/box-tools.ts +239 -0
  113. package/src/electron/agent/tools/builtin-settings.ts +34 -0
  114. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  115. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  116. package/src/electron/agent/tools/notion-tools.ts +330 -0
  117. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  118. package/src/electron/agent/tools/registry.ts +565 -0
  119. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  120. package/src/electron/agent/tools/shell-tools.ts +11 -3
  121. package/src/electron/agent/tools/x-tools.ts +1 -1
  122. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  123. package/src/electron/gateway/index.ts +1 -0
  124. package/src/electron/gateway/router.ts +134 -149
  125. package/src/electron/ipc/canvas-handlers.ts +10 -0
  126. package/src/electron/ipc/handlers.ts +673 -153
  127. package/src/electron/main.ts +35 -0
  128. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  129. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  130. package/src/electron/memory/MemoryService.ts +5 -1
  131. package/src/electron/preload.ts +167 -4
  132. package/src/electron/settings/box-manager.ts +58 -0
  133. package/src/electron/settings/dropbox-manager.ts +58 -0
  134. package/src/electron/settings/google-drive-manager.ts +58 -0
  135. package/src/electron/settings/notion-manager.ts +60 -0
  136. package/src/electron/settings/onedrive-manager.ts +58 -0
  137. package/src/electron/settings/sharepoint-manager.ts +58 -0
  138. package/src/electron/utils/box-api.ts +184 -0
  139. package/src/electron/utils/dropbox-api.ts +171 -0
  140. package/src/electron/utils/env-migration.ts +22 -0
  141. package/src/electron/utils/google-drive-api.ts +183 -0
  142. package/src/electron/utils/notion-api.ts +126 -0
  143. package/src/electron/utils/onedrive-api.ts +137 -0
  144. package/src/electron/utils/sharepoint-api.ts +132 -0
  145. package/src/electron/utils/validation.ts +102 -1
  146. package/src/electron/utils/x-cli.ts +1 -1
  147. package/src/renderer/App.tsx +20 -2
  148. package/src/renderer/components/BoxSettings.tsx +203 -0
  149. package/src/renderer/components/BrowserView.tsx +101 -0
  150. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  151. package/src/renderer/components/CanvasPreview.tsx +68 -1
  152. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  153. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  154. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  155. package/src/renderer/components/DropboxSettings.tsx +202 -0
  156. package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
  157. package/src/renderer/components/MCPSettings.tsx +56 -0
  158. package/src/renderer/components/MainContent.tsx +270 -34
  159. package/src/renderer/components/NotionSettings.tsx +231 -0
  160. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  161. package/src/renderer/components/OnboardingModal.tsx +70 -1
  162. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  163. package/src/renderer/components/Settings.tsx +611 -8
  164. package/src/renderer/components/SharePointSettings.tsx +224 -0
  165. package/src/renderer/components/Sidebar.tsx +25 -9
  166. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  167. package/src/renderer/styles/index.css +438 -25
  168. package/src/shared/channelMessages.ts +367 -4
  169. package/src/shared/llm-provider-catalog.ts +217 -0
  170. package/src/shared/types.ts +226 -1
@@ -0,0 +1,117 @@
1
+ import { LLMProvider, LLMProviderConfig, LLMRequest, LLMResponse } from './types';
2
+ import { OpenAICompatibleProvider } from './openai-compatible-provider';
3
+
4
+ const COPILOT_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token';
5
+ const DEFAULT_COPILOT_BASE_URL = 'https://api.individual.githubcopilot.com';
6
+
7
+ type CopilotTokenCache = {
8
+ token: string;
9
+ expiresAt: number;
10
+ baseUrl: string;
11
+ };
12
+
13
+ function isTokenValid(cache: CopilotTokenCache, now = Date.now()): boolean {
14
+ return cache.expiresAt - now > 5 * 60 * 1000;
15
+ }
16
+
17
+ function parseCopilotTokenResponse(payload: any): { token: string; expiresAt: number } {
18
+ if (!payload || typeof payload !== 'object') {
19
+ throw new Error('Unexpected response from Copilot token endpoint');
20
+ }
21
+ const token = payload.token;
22
+ const expiresAt = payload.expires_at;
23
+ if (typeof token !== 'string' || !token.trim()) {
24
+ throw new Error('Copilot token response missing token');
25
+ }
26
+
27
+ if (typeof expiresAt === 'number' && Number.isFinite(expiresAt)) {
28
+ return { token, expiresAt: expiresAt > 10_000_000_000 ? expiresAt : expiresAt * 1000 };
29
+ }
30
+
31
+ if (typeof expiresAt === 'string' && expiresAt.trim()) {
32
+ const parsed = Number.parseInt(expiresAt, 10);
33
+ if (!Number.isFinite(parsed)) {
34
+ throw new Error('Copilot token response has invalid expires_at');
35
+ }
36
+ return { token, expiresAt: parsed > 10_000_000_000 ? parsed : parsed * 1000 };
37
+ }
38
+
39
+ throw new Error('Copilot token response missing expires_at');
40
+ }
41
+
42
+ function deriveCopilotBaseUrl(token: string): string {
43
+ const match = token.match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i);
44
+ const proxyEp = match?.[1]?.trim();
45
+ if (!proxyEp) return DEFAULT_COPILOT_BASE_URL;
46
+ const host = proxyEp.replace(/^https?:\/\//, '').replace(/^proxy\./i, 'api.');
47
+ return host ? `https://${host}` : DEFAULT_COPILOT_BASE_URL;
48
+ }
49
+
50
+ export class GitHubCopilotProvider implements LLMProvider {
51
+ readonly type = 'github-copilot' as const;
52
+ private githubToken: string;
53
+ private model: string;
54
+ private static cache?: CopilotTokenCache;
55
+
56
+ constructor(config: LLMProviderConfig) {
57
+ const token = config.providerApiKey;
58
+ if (!token) {
59
+ throw new Error('GitHub token is required for Copilot. Configure it in Settings.');
60
+ }
61
+ this.githubToken = token;
62
+ this.model = config.model || 'gpt-4o';
63
+ }
64
+
65
+ async createMessage(request: LLMRequest): Promise<LLMResponse> {
66
+ const client = await this.getClient(request.model || this.model);
67
+ return client.createMessage(request);
68
+ }
69
+
70
+ async testConnection(): Promise<{ success: boolean; error?: string }> {
71
+ try {
72
+ const client = await this.getClient(this.model);
73
+ return await client.testConnection();
74
+ } catch (error: any) {
75
+ return { success: false, error: error.message || 'Failed to connect to Copilot' };
76
+ }
77
+ }
78
+
79
+ private async getClient(model: string): Promise<OpenAICompatibleProvider> {
80
+ const auth = await this.getCopilotAuth();
81
+ return new OpenAICompatibleProvider({
82
+ type: 'github-copilot',
83
+ providerName: 'GitHub Copilot',
84
+ apiKey: auth.token,
85
+ baseUrl: auth.baseUrl,
86
+ defaultModel: model,
87
+ });
88
+ }
89
+
90
+ private async getCopilotAuth(): Promise<CopilotTokenCache> {
91
+ if (GitHubCopilotProvider.cache && isTokenValid(GitHubCopilotProvider.cache)) {
92
+ return GitHubCopilotProvider.cache;
93
+ }
94
+
95
+ const response = await fetch(COPILOT_TOKEN_URL, {
96
+ method: 'GET',
97
+ headers: {
98
+ Accept: 'application/json',
99
+ Authorization: `Bearer ${this.githubToken}`,
100
+ },
101
+ });
102
+
103
+ if (!response.ok) {
104
+ throw new Error(`Copilot token exchange failed: HTTP ${response.status}`);
105
+ }
106
+
107
+ const json = await response.json();
108
+ const parsed = parseCopilotTokenResponse(json);
109
+ const cache: CopilotTokenCache = {
110
+ token: parsed.token,
111
+ expiresAt: parsed.expiresAt,
112
+ baseUrl: deriveCopilotBaseUrl(parsed.token),
113
+ };
114
+ GitHubCopilotProvider.cache = cache;
115
+ return cache;
116
+ }
117
+ }
@@ -0,0 +1,39 @@
1
+ import { LLMProvider, LLMProviderConfig, LLMRequest, LLMResponse } from './types';
2
+ import { OpenAICompatibleProvider } from './openai-compatible-provider';
3
+
4
+ const GROQ_BASE_URL = 'https://api.groq.com/openai/v1';
5
+ const DEFAULT_GROQ_MODEL = 'llama-3.1-8b-instant';
6
+
7
+ export class GroqProvider implements LLMProvider {
8
+ readonly type = 'groq' as const;
9
+ private client: OpenAICompatibleProvider;
10
+
11
+ constructor(config: LLMProviderConfig) {
12
+ const apiKey = config.groqApiKey;
13
+ if (!apiKey) {
14
+ throw new Error('Groq API key is required. Configure it in Settings.');
15
+ }
16
+
17
+ const baseUrl = config.groqBaseUrl || GROQ_BASE_URL;
18
+
19
+ this.client = new OpenAICompatibleProvider({
20
+ type: 'groq',
21
+ providerName: 'Groq',
22
+ apiKey,
23
+ baseUrl,
24
+ defaultModel: config.model || DEFAULT_GROQ_MODEL,
25
+ });
26
+ }
27
+
28
+ createMessage(request: LLMRequest): Promise<LLMResponse> {
29
+ return this.client.createMessage(request);
30
+ }
31
+
32
+ testConnection() {
33
+ return this.client.testConnection();
34
+ }
35
+
36
+ getAvailableModels() {
37
+ return this.client.getAvailableModels();
38
+ }
39
+ }
@@ -5,5 +5,10 @@ export { OllamaProvider } from './ollama-provider';
5
5
  export { GeminiProvider } from './gemini-provider';
6
6
  export { OpenRouterProvider } from './openrouter-provider';
7
7
  export { OpenAIProvider } from './openai-provider';
8
+ export { GroqProvider } from './groq-provider';
9
+ export { XAIProvider } from './xai-provider';
10
+ export { KimiProvider } from './kimi-provider';
11
+ export { AnthropicCompatibleProvider } from './anthropic-compatible-provider';
12
+ export { GitHubCopilotProvider } from './github-copilot-provider';
8
13
  export { OpenAIOAuth, OpenAIOAuthTokens } from './openai-oauth';
9
14
  export { LLMProviderFactory, LLMSettings, CachedModelInfo } from './provider-factory';
@@ -0,0 +1,39 @@
1
+ import { LLMProvider, LLMProviderConfig, LLMRequest, LLMResponse } from './types';
2
+ import { OpenAICompatibleProvider } from './openai-compatible-provider';
3
+
4
+ const KIMI_BASE_URL = 'https://api.moonshot.ai/v1';
5
+ const DEFAULT_KIMI_MODEL = 'kimi-k2.5';
6
+
7
+ export class KimiProvider implements LLMProvider {
8
+ readonly type = 'kimi' as const;
9
+ private client: OpenAICompatibleProvider;
10
+
11
+ constructor(config: LLMProviderConfig) {
12
+ const apiKey = config.kimiApiKey;
13
+ if (!apiKey) {
14
+ throw new Error('Kimi API key is required. Configure it in Settings.');
15
+ }
16
+
17
+ const baseUrl = config.kimiBaseUrl || KIMI_BASE_URL;
18
+
19
+ this.client = new OpenAICompatibleProvider({
20
+ type: 'kimi',
21
+ providerName: 'Kimi',
22
+ apiKey,
23
+ baseUrl,
24
+ defaultModel: config.model || DEFAULT_KIMI_MODEL,
25
+ });
26
+ }
27
+
28
+ createMessage(request: LLMRequest): Promise<LLMResponse> {
29
+ return this.client.createMessage(request);
30
+ }
31
+
32
+ testConnection() {
33
+ return this.client.testConnection();
34
+ }
35
+
36
+ getAvailableModels() {
37
+ return this.client.getAvailableModels();
38
+ }
39
+ }
@@ -0,0 +1,153 @@
1
+ import {
2
+ LLMProvider,
3
+ LLMProviderType,
4
+ LLMRequest,
5
+ LLMResponse,
6
+ } from './types';
7
+ import {
8
+ toOpenAICompatibleMessages,
9
+ toOpenAICompatibleTools,
10
+ fromOpenAICompatibleResponse,
11
+ } from './openai-compatible';
12
+
13
+ export interface OpenAICompatibleProviderOptions {
14
+ type: LLMProviderType;
15
+ providerName: string;
16
+ apiKey: string;
17
+ baseUrl: string;
18
+ defaultModel: string;
19
+ extraHeaders?: Record<string, string>;
20
+ }
21
+
22
+ export class OpenAICompatibleProvider implements LLMProvider {
23
+ readonly type: LLMProviderType;
24
+ private apiKey: string;
25
+ private baseUrl: string;
26
+ private defaultModel: string;
27
+ private providerName: string;
28
+ private extraHeaders?: Record<string, string>;
29
+
30
+ constructor(options: OpenAICompatibleProviderOptions) {
31
+ this.type = options.type;
32
+ this.apiKey = options.apiKey;
33
+ this.baseUrl = options.baseUrl;
34
+ this.defaultModel = options.defaultModel;
35
+ this.providerName = options.providerName;
36
+ this.extraHeaders = options.extraHeaders;
37
+ }
38
+
39
+ async createMessage(request: LLMRequest): Promise<LLMResponse> {
40
+ const messages = toOpenAICompatibleMessages(request.messages, request.system);
41
+ const tools = request.tools ? toOpenAICompatibleTools(request.tools) : undefined;
42
+
43
+ try {
44
+ const model = request.model || this.defaultModel;
45
+ console.log(`[${this.providerName}] Calling API with model: ${model}`);
46
+
47
+ const headers: Record<string, string> = {
48
+ 'Content-Type': 'application/json',
49
+ ...(this.extraHeaders || {}),
50
+ };
51
+ if (this.apiKey) {
52
+ headers.Authorization = `Bearer ${this.apiKey}`;
53
+ }
54
+
55
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
56
+ method: 'POST',
57
+ headers,
58
+ body: JSON.stringify({
59
+ model,
60
+ messages,
61
+ max_tokens: request.maxTokens,
62
+ ...(tools && { tools, tool_choice: 'auto' }),
63
+ }),
64
+ signal: request.signal,
65
+ });
66
+
67
+ if (!response.ok) {
68
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
69
+ throw new Error(
70
+ `${this.providerName} API error: ${response.status} ${response.statusText}` +
71
+ (errorData.error?.message ? ` - ${errorData.error.message}` : '')
72
+ );
73
+ }
74
+
75
+ const data = await response.json() as any;
76
+ return fromOpenAICompatibleResponse(data);
77
+ } catch (error: any) {
78
+ if (error.name === 'AbortError' || error.message?.includes('aborted')) {
79
+ console.log(`[${this.providerName}] Request aborted`);
80
+ throw new Error('Request cancelled');
81
+ }
82
+
83
+ console.error(`[${this.providerName}] API error:`, {
84
+ message: error.message,
85
+ status: error.status,
86
+ });
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ async testConnection(): Promise<{ success: boolean; error?: string }> {
92
+ try {
93
+ const headers: Record<string, string> = {
94
+ 'Content-Type': 'application/json',
95
+ ...(this.extraHeaders || {}),
96
+ };
97
+ if (this.apiKey) {
98
+ headers.Authorization = `Bearer ${this.apiKey}`;
99
+ }
100
+
101
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
102
+ method: 'POST',
103
+ headers,
104
+ body: JSON.stringify({
105
+ model: this.defaultModel,
106
+ messages: [{ role: 'user', content: 'Hi' }],
107
+ max_tokens: 10,
108
+ }),
109
+ });
110
+
111
+ if (!response.ok) {
112
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
113
+ return {
114
+ success: false,
115
+ error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
116
+ };
117
+ }
118
+
119
+ return { success: true };
120
+ } catch (error: any) {
121
+ return {
122
+ success: false,
123
+ error: error.message || `Failed to connect to ${this.providerName} API`,
124
+ };
125
+ }
126
+ }
127
+
128
+ async getAvailableModels(): Promise<Array<{ id: string; name: string }>> {
129
+ try {
130
+ const headers: Record<string, string> = {};
131
+ if (this.apiKey) {
132
+ headers.Authorization = `Bearer ${this.apiKey}`;
133
+ }
134
+
135
+ const response = await fetch(`${this.baseUrl}/models`, {
136
+ headers,
137
+ });
138
+
139
+ if (!response.ok) {
140
+ return [];
141
+ }
142
+
143
+ const data = await response.json() as { data?: any[] };
144
+ return (data.data || []).map((model: any) => ({
145
+ id: model.id,
146
+ name: model.id,
147
+ }));
148
+ } catch (error) {
149
+ console.error(`[${this.providerName}] Failed to fetch models:`, error);
150
+ return [];
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ LLMContent,
3
+ LLMMessage,
4
+ LLMResponse,
5
+ LLMTool,
6
+ } from './types';
7
+
8
+ export function toOpenAICompatibleMessages(
9
+ messages: LLMMessage[],
10
+ system?: string
11
+ ): Array<{ role: string; content: any; tool_call_id?: string; tool_calls?: any[] }> {
12
+ const result: Array<{ role: string; content: any; tool_call_id?: string; tool_calls?: any[] }> = [];
13
+
14
+ if (system) {
15
+ result.push({ role: 'system', content: system });
16
+ }
17
+
18
+ for (const msg of messages) {
19
+ if (typeof msg.content === 'string') {
20
+ result.push({ role: msg.role, content: msg.content });
21
+ continue;
22
+ }
23
+
24
+ for (const item of msg.content) {
25
+ if (item.type === 'tool_result') {
26
+ result.push({
27
+ role: 'tool',
28
+ content: item.content,
29
+ tool_call_id: item.tool_use_id,
30
+ });
31
+ } else if (item.type === 'tool_use') {
32
+ result.push({
33
+ role: 'assistant',
34
+ content: null,
35
+ tool_calls: [{
36
+ id: item.id,
37
+ type: 'function',
38
+ function: {
39
+ name: item.name,
40
+ arguments: JSON.stringify(item.input),
41
+ },
42
+ }],
43
+ });
44
+ } else if (item.type === 'text') {
45
+ result.push({ role: msg.role, content: item.text });
46
+ }
47
+ }
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ export function toOpenAICompatibleTools(tools: LLMTool[]): Array<{
54
+ type: 'function';
55
+ function: {
56
+ name: string;
57
+ description: string;
58
+ parameters: any;
59
+ };
60
+ }> {
61
+ return tools.map((tool) => ({
62
+ type: 'function' as const,
63
+ function: {
64
+ name: tool.name,
65
+ description: tool.description,
66
+ parameters: tool.input_schema,
67
+ },
68
+ }));
69
+ }
70
+
71
+ export function fromOpenAICompatibleResponse(response: any): LLMResponse {
72
+ const content: LLMContent[] = [];
73
+ const choice = response.choices?.[0];
74
+
75
+ if (!choice) {
76
+ return {
77
+ content: [{ type: 'text', text: '' }],
78
+ stopReason: 'end_turn',
79
+ };
80
+ }
81
+
82
+ const message = choice.message;
83
+
84
+ if (message?.content) {
85
+ content.push({
86
+ type: 'text',
87
+ text: message.content,
88
+ });
89
+ }
90
+
91
+ if (message?.tool_calls) {
92
+ for (const toolCall of message.tool_calls) {
93
+ if (toolCall.type === 'function') {
94
+ content.push({
95
+ type: 'tool_use',
96
+ id: toolCall.id,
97
+ name: toolCall.function.name,
98
+ input: JSON.parse(toolCall.function.arguments || '{}'),
99
+ });
100
+ }
101
+ }
102
+ }
103
+
104
+ if (content.length === 0) {
105
+ content.push({ type: 'text', text: '' });
106
+ }
107
+
108
+ return {
109
+ content,
110
+ stopReason: mapStopReason(choice.finish_reason),
111
+ usage: response.usage
112
+ ? {
113
+ inputTokens: response.usage.prompt_tokens || 0,
114
+ outputTokens: response.usage.completion_tokens || 0,
115
+ }
116
+ : undefined,
117
+ };
118
+ }
119
+
120
+ export function mapStopReason(finishReason?: string): LLMResponse['stopReason'] {
121
+ switch (finishReason) {
122
+ case 'stop':
123
+ return 'end_turn';
124
+ case 'length':
125
+ return 'max_tokens';
126
+ case 'tool_calls':
127
+ return 'tool_use';
128
+ case 'content_filter':
129
+ return 'stop_sequence';
130
+ default:
131
+ return 'end_turn';
132
+ }
133
+ }
@@ -20,11 +20,12 @@ export interface OpenAIOAuthTokens {
20
20
  * Convert pi-ai OAuthCredentials to our token format
21
21
  */
22
22
  function credentialsToTokens(credentials: OAuthCredentials): OpenAIOAuthTokens {
23
+ const email = typeof credentials.email === 'string' ? credentials.email : undefined;
23
24
  return {
24
25
  access_token: credentials.access,
25
26
  refresh_token: credentials.refresh,
26
27
  expires_at: credentials.expires,
27
- email: credentials.email,
28
+ email,
28
29
  };
29
30
  }
30
31
 
@@ -15,7 +15,7 @@ import {
15
15
  export class OpenRouterProvider implements LLMProvider {
16
16
  readonly type = 'openrouter' as const;
17
17
  private apiKey: string;
18
- private baseUrl = 'https://openrouter.ai/api/v1';
18
+ private baseUrl: string;
19
19
  private defaultModel: string;
20
20
 
21
21
  constructor(config: LLMProviderConfig) {
@@ -25,6 +25,7 @@ export class OpenRouterProvider implements LLMProvider {
25
25
  }
26
26
 
27
27
  this.apiKey = apiKey;
28
+ this.baseUrl = config.openrouterBaseUrl || 'https://openrouter.ai/api/v1';
28
29
  this.defaultModel = config.model || 'anthropic/claude-3.5-sonnet';
29
30
  }
30
31