@nordsym/apiclaw 1.8.7 → 1.8.9

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 (156) hide show
  1. package/README.md +58 -30
  2. package/convex/adminActivate.d.ts +3 -0
  3. package/convex/adminActivate.d.ts.map +1 -1
  4. package/convex/adminActivate.js +46 -0
  5. package/convex/adminActivate.js.map +1 -1
  6. package/convex/adminActivate.ts +1 -2
  7. package/convex/adminStats.d.ts +9 -0
  8. package/convex/adminStats.d.ts.map +1 -1
  9. package/convex/adminStats.js +282 -0
  10. package/convex/adminStats.js.map +1 -1
  11. package/convex/adminStats.ts +5 -3
  12. package/convex/agents.d.ts +84 -0
  13. package/convex/agents.js +809 -0
  14. package/convex/analytics.d.ts +5 -0
  15. package/convex/analytics.js +167 -0
  16. package/convex/apiKeys.d.ts +6 -0
  17. package/convex/apiKeys.d.ts.map +1 -0
  18. package/convex/apiKeys.js +186 -0
  19. package/convex/apiKeys.js.map +1 -0
  20. package/convex/backfillAnalytics.d.ts +2 -0
  21. package/convex/backfillAnalytics.js +20 -0
  22. package/convex/backfillSearchLogs.d.ts +2 -0
  23. package/convex/backfillSearchLogs.js +29 -0
  24. package/convex/billing.d.ts +88 -0
  25. package/convex/billing.d.ts.map +1 -1
  26. package/convex/billing.js +643 -0
  27. package/convex/billing.js.map +1 -1
  28. package/convex/billing.ts +2 -14
  29. package/convex/capabilities.d.ts +9 -0
  30. package/convex/capabilities.js +145 -0
  31. package/convex/chains.d.ts +68 -0
  32. package/convex/chains.js +1105 -0
  33. package/convex/credits.d.ts +25 -0
  34. package/convex/credits.js +186 -0
  35. package/convex/crons.d.ts +3 -0
  36. package/convex/crons.js +17 -0
  37. package/convex/debugFilestackLogs.d.ts +2 -0
  38. package/convex/debugFilestackLogs.js +17 -0
  39. package/convex/debugGetToken.d.ts +2 -0
  40. package/convex/debugGetToken.js +18 -0
  41. package/convex/directCall.d.ts +72 -0
  42. package/convex/directCall.d.ts.map +1 -1
  43. package/convex/directCall.js +663 -0
  44. package/convex/directCall.js.map +1 -1
  45. package/convex/earnProgress.d.ts +58 -0
  46. package/convex/earnProgress.js +649 -0
  47. package/convex/email.d.ts +14 -0
  48. package/convex/email.js +300 -0
  49. package/convex/email.js.map +1 -1
  50. package/convex/feedback.d.ts +7 -0
  51. package/convex/feedback.js +227 -0
  52. package/convex/http.d.ts +3 -0
  53. package/convex/http.d.ts.map +1 -1
  54. package/convex/http.js +2135 -0
  55. package/convex/http.js.map +1 -1
  56. package/convex/http.ts +275 -3
  57. package/convex/inbound.d.ts +2 -0
  58. package/convex/inbound.js +32 -0
  59. package/convex/logs.d.ts +48 -0
  60. package/convex/logs.d.ts.map +1 -1
  61. package/convex/logs.js +623 -0
  62. package/convex/logs.js.map +1 -1
  63. package/convex/migrateFilestack.d.ts +2 -0
  64. package/convex/migrateFilestack.js +74 -0
  65. package/convex/migratePartnersProd.d.ts +8 -0
  66. package/convex/migratePartnersProd.js +165 -0
  67. package/convex/migratePratham.d.ts +2 -0
  68. package/convex/migratePratham.js +121 -0
  69. package/convex/migrateProviderWorkspaces.d.ts +13 -0
  70. package/convex/migrateProviderWorkspaces.d.ts.map +1 -1
  71. package/convex/migrateProviderWorkspaces.js +141 -0
  72. package/convex/migrateProviderWorkspaces.js.map +1 -1
  73. package/convex/mou.d.ts +6 -0
  74. package/convex/mou.js +82 -0
  75. package/convex/providerKeys.d.ts +31 -0
  76. package/convex/providerKeys.js +257 -0
  77. package/convex/providers.d.ts +35 -0
  78. package/convex/providers.d.ts.map +1 -1
  79. package/convex/providers.js +1027 -0
  80. package/convex/providers.js.map +1 -1
  81. package/convex/purchases.d.ts +7 -0
  82. package/convex/purchases.js +157 -0
  83. package/convex/ratelimit.d.ts +4 -0
  84. package/convex/ratelimit.js +91 -0
  85. package/convex/schema.ts +4 -4
  86. package/convex/searchLogs.d.ts +13 -0
  87. package/convex/searchLogs.d.ts.map +1 -1
  88. package/convex/searchLogs.js +241 -0
  89. package/convex/searchLogs.js.map +1 -1
  90. package/convex/seedAPILayerAPIs.d.ts +7 -0
  91. package/convex/seedAPILayerAPIs.js +177 -0
  92. package/convex/seedDirectCallConfigs.d.ts +2 -0
  93. package/convex/seedDirectCallConfigs.js +324 -0
  94. package/convex/seedPratham.d.ts +6 -0
  95. package/convex/seedPratham.d.ts.map +1 -1
  96. package/convex/seedPratham.js +149 -0
  97. package/convex/seedPratham.js.map +1 -1
  98. package/convex/seedPratham.ts +1 -2
  99. package/convex/spendAlerts.d.ts +36 -0
  100. package/convex/spendAlerts.js +380 -0
  101. package/convex/spendAlerts.js.map +1 -1
  102. package/convex/stripeActions.d.ts +19 -0
  103. package/convex/stripeActions.d.ts.map +1 -1
  104. package/convex/stripeActions.js +432 -0
  105. package/convex/stripeActions.js.map +1 -1
  106. package/convex/stripeActions.ts +25 -3
  107. package/convex/teams.d.ts +21 -0
  108. package/convex/teams.js +215 -0
  109. package/convex/telemetry.d.ts +4 -0
  110. package/convex/telemetry.js +74 -0
  111. package/convex/updateAPIStatus.d.ts +6 -0
  112. package/convex/updateAPIStatus.d.ts.map +1 -1
  113. package/convex/updateAPIStatus.js +39 -0
  114. package/convex/updateAPIStatus.js.map +1 -1
  115. package/convex/usage.d.ts +27 -0
  116. package/convex/usage.js +229 -0
  117. package/convex/waitlist.d.ts +4 -0
  118. package/convex/waitlist.js +49 -0
  119. package/convex/webhooks.d.ts +12 -0
  120. package/convex/webhooks.js +410 -0
  121. package/convex/workspaceSettings.d.ts +7 -0
  122. package/convex/workspaceSettings.d.ts.map +1 -0
  123. package/convex/workspaceSettings.js +128 -0
  124. package/convex/workspaceSettings.js.map +1 -0
  125. package/convex/workspaces.d.ts +33 -0
  126. package/convex/workspaces.d.ts.map +1 -1
  127. package/convex/workspaces.js +989 -0
  128. package/convex/workspaces.js.map +1 -1
  129. package/convex/workspaces.ts +18 -20
  130. package/dist/bin.js +0 -0
  131. package/dist/cli/commands/demo.js +1 -1
  132. package/dist/cli/commands/demo.js.map +1 -1
  133. package/dist/cli/commands/doctor.js.map +1 -1
  134. package/dist/cli/commands/login.js.map +1 -1
  135. package/dist/cli/commands/setup.js.map +1 -1
  136. package/dist/credentials.d.ts.map +1 -1
  137. package/dist/credentials.js +15 -0
  138. package/dist/credentials.js.map +1 -1
  139. package/dist/discovery.js.map +1 -1
  140. package/dist/execute.js.map +1 -1
  141. package/dist/index.js +1 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/mcp-analytics.js +1 -1
  144. package/dist/mcp-analytics.js.map +1 -1
  145. package/dist/open-apis.d.ts.map +1 -1
  146. package/dist/open-apis.js +94 -2
  147. package/dist/open-apis.js.map +1 -1
  148. package/dist/ui/errors.js.map +1 -1
  149. package/dist/ui/prompts.js.map +1 -1
  150. package/package.json +1 -1
  151. package/src/cli/commands/demo.ts +1 -1
  152. package/src/credentials.ts +16 -0
  153. package/src/index.ts +1 -1
  154. package/src/mcp-analytics.ts +1 -1
  155. package/src/open-apis.ts +114 -4
  156. package/src/types/convex-api.d.ts +20 -0
package/convex/http.js ADDED
@@ -0,0 +1,2135 @@
1
+ import { httpRouter } from "convex/server";
2
+ import { httpAction } from "./_generated/server";
3
+ import { api, internal } from "./_generated/api";
4
+ import { createCheckoutSession, createPortalSession, handleStripeWebhook, checkoutOptions, portalOptions, webhookOptions, } from "./stripeActions";
5
+ const http = httpRouter();
6
+ const PROVIDERS = {
7
+ openrouter: {
8
+ name: "OpenRouter",
9
+ description: "Multi-model LLM API. Access GPT, Claude, Llama, Gemini, and 800+ models.",
10
+ category: "llm",
11
+ pricing: "Varies by model",
12
+ regions: ["Global"],
13
+ tags: ["llm", "ai", "gpt", "claude", "gemini", "llama"],
14
+ isLLM: true,
15
+ envKey: "OPENROUTER_API_KEY",
16
+ baseUrl: "https://openrouter.ai/api/v1/chat/completions",
17
+ speed: "medium",
18
+ costTier: "medium",
19
+ },
20
+ groq: {
21
+ name: "Groq",
22
+ description: "Ultra-fast LLM inference. Llama, Mixtral, Gemma at lightning speed.",
23
+ category: "llm",
24
+ pricing: "~$0.05-0.27/M tokens",
25
+ regions: ["Global"],
26
+ tags: ["llm", "fast", "llama", "mixtral", "gemma"],
27
+ isLLM: true,
28
+ envKey: "GROQ_API_KEY",
29
+ baseUrl: "https://api.groq.com/openai/v1/chat/completions",
30
+ speed: "fast",
31
+ costTier: "cheap",
32
+ },
33
+ mistral: {
34
+ name: "Mistral",
35
+ description: "Mistral AI models. Efficient European LLMs with strong coding.",
36
+ category: "llm",
37
+ pricing: "~$0.10-2.00/M tokens",
38
+ regions: ["EU", "Global"],
39
+ tags: ["llm", "mistral", "eu", "coding", "embeddings"],
40
+ isLLM: true,
41
+ envKey: "MISTRAL_API_KEY",
42
+ baseUrl: "https://api.mistral.ai/v1/chat/completions",
43
+ speed: "fast",
44
+ costTier: "cheap",
45
+ },
46
+ together: {
47
+ name: "Together AI",
48
+ description: "Open-source model inference. Llama, Qwen, DeepSeek at scale.",
49
+ category: "llm",
50
+ pricing: "~$0.10-0.90/M tokens",
51
+ regions: ["Global"],
52
+ tags: ["llm", "open-source", "llama", "qwen", "deepseek"],
53
+ isLLM: true,
54
+ envKey: "TOGETHER_API_KEY",
55
+ baseUrl: "https://api.together.xyz/v1/chat/completions",
56
+ speed: "fast",
57
+ costTier: "cheap",
58
+ },
59
+ cohere: {
60
+ name: "Cohere",
61
+ description: "Enterprise LLM with strong RAG and reranking capabilities.",
62
+ category: "llm",
63
+ pricing: "~$0.15-2.50/M tokens",
64
+ regions: ["Global"],
65
+ tags: ["llm", "rag", "rerank", "enterprise", "embeddings"],
66
+ isLLM: false, // Cohere uses non-OpenAI-compatible API format
67
+ envKey: "COHERE_API_KEY",
68
+ speed: "medium",
69
+ costTier: "medium",
70
+ },
71
+ "46elks": {
72
+ name: "46elks",
73
+ description: "SMS API for EU/Nordics. GDPR compliant.",
74
+ category: "sms",
75
+ pricing: "~$0.035/SMS",
76
+ regions: ["EU", "Nordic"],
77
+ tags: ["sms", "eu", "gdpr", "nordic"],
78
+ isLLM: false,
79
+ envKey: "ELKS_API_KEY",
80
+ speed: "fast",
81
+ costTier: "cheap",
82
+ },
83
+ twilio: {
84
+ name: "Twilio",
85
+ description: "SMS and Voice API. Global coverage.",
86
+ category: "sms",
87
+ pricing: "~$0.04/SMS, ~$0.01/min voice",
88
+ regions: ["Global"],
89
+ tags: ["sms", "voice", "global"],
90
+ isLLM: false,
91
+ envKey: "TWILIO_AUTH_TOKEN",
92
+ speed: "fast",
93
+ costTier: "cheap",
94
+ },
95
+ resend: {
96
+ name: "Resend",
97
+ description: "Modern email API. Developer-friendly.",
98
+ category: "email",
99
+ pricing: "~$0.001/email",
100
+ regions: ["Global"],
101
+ tags: ["email", "transactional"],
102
+ isLLM: false,
103
+ envKey: "RESEND_API_KEY",
104
+ speed: "fast",
105
+ costTier: "free",
106
+ },
107
+ brave_search: {
108
+ name: "Brave Search",
109
+ description: "Privacy-focused web search API.",
110
+ category: "search",
111
+ pricing: "~$0.005/search",
112
+ regions: ["Global"],
113
+ tags: ["search", "web", "privacy"],
114
+ isLLM: false,
115
+ envKey: "BRAVE_API_KEY",
116
+ speed: "fast",
117
+ costTier: "cheap",
118
+ },
119
+ serper: {
120
+ name: "Serper",
121
+ description: "Google Search API. Fast SERP results for AI agents.",
122
+ category: "search",
123
+ pricing: "~$0.001/search",
124
+ regions: ["Global"],
125
+ tags: ["search", "google", "serp"],
126
+ isLLM: false,
127
+ envKey: "SERPER_API_KEY",
128
+ speed: "fast",
129
+ costTier: "cheap",
130
+ },
131
+ elevenlabs: {
132
+ name: "ElevenLabs",
133
+ description: "Text-to-speech API. High quality AI voices.",
134
+ category: "tts",
135
+ pricing: "~$0.0003/char",
136
+ regions: ["Global"],
137
+ tags: ["tts", "voice", "audio", "speech"],
138
+ isLLM: false,
139
+ envKey: "ELEVENLABS_API_KEY",
140
+ speed: "medium",
141
+ costTier: "medium",
142
+ },
143
+ deepgram: {
144
+ name: "Deepgram",
145
+ description: "Speech-to-text API. Fast, accurate transcription with Nova-3.",
146
+ category: "stt",
147
+ pricing: "~$0.0043/min",
148
+ regions: ["Global"],
149
+ tags: ["stt", "transcription", "voice", "audio"],
150
+ isLLM: false,
151
+ envKey: "DEEPGRAM_API_KEY",
152
+ speed: "fast",
153
+ costTier: "cheap",
154
+ },
155
+ assemblyai: {
156
+ name: "AssemblyAI",
157
+ description: "Speech-to-text with speaker diarization, summarization, and sentiment.",
158
+ category: "stt",
159
+ pricing: "~$0.01/min",
160
+ regions: ["Global"],
161
+ tags: ["stt", "transcription", "diarization", "sentiment"],
162
+ isLLM: false,
163
+ envKey: "ASSEMBLYAI_API_KEY",
164
+ speed: "medium",
165
+ costTier: "cheap",
166
+ },
167
+ replicate: {
168
+ name: "Replicate",
169
+ description: "Run AI models (Whisper, SDXL, Llama, Flux, etc). Pay per prediction.",
170
+ category: "ai",
171
+ pricing: "Varies by model",
172
+ regions: ["Global"],
173
+ tags: ["ai", "ml", "whisper", "image", "audio", "transcription"],
174
+ isLLM: false,
175
+ envKey: "REPLICATE_API_TOKEN",
176
+ speed: "slow",
177
+ costTier: "medium",
178
+ },
179
+ stability: {
180
+ name: "Stability AI",
181
+ description: "Image generation API. Stable Diffusion 3, SDXL.",
182
+ category: "image",
183
+ pricing: "~$0.03/image",
184
+ regions: ["Global"],
185
+ tags: ["image", "generation", "stable-diffusion", "sdxl"],
186
+ isLLM: false,
187
+ envKey: "STABILITY_API_KEY",
188
+ speed: "slow",
189
+ costTier: "medium",
190
+ },
191
+ firecrawl: {
192
+ name: "Firecrawl",
193
+ description: "Web scraping and crawling API. Extract clean data from any URL.",
194
+ category: "scraping",
195
+ pricing: "~$0.001/page",
196
+ regions: ["Global"],
197
+ tags: ["scraping", "web", "crawl", "extract"],
198
+ isLLM: false,
199
+ envKey: "FIRECRAWL_API_KEY",
200
+ speed: "medium",
201
+ costTier: "cheap",
202
+ },
203
+ github: {
204
+ name: "GitHub",
205
+ description: "GitHub API. Search repos, manage code, access developer data.",
206
+ category: "code",
207
+ pricing: "Free tier available",
208
+ regions: ["Global"],
209
+ tags: ["github", "code", "repos", "developer"],
210
+ isLLM: false,
211
+ envKey: "GITHUB_TOKEN",
212
+ speed: "fast",
213
+ costTier: "free",
214
+ },
215
+ e2b: {
216
+ name: "E2B",
217
+ description: "Secure code sandbox for AI agents. Run Python, shell in isolated environments.",
218
+ category: "sandbox",
219
+ pricing: "$0.000028/s (2 vCPU)",
220
+ regions: ["Global"],
221
+ tags: ["sandbox", "code", "python", "execution", "ai", "agents"],
222
+ isLLM: false,
223
+ envKey: "E2B_API_KEY",
224
+ speed: "medium",
225
+ costTier: "cheap",
226
+ },
227
+ apilayer: {
228
+ name: "APILayer",
229
+ description: "14 APIs: exchange rates, market data, aviation, PDF, screenshots, email/phone verification, VAT, news, scraping, and more.",
230
+ category: "multi",
231
+ pricing: "Free tier available, paid plans per API",
232
+ regions: ["Global"],
233
+ tags: ["exchange", "stocks", "aviation", "pdf", "screenshot", "verification", "vat", "news", "scraping"],
234
+ isLLM: false,
235
+ envKey: "APILAYER_API_KEY",
236
+ speed: "medium",
237
+ costTier: "cheap",
238
+ },
239
+ voyage: {
240
+ name: "Voyage AI",
241
+ description: "State-of-the-art embeddings for RAG and agent memory. Best-in-class retrieval quality.",
242
+ category: "embeddings",
243
+ pricing: "~$0.02-0.18/M tokens",
244
+ regions: ["Global"],
245
+ tags: ["embeddings", "rag", "agent-memory", "retrieval", "voyage-3", "code-embeddings"],
246
+ isLLM: false,
247
+ envKey: "VOYAGE_API_KEY",
248
+ speed: "fast",
249
+ costTier: "cheap",
250
+ },
251
+ };
252
+ // ==============================================
253
+ // INTELLIGENT LLM ROUTER
254
+ // ==============================================
255
+ // Model-to-provider mapping: which direct providers can serve which model patterns
256
+ const MODEL_PROVIDER_MAP = [
257
+ // Groq-native models
258
+ { pattern: /^(groq\/)?llama-3\.3-70b/i, provider: "groq", nativeModel: "llama-3.3-70b-versatile" },
259
+ { pattern: /^(groq\/)?llama-3\.1-8b/i, provider: "groq", nativeModel: "llama-3.1-8b-instant" },
260
+ { pattern: /^(groq\/)?gemma2?-9b/i, provider: "groq", nativeModel: "gemma2-9b-it" },
261
+ { pattern: /^(groq\/)?mixtral-8x7b/i, provider: "groq", nativeModel: "mixtral-8x7b-32768" },
262
+ // Mistral-native models
263
+ { pattern: /^(mistralai\/)?mistral-small/i, provider: "mistral", nativeModel: "mistral-small-latest" },
264
+ { pattern: /^(mistralai\/)?mistral-large/i, provider: "mistral", nativeModel: "mistral-large-latest" },
265
+ { pattern: /^(mistralai\/)?mistral-medium/i, provider: "mistral", nativeModel: "mistral-medium-latest" },
266
+ { pattern: /^(mistralai\/)?codestral/i, provider: "mistral", nativeModel: "codestral-latest" },
267
+ { pattern: /^(mistralai\/)?pixtral/i, provider: "mistral", nativeModel: "pixtral-large-latest" },
268
+ // Together-native models
269
+ { pattern: /^(together\/)?meta-llama\/Llama-3\.3-70B/i, provider: "together", nativeModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo" },
270
+ { pattern: /^(together\/)?Qwen\/Qwen2\.5-72B/i, provider: "together", nativeModel: "Qwen/Qwen2.5-72B-Instruct-Turbo" },
271
+ { pattern: /^(together\/)?deepseek-ai\/DeepSeek-R1/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-R1" },
272
+ { pattern: /^(together\/)?deepseek-ai\/DeepSeek-V3/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-V3" },
273
+ ];
274
+ function routeLLMRequest(requestedModel, settings) {
275
+ // 1. Check direct provider matches for the requested model
276
+ for (const mapping of MODEL_PROVIDER_MAP) {
277
+ if (!mapping.pattern.test(requestedModel))
278
+ continue;
279
+ if (settings.blockedProviders.includes(mapping.provider))
280
+ continue;
281
+ const providerMeta = PROVIDERS[mapping.provider];
282
+ if (!providerMeta?.isLLM || !providerMeta.envKey || !providerMeta.baseUrl)
283
+ continue;
284
+ const apiKey = process.env[providerMeta.envKey];
285
+ if (!apiKey)
286
+ continue;
287
+ // For "highest_quality" mode, prefer OpenRouter (more model options)
288
+ if (settings.routingMode === "highest_quality" && !settings.preferredProviders.includes(mapping.provider)) {
289
+ continue;
290
+ }
291
+ return {
292
+ provider: mapping.provider,
293
+ model: mapping.nativeModel,
294
+ baseUrl: providerMeta.baseUrl,
295
+ apiKey,
296
+ reason: `direct_${mapping.provider}`,
297
+ };
298
+ }
299
+ // 2. Routing mode preferences for unknown models
300
+ if (settings.routingMode === "fastest") {
301
+ // Try Groq first (fastest inference), then Together, then Mistral
302
+ for (const fastProvider of ["groq", "together", "mistral"]) {
303
+ if (settings.blockedProviders.includes(fastProvider))
304
+ continue;
305
+ const meta = PROVIDERS[fastProvider];
306
+ if (!meta?.isLLM || !meta.envKey || !meta.baseUrl)
307
+ continue;
308
+ const key = process.env[meta.envKey];
309
+ if (!key)
310
+ continue;
311
+ // Only route if the model looks like it belongs to this provider
312
+ // Don't send anthropic/claude to groq
313
+ if (requestedModel.includes("anthropic/") || requestedModel.includes("openai/") || requestedModel.includes("google/"))
314
+ break;
315
+ return {
316
+ provider: fastProvider,
317
+ model: requestedModel,
318
+ baseUrl: meta.baseUrl,
319
+ apiKey: key,
320
+ reason: `fastest_mode_${fastProvider}`,
321
+ };
322
+ }
323
+ }
324
+ // 3. Preferred providers check
325
+ for (const preferred of settings.preferredProviders) {
326
+ if (settings.blockedProviders.includes(preferred))
327
+ continue;
328
+ const meta = PROVIDERS[preferred];
329
+ if (!meta?.isLLM || !meta.envKey || !meta.baseUrl)
330
+ continue;
331
+ const key = process.env[meta.envKey];
332
+ if (!key)
333
+ continue;
334
+ return {
335
+ provider: preferred,
336
+ model: requestedModel,
337
+ baseUrl: meta.baseUrl,
338
+ apiKey: key,
339
+ reason: `preferred_${preferred}`,
340
+ };
341
+ }
342
+ // 4. Fallback to OpenRouter
343
+ if (!settings.blockedProviders.includes("openrouter") && settings.allowOpenRouterFallback !== false) {
344
+ const orKey = process.env.OPENROUTER_API_KEY;
345
+ if (orKey) {
346
+ return {
347
+ provider: "openrouter",
348
+ model: requestedModel,
349
+ baseUrl: "https://openrouter.ai/api/v1/chat/completions",
350
+ apiKey: orKey,
351
+ reason: "openrouter_fallback",
352
+ extraHeaders: {
353
+ "HTTP-Referer": "https://apiclaw.cloud",
354
+ "X-Title": "APIClaw Gateway",
355
+ },
356
+ };
357
+ }
358
+ }
359
+ return null; // No provider available
360
+ }
361
+ // CORS headers
362
+ const corsHeaders = {
363
+ "Access-Control-Allow-Origin": "*",
364
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
365
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
366
+ };
367
+ // Helper for JSON responses
368
+ function jsonResponse(data, status = 200) {
369
+ return new Response(JSON.stringify(data), {
370
+ status,
371
+ headers: { "Content-Type": "application/json", ...corsHeaders },
372
+ });
373
+ }
374
+ // ============================================
375
+ // UNIFIED AUTH: resolves workspace from any auth method
376
+ // Priority: 1) Authorization: Bearer sk-claw-... (API key)
377
+ // 2) X-APIClaw-Identifier (legacy MCP workspace ID)
378
+ // 3) Anonymous (still allowed, just untracked)
379
+ // ============================================
380
+ async function resolveWorkspaceFromRequest(ctx, request) {
381
+ // 1. Check for API key auth (Bearer sk-claw-...)
382
+ const authHeader = request.headers.get("Authorization");
383
+ if (authHeader?.startsWith("Bearer sk-claw-")) {
384
+ const rawKey = authHeader.slice(7); // Remove "Bearer "
385
+ try {
386
+ const resolved = await ctx.runQuery(internal.apiKeys.resolveKey, { rawKey });
387
+ if (resolved) {
388
+ // Touch lastUsedAt (fire and forget)
389
+ ctx.runMutation(api.apiKeys.touchKey, { keyId: resolved.keyId }).catch(() => { });
390
+ return { workspaceId: resolved.workspaceId, keyId: resolved.keyId, authMethod: "api-key" };
391
+ }
392
+ }
393
+ catch (e) {
394
+ console.error("[Auth] API key resolution failed:", e.message);
395
+ }
396
+ // Invalid key - don't fall through to anonymous
397
+ return { authMethod: "anonymous" };
398
+ }
399
+ // 2. Check for legacy identifier
400
+ const identifier = request.headers.get("X-APIClaw-Identifier");
401
+ if (identifier && !identifier.startsWith("anon:") && identifier !== "unknown" && identifier.length > 20) {
402
+ return { workspaceId: identifier, authMethod: "identifier" };
403
+ }
404
+ // 3. Anonymous
405
+ return { authMethod: "anonymous" };
406
+ }
407
+ // Helper to validate session and log API usage
408
+ async function validateAndLogProxyCall(ctx, request, provider, action) {
409
+ const subagentId = request.headers.get("X-APIClaw-Subagent") || "main";
410
+ // Resolve workspace from any auth method
411
+ const auth = await resolveWorkspaceFromRequest(ctx, request);
412
+ const resolvedWorkspaceId = auth.workspaceId;
413
+ const identifier = request.headers.get("X-APIClaw-Identifier") || auth.workspaceId || "unknown";
414
+ console.log("[Proxy] Call received", { provider, action, authMethod: auth.authMethod, workspaceId: resolvedWorkspaceId, subagentId });
415
+ // ALWAYS log to analytics (even if identifier is missing)
416
+ try {
417
+ const result = await ctx.runMutation(api.analytics.log, {
418
+ event: "api_call",
419
+ provider,
420
+ identifier: identifier,
421
+ workspaceId: resolvedWorkspaceId,
422
+ metadata: { action, subagentId, authMethod: auth.authMethod },
423
+ });
424
+ console.log("[Proxy] Analytics logged:", result);
425
+ }
426
+ catch (e) {
427
+ console.error("[Proxy] Analytics logging failed:", e.message, e.stack);
428
+ }
429
+ // If we have a workspace, log and increment usage
430
+ if (resolvedWorkspaceId) {
431
+ try {
432
+ await ctx.runMutation(api.logs.createProxyLog, {
433
+ workspaceId: resolvedWorkspaceId,
434
+ provider,
435
+ action,
436
+ subagentId,
437
+ });
438
+ await ctx.runMutation(api.workspaces.incrementUsage, {
439
+ workspaceId: resolvedWorkspaceId,
440
+ });
441
+ console.log("[Proxy] Workspace logged for:", resolvedWorkspaceId);
442
+ return { valid: true, workspaceId: resolvedWorkspaceId, subagentId, authMethod: auth.authMethod };
443
+ }
444
+ catch (e) {
445
+ console.error("[Proxy] Workspace logging failed:", e.message);
446
+ }
447
+ }
448
+ // Return success regardless (don't block API calls)
449
+ return { valid: true, subagentId, authMethod: auth.authMethod };
450
+ }
451
+ // OPTIONS handler for CORS
452
+ http.route({
453
+ path: "/api/discover",
454
+ method: "OPTIONS",
455
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
456
+ });
457
+ http.route({
458
+ path: "/api/details",
459
+ method: "OPTIONS",
460
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
461
+ });
462
+ http.route({
463
+ path: "/api/balance",
464
+ method: "OPTIONS",
465
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
466
+ });
467
+ http.route({
468
+ path: "/api/purchase",
469
+ method: "OPTIONS",
470
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
471
+ });
472
+ http.route({
473
+ path: "/admin/grant-credits",
474
+ method: "OPTIONS",
475
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
476
+ });
477
+ // Discover APIs
478
+ http.route({
479
+ path: "/api/discover",
480
+ method: "POST",
481
+ handler: httpAction(async (ctx, request) => {
482
+ try {
483
+ const startTime = Date.now();
484
+ const body = await request.json();
485
+ const query = (body.query || "").toLowerCase();
486
+ // Get optional auth context
487
+ const sessionToken = request.headers.get("X-APIClaw-Session");
488
+ const userAgent = request.headers.get("User-Agent");
489
+ const results = Object.entries(PROVIDERS)
490
+ .filter(([id, provider]) => {
491
+ if (!query)
492
+ return true;
493
+ return (provider.name.toLowerCase().includes(query) ||
494
+ provider.description.toLowerCase().includes(query) ||
495
+ provider.category.toLowerCase().includes(query) ||
496
+ provider.tags.some((tag) => tag.includes(query)));
497
+ })
498
+ .map(([id, provider]) => ({
499
+ providerId: id,
500
+ ...provider,
501
+ }));
502
+ const responseTimeMs = Date.now() - startTime;
503
+ // Log the search (fire and forget)
504
+ if (query) {
505
+ ctx.runMutation(internal.searchLogs.logSearch, {
506
+ query: body.query || "", // Original query (not lowercased)
507
+ resultsCount: results.length,
508
+ matchedProviders: results.map(r => r.providerId),
509
+ sessionToken: sessionToken || undefined,
510
+ userAgent: userAgent || undefined,
511
+ responseTimeMs,
512
+ }).catch(() => { }); // Ignore errors, don't block response
513
+ }
514
+ return jsonResponse({ providers: results, total: results.length });
515
+ }
516
+ catch (e) {
517
+ return jsonResponse({ error: "Invalid request" }, 400);
518
+ }
519
+ }),
520
+ });
521
+ // Get provider details
522
+ http.route({
523
+ path: "/api/details",
524
+ method: "POST",
525
+ handler: httpAction(async (ctx, request) => {
526
+ try {
527
+ const body = await request.json();
528
+ const { providerId } = body;
529
+ if (!providerId) {
530
+ return jsonResponse({ error: "providerId required" }, 400);
531
+ }
532
+ const provider = PROVIDERS[providerId];
533
+ if (!provider) {
534
+ return jsonResponse({ error: "Provider not found" }, 404);
535
+ }
536
+ return jsonResponse({
537
+ providerId,
538
+ ...provider,
539
+ creditsPerDollar: getCreditsPerDollar(providerId),
540
+ documentation: `https://apiclaw.com/docs/${providerId}`,
541
+ });
542
+ }
543
+ catch (e) {
544
+ return jsonResponse({ error: "Invalid request" }, 400);
545
+ }
546
+ }),
547
+ });
548
+ // Check balance
549
+ http.route({
550
+ path: "/api/balance",
551
+ method: "GET",
552
+ handler: httpAction(async (ctx, request) => {
553
+ const url = new URL(request.url);
554
+ const agentId = url.searchParams.get("agentId");
555
+ if (!agentId) {
556
+ return jsonResponse({ error: "agentId required" }, 400);
557
+ }
558
+ const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
559
+ if (!credits) {
560
+ return jsonResponse({
561
+ agentId,
562
+ balanceUsd: 0,
563
+ currency: "USD",
564
+ message: "No account found. Top up to get started!",
565
+ });
566
+ }
567
+ return jsonResponse({
568
+ agentId: credits.agentId,
569
+ balanceUsd: credits.balanceUsd,
570
+ currency: credits.currency,
571
+ });
572
+ }),
573
+ });
574
+ // Purchase API access
575
+ http.route({
576
+ path: "/api/purchase",
577
+ method: "POST",
578
+ handler: httpAction(async (ctx, request) => {
579
+ try {
580
+ const body = await request.json();
581
+ const { agentId, providerId, amountUsd } = body;
582
+ if (!agentId || !providerId || !amountUsd) {
583
+ return jsonResponse({ error: "agentId, providerId, and amountUsd required" }, 400);
584
+ }
585
+ if (amountUsd < 1 || amountUsd > 1000) {
586
+ return jsonResponse({ error: "amountUsd must be between 1 and 1000" }, 400);
587
+ }
588
+ const provider = PROVIDERS[providerId];
589
+ if (!provider) {
590
+ return jsonResponse({ error: "Provider not found" }, 404);
591
+ }
592
+ // Check balance first
593
+ const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
594
+ if (!credits || credits.balanceUsd < amountUsd) {
595
+ return jsonResponse({
596
+ error: "Insufficient balance",
597
+ currentBalance: credits?.balanceUsd || 0,
598
+ required: amountUsd,
599
+ }, 402);
600
+ }
601
+ // Execute purchase
602
+ const purchase = await ctx.runMutation(api.purchases.purchaseAccess, {
603
+ agentId,
604
+ providerId,
605
+ amountUsd,
606
+ credentials: generateCredentials(providerId),
607
+ });
608
+ if (!purchase) {
609
+ return jsonResponse({ error: "Purchase failed" }, 500);
610
+ }
611
+ return jsonResponse({
612
+ success: true,
613
+ purchase: {
614
+ id: purchase._id,
615
+ providerId: purchase.providerId,
616
+ amountUsd: purchase.amountUsd,
617
+ creditsGranted: purchase.creditsGranted,
618
+ status: purchase.status,
619
+ },
620
+ message: `Successfully purchased $${amountUsd} of ${provider.name} credits`,
621
+ });
622
+ }
623
+ catch (e) {
624
+ return jsonResponse({ error: e.message || "Purchase failed" }, 400);
625
+ }
626
+ }),
627
+ });
628
+ // Admin: Grant credits
629
+ http.route({
630
+ path: "/admin/grant-credits",
631
+ method: "POST",
632
+ handler: httpAction(async (ctx, request) => {
633
+ try {
634
+ const body = await request.json();
635
+ const { agentId, amount, reason } = body;
636
+ if (!agentId || !amount) {
637
+ return jsonResponse({ error: "agentId and amount required" }, 400);
638
+ }
639
+ // TODO: Add admin auth check here
640
+ // For now, allow grants (this is for Hivr integration)
641
+ const result = await ctx.runMutation(api.credits.addCredits, {
642
+ agentId,
643
+ amountUsd: amount,
644
+ source: reason || "admin_grant",
645
+ });
646
+ return jsonResponse({
647
+ success: true,
648
+ agentId,
649
+ credited: amount,
650
+ newBalance: result?.balanceUsd,
651
+ reason,
652
+ });
653
+ }
654
+ catch (e) {
655
+ return jsonResponse({ error: e.message || "Grant failed" }, 400);
656
+ }
657
+ }),
658
+ });
659
+ // Helper functions
660
+ function getCreditsPerDollar(providerId) {
661
+ const rates = {
662
+ "46elks": 30,
663
+ twilio: 25,
664
+ resend: 1000,
665
+ brave_search: 200,
666
+ openrouter: 100,
667
+ elevenlabs: 3333,
668
+ };
669
+ return rates[providerId] || 100;
670
+ }
671
+ function generateCredentials(providerId) {
672
+ // In production, this would generate or retrieve actual API keys
673
+ // For now, return placeholder indicating how to use
674
+ return {
675
+ type: "apiclaw_proxy",
676
+ endpoint: `https://brilliant-puffin-712.convex.site/proxy/${providerId}`,
677
+ note: "Use APIClaw proxy endpoint. Credentials managed automatically.",
678
+ };
679
+ }
680
+ export default http;
681
+ // ==============================================
682
+ // DIRECT CALL PROXY ENDPOINTS
683
+ // ==============================================
684
+ // OpenRouter proxy
685
+ http.route({
686
+ path: "/proxy/openrouter",
687
+ method: "POST",
688
+ handler: httpAction(async (ctx, request) => {
689
+ // Validate session and log usage
690
+ await validateAndLogProxyCall(ctx, request, "openrouter", "chat");
691
+ const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY;
692
+ if (!OPENROUTER_KEY) {
693
+ return jsonResponse({ error: "OpenRouter not configured" }, 500);
694
+ }
695
+ try {
696
+ const body = await request.json();
697
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
698
+ method: "POST",
699
+ headers: {
700
+ "Authorization": `Bearer ${OPENROUTER_KEY}`,
701
+ "Content-Type": "application/json",
702
+ "HTTP-Referer": "https://apiclaw.cloud",
703
+ "X-Title": "APIClaw",
704
+ },
705
+ body: JSON.stringify(body),
706
+ });
707
+ const data = await response.json();
708
+ return jsonResponse(data, response.status);
709
+ }
710
+ catch (e) {
711
+ return jsonResponse({ error: e.message }, 500);
712
+ }
713
+ }),
714
+ });
715
+ // Brave Search proxy
716
+ http.route({
717
+ path: "/proxy/brave_search",
718
+ method: "POST",
719
+ handler: httpAction(async (ctx, request) => {
720
+ // Validate session and log usage
721
+ await validateAndLogProxyCall(ctx, request, "brave_search", "search");
722
+ const BRAVE_KEY = process.env.BRAVE_API_KEY;
723
+ if (!BRAVE_KEY) {
724
+ return jsonResponse({ error: "Brave Search not configured" }, 500);
725
+ }
726
+ try {
727
+ const body = await request.json();
728
+ const { query, count = 10 } = body;
729
+ const url = new URL("https://api.search.brave.com/res/v1/web/search");
730
+ url.searchParams.set("q", query);
731
+ url.searchParams.set("count", String(count));
732
+ const response = await fetch(url.toString(), {
733
+ headers: { "X-Subscription-Token": BRAVE_KEY },
734
+ });
735
+ const data = await response.json();
736
+ return jsonResponse(data, response.status);
737
+ }
738
+ catch (e) {
739
+ return jsonResponse({ error: e.message }, 500);
740
+ }
741
+ }),
742
+ });
743
+ // Resend email proxy
744
+ http.route({
745
+ path: "/proxy/resend",
746
+ method: "POST",
747
+ handler: httpAction(async (ctx, request) => {
748
+ // Validate session and log usage
749
+ await validateAndLogProxyCall(ctx, request, "resend", "send_email");
750
+ const RESEND_KEY = process.env.RESEND_API_KEY;
751
+ if (!RESEND_KEY) {
752
+ return jsonResponse({ error: "Resend not configured" }, 500);
753
+ }
754
+ try {
755
+ const body = await request.json();
756
+ const response = await fetch("https://api.resend.com/emails", {
757
+ method: "POST",
758
+ headers: {
759
+ "Authorization": `Bearer ${RESEND_KEY}`,
760
+ "Content-Type": "application/json",
761
+ },
762
+ body: JSON.stringify(body),
763
+ });
764
+ const data = await response.json();
765
+ return jsonResponse(data, response.status);
766
+ }
767
+ catch (e) {
768
+ return jsonResponse({ error: e.message }, 500);
769
+ }
770
+ }),
771
+ });
772
+ // ElevenLabs TTS proxy
773
+ http.route({
774
+ path: "/proxy/elevenlabs",
775
+ method: "POST",
776
+ handler: httpAction(async (ctx, request) => {
777
+ // Validate session and log usage
778
+ await validateAndLogProxyCall(ctx, request, "elevenlabs", "text_to_speech");
779
+ const ELEVENLABS_KEY = process.env.ELEVENLABS_API_KEY;
780
+ if (!ELEVENLABS_KEY) {
781
+ return jsonResponse({ error: "ElevenLabs not configured" }, 500);
782
+ }
783
+ try {
784
+ const body = await request.json();
785
+ const { text, voice_id = "21m00Tcm4TlvDq8ikWAM" } = body;
786
+ const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`, {
787
+ method: "POST",
788
+ headers: {
789
+ "xi-api-key": ELEVENLABS_KEY,
790
+ "Content-Type": "application/json",
791
+ },
792
+ body: JSON.stringify({
793
+ text,
794
+ model_id: "eleven_turbo_v2",
795
+ }),
796
+ });
797
+ if (!response.ok) {
798
+ const error = await response.text();
799
+ return jsonResponse({ error }, response.status);
800
+ }
801
+ // Return audio as base64
802
+ const arrayBuffer = await response.arrayBuffer();
803
+ const base64 = Buffer.from(arrayBuffer).toString("base64");
804
+ return jsonResponse({
805
+ audio_base64: base64,
806
+ content_type: "audio/mpeg",
807
+ });
808
+ }
809
+ catch (e) {
810
+ return jsonResponse({ error: e.message }, 500);
811
+ }
812
+ }),
813
+ });
814
+ http.route({
815
+ path: "/proxy/openrouter",
816
+ method: "OPTIONS",
817
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
818
+ });
819
+ http.route({
820
+ path: "/proxy/brave_search",
821
+ method: "OPTIONS",
822
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
823
+ });
824
+ http.route({
825
+ path: "/proxy/resend",
826
+ method: "OPTIONS",
827
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
828
+ });
829
+ http.route({
830
+ path: "/proxy/elevenlabs",
831
+ method: "OPTIONS",
832
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
833
+ });
834
+ // 46elks SMS proxy
835
+ http.route({
836
+ path: "/proxy/46elks",
837
+ method: "POST",
838
+ handler: httpAction(async (ctx, request) => {
839
+ // Validate session and log usage
840
+ await validateAndLogProxyCall(ctx, request, "46elks", "send_sms");
841
+ const ELKS_USER = process.env.ELKS_API_USER;
842
+ const ELKS_PASS = process.env.ELKS_API_PASSWORD;
843
+ if (!ELKS_USER || !ELKS_PASS) {
844
+ return jsonResponse({ error: "46elks not configured" }, 500);
845
+ }
846
+ try {
847
+ const body = await request.json();
848
+ const { to, message, from = "APIClaw" } = body;
849
+ const auth = btoa(`${ELKS_USER}:${ELKS_PASS}`);
850
+ const response = await fetch("https://api.46elks.com/a1/sms", {
851
+ method: "POST",
852
+ headers: {
853
+ "Authorization": `Basic ${auth}`,
854
+ "Content-Type": "application/x-www-form-urlencoded",
855
+ },
856
+ body: new URLSearchParams({ from, to, message }),
857
+ });
858
+ const data = await response.json();
859
+ return jsonResponse(data, response.status);
860
+ }
861
+ catch (e) {
862
+ return jsonResponse({ error: e.message }, 500);
863
+ }
864
+ }),
865
+ });
866
+ // Twilio SMS proxy
867
+ http.route({
868
+ path: "/proxy/twilio",
869
+ method: "POST",
870
+ handler: httpAction(async (ctx, request) => {
871
+ // Validate session and log usage
872
+ await validateAndLogProxyCall(ctx, request, "twilio", "send_sms");
873
+ const TWILIO_SID = process.env.TWILIO_ACCOUNT_SID;
874
+ const TWILIO_TOKEN = process.env.TWILIO_AUTH_TOKEN;
875
+ if (!TWILIO_SID || !TWILIO_TOKEN) {
876
+ return jsonResponse({ error: "Twilio not configured" }, 500);
877
+ }
878
+ try {
879
+ const body = await request.json();
880
+ const { to, message, from } = body;
881
+ if (!from) {
882
+ return jsonResponse({ error: "Twilio requires 'from' number" }, 400);
883
+ }
884
+ const auth = btoa(`${TWILIO_SID}:${TWILIO_TOKEN}`);
885
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json`, {
886
+ method: "POST",
887
+ headers: {
888
+ "Authorization": `Basic ${auth}`,
889
+ "Content-Type": "application/x-www-form-urlencoded",
890
+ },
891
+ body: new URLSearchParams({ To: to, From: from, Body: message }),
892
+ });
893
+ const data = await response.json();
894
+ return jsonResponse(data, response.status);
895
+ }
896
+ catch (e) {
897
+ return jsonResponse({ error: e.message }, 500);
898
+ }
899
+ }),
900
+ });
901
+ // CORS for new endpoints
902
+ http.route({
903
+ path: "/proxy/46elks",
904
+ method: "OPTIONS",
905
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
906
+ });
907
+ http.route({
908
+ path: "/proxy/twilio",
909
+ method: "OPTIONS",
910
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
911
+ });
912
+ // GitHub API proxy
913
+ http.route({
914
+ path: "/proxy/github",
915
+ method: "POST",
916
+ handler: httpAction(async (ctx, request) => {
917
+ // Validate session and log usage
918
+ const body = await request.json();
919
+ const action = body.action || "search_repos";
920
+ await validateAndLogProxyCall(ctx, request, "github", action);
921
+ const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
922
+ if (!GITHUB_TOKEN) {
923
+ return jsonResponse({ error: "GitHub not configured" }, 500);
924
+ }
925
+ try {
926
+ const { action, ...params } = body;
927
+ let url;
928
+ let method = "GET";
929
+ let fetchBody;
930
+ // Route based on action
931
+ switch (action) {
932
+ case "search_repos":
933
+ const { query, sort = "stars", limit = 10 } = params;
934
+ url = `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`;
935
+ break;
936
+ case "get_repo":
937
+ const { owner, repo } = params;
938
+ url = `https://api.github.com/repos/${owner}/${repo}`;
939
+ break;
940
+ case "list_issues":
941
+ const { owner: issueOwner, repo: issueRepo, state = "open", limit: issueLimit = 10 } = params;
942
+ url = `https://api.github.com/repos/${issueOwner}/${issueRepo}/issues?state=${state}&per_page=${issueLimit}`;
943
+ break;
944
+ case "create_issue":
945
+ const { owner: createOwner, repo: createRepo, title, body: issueBody = "" } = params;
946
+ url = `https://api.github.com/repos/${createOwner}/${createRepo}/issues`;
947
+ method = "POST";
948
+ fetchBody = JSON.stringify({ title, body: issueBody });
949
+ break;
950
+ case "get_file":
951
+ const { owner: fileOwner, repo: fileRepo, path } = params;
952
+ url = `https://api.github.com/repos/${fileOwner}/${fileRepo}/contents/${path}`;
953
+ break;
954
+ default:
955
+ return jsonResponse({ error: `Unknown action: ${action}` }, 400);
956
+ }
957
+ const response = await fetch(url, {
958
+ method,
959
+ headers: {
960
+ "Authorization": `Bearer ${GITHUB_TOKEN}`,
961
+ "Accept": "application/vnd.github+json",
962
+ "User-Agent": "APIClaw",
963
+ ...(fetchBody ? { "Content-Type": "application/json" } : {}),
964
+ },
965
+ ...(fetchBody ? { body: fetchBody } : {}),
966
+ });
967
+ const data = await response.json();
968
+ return jsonResponse(data, response.status);
969
+ }
970
+ catch (e) {
971
+ return jsonResponse({ error: e.message }, 500);
972
+ }
973
+ }),
974
+ });
975
+ http.route({
976
+ path: "/proxy/github",
977
+ method: "OPTIONS",
978
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
979
+ });
980
+ // ==============================================
981
+ // SERPER (Google Search) PROXY
982
+ // ==============================================
983
+ http.route({
984
+ path: "/proxy/serper",
985
+ method: "POST",
986
+ handler: httpAction(async (ctx, request) => {
987
+ await validateAndLogProxyCall(ctx, request, "serper", "search");
988
+ const SERPER_KEY = process.env.SERPER_API_KEY;
989
+ if (!SERPER_KEY) {
990
+ return jsonResponse({ error: "Serper not configured" }, 500);
991
+ }
992
+ try {
993
+ const body = await request.json();
994
+ const { query, q, num = 10, gl = "us", hl = "en" } = body;
995
+ const searchQuery = query || q;
996
+ if (!searchQuery) {
997
+ return jsonResponse({ error: "query required" }, 400);
998
+ }
999
+ const response = await fetch("https://google.serper.dev/search", {
1000
+ method: "POST",
1001
+ headers: {
1002
+ "X-API-KEY": SERPER_KEY,
1003
+ "Content-Type": "application/json",
1004
+ },
1005
+ body: JSON.stringify({ q: searchQuery, num, gl, hl }),
1006
+ });
1007
+ const data = await response.json();
1008
+ return jsonResponse(data, response.status);
1009
+ }
1010
+ catch (e) {
1011
+ return jsonResponse({ error: e.message }, 500);
1012
+ }
1013
+ }),
1014
+ });
1015
+ http.route({
1016
+ path: "/proxy/serper",
1017
+ method: "OPTIONS",
1018
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1019
+ });
1020
+ // ==============================================
1021
+ // FIRECRAWL (Web Scraping) PROXY
1022
+ // ==============================================
1023
+ http.route({
1024
+ path: "/proxy/firecrawl",
1025
+ method: "POST",
1026
+ handler: httpAction(async (ctx, request) => {
1027
+ await validateAndLogProxyCall(ctx, request, "firecrawl", "scrape");
1028
+ const FIRECRAWL_KEY = process.env.FIRECRAWL_API_KEY;
1029
+ if (!FIRECRAWL_KEY) {
1030
+ return jsonResponse({ error: "Firecrawl not configured" }, 500);
1031
+ }
1032
+ try {
1033
+ const body = await request.json();
1034
+ const { url, formats = ["markdown"], onlyMainContent = true } = body;
1035
+ if (!url) {
1036
+ return jsonResponse({ error: "url required" }, 400);
1037
+ }
1038
+ const response = await fetch("https://api.firecrawl.dev/v1/scrape", {
1039
+ method: "POST",
1040
+ headers: {
1041
+ Authorization: `Bearer ${FIRECRAWL_KEY}`,
1042
+ "Content-Type": "application/json",
1043
+ },
1044
+ body: JSON.stringify({ url, formats, onlyMainContent }),
1045
+ });
1046
+ const data = await response.json();
1047
+ return jsonResponse(data, response.status);
1048
+ }
1049
+ catch (e) {
1050
+ return jsonResponse({ error: e.message }, 500);
1051
+ }
1052
+ }),
1053
+ });
1054
+ http.route({
1055
+ path: "/proxy/firecrawl",
1056
+ method: "OPTIONS",
1057
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1058
+ });
1059
+ // ==============================================
1060
+ // GROQ (LLM) PROXY
1061
+ // ==============================================
1062
+ http.route({
1063
+ path: "/proxy/groq",
1064
+ method: "POST",
1065
+ handler: httpAction(async (ctx, request) => {
1066
+ await validateAndLogProxyCall(ctx, request, "groq", "chat");
1067
+ const GROQ_KEY = process.env.GROQ_API_KEY;
1068
+ if (!GROQ_KEY) {
1069
+ return jsonResponse({ error: "Groq not configured" }, 500);
1070
+ }
1071
+ try {
1072
+ const body = await request.json();
1073
+ const { model = "llama-3.3-70b-versatile", messages, temperature = 0.7, max_tokens = 1024 } = body;
1074
+ if (!messages) {
1075
+ return jsonResponse({ error: "messages required" }, 400);
1076
+ }
1077
+ const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
1078
+ method: "POST",
1079
+ headers: {
1080
+ Authorization: `Bearer ${GROQ_KEY}`,
1081
+ "Content-Type": "application/json",
1082
+ },
1083
+ body: JSON.stringify({ model, messages, temperature, max_tokens }),
1084
+ });
1085
+ const data = await response.json();
1086
+ return jsonResponse(data, response.status);
1087
+ }
1088
+ catch (e) {
1089
+ return jsonResponse({ error: e.message }, 500);
1090
+ }
1091
+ }),
1092
+ });
1093
+ http.route({
1094
+ path: "/proxy/groq",
1095
+ method: "OPTIONS",
1096
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1097
+ });
1098
+ // ==============================================
1099
+ // MISTRAL (LLM/Embeddings) PROXY
1100
+ // ==============================================
1101
+ http.route({
1102
+ path: "/proxy/mistral",
1103
+ method: "POST",
1104
+ handler: httpAction(async (ctx, request) => {
1105
+ await validateAndLogProxyCall(ctx, request, "mistral", "chat");
1106
+ const MISTRAL_KEY = process.env.MISTRAL_API_KEY;
1107
+ if (!MISTRAL_KEY) {
1108
+ return jsonResponse({ error: "Mistral not configured" }, 500);
1109
+ }
1110
+ try {
1111
+ const body = await request.json();
1112
+ const { model = "mistral-small-latest", messages, temperature = 0.7, max_tokens = 1024 } = body;
1113
+ if (!messages) {
1114
+ return jsonResponse({ error: "messages required" }, 400);
1115
+ }
1116
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
1117
+ method: "POST",
1118
+ headers: {
1119
+ Authorization: `Bearer ${MISTRAL_KEY}`,
1120
+ "Content-Type": "application/json",
1121
+ },
1122
+ body: JSON.stringify({ model, messages, temperature, max_tokens }),
1123
+ });
1124
+ const data = await response.json();
1125
+ return jsonResponse(data, response.status);
1126
+ }
1127
+ catch (e) {
1128
+ return jsonResponse({ error: e.message }, 500);
1129
+ }
1130
+ }),
1131
+ });
1132
+ http.route({
1133
+ path: "/proxy/mistral",
1134
+ method: "OPTIONS",
1135
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1136
+ });
1137
+ // ==============================================
1138
+ // COHERE (LLM/Rerank) PROXY
1139
+ // ==============================================
1140
+ http.route({
1141
+ path: "/proxy/cohere",
1142
+ method: "POST",
1143
+ handler: httpAction(async (ctx, request) => {
1144
+ await validateAndLogProxyCall(ctx, request, "cohere", "chat");
1145
+ const COHERE_KEY = process.env.COHERE_API_KEY;
1146
+ if (!COHERE_KEY) {
1147
+ return jsonResponse({ error: "Cohere not configured" }, 500);
1148
+ }
1149
+ try {
1150
+ const body = await request.json();
1151
+ const { model = "command-a-03-2025", message, chat_history, temperature = 0.7, max_tokens = 1024 } = body;
1152
+ if (!message) {
1153
+ return jsonResponse({ error: "message required" }, 400);
1154
+ }
1155
+ const response = await fetch("https://api.cohere.com/v2/chat", {
1156
+ method: "POST",
1157
+ headers: {
1158
+ Authorization: `Bearer ${COHERE_KEY}`,
1159
+ "Content-Type": "application/json",
1160
+ },
1161
+ body: JSON.stringify({ model, message, chat_history, temperature, max_tokens }),
1162
+ });
1163
+ const data = await response.json();
1164
+ return jsonResponse(data, response.status);
1165
+ }
1166
+ catch (e) {
1167
+ return jsonResponse({ error: e.message }, 500);
1168
+ }
1169
+ }),
1170
+ });
1171
+ http.route({
1172
+ path: "/proxy/cohere",
1173
+ method: "OPTIONS",
1174
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1175
+ });
1176
+ // ==============================================
1177
+ // REPLICATE (ML Models) PROXY
1178
+ // ==============================================
1179
+ http.route({
1180
+ path: "/proxy/replicate",
1181
+ method: "POST",
1182
+ handler: httpAction(async (ctx, request) => {
1183
+ await validateAndLogProxyCall(ctx, request, "replicate", "prediction");
1184
+ const REPLICATE_KEY = process.env.REPLICATE_API_TOKEN;
1185
+ if (!REPLICATE_KEY) {
1186
+ return jsonResponse({ error: "Replicate not configured" }, 500);
1187
+ }
1188
+ try {
1189
+ const body = await request.json();
1190
+ const { model, input, version } = body;
1191
+ if (!model && !version) {
1192
+ return jsonResponse({ error: "model or version required" }, 400);
1193
+ }
1194
+ const endpoint = version
1195
+ ? "https://api.replicate.com/v1/predictions"
1196
+ : `https://api.replicate.com/v1/models/${model}/predictions`;
1197
+ const payload = version ? { version, input } : { input };
1198
+ const response = await fetch(endpoint, {
1199
+ method: "POST",
1200
+ headers: {
1201
+ Authorization: `Bearer ${REPLICATE_KEY}`,
1202
+ "Content-Type": "application/json",
1203
+ Prefer: "wait",
1204
+ },
1205
+ body: JSON.stringify(payload),
1206
+ });
1207
+ const data = await response.json();
1208
+ return jsonResponse(data, response.status);
1209
+ }
1210
+ catch (e) {
1211
+ return jsonResponse({ error: e.message }, 500);
1212
+ }
1213
+ }),
1214
+ });
1215
+ http.route({
1216
+ path: "/proxy/replicate",
1217
+ method: "OPTIONS",
1218
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1219
+ });
1220
+ // ==============================================
1221
+ // DEEPGRAM (Speech-to-Text) PROXY
1222
+ // ==============================================
1223
+ http.route({
1224
+ path: "/proxy/deepgram",
1225
+ method: "POST",
1226
+ handler: httpAction(async (ctx, request) => {
1227
+ await validateAndLogProxyCall(ctx, request, "deepgram", "transcribe");
1228
+ const DEEPGRAM_KEY = process.env.DEEPGRAM_API_KEY;
1229
+ if (!DEEPGRAM_KEY) {
1230
+ return jsonResponse({ error: "Deepgram not configured" }, 500);
1231
+ }
1232
+ try {
1233
+ const body = await request.json();
1234
+ const { url, model = "nova-3", language = "en", smart_format = true } = body;
1235
+ if (!url) {
1236
+ return jsonResponse({ error: "url required (audio file URL)" }, 400);
1237
+ }
1238
+ const params = new URLSearchParams({
1239
+ model,
1240
+ language,
1241
+ smart_format: String(smart_format),
1242
+ });
1243
+ const response = await fetch(`https://api.deepgram.com/v1/listen?${params}`, {
1244
+ method: "POST",
1245
+ headers: {
1246
+ Authorization: `Token ${DEEPGRAM_KEY}`,
1247
+ "Content-Type": "application/json",
1248
+ },
1249
+ body: JSON.stringify({ url }),
1250
+ });
1251
+ const data = await response.json();
1252
+ return jsonResponse(data, response.status);
1253
+ }
1254
+ catch (e) {
1255
+ return jsonResponse({ error: e.message }, 500);
1256
+ }
1257
+ }),
1258
+ });
1259
+ http.route({
1260
+ path: "/proxy/deepgram",
1261
+ method: "OPTIONS",
1262
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1263
+ });
1264
+ // ==============================================
1265
+ // E2B (Code Sandbox) PROXY
1266
+ // ==============================================
1267
+ http.route({
1268
+ path: "/proxy/e2b",
1269
+ method: "POST",
1270
+ handler: httpAction(async (ctx, request) => {
1271
+ await validateAndLogProxyCall(ctx, request, "e2b", "execute");
1272
+ const E2B_KEY = process.env.E2B_API_KEY;
1273
+ if (!E2B_KEY) {
1274
+ return jsonResponse({ error: "E2B not configured" }, 500);
1275
+ }
1276
+ try {
1277
+ const body = await request.json();
1278
+ const { code, language = "python", template = "base" } = body;
1279
+ if (!code) {
1280
+ return jsonResponse({ error: "code required" }, 400);
1281
+ }
1282
+ const response = await fetch("https://api.e2b.dev/sandboxes", {
1283
+ method: "POST",
1284
+ headers: {
1285
+ "X-API-Key": E2B_KEY,
1286
+ "Content-Type": "application/json",
1287
+ },
1288
+ body: JSON.stringify({ templateID: template, metadata: { language } }),
1289
+ });
1290
+ const sandbox = await response.json();
1291
+ if (!response.ok) {
1292
+ return jsonResponse(sandbox, response.status);
1293
+ }
1294
+ const execResponse = await fetch(`https://api.e2b.dev/sandboxes/${sandbox.sandboxID}/code/execution`, {
1295
+ method: "POST",
1296
+ headers: {
1297
+ "X-API-Key": E2B_KEY,
1298
+ "Content-Type": "application/json",
1299
+ },
1300
+ body: JSON.stringify({ code, language }),
1301
+ });
1302
+ const result = await execResponse.json();
1303
+ return jsonResponse(result, execResponse.status);
1304
+ }
1305
+ catch (e) {
1306
+ return jsonResponse({ error: e.message }, 500);
1307
+ }
1308
+ }),
1309
+ });
1310
+ http.route({
1311
+ path: "/proxy/e2b",
1312
+ method: "OPTIONS",
1313
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1314
+ });
1315
+ // ==============================================
1316
+ // TOGETHER AI (Open-source LLM Inference) PROXY
1317
+ // ==============================================
1318
+ http.route({
1319
+ path: "/proxy/together",
1320
+ method: "POST",
1321
+ handler: httpAction(async (ctx, request) => {
1322
+ await validateAndLogProxyCall(ctx, request, "together", "chat");
1323
+ const TOGETHER_KEY = process.env.TOGETHER_API_KEY;
1324
+ if (!TOGETHER_KEY) {
1325
+ return jsonResponse({ error: "Together AI not configured" }, 500);
1326
+ }
1327
+ try {
1328
+ const body = await request.json();
1329
+ const { model = "meta-llama/Llama-3.3-70B-Instruct-Turbo", messages, temperature = 0.7, max_tokens = 1024 } = body;
1330
+ if (!messages || !Array.isArray(messages)) {
1331
+ return jsonResponse({ error: "messages array required" }, 400);
1332
+ }
1333
+ const response = await fetch("https://api.together.xyz/v1/chat/completions", {
1334
+ method: "POST",
1335
+ headers: {
1336
+ Authorization: `Bearer ${TOGETHER_KEY}`,
1337
+ "Content-Type": "application/json",
1338
+ },
1339
+ body: JSON.stringify({ model, messages, temperature, max_tokens }),
1340
+ });
1341
+ const data = await response.json();
1342
+ return jsonResponse(data, response.status);
1343
+ }
1344
+ catch (e) {
1345
+ return jsonResponse({ error: e.message }, 500);
1346
+ }
1347
+ }),
1348
+ });
1349
+ http.route({
1350
+ path: "/proxy/together",
1351
+ method: "OPTIONS",
1352
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1353
+ });
1354
+ // ==============================================
1355
+ // STABILITY AI (Image Generation) PROXY
1356
+ // ==============================================
1357
+ http.route({
1358
+ path: "/proxy/stability",
1359
+ method: "POST",
1360
+ handler: httpAction(async (ctx, request) => {
1361
+ await validateAndLogProxyCall(ctx, request, "stability", "generate");
1362
+ const STABILITY_KEY = process.env.STABILITY_API_KEY;
1363
+ if (!STABILITY_KEY) {
1364
+ return jsonResponse({ error: "Stability AI not configured" }, 500);
1365
+ }
1366
+ try {
1367
+ const body = await request.json();
1368
+ const { prompt, model = "sd3.5-large", output_format = "png", aspect_ratio = "1:1" } = body;
1369
+ if (!prompt) {
1370
+ return jsonResponse({ error: "prompt required" }, 400);
1371
+ }
1372
+ const formData = new FormData();
1373
+ formData.append("prompt", prompt);
1374
+ formData.append("output_format", output_format);
1375
+ formData.append("aspect_ratio", aspect_ratio);
1376
+ const response = await fetch(`https://api.stability.ai/v2beta/stable-image/generate/${model}`, {
1377
+ method: "POST",
1378
+ headers: {
1379
+ Authorization: `Bearer ${STABILITY_KEY}`,
1380
+ Accept: "application/json",
1381
+ },
1382
+ body: formData,
1383
+ });
1384
+ const data = await response.json();
1385
+ return jsonResponse(data, response.status);
1386
+ }
1387
+ catch (e) {
1388
+ return jsonResponse({ error: e.message }, 500);
1389
+ }
1390
+ }),
1391
+ });
1392
+ http.route({
1393
+ path: "/proxy/stability",
1394
+ method: "OPTIONS",
1395
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1396
+ });
1397
+ // ==============================================
1398
+ // ASSEMBLYAI (Audio Intelligence) PROXY
1399
+ // ==============================================
1400
+ http.route({
1401
+ path: "/proxy/assemblyai",
1402
+ method: "POST",
1403
+ handler: httpAction(async (ctx, request) => {
1404
+ await validateAndLogProxyCall(ctx, request, "assemblyai", "transcribe");
1405
+ const ASSEMBLYAI_KEY = process.env.ASSEMBLYAI_API_KEY;
1406
+ if (!ASSEMBLYAI_KEY) {
1407
+ return jsonResponse({ error: "AssemblyAI not configured" }, 500);
1408
+ }
1409
+ try {
1410
+ const body = await request.json();
1411
+ const { audio_url, language_detection = true, speaker_labels = true } = body;
1412
+ if (!audio_url) {
1413
+ return jsonResponse({ error: "audio_url required" }, 400);
1414
+ }
1415
+ const response = await fetch("https://api.assemblyai.com/v2/transcript", {
1416
+ method: "POST",
1417
+ headers: {
1418
+ Authorization: ASSEMBLYAI_KEY,
1419
+ "Content-Type": "application/json",
1420
+ },
1421
+ body: JSON.stringify({ audio_url, language_detection, speaker_labels }),
1422
+ });
1423
+ const data = await response.json();
1424
+ return jsonResponse(data, response.status);
1425
+ }
1426
+ catch (e) {
1427
+ return jsonResponse({ error: e.message }, 500);
1428
+ }
1429
+ }),
1430
+ });
1431
+ http.route({
1432
+ path: "/proxy/assemblyai",
1433
+ method: "OPTIONS",
1434
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1435
+ });
1436
+ // ==============================================
1437
+ // APILAYER (Multi-API: Exchange, Stocks, Aviation, etc.) PROXY
1438
+ // ==============================================
1439
+ http.route({
1440
+ path: "/proxy/apilayer",
1441
+ method: "POST",
1442
+ handler: httpAction(async (ctx, request) => {
1443
+ await validateAndLogProxyCall(ctx, request, "apilayer", "call");
1444
+ const APILAYER_KEY = process.env.APILAYER_API_KEY;
1445
+ if (!APILAYER_KEY) {
1446
+ return jsonResponse({ error: "APILayer not configured" }, 500);
1447
+ }
1448
+ try {
1449
+ const body = await request.json();
1450
+ const { service, endpoint, params = {} } = body;
1451
+ if (!service || !endpoint) {
1452
+ return jsonResponse({ error: "service and endpoint required (e.g. service:'exchangerates', endpoint:'/latest')" }, 400);
1453
+ }
1454
+ const queryString = new URLSearchParams(params).toString();
1455
+ const url = `https://api.apilayer.com/${service}${endpoint}${queryString ? '?' + queryString : ''}`;
1456
+ const response = await fetch(url, {
1457
+ method: "GET",
1458
+ headers: {
1459
+ apikey: APILAYER_KEY,
1460
+ },
1461
+ });
1462
+ const data = await response.json();
1463
+ return jsonResponse(data, response.status);
1464
+ }
1465
+ catch (e) {
1466
+ return jsonResponse({ error: e.message }, 500);
1467
+ }
1468
+ }),
1469
+ });
1470
+ http.route({
1471
+ path: "/proxy/apilayer",
1472
+ method: "OPTIONS",
1473
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1474
+ });
1475
+ // ==============================================
1476
+ // WORKSPACE / MAGIC LINK ENDPOINTS
1477
+ // ==============================================
1478
+ // Create magic link and send email
1479
+ http.route({
1480
+ path: "/workspace/magic-link",
1481
+ method: "POST",
1482
+ handler: httpAction(async (ctx, request) => {
1483
+ try {
1484
+ const body = await request.json();
1485
+ const { email, fingerprint } = body;
1486
+ if (!email || !email.includes("@")) {
1487
+ return jsonResponse({ error: "Valid email required" }, 400);
1488
+ }
1489
+ // Create magic link
1490
+ const result = await ctx.runMutation(api.workspaces.createMagicLink, {
1491
+ email: email.toLowerCase(),
1492
+ fingerprint,
1493
+ });
1494
+ // Send email directly - SIMPLE HTML (complex tables get stripped by Gmail)
1495
+ const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${result.token}`;
1496
+ const html = `<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;">
1497
+ <h1>🦞 APIClaw</h1>
1498
+ <h2>An AI Agent Wants to Connect</h2>
1499
+ <p>Click below to verify your email and activate your workspace.</p>
1500
+ <p><a href="${verifyUrl}" style="background:#ef4444;color:white;padding:14px 32px;border-radius:8px;text-decoration:none;display:inline-block;">Verify Email</a></p>
1501
+ <p style="color:#666;font-size:13px;">Free tier: 50 API calls. This link expires in 1 hour.</p>
1502
+ <p style="color:#999;font-size:11px;">Or copy this link: ${verifyUrl}</p>
1503
+ </div>`;
1504
+ const RESEND_KEY = process.env.RESEND_API_KEY;
1505
+ if (!RESEND_KEY) {
1506
+ console.error("RESEND_API_KEY not configured");
1507
+ return jsonResponse({ error: "Email service not configured" }, 500);
1508
+ }
1509
+ const emailResponse = await fetch("https://api.resend.com/emails", {
1510
+ method: "POST",
1511
+ headers: {
1512
+ "Authorization": `Bearer ${RESEND_KEY}`,
1513
+ "Content-Type": "application/json",
1514
+ },
1515
+ body: JSON.stringify({
1516
+ from: "APIClaw <noreply@apiclaw.cloud>",
1517
+ to: email.toLowerCase(),
1518
+ subject: "🦞 Verify Your Email — APIClaw",
1519
+ html: html,
1520
+ }),
1521
+ });
1522
+ if (!emailResponse.ok) {
1523
+ const errorText = await emailResponse.text();
1524
+ console.error("Resend error:", emailResponse.status, errorText);
1525
+ return jsonResponse({ error: "Failed to send email", details: errorText }, 500);
1526
+ }
1527
+ const emailResult = await emailResponse.json();
1528
+ console.log("Email sent successfully:", emailResult.id);
1529
+ return jsonResponse({
1530
+ success: true,
1531
+ token: result.token,
1532
+ expiresAt: result.expiresAt,
1533
+ message: "Magic link sent! Check your email.",
1534
+ emailId: emailResult.id,
1535
+ });
1536
+ }
1537
+ catch (e) {
1538
+ console.error("Magic link error:", e);
1539
+ return jsonResponse({ error: e.message || "Failed to create magic link" }, 500);
1540
+ }
1541
+ }),
1542
+ });
1543
+ http.route({
1544
+ path: "/workspace/magic-link",
1545
+ method: "OPTIONS",
1546
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1547
+ });
1548
+ // Poll magic link status (for agents to check if user clicked)
1549
+ http.route({
1550
+ path: "/workspace/poll",
1551
+ method: "GET",
1552
+ handler: httpAction(async (ctx, request) => {
1553
+ const url = new URL(request.url);
1554
+ const token = url.searchParams.get("token");
1555
+ if (!token) {
1556
+ return jsonResponse({ error: "token required" }, 400);
1557
+ }
1558
+ const result = await ctx.runQuery(api.workspaces.pollMagicLink, { token });
1559
+ return jsonResponse(result);
1560
+ }),
1561
+ });
1562
+ http.route({
1563
+ path: "/workspace/poll",
1564
+ method: "OPTIONS",
1565
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1566
+ });
1567
+ // Verify session token
1568
+ http.route({
1569
+ path: "/workspace/verify-session",
1570
+ method: "GET",
1571
+ handler: httpAction(async (ctx, request) => {
1572
+ const url = new URL(request.url);
1573
+ const sessionToken = url.searchParams.get("sessionToken");
1574
+ if (!sessionToken) {
1575
+ return jsonResponse({ error: "sessionToken required" }, 400);
1576
+ }
1577
+ const result = await ctx.runQuery(api.workspaces.verifySession, { sessionToken });
1578
+ if (!result) {
1579
+ return jsonResponse({ error: "Invalid or expired session" }, 401);
1580
+ }
1581
+ return jsonResponse(result);
1582
+ }),
1583
+ });
1584
+ http.route({
1585
+ path: "/workspace/verify-session",
1586
+ method: "OPTIONS",
1587
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1588
+ });
1589
+ // Get workspace by email
1590
+ http.route({
1591
+ path: "/workspace/by-email",
1592
+ method: "GET",
1593
+ handler: httpAction(async (ctx, request) => {
1594
+ const url = new URL(request.url);
1595
+ const email = url.searchParams.get("email");
1596
+ if (!email) {
1597
+ return jsonResponse({ error: "email required" }, 400);
1598
+ }
1599
+ const result = await ctx.runQuery(api.workspaces.getByEmail, { email });
1600
+ if (!result) {
1601
+ return jsonResponse({ exists: false });
1602
+ }
1603
+ return jsonResponse({ exists: true, workspace: result });
1604
+ }),
1605
+ });
1606
+ http.route({
1607
+ path: "/workspace/by-email",
1608
+ method: "OPTIONS",
1609
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1610
+ });
1611
+ // Send reminder email
1612
+ http.route({
1613
+ path: "/workspace/send-reminder",
1614
+ method: "POST",
1615
+ handler: httpAction(async (ctx, request) => {
1616
+ try {
1617
+ const body = await request.json();
1618
+ const { email, token } = body;
1619
+ if (!email || !token) {
1620
+ return jsonResponse({ error: "email and token required" }, 400);
1621
+ }
1622
+ await ctx.runAction(api.email.sendReminderEmail, { email, token });
1623
+ return jsonResponse({ success: true });
1624
+ }
1625
+ catch (e) {
1626
+ return jsonResponse({ error: e.message }, 500);
1627
+ }
1628
+ }),
1629
+ });
1630
+ http.route({
1631
+ path: "/workspace/send-reminder",
1632
+ method: "OPTIONS",
1633
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1634
+ });
1635
+ // ==============================================
1636
+ // STRIPE BILLING ENDPOINTS
1637
+ // ==============================================
1638
+ // Create checkout session
1639
+ http.route({
1640
+ path: "/api/billing/checkout",
1641
+ method: "POST",
1642
+ handler: createCheckoutSession,
1643
+ });
1644
+ http.route({
1645
+ path: "/api/billing/checkout",
1646
+ method: "OPTIONS",
1647
+ handler: checkoutOptions,
1648
+ });
1649
+ // Create billing portal session
1650
+ http.route({
1651
+ path: "/api/billing/portal",
1652
+ method: "POST",
1653
+ handler: createPortalSession,
1654
+ });
1655
+ http.route({
1656
+ path: "/api/billing/portal",
1657
+ method: "OPTIONS",
1658
+ handler: portalOptions,
1659
+ });
1660
+ // Stripe webhook handler
1661
+ http.route({
1662
+ path: "/api/webhooks/stripe",
1663
+ method: "POST",
1664
+ handler: handleStripeWebhook,
1665
+ });
1666
+ http.route({
1667
+ path: "/api/webhooks/stripe",
1668
+ method: "OPTIONS",
1669
+ handler: webhookOptions,
1670
+ });
1671
+ // Test endpoint to debug logging
1672
+ http.route({
1673
+ path: "/proxy/test-logging",
1674
+ method: "POST",
1675
+ handler: httpAction(async (ctx, request) => {
1676
+ const identifier = request.headers.get("X-APIClaw-Identifier");
1677
+ try {
1678
+ const logId = await ctx.runMutation(api.analytics.log, {
1679
+ event: "test_endpoint",
1680
+ provider: "test",
1681
+ identifier: identifier || "test",
1682
+ metadata: { test: true },
1683
+ });
1684
+ return jsonResponse({
1685
+ success: true,
1686
+ identifier,
1687
+ logId,
1688
+ message: "Logged successfully"
1689
+ });
1690
+ }
1691
+ catch (e) {
1692
+ return jsonResponse({
1693
+ success: false,
1694
+ error: e.message,
1695
+ stack: e.stack
1696
+ }, 500);
1697
+ }
1698
+ }),
1699
+ });
1700
+ // ==============================================
1701
+ // GATEWAY v1 — Unified API Layer for AI Agents
1702
+ // ==============================================
1703
+ // OpenAI-compatible /v1/chat/completions endpoint.
1704
+ // Accepts: Authorization: Bearer sk-claw-...
1705
+ // Routes to the best available LLM provider (OpenRouter by default).
1706
+ // This is what OpenClaw and any agent configures as their API endpoint.
1707
+ // ==============================================
1708
+ // Helper: extract Bearer token from Authorization header
1709
+ function extractBearerToken(request) {
1710
+ const auth = request.headers.get("Authorization");
1711
+ if (!auth?.startsWith("Bearer "))
1712
+ return null;
1713
+ return auth.slice(7);
1714
+ }
1715
+ // Helper: require API key auth, return 401 if missing
1716
+ async function requireApiKeyAuth(ctx, request) {
1717
+ const auth = await resolveWorkspaceFromRequest(ctx, request);
1718
+ if (auth.authMethod !== "api-key" || !auth.workspaceId || !auth.keyId) {
1719
+ return jsonResponse({
1720
+ error: {
1721
+ message: "Invalid API key. Generate one at https://apiclaw.cloud/workspace?tab=api-keys",
1722
+ type: "invalid_api_key",
1723
+ code: "invalid_api_key",
1724
+ },
1725
+ }, 401);
1726
+ }
1727
+ return { workspaceId: auth.workspaceId, keyId: auth.keyId };
1728
+ }
1729
+ // /v1/chat/completions — OpenAI-compatible LLM gateway with intelligent routing
1730
+ http.route({
1731
+ path: "/v1/chat/completions",
1732
+ method: "POST",
1733
+ handler: httpAction(async (ctx, request) => {
1734
+ const startTime = Date.now();
1735
+ // Require API key auth
1736
+ const authResult = await requireApiKeyAuth(ctx, request);
1737
+ if (authResult instanceof Response)
1738
+ return authResult;
1739
+ const { workspaceId } = authResult;
1740
+ // Parse body
1741
+ let body;
1742
+ try {
1743
+ body = await request.json();
1744
+ }
1745
+ catch {
1746
+ return jsonResponse({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }, 400);
1747
+ }
1748
+ const { model, messages, stream, ...rest } = body;
1749
+ if (!messages || !Array.isArray(messages)) {
1750
+ return jsonResponse({ error: { message: "messages array is required", type: "invalid_request_error" } }, 400);
1751
+ }
1752
+ // Request-level overrides (X-APIClaw-Route header)
1753
+ const routeOverride = request.headers.get("X-APIClaw-Route"); // e.g. "fastest" or "groq"
1754
+ // Load workspace settings
1755
+ let settings;
1756
+ try {
1757
+ settings = await ctx.runQuery(internal.workspaceSettings.getForRouting, { workspaceId });
1758
+ }
1759
+ catch {
1760
+ settings = {
1761
+ routingMode: "balanced",
1762
+ defaultModel: null,
1763
+ preferredProviders: [],
1764
+ blockedProviders: [],
1765
+ allowOpenRouterFallback: true,
1766
+ };
1767
+ }
1768
+ // Apply request-level overrides
1769
+ const effectiveRoutingMode = routeOverride && ["best_price", "highest_quality", "fastest", "balanced"].includes(routeOverride)
1770
+ ? routeOverride
1771
+ : settings.routingMode;
1772
+ // If routeOverride is a provider name, add it as preferred
1773
+ const effectivePreferred = routeOverride && PROVIDERS[routeOverride]?.isLLM
1774
+ ? [routeOverride, ...settings.preferredProviders]
1775
+ : settings.preferredProviders;
1776
+ const effectiveModel = model || settings.defaultModel || "anthropic/claude-sonnet-4-6";
1777
+ // Route the request
1778
+ const route = routeLLMRequest(effectiveModel, {
1779
+ routingMode: effectiveRoutingMode,
1780
+ preferredProviders: effectivePreferred,
1781
+ blockedProviders: settings.blockedProviders,
1782
+ allowOpenRouterFallback: settings.allowOpenRouterFallback,
1783
+ });
1784
+ if (!route) {
1785
+ return jsonResponse({ error: { message: "No LLM provider available. Check workspace settings.", type: "server_error" } }, 503);
1786
+ }
1787
+ // Log usage
1788
+ try {
1789
+ await ctx.runMutation(api.analytics.log, {
1790
+ event: "api_call",
1791
+ provider: "gateway",
1792
+ identifier: workspaceId,
1793
+ workspaceId: workspaceId,
1794
+ metadata: {
1795
+ action: "chat_completions",
1796
+ model: effectiveModel,
1797
+ routedTo: route.provider,
1798
+ routeReason: route.reason,
1799
+ authMethod: "api-key",
1800
+ },
1801
+ });
1802
+ await ctx.runMutation(api.logs.createProxyLog, {
1803
+ workspaceId: workspaceId,
1804
+ provider: route.provider,
1805
+ action: "chat_completions",
1806
+ subagentId: request.headers.get("X-APIClaw-Subagent") || "main",
1807
+ });
1808
+ await ctx.runMutation(api.workspaces.incrementUsage, {
1809
+ workspaceId: workspaceId,
1810
+ });
1811
+ }
1812
+ catch (e) {
1813
+ console.error("[Gateway] Logging failed:", e.message);
1814
+ }
1815
+ // Forward to the chosen provider
1816
+ try {
1817
+ const requestBody = {
1818
+ model: route.model,
1819
+ messages,
1820
+ stream: stream || false,
1821
+ ...rest,
1822
+ };
1823
+ const headers = {
1824
+ "Authorization": `Bearer ${route.apiKey}`,
1825
+ "Content-Type": "application/json",
1826
+ ...(route.extraHeaders || {}),
1827
+ };
1828
+ const response = await fetch(route.baseUrl, {
1829
+ method: "POST",
1830
+ headers,
1831
+ body: JSON.stringify(requestBody),
1832
+ });
1833
+ // For streaming responses, proxy the stream directly
1834
+ if (stream && response.body) {
1835
+ return new Response(response.body, {
1836
+ status: response.status,
1837
+ headers: {
1838
+ "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
1839
+ "Cache-Control": "no-cache",
1840
+ "Connection": "keep-alive",
1841
+ ...corsHeaders,
1842
+ },
1843
+ });
1844
+ }
1845
+ // Non-streaming: return JSON
1846
+ const data = await response.json();
1847
+ const latencyMs = Date.now() - startTime;
1848
+ // Add APIClaw metadata
1849
+ if (data && typeof data === "object") {
1850
+ data._apiclaw = {
1851
+ latencyMs,
1852
+ provider: route.provider,
1853
+ routeReason: route.reason,
1854
+ model: route.model,
1855
+ gateway: "v1",
1856
+ };
1857
+ }
1858
+ return jsonResponse(data, response.status);
1859
+ }
1860
+ catch (e) {
1861
+ return jsonResponse({ error: { message: e.message, type: "server_error" } }, 500);
1862
+ }
1863
+ }),
1864
+ });
1865
+ http.route({
1866
+ path: "/v1/chat/completions",
1867
+ method: "OPTIONS",
1868
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1869
+ });
1870
+ // Map a model string to a backend. Supports prefixed (voyage/voyage-3-large)
1871
+ // and bare model names (text-embedding-3-small, mistral-embed, voyage-3-large).
1872
+ function resolveEmbeddingBackend(requestedModel) {
1873
+ const raw = (requestedModel || "voyage/voyage-3-large").trim();
1874
+ let provider = null;
1875
+ let model = raw;
1876
+ // Explicit prefix
1877
+ if (raw.startsWith("voyage/")) {
1878
+ provider = "voyage";
1879
+ model = raw.slice(7);
1880
+ }
1881
+ else if (raw.startsWith("mistral/")) {
1882
+ provider = "mistral";
1883
+ model = raw.slice(8);
1884
+ }
1885
+ else if (raw.startsWith("openai/")) {
1886
+ provider = "openai";
1887
+ model = raw.slice(7);
1888
+ }
1889
+ else if (raw.startsWith("cohere/")) {
1890
+ provider = "cohere";
1891
+ model = raw.slice(7);
1892
+ }
1893
+ else {
1894
+ // Auto-detect from bare model name
1895
+ if (raw.startsWith("voyage-"))
1896
+ provider = "voyage";
1897
+ else if (raw.startsWith("mistral-embed") || raw === "mistral-embed")
1898
+ provider = "mistral";
1899
+ else if (raw.startsWith("text-embedding-") || raw.startsWith("ada-"))
1900
+ provider = "openai";
1901
+ else if (raw.startsWith("embed-"))
1902
+ provider = "cohere";
1903
+ else
1904
+ return null;
1905
+ }
1906
+ switch (provider) {
1907
+ case "voyage":
1908
+ return {
1909
+ provider,
1910
+ baseUrl: "https://api.voyageai.com/v1/embeddings",
1911
+ apiKey: process.env.VOYAGE_API_KEY,
1912
+ model: model || "voyage-3-large",
1913
+ format: "openai",
1914
+ };
1915
+ case "mistral":
1916
+ return {
1917
+ provider,
1918
+ baseUrl: "https://api.mistral.ai/v1/embeddings",
1919
+ apiKey: process.env.MISTRAL_API_KEY,
1920
+ model: model || "mistral-embed",
1921
+ format: "openai",
1922
+ };
1923
+ case "openai":
1924
+ return {
1925
+ provider,
1926
+ baseUrl: "https://api.openai.com/v1/embeddings",
1927
+ apiKey: process.env.OPENAI_API_KEY,
1928
+ model: model || "text-embedding-3-small",
1929
+ format: "openai",
1930
+ };
1931
+ case "cohere":
1932
+ return {
1933
+ provider,
1934
+ baseUrl: "https://api.cohere.com/v2/embed",
1935
+ apiKey: process.env.COHERE_API_KEY,
1936
+ model: model || "embed-v4.0",
1937
+ format: "cohere",
1938
+ };
1939
+ }
1940
+ }
1941
+ // /v1/embeddings — POST
1942
+ http.route({
1943
+ path: "/v1/embeddings",
1944
+ method: "POST",
1945
+ handler: httpAction(async (ctx, request) => {
1946
+ const startTime = Date.now();
1947
+ const authResult = await requireApiKeyAuth(ctx, request);
1948
+ if (authResult instanceof Response)
1949
+ return authResult;
1950
+ const { workspaceId } = authResult;
1951
+ let body;
1952
+ try {
1953
+ body = await request.json();
1954
+ }
1955
+ catch {
1956
+ return jsonResponse({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }, 400);
1957
+ }
1958
+ const { model, input, encoding_format, dimensions, user, input_type } = body;
1959
+ if (input === undefined || input === null) {
1960
+ return jsonResponse({ error: { message: "input is required", type: "invalid_request_error" } }, 400);
1961
+ }
1962
+ const backend = resolveEmbeddingBackend(model);
1963
+ if (!backend) {
1964
+ return jsonResponse({ error: { message: `Unknown embedding model: ${model}. Use voyage/*, mistral/*, openai/*, or cohere/* prefix.`, type: "invalid_request_error" } }, 400);
1965
+ }
1966
+ if (!backend.apiKey) {
1967
+ return jsonResponse({ error: { message: `Provider ${backend.provider} is not configured (missing ${backend.provider.toUpperCase()}_API_KEY).`, type: "server_error" } }, 503);
1968
+ }
1969
+ // Log usage
1970
+ try {
1971
+ await ctx.runMutation(api.analytics.log, {
1972
+ event: "api_call",
1973
+ provider: "gateway",
1974
+ identifier: workspaceId,
1975
+ workspaceId: workspaceId,
1976
+ metadata: {
1977
+ action: "embeddings",
1978
+ model: `${backend.provider}/${backend.model}`,
1979
+ routedTo: backend.provider,
1980
+ authMethod: "api-key",
1981
+ },
1982
+ });
1983
+ await ctx.runMutation(api.logs.createProxyLog, {
1984
+ workspaceId: workspaceId,
1985
+ provider: backend.provider,
1986
+ action: "embeddings",
1987
+ subagentId: request.headers.get("X-APIClaw-Subagent") || "main",
1988
+ });
1989
+ await ctx.runMutation(api.workspaces.incrementUsage, {
1990
+ workspaceId: workspaceId,
1991
+ });
1992
+ }
1993
+ catch (e) {
1994
+ console.error("[Gateway] Embeddings logging failed:", e.message);
1995
+ }
1996
+ try {
1997
+ let providerRequestBody;
1998
+ let providerHeaders = {
1999
+ "Content-Type": "application/json",
2000
+ "Authorization": `Bearer ${backend.apiKey}`,
2001
+ };
2002
+ if (backend.format === "openai") {
2003
+ // OpenAI-compatible passthrough (Voyage, Mistral, OpenAI)
2004
+ providerRequestBody = {
2005
+ model: backend.model,
2006
+ input,
2007
+ ...(encoding_format !== undefined ? { encoding_format } : {}),
2008
+ ...(dimensions !== undefined ? { dimensions } : {}),
2009
+ ...(user !== undefined ? { user } : {}),
2010
+ ...(input_type !== undefined ? { input_type } : {}),
2011
+ };
2012
+ }
2013
+ else {
2014
+ // Cohere v2 format
2015
+ const texts = Array.isArray(input) ? input : [String(input)];
2016
+ providerRequestBody = {
2017
+ model: backend.model,
2018
+ texts,
2019
+ input_type: input_type || "search_document",
2020
+ embedding_types: ["float"],
2021
+ };
2022
+ }
2023
+ const response = await fetch(backend.baseUrl, {
2024
+ method: "POST",
2025
+ headers: providerHeaders,
2026
+ body: JSON.stringify(providerRequestBody),
2027
+ });
2028
+ const providerData = await response.json();
2029
+ const latencyMs = Date.now() - startTime;
2030
+ if (!response.ok) {
2031
+ return jsonResponse({
2032
+ error: {
2033
+ message: providerData?.error?.message || providerData?.message || `${backend.provider} error`,
2034
+ type: "provider_error",
2035
+ provider: backend.provider,
2036
+ },
2037
+ _apiclaw: { latencyMs, provider: backend.provider, gateway: "v1" },
2038
+ }, response.status);
2039
+ }
2040
+ // Normalize Cohere response to OpenAI format
2041
+ let openAIData;
2042
+ if (backend.format === "cohere") {
2043
+ const cohereEmbeddings = providerData?.embeddings?.float || providerData?.embeddings || [];
2044
+ openAIData = {
2045
+ object: "list",
2046
+ data: cohereEmbeddings.map((embedding, index) => ({
2047
+ object: "embedding",
2048
+ embedding,
2049
+ index,
2050
+ })),
2051
+ model: `cohere/${backend.model}`,
2052
+ usage: {
2053
+ prompt_tokens: providerData?.meta?.billed_units?.input_tokens || 0,
2054
+ total_tokens: providerData?.meta?.billed_units?.input_tokens || 0,
2055
+ },
2056
+ };
2057
+ }
2058
+ else {
2059
+ // Already OpenAI-format
2060
+ openAIData = providerData;
2061
+ if (openAIData && typeof openAIData === "object" && !openAIData.model) {
2062
+ openAIData.model = `${backend.provider}/${backend.model}`;
2063
+ }
2064
+ }
2065
+ if (openAIData && typeof openAIData === "object") {
2066
+ openAIData._apiclaw = {
2067
+ latencyMs,
2068
+ provider: backend.provider,
2069
+ model: backend.model,
2070
+ gateway: "v1",
2071
+ };
2072
+ }
2073
+ return jsonResponse(openAIData, 200);
2074
+ }
2075
+ catch (e) {
2076
+ return jsonResponse({ error: { message: e.message, type: "server_error" } }, 500);
2077
+ }
2078
+ }),
2079
+ });
2080
+ http.route({
2081
+ path: "/v1/embeddings",
2082
+ method: "OPTIONS",
2083
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2084
+ });
2085
+ // /v1/models — List available models through APIClaw
2086
+ http.route({
2087
+ path: "/v1/models",
2088
+ method: "GET",
2089
+ handler: httpAction(async (ctx, request) => {
2090
+ // API key auth optional for models listing
2091
+ const models = [
2092
+ // OpenRouter models (main LLM backbone)
2093
+ { id: "anthropic/claude-sonnet-4-6", object: "model", owned_by: "anthropic", via: "openrouter" },
2094
+ { id: "anthropic/claude-haiku-3.5", object: "model", owned_by: "anthropic", via: "openrouter" },
2095
+ { id: "anthropic/claude-opus-4", object: "model", owned_by: "anthropic", via: "openrouter" },
2096
+ { id: "openai/gpt-4o", object: "model", owned_by: "openai", via: "openrouter" },
2097
+ { id: "openai/gpt-4o-mini", object: "model", owned_by: "openai", via: "openrouter" },
2098
+ { id: "openai/o3-mini", object: "model", owned_by: "openai", via: "openrouter" },
2099
+ { id: "google/gemini-2.5-pro-preview", object: "model", owned_by: "google", via: "openrouter" },
2100
+ { id: "google/gemini-2.5-flash-preview", object: "model", owned_by: "google", via: "openrouter" },
2101
+ { id: "meta-llama/llama-3.3-70b-instruct", object: "model", owned_by: "meta", via: "openrouter" },
2102
+ { id: "mistralai/mistral-large-latest", object: "model", owned_by: "mistral", via: "openrouter" },
2103
+ { id: "deepseek/deepseek-r1", object: "model", owned_by: "deepseek", via: "openrouter" },
2104
+ { id: "deepseek/deepseek-chat", object: "model", owned_by: "deepseek", via: "openrouter" },
2105
+ { id: "qwen/qwen-2.5-72b-instruct", object: "model", owned_by: "qwen", via: "openrouter" },
2106
+ // Embedding models via /v1/embeddings
2107
+ { id: "voyage/voyage-3-large", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
2108
+ { id: "voyage/voyage-3", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
2109
+ { id: "voyage/voyage-3-lite", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
2110
+ { id: "voyage/voyage-code-3", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
2111
+ { id: "voyage/voyage-multilingual-2", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
2112
+ { id: "mistral/mistral-embed", object: "model", owned_by: "mistral", via: "mistral", endpoint: "/v1/embeddings" },
2113
+ { id: "openai/text-embedding-3-small", object: "model", owned_by: "openai", via: "openai", endpoint: "/v1/embeddings" },
2114
+ { id: "openai/text-embedding-3-large", object: "model", owned_by: "openai", via: "openai", endpoint: "/v1/embeddings" },
2115
+ { id: "openai/text-embedding-ada-002", object: "model", owned_by: "openai", via: "openai", endpoint: "/v1/embeddings" },
2116
+ { id: "cohere/embed-v4.0", object: "model", owned_by: "cohere", via: "cohere", endpoint: "/v1/embeddings" },
2117
+ { id: "cohere/embed-multilingual-v3", object: "model", owned_by: "cohere", via: "cohere", endpoint: "/v1/embeddings" },
2118
+ ];
2119
+ return jsonResponse({
2120
+ object: "list",
2121
+ data: models,
2122
+ _apiclaw: {
2123
+ gateway: "v1",
2124
+ note: "These models are available through APIClaw's unified gateway. All 800+ OpenRouter chat models + embedding models across Voyage, Mistral, OpenAI, and Cohere.",
2125
+ non_llm_apis: Object.keys(PROVIDERS).length + " Direct Call providers (SMS, email, search, TTS, embeddings, code execution, scraping, and more)",
2126
+ },
2127
+ });
2128
+ }),
2129
+ });
2130
+ http.route({
2131
+ path: "/v1/models",
2132
+ method: "OPTIONS",
2133
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2134
+ });
2135
+ //# sourceMappingURL=http.js.map