@nordsym/apiclaw 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +15 -2
  2. package/dist/bin-http.js +0 -0
  3. package/dist/bin.bundled.js +79288 -0
  4. package/dist/gateway-client.d.ts.map +1 -1
  5. package/dist/gateway-client.js +24 -2
  6. package/dist/gateway-client.js.map +1 -1
  7. package/dist/index.bundled.js +61263 -0
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -2
  11. package/.claude/settings.local.json +0 -13
  12. package/.env.prod +0 -1
  13. package/apiclaw-README.md +0 -494
  14. package/convex/_generated/api.d.ts +0 -145
  15. package/convex/_generated/api.js +0 -23
  16. package/convex/_generated/dataModel.d.ts +0 -60
  17. package/convex/_generated/server.d.ts +0 -143
  18. package/convex/_generated/server.js +0 -93
  19. package/convex/_listWorkspaces.ts +0 -13
  20. package/convex/adminActivate.ts +0 -53
  21. package/convex/adminStats.ts +0 -306
  22. package/convex/agents.ts +0 -939
  23. package/convex/analytics.ts +0 -187
  24. package/convex/apiKeys.ts +0 -220
  25. package/convex/backfillAnalytics.ts +0 -272
  26. package/convex/backfillSearchLogs.ts +0 -35
  27. package/convex/billing.ts +0 -834
  28. package/convex/capabilities.ts +0 -157
  29. package/convex/chains.ts +0 -1318
  30. package/convex/credits.ts +0 -211
  31. package/convex/crons.ts +0 -65
  32. package/convex/debugFilestackLogs.ts +0 -16
  33. package/convex/debugGetToken.ts +0 -18
  34. package/convex/directCall.ts +0 -713
  35. package/convex/earnProgress.ts +0 -753
  36. package/convex/email.ts +0 -329
  37. package/convex/feedback.ts +0 -265
  38. package/convex/funnel.ts +0 -431
  39. package/convex/guards.ts +0 -174
  40. package/convex/http.ts +0 -3756
  41. package/convex/inbound.ts +0 -32
  42. package/convex/logs.ts +0 -701
  43. package/convex/migrateFilestack.ts +0 -81
  44. package/convex/migratePartnersProd.ts +0 -174
  45. package/convex/migratePratham.ts +0 -126
  46. package/convex/migrateProviderWorkspaces.ts +0 -175
  47. package/convex/mou.ts +0 -91
  48. package/convex/nurture.ts +0 -355
  49. package/convex/providerKeys.ts +0 -289
  50. package/convex/providers.ts +0 -1135
  51. package/convex/purchases.ts +0 -183
  52. package/convex/ratelimit.ts +0 -104
  53. package/convex/schema.ts +0 -926
  54. package/convex/searchLogs.ts +0 -265
  55. package/convex/seedAPILayerAPIs.ts +0 -191
  56. package/convex/seedDirectCallConfigs.ts +0 -336
  57. package/convex/seedPratham.ts +0 -149
  58. package/convex/spendAlerts.ts +0 -442
  59. package/convex/stripeActions.ts +0 -607
  60. package/convex/teams.ts +0 -243
  61. package/convex/telemetry.ts +0 -81
  62. package/convex/tsconfig.json +0 -25
  63. package/convex/updateAPIStatus.ts +0 -44
  64. package/convex/usage.ts +0 -260
  65. package/convex/usageReports.ts +0 -357
  66. package/convex/waitlist.ts +0 -55
  67. package/convex/webhooks.ts +0 -494
  68. package/convex/workspaceSettings.ts +0 -143
  69. package/convex/workspaces.ts +0 -1331
  70. package/convex.json +0 -3
  71. package/direct-test.mjs +0 -51
  72. package/email-templates/filestack-provider-outreach.html +0 -162
  73. package/email-templates/partnership-template.html +0 -116
  74. package/email-templates/pratham-draft-preview.txt +0 -57
  75. package/email-templates/pratham-partnership-draft.html +0 -141
  76. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  77. package/reports/pipeline/PIPELINE-REPORT.json +0 -153
  78. package/reports/pipeline/acquire_apisguru.json +0 -17
  79. package/reports/pipeline/capabilities.json +0 -38
  80. package/reports/pipeline/discover_azure_recursive.json +0 -1551
  81. package/reports/pipeline/discover_github.json +0 -25
  82. package/reports/pipeline/discover_github_repos.json +0 -49
  83. package/reports/pipeline/discover_swaggerhub.json +0 -24
  84. package/reports/pipeline/discover_well_known.json +0 -23
  85. package/reports/pipeline/fetch_specs.json +0 -19
  86. package/reports/pipeline/generate_providers.json +0 -14
  87. package/reports/pipeline/match_registry.json +0 -11
  88. package/reports/pipeline/parse_specs.json +0 -17
  89. package/reports/pipeline/promote_candidates.json +0 -34
  90. package/reports/pipeline/validate.json +0 -30
  91. package/reports/pipeline/validate_smoke_details.json +0 -3835
  92. package/reports/session-report-2026-04-05.html +0 -433
  93. package/seed-apis-direct.mjs +0 -106
  94. package/src/access-control.ts +0 -174
  95. package/src/adapters/base.ts +0 -364
  96. package/src/adapters/claude-desktop.ts +0 -41
  97. package/src/adapters/cline.ts +0 -88
  98. package/src/adapters/continue.ts +0 -91
  99. package/src/adapters/cursor.ts +0 -43
  100. package/src/adapters/custom.ts +0 -188
  101. package/src/adapters/detect.ts +0 -202
  102. package/src/adapters/index.ts +0 -47
  103. package/src/adapters/windsurf.ts +0 -44
  104. package/src/bin-http.ts +0 -45
  105. package/src/bin.ts +0 -34
  106. package/src/capability-router.ts +0 -331
  107. package/src/chainExecutor.ts +0 -730
  108. package/src/chainResolver.test.ts +0 -246
  109. package/src/chainResolver.ts +0 -658
  110. package/src/cli/commands/demo.ts +0 -109
  111. package/src/cli/commands/doctor.ts +0 -435
  112. package/src/cli/commands/index.ts +0 -9
  113. package/src/cli/commands/login.ts +0 -203
  114. package/src/cli/commands/mcp-install.ts +0 -373
  115. package/src/cli/commands/restore.ts +0 -333
  116. package/src/cli/commands/setup.ts +0 -297
  117. package/src/cli/commands/uninstall.ts +0 -240
  118. package/src/cli/index.ts +0 -148
  119. package/src/cli.ts +0 -370
  120. package/src/confirmation.ts +0 -296
  121. package/src/credentials.ts +0 -455
  122. package/src/credits.ts +0 -329
  123. package/src/crypto.ts +0 -75
  124. package/src/discovery.ts +0 -568
  125. package/src/enterprise/env.ts +0 -156
  126. package/src/enterprise/index.ts +0 -7
  127. package/src/enterprise/script-generator.ts +0 -481
  128. package/src/execute-dynamic.ts +0 -617
  129. package/src/execute.ts +0 -2386
  130. package/src/funnel-client.ts +0 -168
  131. package/src/funnel.test.ts +0 -187
  132. package/src/gateway-client.ts +0 -192
  133. package/src/hivr-whitelist.ts +0 -110
  134. package/src/http-api.ts +0 -286
  135. package/src/http-server-minimal.ts +0 -154
  136. package/src/index.ts +0 -2702
  137. package/src/intelligent-gateway.ts +0 -339
  138. package/src/mcp-analytics.ts +0 -156
  139. package/src/metered.ts +0 -149
  140. package/src/open-apis-generated.ts +0 -157
  141. package/src/open-apis.ts +0 -558
  142. package/src/postinstall.ts +0 -40
  143. package/src/product-whitelist.ts +0 -246
  144. package/src/proxy.ts +0 -36
  145. package/src/registration-guard.ts +0 -117
  146. package/src/session.ts +0 -129
  147. package/src/stripe.ts +0 -497
  148. package/src/telemetry.ts +0 -71
  149. package/src/test.ts +0 -135
  150. package/src/types/convex-api.d.ts +0 -20
  151. package/src/types/convex-api.ts +0 -21
  152. package/src/types.ts +0 -109
  153. package/src/ui/colors.ts +0 -219
  154. package/src/ui/errors.ts +0 -394
  155. package/src/ui/index.ts +0 -17
  156. package/src/ui/prompts.ts +0 -390
  157. package/src/ui/spinner.ts +0 -325
  158. package/src/utils/backup.ts +0 -224
  159. package/src/utils/config.ts +0 -318
  160. package/src/utils/os.ts +0 -124
  161. package/src/utils/paths.ts +0 -203
  162. package/src/webhook.ts +0 -107
  163. package/test-10-working.cjs +0 -97
  164. package/test-14-final.cjs +0 -96
  165. package/test-actual-handlers.ts +0 -92
  166. package/test-apilayer-all-14.ts +0 -249
  167. package/test-apilayer-fixed.ts +0 -248
  168. package/test-direct-endpoints.ts +0 -174
  169. package/test-exact-endpoints.ts +0 -144
  170. package/test-final.ts +0 -83
  171. package/test-full-routing.ts +0 -100
  172. package/test-handlers-correct.ts +0 -217
  173. package/test-numverify-key.ts +0 -41
  174. package/test-via-handlers.ts +0 -92
  175. package/test-worldnews.mjs +0 -26
  176. package/tsconfig.json +0 -20
package/convex/http.ts DELETED
@@ -1,3756 +0,0 @@
1
- import { httpRouter } from "convex/server";
2
- import { httpAction } from "./_generated/server";
3
- import { api, internal } from "./_generated/api";
4
- import { resolveVerifiedOwnerByWorkspaceId } from "./guards";
5
- import {
6
- createCheckoutSession,
7
- createPortalSession,
8
- handleStripeWebhook,
9
- checkoutOptions,
10
- portalOptions,
11
- webhookOptions,
12
- } from "./stripeActions";
13
-
14
- const http = httpRouter();
15
-
16
- // Provider catalog — all 20 Direct Call providers
17
- interface ProviderMeta {
18
- name: string;
19
- description: string;
20
- category: string;
21
- pricing: string;
22
- regions: string[];
23
- tags: string[];
24
- isLLM: boolean; // can serve /v1/chat/completions
25
- envKey?: string; // env var name for API key
26
- baseUrl?: string; // chat completions base URL (LLM providers only)
27
- speed: "fast" | "medium" | "slow"; // latency tier
28
- costTier: "free" | "cheap" | "medium" | "expensive"; // relative cost
29
- }
30
-
31
- const PROVIDERS: Record<string, ProviderMeta> = {
32
- openrouter: {
33
- name: "OpenRouter",
34
- description: "Multi-model LLM API. Access GPT, Claude, Llama, Gemini, and 800+ models.",
35
- category: "llm",
36
- pricing: "Varies by model",
37
- regions: ["Global"],
38
- tags: ["llm", "ai", "gpt", "claude", "gemini", "llama"],
39
- isLLM: true,
40
- envKey: "OPENROUTER_API_KEY",
41
- baseUrl: "https://openrouter.ai/api/v1/chat/completions",
42
- speed: "medium",
43
- costTier: "medium",
44
- },
45
- groq: {
46
- name: "Groq",
47
- description: "Ultra-fast LLM inference. Llama, Mixtral, Gemma at lightning speed.",
48
- category: "llm",
49
- pricing: "~$0.05-0.27/M tokens",
50
- regions: ["Global"],
51
- tags: ["llm", "fast", "llama", "mixtral", "gemma"],
52
- isLLM: true,
53
- envKey: "GROQ_API_KEY",
54
- baseUrl: "https://api.groq.com/openai/v1/chat/completions",
55
- speed: "fast",
56
- costTier: "cheap",
57
- },
58
- mistral: {
59
- name: "Mistral",
60
- description: "Mistral AI models. Efficient European LLMs with strong coding.",
61
- category: "llm",
62
- pricing: "~$0.10-2.00/M tokens",
63
- regions: ["EU", "Global"],
64
- tags: ["llm", "mistral", "eu", "coding", "embeddings"],
65
- isLLM: true,
66
- envKey: "MISTRAL_API_KEY",
67
- baseUrl: "https://api.mistral.ai/v1/chat/completions",
68
- speed: "fast",
69
- costTier: "cheap",
70
- },
71
- together: {
72
- name: "Together AI",
73
- description: "Open-source model inference. Llama, Qwen, DeepSeek at scale.",
74
- category: "llm",
75
- pricing: "~$0.10-0.90/M tokens",
76
- regions: ["Global"],
77
- tags: ["llm", "open-source", "llama", "qwen", "deepseek"],
78
- isLLM: true,
79
- envKey: "TOGETHER_API_KEY",
80
- baseUrl: "https://api.together.xyz/v1/chat/completions",
81
- speed: "fast",
82
- costTier: "cheap",
83
- },
84
- openai: {
85
- name: "OpenAI",
86
- description: "GPT-5.4, GPT-4o, o3, o4-mini. Direct access, no middleman markup.",
87
- category: "llm",
88
- pricing: "~$2.50-15.00/M tokens",
89
- regions: ["Global"],
90
- tags: ["llm", "gpt", "openai", "gpt-5", "o3", "o4", "coding"],
91
- isLLM: true,
92
- envKey: "OPENAI_API_KEY",
93
- baseUrl: "https://api.openai.com/v1/chat/completions",
94
- speed: "medium",
95
- costTier: "expensive",
96
- },
97
- xai: {
98
- name: "xAI",
99
- description: "Grok models by xAI. Reasoning, coding, and real-time knowledge via X/Twitter data.",
100
- category: "llm",
101
- pricing: "~$0.30-3.00/M tokens",
102
- regions: ["Global"],
103
- tags: ["llm", "grok", "reasoning", "xai", "x", "twitter"],
104
- isLLM: true,
105
- envKey: "XAI_API_KEY",
106
- baseUrl: "https://api.x.ai/v1/chat/completions",
107
- speed: "medium",
108
- costTier: "medium",
109
- },
110
- anthropic: {
111
- name: "Anthropic",
112
- description: "Claude models by Anthropic. Best-in-class reasoning, coding, and analysis.",
113
- category: "llm",
114
- pricing: "~$0.80-15.00/M tokens",
115
- regions: ["Global"],
116
- tags: ["llm", "claude", "anthropic", "reasoning", "coding", "analysis"],
117
- isLLM: true,
118
- envKey: "ANTHROPIC_API_KEY",
119
- baseUrl: "https://api.anthropic.com/v1/messages",
120
- speed: "medium",
121
- costTier: "expensive",
122
- },
123
- cohere: {
124
- name: "Cohere",
125
- description: "Enterprise LLM with strong RAG and reranking capabilities.",
126
- category: "llm",
127
- pricing: "~$0.15-2.50/M tokens",
128
- regions: ["Global"],
129
- tags: ["llm", "rag", "rerank", "enterprise", "embeddings"],
130
- isLLM: false, // Cohere uses non-OpenAI-compatible API format
131
- envKey: "COHERE_API_KEY",
132
- speed: "medium",
133
- costTier: "medium",
134
- },
135
- "46elks": {
136
- name: "46elks",
137
- description: "SMS API for EU/Nordics. GDPR compliant.",
138
- category: "sms",
139
- pricing: "~$0.035/SMS",
140
- regions: ["EU", "Nordic"],
141
- tags: ["sms", "eu", "gdpr", "nordic"],
142
- isLLM: false,
143
- envKey: "ELKS_API_KEY",
144
- speed: "fast",
145
- costTier: "cheap",
146
- },
147
- twilio: {
148
- name: "Twilio",
149
- description: "SMS and Voice API. Global coverage.",
150
- category: "sms",
151
- pricing: "~$0.04/SMS, ~$0.01/min voice",
152
- regions: ["Global"],
153
- tags: ["sms", "voice", "global"],
154
- isLLM: false,
155
- envKey: "TWILIO_AUTH_TOKEN",
156
- speed: "fast",
157
- costTier: "cheap",
158
- },
159
- resend: {
160
- name: "Resend",
161
- description: "Modern email API. Developer-friendly.",
162
- category: "email",
163
- pricing: "~$0.001/email",
164
- regions: ["Global"],
165
- tags: ["email", "transactional"],
166
- isLLM: false,
167
- envKey: "RESEND_API_KEY",
168
- speed: "fast",
169
- costTier: "free",
170
- },
171
- brave_search: {
172
- name: "Brave Search",
173
- description: "Privacy-focused web search API.",
174
- category: "search",
175
- pricing: "~$0.005/search",
176
- regions: ["Global"],
177
- tags: ["search", "web", "privacy"],
178
- isLLM: false,
179
- envKey: "BRAVE_API_KEY",
180
- speed: "fast",
181
- costTier: "cheap",
182
- },
183
- serper: {
184
- name: "Serper",
185
- description: "Google Search API. Fast SERP results for AI agents.",
186
- category: "search",
187
- pricing: "~$0.001/search",
188
- regions: ["Global"],
189
- tags: ["search", "google", "serp"],
190
- isLLM: false,
191
- envKey: "SERPER_API_KEY",
192
- speed: "fast",
193
- costTier: "cheap",
194
- },
195
- elevenlabs: {
196
- name: "ElevenLabs",
197
- description: "Text-to-speech API. High quality AI voices.",
198
- category: "tts",
199
- pricing: "~$0.0003/char",
200
- regions: ["Global"],
201
- tags: ["tts", "voice", "audio", "speech"],
202
- isLLM: false,
203
- envKey: "ELEVENLABS_API_KEY",
204
- speed: "medium",
205
- costTier: "medium",
206
- },
207
- deepgram: {
208
- name: "Deepgram",
209
- description: "Speech-to-text API. Fast, accurate transcription with Nova-3.",
210
- category: "stt",
211
- pricing: "~$0.0043/min",
212
- regions: ["Global"],
213
- tags: ["stt", "transcription", "voice", "audio"],
214
- isLLM: false,
215
- envKey: "DEEPGRAM_API_KEY",
216
- speed: "fast",
217
- costTier: "cheap",
218
- },
219
- assemblyai: {
220
- name: "AssemblyAI",
221
- description: "Speech-to-text with speaker diarization, summarization, and sentiment.",
222
- category: "stt",
223
- pricing: "~$0.01/min",
224
- regions: ["Global"],
225
- tags: ["stt", "transcription", "diarization", "sentiment"],
226
- isLLM: false,
227
- envKey: "ASSEMBLYAI_API_KEY",
228
- speed: "medium",
229
- costTier: "cheap",
230
- },
231
- replicate: {
232
- name: "Replicate",
233
- description: "Run AI models (Whisper, SDXL, Llama, Flux, etc). Pay per prediction.",
234
- category: "ai",
235
- pricing: "Varies by model",
236
- regions: ["Global"],
237
- tags: ["ai", "ml", "whisper", "image", "audio", "transcription"],
238
- isLLM: false,
239
- envKey: "REPLICATE_API_TOKEN",
240
- speed: "slow",
241
- costTier: "medium",
242
- },
243
- stability: {
244
- name: "Stability AI",
245
- description: "Image generation API. Stable Diffusion 3, SDXL.",
246
- category: "image",
247
- pricing: "~$0.03/image",
248
- regions: ["Global"],
249
- tags: ["image", "generation", "stable-diffusion", "sdxl"],
250
- isLLM: false,
251
- envKey: "STABILITY_API_KEY",
252
- speed: "slow",
253
- costTier: "medium",
254
- },
255
- firecrawl: {
256
- name: "Firecrawl",
257
- description: "Web scraping and crawling API. Extract clean data from any URL.",
258
- category: "scraping",
259
- pricing: "~$0.001/page",
260
- regions: ["Global"],
261
- tags: ["scraping", "web", "crawl", "extract"],
262
- isLLM: false,
263
- envKey: "FIRECRAWL_API_KEY",
264
- speed: "medium",
265
- costTier: "cheap",
266
- },
267
- github: {
268
- name: "GitHub",
269
- description: "GitHub API. Search repos, manage code, access developer data.",
270
- category: "code",
271
- pricing: "Free tier available",
272
- regions: ["Global"],
273
- tags: ["github", "code", "repos", "developer"],
274
- isLLM: false,
275
- envKey: "GITHUB_TOKEN",
276
- speed: "fast",
277
- costTier: "free",
278
- },
279
- e2b: {
280
- name: "E2B",
281
- description: "Secure code sandbox for AI agents. Run Python, shell in isolated environments.",
282
- category: "sandbox",
283
- pricing: "$0.000028/s (2 vCPU)",
284
- regions: ["Global"],
285
- tags: ["sandbox", "code", "python", "execution", "ai", "agents"],
286
- isLLM: false,
287
- envKey: "E2B_API_KEY",
288
- speed: "medium",
289
- costTier: "cheap",
290
- },
291
- apilayer: {
292
- name: "APILayer",
293
- description: "14 APIs: exchange rates, market data, aviation, PDF, screenshots, email/phone verification, VAT, news, scraping, and more.",
294
- category: "multi",
295
- pricing: "Free tier available, paid plans per API",
296
- regions: ["Global"],
297
- tags: ["exchange", "stocks", "aviation", "pdf", "screenshot", "verification", "vat", "news", "scraping"],
298
- isLLM: false,
299
- envKey: "APILAYER_API_KEY",
300
- speed: "medium",
301
- costTier: "cheap",
302
- },
303
- voyage: {
304
- name: "Voyage AI",
305
- description: "State-of-the-art embeddings for RAG and agent memory. Best-in-class retrieval quality.",
306
- category: "embeddings",
307
- pricing: "~$0.02-0.18/M tokens",
308
- regions: ["Global"],
309
- tags: ["embeddings", "rag", "agent-memory", "retrieval", "voyage-3", "code-embeddings"],
310
- isLLM: false,
311
- envKey: "VOYAGE_API_KEY",
312
- speed: "fast",
313
- costTier: "cheap",
314
- },
315
- };
316
-
317
- // ==============================================
318
- // PROVIDER COST TABLE (per million tokens, USD)
319
- // ==============================================
320
- const MODEL_COSTS: Record<string, { input: number; output: number }> = {
321
- // OpenAI
322
- "gpt-5.4": { input: 12.50, output: 50.00 },
323
- "gpt-5": { input: 10.00, output: 40.00 },
324
- "gpt-4o": { input: 2.50, output: 10.00 },
325
- "gpt-4o-mini": { input: 0.15, output: 0.60 },
326
- "gpt-4.1": { input: 2.00, output: 8.00 },
327
- "o3": { input: 10.00, output: 40.00 },
328
- "o4-mini": { input: 1.10, output: 4.40 },
329
- // Groq (heavily discounted)
330
- "llama-3.3-70b-versatile": { input: 0.059, output: 0.079 },
331
- "llama-3.1-8b-instant": { input: 0.05, output: 0.08 },
332
- "llama-3.1-70b-versatile": { input: 0.059, output: 0.079 },
333
- "gemma2-9b-it": { input: 0.02, output: 0.02 },
334
- "mixtral-8x7b-32768": { input: 0.024, output: 0.024 },
335
- // Mistral
336
- "mistral-small-latest": { input: 0.10, output: 0.30 },
337
- "mistral-large-latest": { input: 2.00, output: 6.00 },
338
- "mistral-medium-latest": { input: 0.40, output: 1.20 },
339
- "codestral-latest": { input: 0.30, output: 0.90 },
340
- "pixtral-large-latest": { input: 2.00, output: 6.00 },
341
- "open-mistral-nemo": { input: 0.15, output: 0.15 },
342
- // Together
343
- "deepseek-ai/DeepSeek-R1": { input: 0.55, output: 2.19 },
344
- "deepseek-ai/DeepSeek-V3": { input: 0.30, output: 0.88 },
345
- "meta-llama/Llama-3.3-70B-Instruct-Turbo": { input: 0.18, output: 0.18 },
346
- "Qwen/Qwen2.5-72B-Instruct-Turbo": { input: 0.18, output: 0.18 },
347
- // xAI
348
- "grok-4.20-reasoning": { input: 3.00, output: 15.00 },
349
- "grok-3": { input: 3.00, output: 15.00 },
350
- "grok-3-mini": { input: 0.30, output: 0.50 },
351
- "grok-2-latest": { input: 2.00, output: 10.00 },
352
- // Anthropic (direct or via OpenRouter)
353
- "claude-sonnet-4-6": { input: 3.00, output: 15.00 },
354
- "claude-opus-4-6": { input: 15.00, output: 75.00 },
355
- "claude-opus-4": { input: 15.00, output: 75.00 },
356
- "claude-4-sonnet": { input: 3.00, output: 15.00 },
357
- "claude-4-opus": { input: 15.00, output: 75.00 },
358
- "claude-3.5-sonnet": { input: 3.00, output: 15.00 },
359
- "claude-3-5-sonnet-20241022": { input: 3.00, output: 15.00 },
360
- "claude-haiku-4-5": { input: 0.80, output: 4.00 },
361
- "claude-3-5-haiku-20241022": { input: 0.80, output: 4.00 },
362
- "anthropic/claude-sonnet-4-6": { input: 3.00, output: 15.00 },
363
- "anthropic/claude-haiku-3.5": { input: 0.80, output: 4.00 },
364
- };
365
-
366
- // APIClaw margin: 15% on top of provider cost (market standard)
367
- const APICLAW_MARGIN = 0.15;
368
-
369
- function calculateCallCost(model: string, usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }): { providerCost: number; apiclawCost: number } {
370
- if (!usage) return { providerCost: 0, apiclawCost: 0 };
371
-
372
- // Find cost entry (try exact match, then partial)
373
- let costs = MODEL_COSTS[model];
374
- if (!costs) {
375
- const modelLower = model.toLowerCase();
376
- const key = Object.keys(MODEL_COSTS).find(k => modelLower.includes(k.toLowerCase()));
377
- if (key) costs = MODEL_COSTS[key];
378
- }
379
- if (!costs) {
380
- // Unknown model -- estimate at medium tier
381
- costs = { input: 1.00, output: 3.00 };
382
- }
383
-
384
- const inputTokens = usage.prompt_tokens || 0;
385
- const outputTokens = usage.completion_tokens || 0;
386
-
387
- const providerCost = (inputTokens * costs.input + outputTokens * costs.output) / 1_000_000;
388
- const apiclawCost = providerCost * (1 + APICLAW_MARGIN);
389
-
390
- return { providerCost, apiclawCost };
391
- }
392
-
393
- // ==============================================
394
- // INTELLIGENT LLM ROUTER
395
- // ==============================================
396
-
397
- // Model-to-provider mapping: which direct providers can serve which model patterns
398
- const MODEL_PROVIDER_MAP: { pattern: RegExp; provider: string; nativeModel: string }[] = [
399
- // Groq-native models (ultra-fast inference)
400
- { pattern: /^(groq\/)?llama-3\.3-70b/i, provider: "groq", nativeModel: "llama-3.3-70b-versatile" },
401
- { pattern: /^(groq\/)?llama-3\.1-8b/i, provider: "groq", nativeModel: "llama-3.1-8b-instant" },
402
- { pattern: /^(groq\/)?llama-3\.1-70b/i, provider: "groq", nativeModel: "llama-3.1-70b-versatile" },
403
- { pattern: /^(groq\/)?gemma2?-9b/i, provider: "groq", nativeModel: "gemma2-9b-it" },
404
- { pattern: /^(groq\/)?mixtral-8x7b/i, provider: "groq", nativeModel: "mixtral-8x7b-32768" },
405
- // Mistral-native models
406
- { pattern: /^(mistralai\/)?mistral-small/i, provider: "mistral", nativeModel: "mistral-small-latest" },
407
- { pattern: /^(mistralai\/)?mistral-large/i, provider: "mistral", nativeModel: "mistral-large-latest" },
408
- { pattern: /^(mistralai\/)?mistral-medium/i, provider: "mistral", nativeModel: "mistral-medium-latest" },
409
- { pattern: /^(mistralai\/)?codestral/i, provider: "mistral", nativeModel: "codestral-latest" },
410
- { pattern: /^(mistralai\/)?pixtral/i, provider: "mistral", nativeModel: "pixtral-large-latest" },
411
- { pattern: /^(mistralai\/)?mistral-nemo/i, provider: "mistral", nativeModel: "open-mistral-nemo" },
412
- // Together-native models (open-source at scale)
413
- { pattern: /^(together\/)?meta-llama\/Llama-3\.3-70B/i, provider: "together", nativeModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo" },
414
- { pattern: /^(together\/)?Qwen\/Qwen2\.5-72B/i, provider: "together", nativeModel: "Qwen/Qwen2.5-72B-Instruct-Turbo" },
415
- { pattern: /^(together\/)?deepseek-ai\/DeepSeek-R1/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-R1" },
416
- { pattern: /^(together\/)?deepseek-ai\/DeepSeek-V3/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-V3" },
417
- // OpenAI direct models
418
- { pattern: /^(openai\/)?gpt-5\.4/i, provider: "openai", nativeModel: "gpt-5.4" },
419
- { pattern: /^(openai\/)?gpt-5/i, provider: "openai", nativeModel: "gpt-5" },
420
- { pattern: /^(openai\/)?gpt-4o/i, provider: "openai", nativeModel: "gpt-4o" },
421
- { pattern: /^(openai\/)?gpt-4\.1/i, provider: "openai", nativeModel: "gpt-4.1" },
422
- { pattern: /^(openai\/)?o3/i, provider: "openai", nativeModel: "o3" },
423
- { pattern: /^(openai\/)?o4-mini/i, provider: "openai", nativeModel: "o4-mini" },
424
- // xAI/Grok models
425
- { pattern: /^(xai\/)?grok-4/i, provider: "xai", nativeModel: "grok-4.20-reasoning" },
426
- { pattern: /^(xai\/)?grok-3-mini/i, provider: "xai", nativeModel: "grok-3-mini" },
427
- { pattern: /^(xai\/)?grok-3/i, provider: "xai", nativeModel: "grok-3" },
428
- { pattern: /^(xai\/)?grok-2/i, provider: "xai", nativeModel: "grok-2-latest" },
429
- // Anthropic direct models
430
- { pattern: /^(anthropic\/)?claude-sonnet-4-6/i, provider: "anthropic", nativeModel: "claude-sonnet-4-6-20250514" },
431
- { pattern: /^(anthropic\/)?claude-4-sonnet/i, provider: "anthropic", nativeModel: "claude-sonnet-4-6-20250514" },
432
- { pattern: /^(anthropic\/)?claude-opus-4/i, provider: "anthropic", nativeModel: "claude-opus-4-6-20250514" },
433
- { pattern: /^(anthropic\/)?claude-4-opus/i, provider: "anthropic", nativeModel: "claude-opus-4-6-20250514" },
434
- { pattern: /^(anthropic\/)?claude-3[\.\-]5-sonnet/i, provider: "anthropic", nativeModel: "claude-3-5-sonnet-20241022" },
435
- { pattern: /^(anthropic\/)?claude-haiku-4/i, provider: "anthropic", nativeModel: "claude-haiku-4-5-20251001" },
436
- { pattern: /^(anthropic\/)?claude-3[\.\-]5-haiku/i, provider: "anthropic", nativeModel: "claude-3-5-haiku-20241022" },
437
- // Shorthand aliases -- route common names to cheapest/fastest direct provider
438
- { pattern: /^deepseek-r1$/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-R1" },
439
- { pattern: /^deepseek-v3$/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-V3" },
440
- { pattern: /^llama-?3\.?3/i, provider: "groq", nativeModel: "llama-3.3-70b-versatile" },
441
- { pattern: /^llama-?3\.?1-?8b/i, provider: "groq", nativeModel: "llama-3.1-8b-instant" },
442
- { pattern: /^qwen-?2\.?5/i, provider: "together", nativeModel: "Qwen/Qwen2.5-72B-Instruct-Turbo" },
443
- ];
444
-
445
- interface RoutingDecision {
446
- provider: string;
447
- model: string;
448
- baseUrl: string;
449
- apiKey: string;
450
- reason: string;
451
- extraHeaders?: Record<string, string>;
452
- }
453
-
454
- // ==============================================
455
- // ADVISOR: Analyzes prompts to pick optimal model+provider
456
- // Runs only when model is "auto" or unspecified and routing mode is "balanced"
457
- // Uses Mistral Small (~$0.00001/decision) for near-zero cost intelligence
458
- // ==============================================
459
-
460
- const ADVISOR_SYSTEM_PROMPT = `You are an LLM routing advisor. Given a user prompt, pick the optimal provider and model.
461
-
462
- PROVIDERS (use exact provider key and model name):
463
-
464
- provider: "mistral", model: "mistral-small-latest" -- Fast, cheap. Simple Q&A, translation, summarization.
465
- provider: "mistral", model: "mistral-large-latest" -- Strong reasoning, coding, complex analysis.
466
- provider: "mistral", model: "codestral-latest" -- Code generation, debugging, technical.
467
- provider: "together", model: "meta-llama/Llama-3.3-70B-Instruct-Turbo" -- Strong open-source all-rounder.
468
- provider: "together", model: "deepseek-ai/DeepSeek-R1" -- Deep reasoning, math, chain-of-thought.
469
- provider: "together", model: "Qwen/Qwen2.5-72B-Instruct-Turbo" -- Multilingual, strong CJK.
470
- provider: "openrouter", model: "anthropic/claude-sonnet-4-6" -- Best quality. Complex multi-step, nuanced writing.
471
- provider: "openrouter", model: "openai/gpt-4o" -- Vision, function calling, broad knowledge.
472
- provider: "openrouter", model: "google/gemini-2.0-flash-001" -- Fast multimodal, long context.
473
-
474
- Respond with ONLY JSON:
475
- {"provider":"mistral","model":"mistral-small-latest","reason":"simple factual query"}`;
476
-
477
- interface AdvisorDecision {
478
- provider: string;
479
- model: string;
480
- reason: string;
481
- }
482
-
483
- async function advisorPickModel(
484
- messages: Array<{ role: string; content: string }>,
485
- settings: { blockedProviders: string[] }
486
- ): Promise<AdvisorDecision | null> {
487
- // Extract first user message for analysis (keep it short)
488
- const userMsg = messages.find(m => m.role === "user");
489
- if (!userMsg) return null;
490
-
491
- const promptPreview = typeof userMsg.content === "string"
492
- ? userMsg.content.slice(0, 500)
493
- : JSON.stringify(userMsg.content).slice(0, 500);
494
-
495
- // Use Mistral Small as the advisor (fast + cheap)
496
- const mistralKey = process.env.MISTRAL_API_KEY;
497
- if (!mistralKey) return null;
498
-
499
- try {
500
- const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
501
- method: "POST",
502
- headers: {
503
- "Authorization": `Bearer ${mistralKey}`,
504
- "Content-Type": "application/json",
505
- },
506
- body: JSON.stringify({
507
- model: "mistral-small-latest",
508
- messages: [
509
- { role: "system", content: ADVISOR_SYSTEM_PROMPT },
510
- { role: "user", content: `Route this prompt:\n\n${promptPreview}` },
511
- ],
512
- max_tokens: 100,
513
- temperature: 0,
514
- }),
515
- });
516
-
517
- if (!response.ok) return null;
518
-
519
- const data: any = await response.json();
520
- const content = data?.choices?.[0]?.message?.content?.trim();
521
- if (!content) return null;
522
-
523
- // Parse JSON response (handle markdown code blocks)
524
- const jsonStr = content.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
525
- const decision = JSON.parse(jsonStr) as AdvisorDecision;
526
-
527
- // Validate the decision
528
- if (!decision.provider || !decision.model) return null;
529
-
530
- // Check if the suggested provider is blocked
531
- if (settings.blockedProviders.includes(decision.provider)) return null;
532
-
533
- return decision;
534
- } catch {
535
- // Advisor failed silently -- fall through to rule-based routing
536
- return null;
537
- }
538
- }
539
-
540
- async function routeLLMRequest(
541
- requestedModel: string,
542
- settings: {
543
- routingMode: string;
544
- preferredProviders: string[];
545
- blockedProviders: string[];
546
- allowOpenRouterFallback: boolean;
547
- },
548
- messages?: Array<{ role: string; content: string }>
549
- ): Promise<RoutingDecision | null> {
550
- // 1. Direct provider match -- always wins, no advisor needed
551
- for (const mapping of MODEL_PROVIDER_MAP) {
552
- if (!mapping.pattern.test(requestedModel)) continue;
553
- if (settings.blockedProviders.includes(mapping.provider)) continue;
554
-
555
- const providerMeta = PROVIDERS[mapping.provider];
556
- if (!providerMeta?.isLLM || !providerMeta.envKey || !providerMeta.baseUrl) continue;
557
-
558
- const apiKey = process.env[providerMeta.envKey];
559
- if (!apiKey) continue;
560
-
561
- // For "highest_quality" mode, prefer OpenRouter (more model options)
562
- if (settings.routingMode === "highest_quality" && !settings.preferredProviders.includes(mapping.provider)) {
563
- continue;
564
- }
565
-
566
- return {
567
- provider: mapping.provider,
568
- model: mapping.nativeModel,
569
- baseUrl: providerMeta.baseUrl,
570
- apiKey,
571
- reason: `direct_${mapping.provider}`,
572
- };
573
- }
574
-
575
- // 2. ADVISOR -- intelligent model selection for ambiguous routing
576
- // Triggers when: model is generic ("auto", empty, or provider-prefixed like "openai/gpt-4o")
577
- // AND routing mode is "balanced" (default)
578
- // AND we have messages to analyze
579
- const isAutoModel = !requestedModel || requestedModel === "auto";
580
- const useAdvisor = isAutoModel && settings.routingMode === "balanced" && messages && messages.length > 0;
581
-
582
- if (useAdvisor) {
583
- const advisorDecision = await advisorPickModel(messages, settings);
584
- if (advisorDecision) {
585
- // Map advisor decision to a routing decision
586
- const providerKey = advisorDecision.provider;
587
- const providerMeta = PROVIDERS[providerKey];
588
-
589
- if (providerMeta?.isLLM && providerMeta.envKey && providerMeta.baseUrl) {
590
- const apiKey = process.env[providerMeta.envKey];
591
- if (apiKey) {
592
- return {
593
- provider: providerKey,
594
- model: advisorDecision.model,
595
- baseUrl: providerMeta.baseUrl,
596
- apiKey,
597
- reason: `advisor_${providerKey}: ${advisorDecision.reason}`,
598
- ...(providerKey === "openrouter" ? {
599
- extraHeaders: { "HTTP-Referer": "https://apiclaw.cloud", "X-Title": "APIClaw Gateway" },
600
- } : {}),
601
- };
602
- }
603
- }
604
-
605
- // Advisor picked a provider we don't have direct keys for -- route via OpenRouter
606
- if (!settings.blockedProviders.includes("openrouter") && settings.allowOpenRouterFallback !== false) {
607
- const orKey = process.env.OPENROUTER_API_KEY;
608
- if (orKey) {
609
- return {
610
- provider: "openrouter",
611
- model: advisorDecision.model,
612
- baseUrl: "https://openrouter.ai/api/v1/chat/completions",
613
- apiKey: orKey,
614
- reason: `advisor_via_openrouter: ${advisorDecision.reason}`,
615
- extraHeaders: { "HTTP-Referer": "https://apiclaw.cloud", "X-Title": "APIClaw Gateway" },
616
- };
617
- }
618
- }
619
- }
620
- // Advisor failed -- fall through to rule-based routing
621
- }
622
-
623
- // 3. Static routing mode preferences (fallback)
624
- if (settings.routingMode === "fastest") {
625
- for (const fastProvider of ["groq", "together", "mistral"]) {
626
- if (settings.blockedProviders.includes(fastProvider)) continue;
627
- const meta = PROVIDERS[fastProvider];
628
- if (!meta?.isLLM || !meta.envKey || !meta.baseUrl) continue;
629
- const key = process.env[meta.envKey];
630
- if (!key) continue;
631
- if (requestedModel.includes("anthropic/") || requestedModel.includes("openai/") || requestedModel.includes("google/")) break;
632
- return {
633
- provider: fastProvider,
634
- model: requestedModel,
635
- baseUrl: meta.baseUrl,
636
- apiKey: key,
637
- reason: `fastest_mode_${fastProvider}`,
638
- };
639
- }
640
- }
641
-
642
- // 4. Preferred providers check
643
- for (const preferred of settings.preferredProviders) {
644
- if (settings.blockedProviders.includes(preferred)) continue;
645
- const meta = PROVIDERS[preferred];
646
- if (!meta?.isLLM || !meta.envKey || !meta.baseUrl) continue;
647
- const key = process.env[meta.envKey];
648
- if (!key) continue;
649
- return {
650
- provider: preferred,
651
- model: requestedModel,
652
- baseUrl: meta.baseUrl,
653
- apiKey: key,
654
- reason: `preferred_${preferred}`,
655
- };
656
- }
657
-
658
- // 5. Fallback to OpenRouter
659
- if (!settings.blockedProviders.includes("openrouter") && settings.allowOpenRouterFallback !== false) {
660
- const orKey = process.env.OPENROUTER_API_KEY;
661
- if (orKey) {
662
- return {
663
- provider: "openrouter",
664
- model: requestedModel,
665
- baseUrl: "https://openrouter.ai/api/v1/chat/completions",
666
- apiKey: orKey,
667
- reason: "openrouter_fallback",
668
- extraHeaders: {
669
- "HTTP-Referer": "https://apiclaw.cloud",
670
- "X-Title": "APIClaw Gateway",
671
- },
672
- };
673
- }
674
- }
675
-
676
- return null; // No provider available
677
- }
678
-
679
- // ==============================================
680
- // ANTHROPIC MESSAGES API TRANSLATION
681
- // Translates OpenAI chat format to/from Anthropic Messages API
682
- // ==============================================
683
-
684
- function openaiToAnthropicRequest(
685
- model: string,
686
- messages: Array<{ role: string; content: any }>,
687
- rest: Record<string, any>
688
- ): { body: any; headers: Record<string, string> } {
689
- // Extract system message
690
- const systemMessages = messages.filter(m => m.role === "system");
691
- const nonSystemMessages = messages.filter(m => m.role !== "system");
692
- const systemText = systemMessages.map(m => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n\n");
693
-
694
- const body: any = {
695
- model,
696
- messages: nonSystemMessages.map(m => ({
697
- role: m.role === "assistant" ? "assistant" : "user",
698
- content: m.content,
699
- })),
700
- max_tokens: rest.max_tokens || rest.max_completion_tokens || 4096,
701
- };
702
- if (systemText) body.system = systemText;
703
- if (rest.temperature !== undefined) body.temperature = rest.temperature;
704
- if (rest.top_p !== undefined) body.top_p = rest.top_p;
705
- if (rest.stop) body.stop_sequences = Array.isArray(rest.stop) ? rest.stop : [rest.stop];
706
-
707
- return { body, headers: {} };
708
- }
709
-
710
- function anthropicToOpenaiResponse(anthropicData: any, model: string): any {
711
- const content = anthropicData.content?.[0]?.text || "";
712
- const inputTokens = anthropicData.usage?.input_tokens || 0;
713
- const outputTokens = anthropicData.usage?.output_tokens || 0;
714
-
715
- return {
716
- id: anthropicData.id || `chatcmpl-${Date.now()}`,
717
- object: "chat.completion",
718
- created: Math.floor(Date.now() / 1000),
719
- model,
720
- choices: [{
721
- index: 0,
722
- message: { role: "assistant", content },
723
- finish_reason: anthropicData.stop_reason === "end_turn" ? "stop" : (anthropicData.stop_reason || "stop"),
724
- }],
725
- usage: {
726
- prompt_tokens: inputTokens,
727
- completion_tokens: outputTokens,
728
- total_tokens: inputTokens + outputTokens,
729
- },
730
- };
731
- }
732
-
733
- // CORS headers
734
- const corsHeaders = {
735
- "Access-Control-Allow-Origin": "*",
736
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
737
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-APIClaw-Internal, X-APIClaw-Subagent",
738
- };
739
-
740
- // Helper for JSON responses
741
- function jsonResponse(data: unknown, status = 200) {
742
- return new Response(JSON.stringify(data), {
743
- status,
744
- headers: { "Content-Type": "application/json", ...corsHeaders },
745
- });
746
- }
747
-
748
- // ============================================
749
- // UNIFIED AUTH: resolves workspace from any auth method
750
- // Priority: 1) Authorization: Bearer sk-claw-... (API key)
751
- // 2) X-APIClaw-Identifier (legacy MCP workspace ID)
752
- // 3) Anonymous (still allowed, just untracked)
753
- // ============================================
754
-
755
- async function resolveWorkspaceFromRequest(
756
- ctx: any,
757
- request: Request
758
- ): Promise<{ workspaceId?: string; keyId?: string; authMethod: "api-key" | "identifier" | "anonymous" }> {
759
- // 1. Check for API key auth (Bearer sk-claw-...)
760
- const authHeader = request.headers.get("Authorization");
761
- if (authHeader?.startsWith("Bearer sk-claw-")) {
762
- const rawKey = authHeader.slice(7); // Remove "Bearer "
763
- try {
764
- const resolved = await ctx.runQuery(internal.apiKeys.resolveKey, { rawKey });
765
- if (resolved) {
766
- // Touch lastUsedAt (fire and forget)
767
- ctx.runMutation(api.apiKeys.touchKey, { keyId: resolved.keyId }).catch(() => {});
768
- return { workspaceId: resolved.workspaceId, keyId: resolved.keyId, authMethod: "api-key" };
769
- }
770
- } catch (e: any) {
771
- console.error("[Auth] API key resolution failed:", e.message);
772
- }
773
- // Invalid key - don't fall through to anonymous
774
- return { authMethod: "anonymous" };
775
- }
776
-
777
- // 2. Check for legacy identifier
778
- const identifier = request.headers.get("X-APIClaw-Identifier");
779
- if (identifier && !identifier.startsWith("anon:") && identifier !== "unknown" && identifier.length > 20) {
780
- return { workspaceId: identifier, authMethod: "identifier" };
781
- }
782
-
783
- // 3. Anonymous
784
- return { authMethod: "anonymous" };
785
- }
786
-
787
- // Helper to validate session and log API usage
788
- async function validateAndLogProxyCall(
789
- ctx: any,
790
- request: Request,
791
- provider: string,
792
- action: string
793
- ): Promise<{ valid: boolean; workspaceId?: string; subagentId?: string; error?: string; authMethod?: string }> {
794
- const subagentId = request.headers.get("X-APIClaw-Subagent") || "main";
795
-
796
- // Resolve workspace from any auth method
797
- const auth = await resolveWorkspaceFromRequest(ctx, request);
798
- const resolvedWorkspaceId = auth.workspaceId;
799
- const identifier = request.headers.get("X-APIClaw-Identifier") || auth.workspaceId || "unknown";
800
-
801
- console.log("[Proxy] Call received", { provider, action, authMethod: auth.authMethod, workspaceId: resolvedWorkspaceId, subagentId });
802
-
803
- // ALWAYS log to analytics (even if identifier is missing)
804
- try {
805
- const result = await ctx.runMutation(api.analytics.log, {
806
- event: "api_call",
807
- provider,
808
- identifier: identifier,
809
- workspaceId: resolvedWorkspaceId as any,
810
- metadata: { action, subagentId, authMethod: auth.authMethod },
811
- });
812
- console.log("[Proxy] Analytics logged:", result);
813
- } catch (e: any) {
814
- console.error("[Proxy] Analytics logging failed:", e.message, e.stack);
815
- }
816
-
817
- // If we have a workspace, log and increment usage
818
- if (resolvedWorkspaceId) {
819
- try {
820
- await ctx.runMutation(api.logs.createProxyLog, {
821
- workspaceId: resolvedWorkspaceId as any,
822
- provider,
823
- action,
824
- subagentId,
825
- });
826
-
827
- await ctx.runMutation(api.workspaces.incrementUsage, {
828
- workspaceId: resolvedWorkspaceId as any,
829
- });
830
-
831
- console.log("[Proxy] Workspace logged for:", resolvedWorkspaceId);
832
- return { valid: true, workspaceId: resolvedWorkspaceId, subagentId, authMethod: auth.authMethod };
833
- } catch (e: any) {
834
- console.error("[Proxy] Workspace logging failed:", e.message);
835
- }
836
- }
837
-
838
- // Return success regardless (don't block API calls)
839
- return { valid: true, subagentId, authMethod: auth.authMethod };
840
- }
841
-
842
- // OPTIONS handler for CORS
843
- http.route({
844
- path: "/api/discover",
845
- method: "OPTIONS",
846
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
847
- });
848
-
849
- http.route({
850
- path: "/api/details",
851
- method: "OPTIONS",
852
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
853
- });
854
-
855
- http.route({
856
- path: "/api/balance",
857
- method: "OPTIONS",
858
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
859
- });
860
-
861
- http.route({
862
- path: "/api/purchase",
863
- method: "OPTIONS",
864
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
865
- });
866
-
867
- http.route({
868
- path: "/admin/grant-credits",
869
- method: "OPTIONS",
870
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
871
- });
872
-
873
- // Full registry discovery — proxies to Vercel catalog (26,704 APIs)
874
- http.route({
875
- path: "/v1/discover",
876
- method: "OPTIONS",
877
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
878
- });
879
-
880
- http.route({
881
- path: "/v1/discover",
882
- method: "POST",
883
- handler: httpAction(async (ctx, request) => {
884
- try {
885
- const body = await request.json();
886
- const query = body.query || "";
887
- const category = body.category || "";
888
- const callableOnly = body.callable_only ?? false;
889
- const page = body.page || 1;
890
- const limit = Math.min(body.limit || 20, 100);
891
-
892
- // Build query params for the Vercel catalog endpoint
893
- const params = new URLSearchParams();
894
- if (query) params.set("q", query);
895
- if (category) params.set("category", category);
896
- if (callableOnly) params.set("callable", "true");
897
- params.set("page", String(page));
898
- params.set("limit", String(limit));
899
-
900
- const catalogUrl = `https://apiclaw.cloud/api/catalog?${params.toString()}`;
901
- const catalogRes = await fetch(catalogUrl);
902
-
903
- if (!catalogRes.ok) {
904
- return jsonResponse({ error: "Registry unavailable" }, 502);
905
- }
906
-
907
- const catalogData = await catalogRes.json() as {
908
- items: Array<{ name: string; description: string; category: string; baseUrl: string; docsUrl: string; auth: string; pricing: string; callable?: boolean }>;
909
- total: number;
910
- page: number;
911
- limit: number;
912
- hasMore: boolean;
913
- categories: Record<string, { total: number; callable: number }>;
914
- totalCallable: number;
915
- };
916
-
917
- // Also include managed providers from PROVIDERS catalog
918
- const managedProviders = Object.entries(PROVIDERS).map(([id, p]) => ({
919
- providerId: id,
920
- name: p.name,
921
- description: p.description,
922
- category: p.category,
923
- managed: true,
924
- }));
925
-
926
- return jsonResponse({
927
- apis: catalogData.items,
928
- total: catalogData.total,
929
- page: catalogData.page,
930
- limit: catalogData.limit,
931
- hasMore: catalogData.hasMore,
932
- categories: catalogData.categories,
933
- totalCallable: catalogData.totalCallable,
934
- managedProviders: managedProviders,
935
- _meta: {
936
- registry: "26,704 discoverable APIs",
937
- managed: `${managedProviders.length} managed providers`,
938
- docs: "https://apiclaw.cloud/docs",
939
- },
940
- });
941
- } catch (e: any) {
942
- return jsonResponse({ error: "Discovery failed", details: e.message }, 500);
943
- }
944
- }),
945
- });
946
-
947
- // Discover managed providers only (legacy endpoint)
948
- http.route({
949
- path: "/api/discover",
950
- method: "POST",
951
- handler: httpAction(async (ctx, request) => {
952
- try {
953
- const startTime = Date.now();
954
- const body = await request.json();
955
- const query = (body.query || "").toLowerCase();
956
-
957
- // Get optional auth context
958
- const sessionToken = request.headers.get("X-APIClaw-Session");
959
- const userAgent = request.headers.get("User-Agent");
960
-
961
- const results = Object.entries(PROVIDERS)
962
- .filter(([id, provider]) => {
963
- if (!query) return true;
964
- return (
965
- provider.name.toLowerCase().includes(query) ||
966
- provider.description.toLowerCase().includes(query) ||
967
- provider.category.toLowerCase().includes(query) ||
968
- provider.tags.some((tag) => tag.includes(query))
969
- );
970
- })
971
- .map(([id, provider]) => ({
972
- providerId: id,
973
- ...provider,
974
- }));
975
-
976
- const responseTimeMs = Date.now() - startTime;
977
-
978
- // Log the search (fire and forget)
979
- if (query) {
980
- ctx.runMutation(internal.searchLogs.logSearch, {
981
- query: body.query || "", // Original query (not lowercased)
982
- resultsCount: results.length,
983
- matchedProviders: results.map(r => r.providerId),
984
- sessionToken: sessionToken || undefined,
985
- userAgent: userAgent || undefined,
986
- responseTimeMs,
987
- }).catch(() => {}); // Ignore errors, don't block response
988
- }
989
-
990
- return jsonResponse({ providers: results, total: results.length });
991
- } catch (e) {
992
- return jsonResponse({ error: "Invalid request" }, 400);
993
- }
994
- }),
995
- });
996
-
997
- // Get provider details
998
- http.route({
999
- path: "/api/details",
1000
- method: "POST",
1001
- handler: httpAction(async (ctx, request) => {
1002
- try {
1003
- const body = await request.json();
1004
- const { providerId } = body;
1005
-
1006
- if (!providerId) {
1007
- return jsonResponse({ error: "providerId required" }, 400);
1008
- }
1009
-
1010
- const provider = PROVIDERS[providerId as keyof typeof PROVIDERS];
1011
- if (!provider) {
1012
- return jsonResponse({ error: "Provider not found" }, 404);
1013
- }
1014
-
1015
- return jsonResponse({
1016
- providerId,
1017
- ...provider,
1018
- creditsPerDollar: getCreditsPerDollar(providerId),
1019
- documentation: `https://apiclaw.com/docs/${providerId}`,
1020
- });
1021
- } catch (e) {
1022
- return jsonResponse({ error: "Invalid request" }, 400);
1023
- }
1024
- }),
1025
- });
1026
-
1027
- // Check balance
1028
- http.route({
1029
- path: "/api/balance",
1030
- method: "GET",
1031
- handler: httpAction(async (ctx, request) => {
1032
- const url = new URL(request.url);
1033
- const agentId = url.searchParams.get("agentId");
1034
-
1035
- if (!agentId) {
1036
- return jsonResponse({ error: "agentId required" }, 400);
1037
- }
1038
-
1039
- const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
1040
-
1041
- if (!credits) {
1042
- return jsonResponse({
1043
- agentId,
1044
- balanceUsd: 0,
1045
- currency: "USD",
1046
- message: "No account found. Top up to get started!",
1047
- });
1048
- }
1049
-
1050
- return jsonResponse({
1051
- agentId: credits.agentId,
1052
- balanceUsd: credits.balanceUsd,
1053
- currency: credits.currency,
1054
- });
1055
- }),
1056
- });
1057
-
1058
- // Purchase API access
1059
- http.route({
1060
- path: "/api/purchase",
1061
- method: "POST",
1062
- handler: httpAction(async (ctx, request) => {
1063
- try {
1064
- const body = await request.json();
1065
- const { agentId, providerId, amountUsd } = body;
1066
-
1067
- if (!agentId || !providerId || !amountUsd) {
1068
- return jsonResponse(
1069
- { error: "agentId, providerId, and amountUsd required" },
1070
- 400
1071
- );
1072
- }
1073
-
1074
- if (amountUsd < 1 || amountUsd > 1000) {
1075
- return jsonResponse(
1076
- { error: "amountUsd must be between 1 and 1000" },
1077
- 400
1078
- );
1079
- }
1080
-
1081
- const provider = PROVIDERS[providerId as keyof typeof PROVIDERS];
1082
- if (!provider) {
1083
- return jsonResponse({ error: "Provider not found" }, 404);
1084
- }
1085
-
1086
- // Check balance first
1087
- const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
1088
- if (!credits || credits.balanceUsd < amountUsd) {
1089
- return jsonResponse(
1090
- {
1091
- error: "Insufficient balance",
1092
- currentBalance: credits?.balanceUsd || 0,
1093
- required: amountUsd,
1094
- },
1095
- 402
1096
- );
1097
- }
1098
-
1099
- // Execute purchase
1100
- const purchase = await ctx.runMutation(api.purchases.purchaseAccess, {
1101
- agentId,
1102
- providerId,
1103
- amountUsd,
1104
- credentials: generateCredentials(providerId),
1105
- });
1106
-
1107
- if (!purchase) {
1108
- return jsonResponse({ error: "Purchase failed" }, 500);
1109
- }
1110
-
1111
- return jsonResponse({
1112
- success: true,
1113
- purchase: {
1114
- id: purchase._id,
1115
- providerId: purchase.providerId,
1116
- amountUsd: purchase.amountUsd,
1117
- creditsGranted: purchase.creditsGranted,
1118
- status: purchase.status,
1119
- },
1120
- message: `Successfully purchased $${amountUsd} of ${provider.name} credits`,
1121
- });
1122
- } catch (e: any) {
1123
- return jsonResponse({ error: e.message || "Purchase failed" }, 400);
1124
- }
1125
- }),
1126
- });
1127
-
1128
- // Admin: Grant credits
1129
- http.route({
1130
- path: "/admin/grant-credits",
1131
- method: "POST",
1132
- handler: httpAction(async (ctx, request) => {
1133
- try {
1134
- const body = await request.json();
1135
- const { agentId, amount, reason } = body;
1136
-
1137
- if (!agentId || !amount) {
1138
- return jsonResponse({ error: "agentId and amount required" }, 400);
1139
- }
1140
-
1141
- // TODO: Add admin auth check here
1142
- // For now, allow grants (this is for Hivr integration)
1143
-
1144
- const result = await ctx.runMutation(api.credits.addCredits, {
1145
- agentId,
1146
- amountUsd: amount,
1147
- source: reason || "admin_grant",
1148
- });
1149
-
1150
- return jsonResponse({
1151
- success: true,
1152
- agentId,
1153
- credited: amount,
1154
- newBalance: result?.balanceUsd,
1155
- reason,
1156
- });
1157
- } catch (e: any) {
1158
- return jsonResponse({ error: e.message || "Grant failed" }, 400);
1159
- }
1160
- }),
1161
- });
1162
-
1163
- // Helper functions
1164
- function getCreditsPerDollar(providerId: string): number {
1165
- const rates: Record<string, number> = {
1166
- "46elks": 30,
1167
- twilio: 25,
1168
- resend: 1000,
1169
- brave_search: 200,
1170
- openrouter: 100,
1171
- elevenlabs: 3333,
1172
- };
1173
- return rates[providerId] || 100;
1174
- }
1175
-
1176
- function generateCredentials(providerId: string): object {
1177
- // In production, this would generate or retrieve actual API keys
1178
- // For now, return placeholder indicating how to use
1179
- return {
1180
- type: "apiclaw_proxy",
1181
- endpoint: `https://brilliant-puffin-712.convex.site/proxy/${providerId}`,
1182
- note: "Use APIClaw proxy endpoint. Credentials managed automatically.",
1183
- };
1184
- }
1185
-
1186
- export default http;
1187
-
1188
- // ==============================================
1189
- // DIRECT CALL PROXY ENDPOINTS
1190
- // ==============================================
1191
-
1192
- // OpenRouter proxy
1193
- http.route({
1194
- path: "/proxy/openrouter",
1195
- method: "POST",
1196
- handler: httpAction(async (ctx, request) => {
1197
- // Validate session and log usage
1198
- await validateAndLogProxyCall(ctx, request, "openrouter", "chat");
1199
-
1200
- const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY;
1201
- if (!OPENROUTER_KEY) {
1202
- return jsonResponse({ error: "OpenRouter not configured" }, 500);
1203
- }
1204
-
1205
- try {
1206
- const body = await request.json();
1207
-
1208
- const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
1209
- method: "POST",
1210
- headers: {
1211
- "Authorization": `Bearer ${OPENROUTER_KEY}`,
1212
- "Content-Type": "application/json",
1213
- "HTTP-Referer": "https://apiclaw.cloud",
1214
- "X-Title": "APIClaw",
1215
- },
1216
- body: JSON.stringify(body),
1217
- });
1218
-
1219
- const data = await response.json();
1220
- return jsonResponse(data, response.status);
1221
- } catch (e: any) {
1222
- return jsonResponse({ error: e.message }, 500);
1223
- }
1224
- }),
1225
- });
1226
-
1227
- // Brave Search proxy
1228
- http.route({
1229
- path: "/proxy/brave_search",
1230
- method: "POST",
1231
- handler: httpAction(async (ctx, request) => {
1232
- // Validate session and log usage
1233
- await validateAndLogProxyCall(ctx, request, "brave_search", "search");
1234
-
1235
- const BRAVE_KEY = process.env.BRAVE_API_KEY;
1236
- if (!BRAVE_KEY) {
1237
- return jsonResponse({ error: "Brave Search not configured" }, 500);
1238
- }
1239
-
1240
- try {
1241
- const body = await request.json();
1242
- const { query, count = 10 } = body;
1243
-
1244
- const url = new URL("https://api.search.brave.com/res/v1/web/search");
1245
- url.searchParams.set("q", query);
1246
- url.searchParams.set("count", String(count));
1247
-
1248
- const response = await fetch(url.toString(), {
1249
- headers: { "X-Subscription-Token": BRAVE_KEY },
1250
- });
1251
-
1252
- const data = await response.json();
1253
- return jsonResponse(data, response.status);
1254
- } catch (e: any) {
1255
- return jsonResponse({ error: e.message }, 500);
1256
- }
1257
- }),
1258
- });
1259
-
1260
- // Resend email proxy
1261
- http.route({
1262
- path: "/proxy/resend",
1263
- method: "POST",
1264
- handler: httpAction(async (ctx, request) => {
1265
- // Validate session and log usage
1266
- await validateAndLogProxyCall(ctx, request, "resend", "send_email");
1267
-
1268
- const RESEND_KEY = process.env.RESEND_API_KEY;
1269
- if (!RESEND_KEY) {
1270
- return jsonResponse({ error: "Resend not configured" }, 500);
1271
- }
1272
-
1273
- try {
1274
- const body = await request.json();
1275
-
1276
- const response = await fetch("https://api.resend.com/emails", {
1277
- method: "POST",
1278
- headers: {
1279
- "Authorization": `Bearer ${RESEND_KEY}`,
1280
- "Content-Type": "application/json",
1281
- },
1282
- body: JSON.stringify(body),
1283
- });
1284
-
1285
- const data = await response.json();
1286
- return jsonResponse(data, response.status);
1287
- } catch (e: any) {
1288
- return jsonResponse({ error: e.message }, 500);
1289
- }
1290
- }),
1291
- });
1292
-
1293
- // ElevenLabs TTS proxy
1294
- http.route({
1295
- path: "/proxy/elevenlabs",
1296
- method: "POST",
1297
- handler: httpAction(async (ctx, request) => {
1298
- // Validate session and log usage
1299
- await validateAndLogProxyCall(ctx, request, "elevenlabs", "text_to_speech");
1300
-
1301
- const ELEVENLABS_KEY = process.env.ELEVENLABS_API_KEY;
1302
- if (!ELEVENLABS_KEY) {
1303
- return jsonResponse({ error: "ElevenLabs not configured" }, 500);
1304
- }
1305
-
1306
- try {
1307
- const body = await request.json();
1308
- const { text, voice_id = "21m00Tcm4TlvDq8ikWAM" } = body;
1309
-
1310
- const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`, {
1311
- method: "POST",
1312
- headers: {
1313
- "xi-api-key": ELEVENLABS_KEY,
1314
- "Content-Type": "application/json",
1315
- },
1316
- body: JSON.stringify({
1317
- text,
1318
- model_id: "eleven_turbo_v2",
1319
- }),
1320
- });
1321
-
1322
- if (!response.ok) {
1323
- const error = await response.text();
1324
- return jsonResponse({ error }, response.status);
1325
- }
1326
-
1327
- // Return audio as base64
1328
- const arrayBuffer = await response.arrayBuffer();
1329
- const base64 = Buffer.from(arrayBuffer).toString("base64");
1330
-
1331
- return jsonResponse({
1332
- audio_base64: base64,
1333
- content_type: "audio/mpeg",
1334
- });
1335
- } catch (e: any) {
1336
- return jsonResponse({ error: e.message }, 500);
1337
- }
1338
- }),
1339
- });
1340
-
1341
- http.route({
1342
- path: "/proxy/openrouter",
1343
- method: "OPTIONS",
1344
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1345
- });
1346
-
1347
- http.route({
1348
- path: "/proxy/brave_search",
1349
- method: "OPTIONS",
1350
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1351
- });
1352
-
1353
- http.route({
1354
- path: "/proxy/resend",
1355
- method: "OPTIONS",
1356
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1357
- });
1358
-
1359
- http.route({
1360
- path: "/proxy/elevenlabs",
1361
- method: "OPTIONS",
1362
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1363
- });
1364
-
1365
- // 46elks SMS proxy
1366
- http.route({
1367
- path: "/proxy/46elks",
1368
- method: "POST",
1369
- handler: httpAction(async (ctx, request) => {
1370
- // Validate session and log usage
1371
- await validateAndLogProxyCall(ctx, request, "46elks", "send_sms");
1372
-
1373
- const ELKS_USER = process.env.ELKS_API_USER;
1374
- const ELKS_PASS = process.env.ELKS_API_PASSWORD;
1375
- if (!ELKS_USER || !ELKS_PASS) {
1376
- return jsonResponse({ error: "46elks not configured" }, 500);
1377
- }
1378
-
1379
- try {
1380
- const body = await request.json();
1381
- const { to, message, from = "APIClaw" } = body;
1382
-
1383
- const auth = btoa(`${ELKS_USER}:${ELKS_PASS}`);
1384
-
1385
- const response = await fetch("https://api.46elks.com/a1/sms", {
1386
- method: "POST",
1387
- headers: {
1388
- "Authorization": `Basic ${auth}`,
1389
- "Content-Type": "application/x-www-form-urlencoded",
1390
- },
1391
- body: new URLSearchParams({ from, to, message }),
1392
- });
1393
-
1394
- const data = await response.json();
1395
- return jsonResponse(data, response.status);
1396
- } catch (e: any) {
1397
- return jsonResponse({ error: e.message }, 500);
1398
- }
1399
- }),
1400
- });
1401
-
1402
- // Twilio SMS proxy
1403
- http.route({
1404
- path: "/proxy/twilio",
1405
- method: "POST",
1406
- handler: httpAction(async (ctx, request) => {
1407
- // Validate session and log usage
1408
- await validateAndLogProxyCall(ctx, request, "twilio", "send_sms");
1409
-
1410
- const TWILIO_SID = process.env.TWILIO_ACCOUNT_SID;
1411
- const TWILIO_TOKEN = process.env.TWILIO_AUTH_TOKEN;
1412
- if (!TWILIO_SID || !TWILIO_TOKEN) {
1413
- return jsonResponse({ error: "Twilio not configured" }, 500);
1414
- }
1415
-
1416
- try {
1417
- const body = await request.json();
1418
- const { to, message, from } = body;
1419
-
1420
- if (!from) {
1421
- return jsonResponse({ error: "Twilio requires 'from' number" }, 400);
1422
- }
1423
-
1424
- const auth = btoa(`${TWILIO_SID}:${TWILIO_TOKEN}`);
1425
-
1426
- const response = await fetch(
1427
- `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json`,
1428
- {
1429
- method: "POST",
1430
- headers: {
1431
- "Authorization": `Basic ${auth}`,
1432
- "Content-Type": "application/x-www-form-urlencoded",
1433
- },
1434
- body: new URLSearchParams({ To: to, From: from, Body: message }),
1435
- }
1436
- );
1437
-
1438
- const data = await response.json();
1439
- return jsonResponse(data, response.status);
1440
- } catch (e: any) {
1441
- return jsonResponse({ error: e.message }, 500);
1442
- }
1443
- }),
1444
- });
1445
-
1446
- // CORS for new endpoints
1447
- http.route({
1448
- path: "/proxy/46elks",
1449
- method: "OPTIONS",
1450
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1451
- });
1452
-
1453
- http.route({
1454
- path: "/proxy/twilio",
1455
- method: "OPTIONS",
1456
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1457
- });
1458
-
1459
- // GitHub API proxy
1460
- http.route({
1461
- path: "/proxy/github",
1462
- method: "POST",
1463
- handler: httpAction(async (ctx, request) => {
1464
- // Validate session and log usage
1465
- const body = await request.json();
1466
- const action = body.action || "search_repos";
1467
- await validateAndLogProxyCall(ctx, request, "github", action);
1468
-
1469
- const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
1470
- if (!GITHUB_TOKEN) {
1471
- return jsonResponse({ error: "GitHub not configured" }, 500);
1472
- }
1473
-
1474
- try {
1475
- const { action, ...params } = body;
1476
- let url: string;
1477
- let method = "GET";
1478
- let fetchBody: string | undefined;
1479
-
1480
- // Route based on action
1481
- switch (action) {
1482
- case "search_repos":
1483
- const { query, sort = "stars", limit = 10 } = params;
1484
- url = `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`;
1485
- break;
1486
-
1487
- case "get_repo":
1488
- const { owner, repo } = params;
1489
- url = `https://api.github.com/repos/${owner}/${repo}`;
1490
- break;
1491
-
1492
- case "list_issues":
1493
- const { owner: issueOwner, repo: issueRepo, state = "open", limit: issueLimit = 10 } = params;
1494
- url = `https://api.github.com/repos/${issueOwner}/${issueRepo}/issues?state=${state}&per_page=${issueLimit}`;
1495
- break;
1496
-
1497
- case "create_issue":
1498
- const { owner: createOwner, repo: createRepo, title, body: issueBody = "" } = params;
1499
- url = `https://api.github.com/repos/${createOwner}/${createRepo}/issues`;
1500
- method = "POST";
1501
- fetchBody = JSON.stringify({ title, body: issueBody });
1502
- break;
1503
-
1504
- case "get_file":
1505
- const { owner: fileOwner, repo: fileRepo, path } = params;
1506
- url = `https://api.github.com/repos/${fileOwner}/${fileRepo}/contents/${path}`;
1507
- break;
1508
-
1509
- default:
1510
- return jsonResponse({ error: `Unknown action: ${action}` }, 400);
1511
- }
1512
-
1513
- const response = await fetch(url, {
1514
- method,
1515
- headers: {
1516
- "Authorization": `Bearer ${GITHUB_TOKEN}`,
1517
- "Accept": "application/vnd.github+json",
1518
- "User-Agent": "APIClaw",
1519
- ...(fetchBody ? { "Content-Type": "application/json" } : {}),
1520
- },
1521
- ...(fetchBody ? { body: fetchBody } : {}),
1522
- });
1523
-
1524
- const data = await response.json();
1525
- return jsonResponse(data, response.status);
1526
- } catch (e: any) {
1527
- return jsonResponse({ error: e.message }, 500);
1528
- }
1529
- }),
1530
- });
1531
-
1532
- http.route({
1533
- path: "/proxy/github",
1534
- method: "OPTIONS",
1535
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1536
- });
1537
-
1538
- // ==============================================
1539
- // SERPER (Google Search) PROXY
1540
- // ==============================================
1541
- http.route({
1542
- path: "/proxy/serper",
1543
- method: "POST",
1544
- handler: httpAction(async (ctx, request) => {
1545
- await validateAndLogProxyCall(ctx, request, "serper", "search");
1546
- const SERPER_KEY = process.env.SERPER_API_KEY;
1547
- if (!SERPER_KEY) {
1548
- return jsonResponse({ error: "Serper not configured" }, 500);
1549
- }
1550
- try {
1551
- const body = await request.json();
1552
- const { query, q, num = 10, gl = "us", hl = "en" } = body;
1553
- const searchQuery = query || q;
1554
- if (!searchQuery) {
1555
- return jsonResponse({ error: "query required" }, 400);
1556
- }
1557
- const response = await fetch("https://google.serper.dev/search", {
1558
- method: "POST",
1559
- headers: {
1560
- "X-API-KEY": SERPER_KEY,
1561
- "Content-Type": "application/json",
1562
- },
1563
- body: JSON.stringify({ q: searchQuery, num, gl, hl }),
1564
- });
1565
- const data = await response.json();
1566
- return jsonResponse(data, response.status);
1567
- } catch (e: any) {
1568
- return jsonResponse({ error: e.message }, 500);
1569
- }
1570
- }),
1571
- });
1572
-
1573
- http.route({
1574
- path: "/proxy/serper",
1575
- method: "OPTIONS",
1576
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1577
- });
1578
-
1579
- // ==============================================
1580
- // FIRECRAWL (Web Scraping) PROXY
1581
- // ==============================================
1582
- http.route({
1583
- path: "/proxy/firecrawl",
1584
- method: "POST",
1585
- handler: httpAction(async (ctx, request) => {
1586
- await validateAndLogProxyCall(ctx, request, "firecrawl", "scrape");
1587
- const FIRECRAWL_KEY = process.env.FIRECRAWL_API_KEY;
1588
- if (!FIRECRAWL_KEY) {
1589
- return jsonResponse({ error: "Firecrawl not configured" }, 500);
1590
- }
1591
- try {
1592
- const body = await request.json();
1593
- const { url, formats = ["markdown"], onlyMainContent = true } = body;
1594
- if (!url) {
1595
- return jsonResponse({ error: "url required" }, 400);
1596
- }
1597
- const response = await fetch("https://api.firecrawl.dev/v1/scrape", {
1598
- method: "POST",
1599
- headers: {
1600
- Authorization: `Bearer ${FIRECRAWL_KEY}`,
1601
- "Content-Type": "application/json",
1602
- },
1603
- body: JSON.stringify({ url, formats, onlyMainContent }),
1604
- });
1605
- const data = await response.json();
1606
- return jsonResponse(data, response.status);
1607
- } catch (e: any) {
1608
- return jsonResponse({ error: e.message }, 500);
1609
- }
1610
- }),
1611
- });
1612
-
1613
- http.route({
1614
- path: "/proxy/firecrawl",
1615
- method: "OPTIONS",
1616
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1617
- });
1618
-
1619
- // ==============================================
1620
- // GROQ (LLM) PROXY
1621
- // ==============================================
1622
- http.route({
1623
- path: "/proxy/groq",
1624
- method: "POST",
1625
- handler: httpAction(async (ctx, request) => {
1626
- await validateAndLogProxyCall(ctx, request, "groq", "chat");
1627
- const GROQ_KEY = process.env.GROQ_API_KEY;
1628
- if (!GROQ_KEY) {
1629
- return jsonResponse({ error: "Groq not configured" }, 500);
1630
- }
1631
- try {
1632
- const body = await request.json();
1633
- const { model = "llama-3.3-70b-versatile", messages, temperature = 0.7, max_tokens = 1024 } = body;
1634
- if (!messages) {
1635
- return jsonResponse({ error: "messages required" }, 400);
1636
- }
1637
- const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
1638
- method: "POST",
1639
- headers: {
1640
- Authorization: `Bearer ${GROQ_KEY}`,
1641
- "Content-Type": "application/json",
1642
- },
1643
- body: JSON.stringify({ model, messages, temperature, max_tokens }),
1644
- });
1645
- const data = await response.json();
1646
- return jsonResponse(data, response.status);
1647
- } catch (e: any) {
1648
- return jsonResponse({ error: e.message }, 500);
1649
- }
1650
- }),
1651
- });
1652
-
1653
- http.route({
1654
- path: "/proxy/groq",
1655
- method: "OPTIONS",
1656
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1657
- });
1658
-
1659
- // ==============================================
1660
- // MISTRAL (LLM/Embeddings) PROXY
1661
- // ==============================================
1662
- http.route({
1663
- path: "/proxy/mistral",
1664
- method: "POST",
1665
- handler: httpAction(async (ctx, request) => {
1666
- await validateAndLogProxyCall(ctx, request, "mistral", "chat");
1667
- const MISTRAL_KEY = process.env.MISTRAL_API_KEY;
1668
- if (!MISTRAL_KEY) {
1669
- return jsonResponse({ error: "Mistral not configured" }, 500);
1670
- }
1671
- try {
1672
- const body = await request.json();
1673
- const { model = "mistral-small-latest", messages, temperature = 0.7, max_tokens = 1024 } = body;
1674
- if (!messages) {
1675
- return jsonResponse({ error: "messages required" }, 400);
1676
- }
1677
- const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
1678
- method: "POST",
1679
- headers: {
1680
- Authorization: `Bearer ${MISTRAL_KEY}`,
1681
- "Content-Type": "application/json",
1682
- },
1683
- body: JSON.stringify({ model, messages, temperature, max_tokens }),
1684
- });
1685
- const data = await response.json();
1686
- return jsonResponse(data, response.status);
1687
- } catch (e: any) {
1688
- return jsonResponse({ error: e.message }, 500);
1689
- }
1690
- }),
1691
- });
1692
-
1693
- http.route({
1694
- path: "/proxy/mistral",
1695
- method: "OPTIONS",
1696
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1697
- });
1698
-
1699
- // ==============================================
1700
- // COHERE (LLM/Rerank) PROXY
1701
- // ==============================================
1702
- http.route({
1703
- path: "/proxy/cohere",
1704
- method: "POST",
1705
- handler: httpAction(async (ctx, request) => {
1706
- await validateAndLogProxyCall(ctx, request, "cohere", "chat");
1707
- const COHERE_KEY = process.env.COHERE_API_KEY;
1708
- if (!COHERE_KEY) {
1709
- return jsonResponse({ error: "Cohere not configured" }, 500);
1710
- }
1711
- try {
1712
- const body = await request.json();
1713
- const { model = "command-a-03-2025", message, chat_history, temperature = 0.7, max_tokens = 1024 } = body;
1714
- if (!message) {
1715
- return jsonResponse({ error: "message required" }, 400);
1716
- }
1717
- const response = await fetch("https://api.cohere.com/v2/chat", {
1718
- method: "POST",
1719
- headers: {
1720
- Authorization: `Bearer ${COHERE_KEY}`,
1721
- "Content-Type": "application/json",
1722
- },
1723
- body: JSON.stringify({ model, message, chat_history, temperature, max_tokens }),
1724
- });
1725
- const data = await response.json();
1726
- return jsonResponse(data, response.status);
1727
- } catch (e: any) {
1728
- return jsonResponse({ error: e.message }, 500);
1729
- }
1730
- }),
1731
- });
1732
-
1733
- http.route({
1734
- path: "/proxy/cohere",
1735
- method: "OPTIONS",
1736
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1737
- });
1738
-
1739
- // ==============================================
1740
- // REPLICATE (ML Models) PROXY
1741
- // ==============================================
1742
- http.route({
1743
- path: "/proxy/replicate",
1744
- method: "POST",
1745
- handler: httpAction(async (ctx, request) => {
1746
- await validateAndLogProxyCall(ctx, request, "replicate", "prediction");
1747
- const REPLICATE_KEY = process.env.REPLICATE_API_TOKEN;
1748
- if (!REPLICATE_KEY) {
1749
- return jsonResponse({ error: "Replicate not configured" }, 500);
1750
- }
1751
- try {
1752
- const body = await request.json();
1753
- const { model, input, version } = body;
1754
- if (!model && !version) {
1755
- return jsonResponse({ error: "model or version required" }, 400);
1756
- }
1757
- const endpoint = version
1758
- ? "https://api.replicate.com/v1/predictions"
1759
- : `https://api.replicate.com/v1/models/${model}/predictions`;
1760
- const payload = version ? { version, input } : { input };
1761
- const response = await fetch(endpoint, {
1762
- method: "POST",
1763
- headers: {
1764
- Authorization: `Bearer ${REPLICATE_KEY}`,
1765
- "Content-Type": "application/json",
1766
- Prefer: "wait",
1767
- },
1768
- body: JSON.stringify(payload),
1769
- });
1770
- const data = await response.json();
1771
- return jsonResponse(data, response.status);
1772
- } catch (e: any) {
1773
- return jsonResponse({ error: e.message }, 500);
1774
- }
1775
- }),
1776
- });
1777
-
1778
- http.route({
1779
- path: "/proxy/replicate",
1780
- method: "OPTIONS",
1781
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1782
- });
1783
-
1784
- // ==============================================
1785
- // DEEPGRAM (Speech-to-Text) PROXY
1786
- // ==============================================
1787
- http.route({
1788
- path: "/proxy/deepgram",
1789
- method: "POST",
1790
- handler: httpAction(async (ctx, request) => {
1791
- await validateAndLogProxyCall(ctx, request, "deepgram", "transcribe");
1792
- const DEEPGRAM_KEY = process.env.DEEPGRAM_API_KEY;
1793
- if (!DEEPGRAM_KEY) {
1794
- return jsonResponse({ error: "Deepgram not configured" }, 500);
1795
- }
1796
- try {
1797
- const body = await request.json();
1798
- const { url, model = "nova-3", language = "en", smart_format = true } = body;
1799
- if (!url) {
1800
- return jsonResponse({ error: "url required (audio file URL)" }, 400);
1801
- }
1802
- const params = new URLSearchParams({
1803
- model,
1804
- language,
1805
- smart_format: String(smart_format),
1806
- });
1807
- const response = await fetch(
1808
- `https://api.deepgram.com/v1/listen?${params}`,
1809
- {
1810
- method: "POST",
1811
- headers: {
1812
- Authorization: `Token ${DEEPGRAM_KEY}`,
1813
- "Content-Type": "application/json",
1814
- },
1815
- body: JSON.stringify({ url }),
1816
- }
1817
- );
1818
- const data = await response.json();
1819
- return jsonResponse(data, response.status);
1820
- } catch (e: any) {
1821
- return jsonResponse({ error: e.message }, 500);
1822
- }
1823
- }),
1824
- });
1825
-
1826
- http.route({
1827
- path: "/proxy/deepgram",
1828
- method: "OPTIONS",
1829
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1830
- });
1831
-
1832
- // ==============================================
1833
- // E2B (Code Sandbox) PROXY
1834
- // ==============================================
1835
- http.route({
1836
- path: "/proxy/e2b",
1837
- method: "POST",
1838
- handler: httpAction(async (ctx, request) => {
1839
- await validateAndLogProxyCall(ctx, request, "e2b", "execute");
1840
- const E2B_KEY = process.env.E2B_API_KEY;
1841
- if (!E2B_KEY) {
1842
- return jsonResponse({ error: "E2B not configured" }, 500);
1843
- }
1844
- try {
1845
- const body = await request.json();
1846
- const { code, language = "python", template = "base" } = body;
1847
- if (!code) {
1848
- return jsonResponse({ error: "code required" }, 400);
1849
- }
1850
- const response = await fetch("https://api.e2b.dev/sandboxes", {
1851
- method: "POST",
1852
- headers: {
1853
- "X-API-Key": E2B_KEY,
1854
- "Content-Type": "application/json",
1855
- },
1856
- body: JSON.stringify({ templateID: template, metadata: { language } }),
1857
- });
1858
- const sandbox = await response.json();
1859
- if (!response.ok) {
1860
- return jsonResponse(sandbox, response.status);
1861
- }
1862
- const execResponse = await fetch(
1863
- `https://api.e2b.dev/sandboxes/${sandbox.sandboxID}/code/execution`,
1864
- {
1865
- method: "POST",
1866
- headers: {
1867
- "X-API-Key": E2B_KEY,
1868
- "Content-Type": "application/json",
1869
- },
1870
- body: JSON.stringify({ code, language }),
1871
- }
1872
- );
1873
- const result = await execResponse.json();
1874
- return jsonResponse(result, execResponse.status);
1875
- } catch (e: any) {
1876
- return jsonResponse({ error: e.message }, 500);
1877
- }
1878
- }),
1879
- });
1880
-
1881
- http.route({
1882
- path: "/proxy/e2b",
1883
- method: "OPTIONS",
1884
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1885
- });
1886
-
1887
- // ==============================================
1888
- // TOGETHER AI (Open-source LLM Inference) PROXY
1889
- // ==============================================
1890
- http.route({
1891
- path: "/proxy/together",
1892
- method: "POST",
1893
- handler: httpAction(async (ctx, request) => {
1894
- await validateAndLogProxyCall(ctx, request, "together", "chat");
1895
- const TOGETHER_KEY = process.env.TOGETHER_API_KEY;
1896
- if (!TOGETHER_KEY) {
1897
- return jsonResponse({ error: "Together AI not configured" }, 500);
1898
- }
1899
- try {
1900
- const body = await request.json();
1901
- const { model = "meta-llama/Llama-3.3-70B-Instruct-Turbo", messages, temperature = 0.7, max_tokens = 1024 } = body;
1902
- if (!messages || !Array.isArray(messages)) {
1903
- return jsonResponse({ error: "messages array required" }, 400);
1904
- }
1905
- const response = await fetch("https://api.together.xyz/v1/chat/completions", {
1906
- method: "POST",
1907
- headers: {
1908
- Authorization: `Bearer ${TOGETHER_KEY}`,
1909
- "Content-Type": "application/json",
1910
- },
1911
- body: JSON.stringify({ model, messages, temperature, max_tokens }),
1912
- });
1913
- const data = await response.json();
1914
- return jsonResponse(data, response.status);
1915
- } catch (e: any) {
1916
- return jsonResponse({ error: e.message }, 500);
1917
- }
1918
- }),
1919
- });
1920
-
1921
- http.route({
1922
- path: "/proxy/together",
1923
- method: "OPTIONS",
1924
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1925
- });
1926
-
1927
- // ==============================================
1928
- // STABILITY AI (Image Generation) PROXY
1929
- // ==============================================
1930
- http.route({
1931
- path: "/proxy/stability",
1932
- method: "POST",
1933
- handler: httpAction(async (ctx, request) => {
1934
- await validateAndLogProxyCall(ctx, request, "stability", "generate");
1935
- const STABILITY_KEY = process.env.STABILITY_API_KEY;
1936
- if (!STABILITY_KEY) {
1937
- return jsonResponse({ error: "Stability AI not configured" }, 500);
1938
- }
1939
- try {
1940
- const body = await request.json();
1941
- const { prompt, model = "sd3.5-large", output_format = "png", aspect_ratio = "1:1" } = body;
1942
- if (!prompt) {
1943
- return jsonResponse({ error: "prompt required" }, 400);
1944
- }
1945
- const formData = new FormData();
1946
- formData.append("prompt", prompt);
1947
- formData.append("output_format", output_format);
1948
- formData.append("aspect_ratio", aspect_ratio);
1949
- const response = await fetch(
1950
- `https://api.stability.ai/v2beta/stable-image/generate/${model}`,
1951
- {
1952
- method: "POST",
1953
- headers: {
1954
- Authorization: `Bearer ${STABILITY_KEY}`,
1955
- Accept: "application/json",
1956
- },
1957
- body: formData,
1958
- }
1959
- );
1960
- const data = await response.json();
1961
- return jsonResponse(data, response.status);
1962
- } catch (e: any) {
1963
- return jsonResponse({ error: e.message }, 500);
1964
- }
1965
- }),
1966
- });
1967
-
1968
- http.route({
1969
- path: "/proxy/stability",
1970
- method: "OPTIONS",
1971
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
1972
- });
1973
-
1974
- // ==============================================
1975
- // ASSEMBLYAI (Audio Intelligence) PROXY
1976
- // ==============================================
1977
- http.route({
1978
- path: "/proxy/assemblyai",
1979
- method: "POST",
1980
- handler: httpAction(async (ctx, request) => {
1981
- await validateAndLogProxyCall(ctx, request, "assemblyai", "transcribe");
1982
- const ASSEMBLYAI_KEY = process.env.ASSEMBLYAI_API_KEY;
1983
- if (!ASSEMBLYAI_KEY) {
1984
- return jsonResponse({ error: "AssemblyAI not configured" }, 500);
1985
- }
1986
- try {
1987
- const body = await request.json();
1988
- const { audio_url, language_detection = true, speaker_labels = true } = body;
1989
- if (!audio_url) {
1990
- return jsonResponse({ error: "audio_url required" }, 400);
1991
- }
1992
- const response = await fetch("https://api.assemblyai.com/v2/transcript", {
1993
- method: "POST",
1994
- headers: {
1995
- Authorization: ASSEMBLYAI_KEY,
1996
- "Content-Type": "application/json",
1997
- },
1998
- body: JSON.stringify({ audio_url, language_detection, speaker_labels }),
1999
- });
2000
- const data = await response.json();
2001
- return jsonResponse(data, response.status);
2002
- } catch (e: any) {
2003
- return jsonResponse({ error: e.message }, 500);
2004
- }
2005
- }),
2006
- });
2007
-
2008
- http.route({
2009
- path: "/proxy/assemblyai",
2010
- method: "OPTIONS",
2011
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2012
- });
2013
-
2014
- // ==============================================
2015
- // APILAYER (Multi-API: Exchange, Stocks, Aviation, etc.) PROXY
2016
- // ==============================================
2017
- http.route({
2018
- path: "/proxy/apilayer",
2019
- method: "POST",
2020
- handler: httpAction(async (ctx, request) => {
2021
- await validateAndLogProxyCall(ctx, request, "apilayer", "call");
2022
- const APILAYER_KEY = process.env.APILAYER_API_KEY;
2023
- if (!APILAYER_KEY) {
2024
- return jsonResponse({ error: "APILayer not configured" }, 500);
2025
- }
2026
- try {
2027
- const body = await request.json();
2028
- const { service, endpoint, params = {} } = body;
2029
- if (!service || !endpoint) {
2030
- return jsonResponse({ error: "service and endpoint required (e.g. service:'exchangerates', endpoint:'/latest')" }, 400);
2031
- }
2032
- const queryString = new URLSearchParams(params).toString();
2033
- const url = `https://api.apilayer.com/${service}${endpoint}${queryString ? '?' + queryString : ''}`;
2034
- const response = await fetch(url, {
2035
- method: "GET",
2036
- headers: {
2037
- apikey: APILAYER_KEY,
2038
- },
2039
- });
2040
- const data = await response.json();
2041
- return jsonResponse(data, response.status);
2042
- } catch (e: any) {
2043
- return jsonResponse({ error: e.message }, 500);
2044
- }
2045
- }),
2046
- });
2047
-
2048
- http.route({
2049
- path: "/proxy/apilayer",
2050
- method: "OPTIONS",
2051
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2052
- });
2053
-
2054
- // ==============================================
2055
- // WORKSPACE / MAGIC LINK ENDPOINTS
2056
- // ==============================================
2057
-
2058
- // Create magic link and send email
2059
- http.route({
2060
- path: "/workspace/magic-link",
2061
- method: "POST",
2062
- handler: httpAction(async (ctx, request) => {
2063
- try {
2064
- const body = await request.json();
2065
- const { email, fingerprint } = body;
2066
-
2067
- if (!email || !email.includes("@")) {
2068
- return jsonResponse({ error: "Valid email required" }, 400);
2069
- }
2070
-
2071
- // Create magic link
2072
- const result = await ctx.runMutation(api.workspaces.createMagicLink, {
2073
- email: email.toLowerCase(),
2074
- fingerprint,
2075
- });
2076
-
2077
- // Send email directly - SIMPLE HTML (complex tables get stripped by Gmail)
2078
- const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${result.token}`;
2079
- const html = `<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;">
2080
- <h1>🦞 APIClaw</h1>
2081
- <h2>An AI Agent Wants to Connect</h2>
2082
- <p>Click below to verify your email and activate your workspace.</p>
2083
- <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>
2084
- <p style="color:#666;font-size:13px;">Free tier: 50 API calls. This link expires in 1 hour.</p>
2085
- <p style="color:#999;font-size:11px;">Or copy this link: ${verifyUrl}</p>
2086
- </div>`;
2087
-
2088
- const RESEND_KEY = process.env.RESEND_API_KEY;
2089
- if (!RESEND_KEY) {
2090
- console.error("RESEND_API_KEY not configured");
2091
- return jsonResponse({ error: "Email service not configured" }, 500);
2092
- }
2093
-
2094
- const emailResponse = await fetch("https://api.resend.com/emails", {
2095
- method: "POST",
2096
- headers: {
2097
- "Authorization": `Bearer ${RESEND_KEY}`,
2098
- "Content-Type": "application/json",
2099
- },
2100
- body: JSON.stringify({
2101
- from: "APIClaw <noreply@apiclaw.cloud>",
2102
- to: email.toLowerCase(),
2103
- subject: "🦞 Verify Your Email — APIClaw",
2104
- html: html,
2105
- }),
2106
- });
2107
-
2108
- if (!emailResponse.ok) {
2109
- const errorText = await emailResponse.text();
2110
- console.error("Resend error:", emailResponse.status, errorText);
2111
- return jsonResponse({ error: "Failed to send email", details: errorText }, 500);
2112
- }
2113
-
2114
- const emailResult = await emailResponse.json();
2115
- console.log("Email sent successfully:", emailResult.id);
2116
-
2117
- return jsonResponse({
2118
- success: true,
2119
- token: result.token,
2120
- expiresAt: result.expiresAt,
2121
- message: "Magic link sent! Check your email.",
2122
- emailId: emailResult.id,
2123
- });
2124
- } catch (e: any) {
2125
- console.error("Magic link error:", e);
2126
- return jsonResponse({ error: e.message || "Failed to create magic link" }, 500);
2127
- }
2128
- }),
2129
- });
2130
-
2131
- http.route({
2132
- path: "/workspace/magic-link",
2133
- method: "OPTIONS",
2134
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2135
- });
2136
-
2137
- // Poll magic link status (for agents to check if user clicked)
2138
- http.route({
2139
- path: "/workspace/poll",
2140
- method: "GET",
2141
- handler: httpAction(async (ctx, request) => {
2142
- const url = new URL(request.url);
2143
- const token = url.searchParams.get("token");
2144
-
2145
- if (!token) {
2146
- return jsonResponse({ error: "token required" }, 400);
2147
- }
2148
-
2149
- const result = await ctx.runQuery(api.workspaces.pollMagicLink, { token });
2150
- return jsonResponse(result);
2151
- }),
2152
- });
2153
-
2154
- http.route({
2155
- path: "/workspace/poll",
2156
- method: "OPTIONS",
2157
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2158
- });
2159
-
2160
- // Verify session token
2161
- http.route({
2162
- path: "/workspace/verify-session",
2163
- method: "GET",
2164
- handler: httpAction(async (ctx, request) => {
2165
- const url = new URL(request.url);
2166
- const sessionToken = url.searchParams.get("sessionToken");
2167
-
2168
- if (!sessionToken) {
2169
- return jsonResponse({ error: "sessionToken required" }, 400);
2170
- }
2171
-
2172
- const result = await ctx.runQuery(api.workspaces.verifySession, { sessionToken });
2173
-
2174
- if (!result) {
2175
- return jsonResponse({ error: "Invalid or expired session" }, 401);
2176
- }
2177
-
2178
- return jsonResponse(result);
2179
- }),
2180
- });
2181
-
2182
- http.route({
2183
- path: "/workspace/verify-session",
2184
- method: "OPTIONS",
2185
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2186
- });
2187
-
2188
- // Get workspace by email
2189
- http.route({
2190
- path: "/workspace/by-email",
2191
- method: "GET",
2192
- handler: httpAction(async (ctx, request) => {
2193
- const url = new URL(request.url);
2194
- const email = url.searchParams.get("email");
2195
-
2196
- if (!email) {
2197
- return jsonResponse({ error: "email required" }, 400);
2198
- }
2199
-
2200
- const result = await ctx.runQuery(api.workspaces.getByEmail, { email });
2201
-
2202
- if (!result) {
2203
- return jsonResponse({ exists: false });
2204
- }
2205
-
2206
- return jsonResponse({ exists: true, workspace: result });
2207
- }),
2208
- });
2209
-
2210
- http.route({
2211
- path: "/workspace/by-email",
2212
- method: "OPTIONS",
2213
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2214
- });
2215
-
2216
- // Send reminder email
2217
- http.route({
2218
- path: "/workspace/send-reminder",
2219
- method: "POST",
2220
- handler: httpAction(async (ctx, request) => {
2221
- try {
2222
- const body = await request.json();
2223
- const { email, token } = body;
2224
-
2225
- if (!email || !token) {
2226
- return jsonResponse({ error: "email and token required" }, 400);
2227
- }
2228
-
2229
- await ctx.runAction(api.email.sendReminderEmail, { email, token });
2230
- return jsonResponse({ success: true });
2231
- } catch (e: any) {
2232
- return jsonResponse({ error: e.message }, 500);
2233
- }
2234
- }),
2235
- });
2236
-
2237
- http.route({
2238
- path: "/workspace/send-reminder",
2239
- method: "OPTIONS",
2240
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2241
- });
2242
-
2243
- // ==============================================
2244
- // STRIPE BILLING ENDPOINTS
2245
- // ==============================================
2246
-
2247
- // Create checkout session
2248
- http.route({
2249
- path: "/api/billing/checkout",
2250
- method: "POST",
2251
- handler: createCheckoutSession,
2252
- });
2253
-
2254
- http.route({
2255
- path: "/api/billing/checkout",
2256
- method: "OPTIONS",
2257
- handler: checkoutOptions,
2258
- });
2259
-
2260
- // Create billing portal session
2261
- http.route({
2262
- path: "/api/billing/portal",
2263
- method: "POST",
2264
- handler: createPortalSession,
2265
- });
2266
-
2267
- http.route({
2268
- path: "/api/billing/portal",
2269
- method: "OPTIONS",
2270
- handler: portalOptions,
2271
- });
2272
-
2273
- // Stripe webhook handler
2274
- http.route({
2275
- path: "/api/webhooks/stripe",
2276
- method: "POST",
2277
- handler: handleStripeWebhook,
2278
- });
2279
-
2280
- http.route({
2281
- path: "/api/webhooks/stripe",
2282
- method: "OPTIONS",
2283
- handler: webhookOptions,
2284
- });
2285
-
2286
- // Test endpoint to debug logging
2287
- http.route({
2288
- path: "/proxy/test-logging",
2289
- method: "POST",
2290
- handler: httpAction(async (ctx, request) => {
2291
- const identifier = request.headers.get("X-APIClaw-Identifier");
2292
-
2293
- try {
2294
- const logId = await ctx.runMutation(api.analytics.log, {
2295
- event: "test_endpoint",
2296
- provider: "test",
2297
- identifier: identifier || "test",
2298
- metadata: { test: true },
2299
- });
2300
-
2301
- return jsonResponse({
2302
- success: true,
2303
- identifier,
2304
- logId,
2305
- message: "Logged successfully"
2306
- });
2307
- } catch (e: any) {
2308
- return jsonResponse({
2309
- success: false,
2310
- error: e.message,
2311
- stack: e.stack
2312
- }, 500);
2313
- }
2314
- }),
2315
- });
2316
-
2317
- // ==============================================
2318
- // GATEWAY v1 — Unified API Layer for AI Agents
2319
- // ==============================================
2320
- // OpenAI-compatible /v1/chat/completions endpoint.
2321
- // Accepts: Authorization: Bearer sk-claw-...
2322
- // Routes to the best available LLM provider (OpenRouter by default).
2323
- // This is what OpenClaw and any agent configures as their API endpoint.
2324
- // ==============================================
2325
-
2326
- // Helper: extract Bearer token from Authorization header
2327
- function extractBearerToken(request: Request): string | null {
2328
- const auth = request.headers.get("Authorization");
2329
- if (!auth?.startsWith("Bearer ")) return null;
2330
- return auth.slice(7);
2331
- }
2332
-
2333
- // Helper: require API key auth, return 401 if missing
2334
- async function requireApiKeyAuth(
2335
- ctx: any,
2336
- request: Request
2337
- ): Promise<{ workspaceId: string; keyId: string } | Response> {
2338
- const auth = await resolveWorkspaceFromRequest(ctx, request);
2339
- if (auth.authMethod !== "api-key" || !auth.workspaceId || !auth.keyId) {
2340
- return jsonResponse(
2341
- {
2342
- error: {
2343
- message: "Invalid API key. Generate one at https://apiclaw.cloud/workspace?tab=api-keys",
2344
- type: "invalid_api_key",
2345
- code: "invalid_api_key",
2346
- },
2347
- },
2348
- 401
2349
- );
2350
- }
2351
- // Verified-owner gate: API key alone isn't enough — workspace must be active+verified.
2352
- const verified = await resolveVerifiedOwnerByWorkspaceId(ctx, auth.workspaceId);
2353
- if (!verified.ok) {
2354
- // Fire-and-forget blocked diagnostic.
2355
- ctx.runMutation(api.funnel.recordEvent, {
2356
- event: "call_api_blocked",
2357
- classification: "human",
2358
- workspaceId: auth.workspaceId as any,
2359
- props: { reason: verified.reason, channel: "http:chat_or_embed" },
2360
- }).catch(() => {});
2361
- return jsonResponse(
2362
- {
2363
- error: {
2364
- message: verified.message,
2365
- type: "verification_required",
2366
- code: verified.reason,
2367
- },
2368
- },
2369
- verified.reason === "quota_exceeded" ? 429 : 403
2370
- );
2371
- }
2372
- return { workspaceId: auth.workspaceId, keyId: auth.keyId };
2373
- }
2374
-
2375
- // /v1/chat/completions — OpenAI-compatible LLM gateway with intelligent routing
2376
- http.route({
2377
- path: "/v1/chat/completions",
2378
- method: "POST",
2379
- handler: httpAction(async (ctx, request) => {
2380
- const startTime = Date.now();
2381
-
2382
- // Require API key auth
2383
- const authResult = await requireApiKeyAuth(ctx, request);
2384
- if (authResult instanceof Response) return authResult;
2385
- const { workspaceId } = authResult;
2386
-
2387
- // Parse body
2388
- let body: any;
2389
- try {
2390
- body = await request.json();
2391
- } catch {
2392
- return jsonResponse({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }, 400);
2393
- }
2394
-
2395
- const { model, messages, stream, ...rest } = body;
2396
- if (!messages || !Array.isArray(messages)) {
2397
- return jsonResponse({ error: { message: "messages array is required", type: "invalid_request_error" } }, 400);
2398
- }
2399
-
2400
- // Request-level overrides (X-APIClaw-Route header)
2401
- const routeOverride = request.headers.get("X-APIClaw-Route"); // e.g. "fastest" or "groq"
2402
-
2403
- // Load workspace settings
2404
- let settings: {
2405
- routingMode: string;
2406
- defaultModel: string | null;
2407
- preferredProviders: string[];
2408
- blockedProviders: string[];
2409
- allowOpenRouterFallback: boolean;
2410
- tier: string;
2411
- };
2412
- try {
2413
- settings = await ctx.runQuery(internal.workspaceSettings.getForRouting, { workspaceId });
2414
- } catch {
2415
- settings = {
2416
- routingMode: "balanced",
2417
- defaultModel: null,
2418
- preferredProviders: [],
2419
- blockedProviders: [],
2420
- allowOpenRouterFallback: true,
2421
- tier: "free",
2422
- };
2423
- }
2424
-
2425
- // Apply request-level overrides
2426
- const effectiveRoutingMode = routeOverride && ["best_price", "highest_quality", "fastest", "balanced"].includes(routeOverride)
2427
- ? routeOverride
2428
- : settings.routingMode;
2429
-
2430
- // If routeOverride is a provider name, add it as preferred
2431
- const effectivePreferred = routeOverride && PROVIDERS[routeOverride]?.isLLM
2432
- ? [routeOverride, ...settings.preferredProviders]
2433
- : settings.preferredProviders;
2434
-
2435
- const effectiveModel = model || settings.defaultModel || "anthropic/claude-sonnet-4-6";
2436
-
2437
- // Route the request (async -- may invoke advisor for intelligent model selection)
2438
- const route = await routeLLMRequest(effectiveModel, {
2439
- routingMode: effectiveRoutingMode,
2440
- preferredProviders: effectivePreferred,
2441
- blockedProviders: settings.blockedProviders,
2442
- allowOpenRouterFallback: settings.allowOpenRouterFallback,
2443
- }, messages);
2444
-
2445
- if (!route) {
2446
- return jsonResponse({ error: { message: "No LLM provider available. Check workspace settings.", type: "server_error" } }, 503);
2447
- }
2448
-
2449
- // Log usage
2450
- try {
2451
- await ctx.runMutation(api.analytics.log, {
2452
- event: "api_call",
2453
- provider: "gateway",
2454
- identifier: workspaceId,
2455
- workspaceId: workspaceId as any,
2456
- metadata: {
2457
- action: "chat_completions",
2458
- model: effectiveModel,
2459
- routedTo: route.provider,
2460
- routeReason: route.reason,
2461
- authMethod: "api-key",
2462
- },
2463
- });
2464
- await ctx.runMutation(api.logs.createProxyLog, {
2465
- workspaceId: workspaceId as any,
2466
- provider: route.provider,
2467
- action: "chat_completions",
2468
- subagentId: request.headers.get("X-APIClaw-Subagent") || "main",
2469
- });
2470
- await ctx.runMutation(api.workspaces.incrementUsage, {
2471
- workspaceId: workspaceId as any,
2472
- });
2473
- } catch (e: any) {
2474
- console.error("[Gateway] Logging failed:", e.message);
2475
- }
2476
-
2477
- // OAuth passthrough — founder tier can supply their own provider token
2478
- // Header: X-APIClaw-OAuth: Bearer <token>
2479
- // Only accepted for founder/partner tiers. Uses caller's token instead of managed key.
2480
- const oauthPassthrough = request.headers.get("X-APIClaw-OAuth");
2481
- const isPremiumTier = settings.tier === "founder" || settings.tier === "partner";
2482
- const effectiveApiKey = (oauthPassthrough && isPremiumTier && route.provider === "openai")
2483
- ? oauthPassthrough.replace(/^Bearer\s+/i, "")
2484
- : route.apiKey;
2485
-
2486
- // Forward to the chosen provider
2487
- try {
2488
- const isAnthropic = route.provider === "anthropic";
2489
- let requestBody: any;
2490
- let headers: Record<string, string>;
2491
-
2492
- if (isAnthropic) {
2493
- // Anthropic Messages API format
2494
- const { body: anthropicBody } = openaiToAnthropicRequest(route.model, messages, rest);
2495
- if (stream) anthropicBody.stream = true;
2496
- requestBody = anthropicBody;
2497
- headers = {
2498
- "x-api-key": effectiveApiKey,
2499
- "anthropic-version": "2023-06-01",
2500
- "Content-Type": "application/json",
2501
- ...(route.extraHeaders || {}),
2502
- };
2503
- } else {
2504
- requestBody = {
2505
- model: route.model,
2506
- messages,
2507
- stream: stream || false,
2508
- ...rest,
2509
- };
2510
- headers = {
2511
- "Authorization": `Bearer ${effectiveApiKey}`,
2512
- "Content-Type": "application/json",
2513
- ...(route.extraHeaders || {}),
2514
- };
2515
- }
2516
-
2517
- let response = await fetch(route.baseUrl, {
2518
- method: "POST",
2519
- headers,
2520
- body: JSON.stringify(requestBody),
2521
- });
2522
-
2523
- // OAuth fallback: if OAuth token fails with 401/403, retry with managed key
2524
- const usedOAuth = oauthPassthrough && isPremiumTier && route.provider === "openai" && effectiveApiKey !== route.apiKey;
2525
- if (usedOAuth && (response.status === 401 || response.status === 403)) {
2526
- console.log(`OAuth token failed (${response.status}), falling back to managed key for ${route.provider}`);
2527
- headers["Authorization"] = `Bearer ${route.apiKey}`;
2528
- response = await fetch(route.baseUrl, {
2529
- method: "POST",
2530
- headers,
2531
- body: JSON.stringify(requestBody),
2532
- });
2533
- }
2534
-
2535
- // For streaming responses, proxy the stream directly
2536
- if (stream && response.body) {
2537
- return new Response(response.body, {
2538
- status: response.status,
2539
- headers: {
2540
- "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
2541
- "Cache-Control": "no-cache",
2542
- "Connection": "keep-alive",
2543
- ...corsHeaders,
2544
- },
2545
- });
2546
- }
2547
-
2548
- // Non-streaming: return JSON
2549
- let data = await response.json();
2550
-
2551
- // Translate Anthropic response to OpenAI format
2552
- if (isAnthropic && response.ok) {
2553
- data = anthropicToOpenaiResponse(data, route.model);
2554
- }
2555
- const latencyMs = Date.now() - startTime;
2556
-
2557
- // Calculate cost from token usage
2558
- const usage = (data as any)?.usage;
2559
- const { providerCost, apiclawCost } = calculateCallCost(route.model, usage);
2560
-
2561
- // Log cost to usage records (fire and forget)
2562
- if (apiclawCost > 0) {
2563
- ctx.runMutation(internal.billing.logCallCost, {
2564
- workspaceId: workspaceId as any,
2565
- provider: route.provider,
2566
- model: route.model,
2567
- providerCostUsd: providerCost,
2568
- apiclawCostUsd: apiclawCost,
2569
- inputTokens: usage?.prompt_tokens || 0,
2570
- outputTokens: usage?.completion_tokens || 0,
2571
- }).catch(() => {});
2572
- }
2573
-
2574
- // Add APIClaw metadata
2575
- if (data && typeof data === "object") {
2576
- (data as any)._apiclaw = {
2577
- latencyMs,
2578
- provider: route.provider,
2579
- routeReason: route.reason,
2580
- model: route.model,
2581
- gateway: "v1",
2582
- cost: {
2583
- providerUsd: Math.round(providerCost * 1_000_000) / 1_000_000,
2584
- totalUsd: Math.round(apiclawCost * 1_000_000) / 1_000_000,
2585
- margin: "15%",
2586
- },
2587
- };
2588
- }
2589
-
2590
- return jsonResponse(data, response.status);
2591
- } catch (e: any) {
2592
- return jsonResponse({ error: { message: e.message, type: "server_error" } }, 500);
2593
- }
2594
- }),
2595
- });
2596
-
2597
- http.route({
2598
- path: "/v1/chat/completions",
2599
- method: "OPTIONS",
2600
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2601
- });
2602
-
2603
- // ==============================================
2604
- // /v1/embeddings — OpenAI-compatible embedding gateway
2605
- // ==============================================
2606
- // Accepts: Authorization: Bearer sk-claw-...
2607
- // Routes by model prefix to Direct Call embedding providers:
2608
- // voyage/* → Voyage AI (default: voyage-3-large)
2609
- // mistral/* → Mistral (mistral-embed)
2610
- // openai/* → OpenAI (text-embedding-3-small, -large, ada-002)
2611
- // cohere/* → Cohere (embed-v4.0, embed-multilingual-v3) — translated
2612
- // Unprefixed model strings auto-route by known model names.
2613
- // ==============================================
2614
-
2615
- type EmbeddingBackend = {
2616
- provider: "voyage" | "mistral" | "openai" | "cohere";
2617
- baseUrl: string;
2618
- apiKey: string | undefined;
2619
- model: string;
2620
- format: "openai" | "cohere";
2621
- };
2622
-
2623
- // Map a model string to a backend. Supports prefixed (voyage/voyage-3-large)
2624
- // and bare model names (text-embedding-3-small, mistral-embed, voyage-3-large).
2625
- function resolveEmbeddingBackend(requestedModel: string | undefined): EmbeddingBackend | null {
2626
- const raw = (requestedModel || "voyage/voyage-3-large").trim();
2627
- let provider: EmbeddingBackend["provider"] | null = null;
2628
- let model = raw;
2629
-
2630
- // Explicit prefix
2631
- if (raw.startsWith("voyage/")) {
2632
- provider = "voyage";
2633
- model = raw.slice(7);
2634
- } else if (raw.startsWith("mistral/")) {
2635
- provider = "mistral";
2636
- model = raw.slice(8);
2637
- } else if (raw.startsWith("openai/")) {
2638
- provider = "openai";
2639
- model = raw.slice(7);
2640
- } else if (raw.startsWith("cohere/")) {
2641
- provider = "cohere";
2642
- model = raw.slice(7);
2643
- } else {
2644
- // Auto-detect from bare model name
2645
- if (raw.startsWith("voyage-")) provider = "voyage";
2646
- else if (raw.startsWith("mistral-embed") || raw === "mistral-embed") provider = "mistral";
2647
- else if (raw.startsWith("text-embedding-") || raw.startsWith("ada-")) provider = "openai";
2648
- else if (raw.startsWith("embed-")) provider = "cohere";
2649
- else return null;
2650
- }
2651
-
2652
- switch (provider) {
2653
- case "voyage":
2654
- return {
2655
- provider,
2656
- baseUrl: "https://api.voyageai.com/v1/embeddings",
2657
- apiKey: process.env.VOYAGE_API_KEY,
2658
- model: model || "voyage-3-large",
2659
- format: "openai",
2660
- };
2661
- case "mistral":
2662
- return {
2663
- provider,
2664
- baseUrl: "https://api.mistral.ai/v1/embeddings",
2665
- apiKey: process.env.MISTRAL_API_KEY,
2666
- model: model || "mistral-embed",
2667
- format: "openai",
2668
- };
2669
- case "openai":
2670
- return {
2671
- provider,
2672
- baseUrl: "https://api.openai.com/v1/embeddings",
2673
- apiKey: process.env.OPENAI_API_KEY,
2674
- model: model || "text-embedding-3-small",
2675
- format: "openai",
2676
- };
2677
- case "cohere":
2678
- return {
2679
- provider,
2680
- baseUrl: "https://api.cohere.com/v2/embed",
2681
- apiKey: process.env.COHERE_API_KEY,
2682
- model: model || "embed-v4.0",
2683
- format: "cohere",
2684
- };
2685
- }
2686
- }
2687
-
2688
- // /v1/embeddings — POST
2689
- http.route({
2690
- path: "/v1/embeddings",
2691
- method: "POST",
2692
- handler: httpAction(async (ctx, request) => {
2693
- const startTime = Date.now();
2694
-
2695
- const authResult = await requireApiKeyAuth(ctx, request);
2696
- if (authResult instanceof Response) return authResult;
2697
- const { workspaceId } = authResult;
2698
-
2699
- let body: any;
2700
- try {
2701
- body = await request.json();
2702
- } catch {
2703
- return jsonResponse({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }, 400);
2704
- }
2705
-
2706
- const { model, input, encoding_format, dimensions, user, input_type } = body;
2707
- if (input === undefined || input === null) {
2708
- return jsonResponse({ error: { message: "input is required", type: "invalid_request_error" } }, 400);
2709
- }
2710
-
2711
- const backend = resolveEmbeddingBackend(model);
2712
- if (!backend) {
2713
- return jsonResponse(
2714
- { error: { message: `Unknown embedding model: ${model}. Use voyage/*, mistral/*, openai/*, or cohere/* prefix.`, type: "invalid_request_error" } },
2715
- 400
2716
- );
2717
- }
2718
- if (!backend.apiKey) {
2719
- return jsonResponse(
2720
- { error: { message: `Provider ${backend.provider} is not configured (missing ${backend.provider.toUpperCase()}_API_KEY).`, type: "server_error" } },
2721
- 503
2722
- );
2723
- }
2724
-
2725
- // Log usage
2726
- try {
2727
- await ctx.runMutation(api.analytics.log, {
2728
- event: "api_call",
2729
- provider: "gateway",
2730
- identifier: workspaceId,
2731
- workspaceId: workspaceId as any,
2732
- metadata: {
2733
- action: "embeddings",
2734
- model: `${backend.provider}/${backend.model}`,
2735
- routedTo: backend.provider,
2736
- authMethod: "api-key",
2737
- },
2738
- });
2739
- await ctx.runMutation(api.logs.createProxyLog, {
2740
- workspaceId: workspaceId as any,
2741
- provider: backend.provider,
2742
- action: "embeddings",
2743
- subagentId: request.headers.get("X-APIClaw-Subagent") || "main",
2744
- });
2745
- await ctx.runMutation(api.workspaces.incrementUsage, {
2746
- workspaceId: workspaceId as any,
2747
- });
2748
- } catch (e: any) {
2749
- console.error("[Gateway] Embeddings logging failed:", e.message);
2750
- }
2751
-
2752
- try {
2753
- let providerRequestBody: any;
2754
- let providerHeaders: Record<string, string> = {
2755
- "Content-Type": "application/json",
2756
- "Authorization": `Bearer ${backend.apiKey}`,
2757
- };
2758
-
2759
- if (backend.format === "openai") {
2760
- // OpenAI-compatible passthrough (Voyage, Mistral, OpenAI)
2761
- providerRequestBody = {
2762
- model: backend.model,
2763
- input,
2764
- ...(encoding_format !== undefined ? { encoding_format } : {}),
2765
- ...(dimensions !== undefined ? { dimensions } : {}),
2766
- ...(user !== undefined ? { user } : {}),
2767
- ...(input_type !== undefined ? { input_type } : {}),
2768
- };
2769
- } else {
2770
- // Cohere v2 format
2771
- const texts = Array.isArray(input) ? input : [String(input)];
2772
- providerRequestBody = {
2773
- model: backend.model,
2774
- texts,
2775
- input_type: input_type || "search_document",
2776
- embedding_types: ["float"],
2777
- };
2778
- }
2779
-
2780
- const response = await fetch(backend.baseUrl, {
2781
- method: "POST",
2782
- headers: providerHeaders,
2783
- body: JSON.stringify(providerRequestBody),
2784
- });
2785
-
2786
- const providerData = await response.json();
2787
- const latencyMs = Date.now() - startTime;
2788
-
2789
- if (!response.ok) {
2790
- return jsonResponse(
2791
- {
2792
- error: {
2793
- message: (providerData as any)?.error?.message || (providerData as any)?.message || `${backend.provider} error`,
2794
- type: "provider_error",
2795
- provider: backend.provider,
2796
- },
2797
- _apiclaw: { latencyMs, provider: backend.provider, gateway: "v1" },
2798
- },
2799
- response.status
2800
- );
2801
- }
2802
-
2803
- // Normalize Cohere response to OpenAI format
2804
- let openAIData: any;
2805
- if (backend.format === "cohere") {
2806
- const cohereEmbeddings: number[][] = (providerData as any)?.embeddings?.float || (providerData as any)?.embeddings || [];
2807
- openAIData = {
2808
- object: "list",
2809
- data: cohereEmbeddings.map((embedding, index) => ({
2810
- object: "embedding",
2811
- embedding,
2812
- index,
2813
- })),
2814
- model: `cohere/${backend.model}`,
2815
- usage: {
2816
- prompt_tokens: (providerData as any)?.meta?.billed_units?.input_tokens || 0,
2817
- total_tokens: (providerData as any)?.meta?.billed_units?.input_tokens || 0,
2818
- },
2819
- };
2820
- } else {
2821
- // Already OpenAI-format
2822
- openAIData = providerData;
2823
- if (openAIData && typeof openAIData === "object" && !openAIData.model) {
2824
- openAIData.model = `${backend.provider}/${backend.model}`;
2825
- }
2826
- }
2827
-
2828
- if (openAIData && typeof openAIData === "object") {
2829
- openAIData._apiclaw = {
2830
- latencyMs,
2831
- provider: backend.provider,
2832
- model: backend.model,
2833
- gateway: "v1",
2834
- };
2835
- }
2836
-
2837
- return jsonResponse(openAIData, 200);
2838
- } catch (e: any) {
2839
- return jsonResponse({ error: { message: e.message, type: "server_error" } }, 500);
2840
- }
2841
- }),
2842
- });
2843
-
2844
- http.route({
2845
- path: "/v1/embeddings",
2846
- method: "OPTIONS",
2847
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2848
- });
2849
-
2850
- // ==============================================
2851
- // /v1/execute — Unified execution gateway
2852
- // ==============================================
2853
- // Single endpoint for ALL API call types:
2854
- // 1. Managed providers (19 providers, APIClaw owns keys)
2855
- // 2. LLM routing (Groq, Mistral, Together, OpenRouter)
2856
- // 3. Open APIs (generic HTTP proxy with caller-supplied baseUrl)
2857
- //
2858
- // Auth: Bearer sk-claw-... OR X-APIClaw-Internal (server-to-server)
2859
- // ==============================================
2860
-
2861
- // Managed provider dispatch: maps provider+action to an upstream HTTP call
2862
- // Returns { url, method, headers, body } or null if unknown
2863
- function buildManagedRequest(
2864
- provider: string,
2865
- action: string,
2866
- params: Record<string, any>
2867
- ): { url: string; method: string; headers: Record<string, string>; body?: string } | null {
2868
- const meta = PROVIDERS[provider];
2869
- if (!meta?.envKey) return null;
2870
-
2871
- const apiKey = process.env[meta.envKey];
2872
- if (!apiKey) return null;
2873
-
2874
- // Provider-specific request builders
2875
- switch (provider) {
2876
- case "brave_search": {
2877
- if (action !== "search") return null;
2878
- const url = new URL("https://api.search.brave.com/res/v1/web/search");
2879
- url.searchParams.set("q", params.query || "");
2880
- url.searchParams.set("count", String(params.count || 10));
2881
- return { url: url.toString(), method: "GET", headers: { "X-Subscription-Token": apiKey } };
2882
- }
2883
- case "serper": {
2884
- if (action !== "search") return null;
2885
- return {
2886
- url: "https://google.serper.dev/search",
2887
- method: "POST",
2888
- headers: { "X-API-KEY": apiKey, "Content-Type": "application/json" },
2889
- body: JSON.stringify({ q: params.query || params.q, num: params.num || 10 }),
2890
- };
2891
- }
2892
- case "resend": {
2893
- if (action !== "send_email") return null;
2894
- return {
2895
- url: "https://api.resend.com/emails",
2896
- method: "POST",
2897
- headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
2898
- body: JSON.stringify(params),
2899
- };
2900
- }
2901
- case "elevenlabs": {
2902
- if (action !== "text_to_speech") return null;
2903
- const voiceId = params.voice_id || "21m00Tcm4TlvDq8ikWAM";
2904
- return {
2905
- url: `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
2906
- method: "POST",
2907
- headers: { "xi-api-key": apiKey, "Content-Type": "application/json" },
2908
- body: JSON.stringify({
2909
- text: params.text,
2910
- model_id: params.model_id || "eleven_multilingual_v2",
2911
- voice_settings: params.voice_settings || { stability: 0.5, similarity_boost: 0.75 },
2912
- }),
2913
- };
2914
- }
2915
- case "deepgram": {
2916
- if (action !== "transcribe") return null;
2917
- const dgUrl = new URL("https://api.deepgram.com/v1/listen");
2918
- if (params.language) dgUrl.searchParams.set("language", params.language);
2919
- if (params.model) dgUrl.searchParams.set("model", params.model);
2920
- dgUrl.searchParams.set("smart_format", "true");
2921
- return {
2922
- url: dgUrl.toString(),
2923
- method: "POST",
2924
- headers: { "Authorization": `Token ${apiKey}`, "Content-Type": "application/json" },
2925
- body: JSON.stringify({ url: params.url || params.audio_url }),
2926
- };
2927
- }
2928
- case "firecrawl": {
2929
- const firecrawlActions: Record<string, string> = {
2930
- scrape: "https://api.firecrawl.dev/v1/scrape",
2931
- crawl: "https://api.firecrawl.dev/v1/crawl",
2932
- map: "https://api.firecrawl.dev/v1/map",
2933
- };
2934
- const fUrl = firecrawlActions[action];
2935
- if (!fUrl) return null;
2936
- return {
2937
- url: fUrl,
2938
- method: "POST",
2939
- headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
2940
- body: JSON.stringify(params),
2941
- };
2942
- }
2943
- case "replicate": {
2944
- if (action !== "run") return null;
2945
- return {
2946
- url: "https://api.replicate.com/v1/predictions",
2947
- method: "POST",
2948
- headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
2949
- body: JSON.stringify({ version: params.version, input: params.input || params }),
2950
- };
2951
- }
2952
- case "stability": {
2953
- if (action !== "generate") return null;
2954
- return {
2955
- url: "https://api.stability.ai/v2beta/stable-image/generate/sd3",
2956
- method: "POST",
2957
- headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", "Accept": "application/json" },
2958
- body: JSON.stringify(params),
2959
- };
2960
- }
2961
- case "github": {
2962
- const ghHeaders = { "Authorization": `Bearer ${apiKey}`, "Accept": "application/vnd.github.v3+json", "User-Agent": "APIClaw-Gateway" };
2963
- if (action === "search_repos") {
2964
- const ghUrl = new URL("https://api.github.com/search/repositories");
2965
- ghUrl.searchParams.set("q", params.query || params.q || "");
2966
- return { url: ghUrl.toString(), method: "GET", headers: ghHeaders };
2967
- }
2968
- if (action === "get_repo") {
2969
- return { url: `https://api.github.com/repos/${params.owner}/${params.repo}`, method: "GET", headers: ghHeaders };
2970
- }
2971
- if (action === "get_file") {
2972
- return { url: `https://api.github.com/repos/${params.owner}/${params.repo}/contents/${params.path}`, method: "GET", headers: ghHeaders };
2973
- }
2974
- return null;
2975
- }
2976
- case "e2b": {
2977
- // E2B sandbox execution is complex (create sandbox, then run code). Simplified for gateway.
2978
- if (action !== "run_code") return null;
2979
- return {
2980
- url: "https://api.e2b.dev/v1/sandboxes",
2981
- method: "POST",
2982
- headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
2983
- body: JSON.stringify({ template: params.template || "base", ...params }),
2984
- };
2985
- }
2986
- case "46elks": {
2987
- if (action !== "send_sms") return null;
2988
- // 46elks uses Basic auth with username:password (envKey has format user:pass)
2989
- const [user, pass] = apiKey.includes(":") ? apiKey.split(":") : [apiKey, ""];
2990
- const basicAuth = typeof btoa !== "undefined" ? btoa(`${user}:${pass}`) : Buffer.from(`${user}:${pass}`).toString("base64");
2991
- return {
2992
- url: "https://api.46elks.com/a1/sms",
2993
- method: "POST",
2994
- headers: { "Authorization": `Basic ${basicAuth}`, "Content-Type": "application/x-www-form-urlencoded" },
2995
- body: new URLSearchParams({ from: params.from || "APIClaw", to: params.to, message: params.message }).toString(),
2996
- };
2997
- }
2998
- case "twilio": {
2999
- // Twilio uses Basic auth. envKey format: accountSid:authToken
3000
- const [sid, token] = apiKey.includes(":") ? apiKey.split(":") : [apiKey, ""];
3001
- const twilioAuth = typeof btoa !== "undefined" ? btoa(`${sid}:${token}`) : Buffer.from(`${sid}:${token}`).toString("base64");
3002
- return {
3003
- url: `https://api.twilio.com/2010-04-01/Accounts/${sid}/Messages.json`,
3004
- method: "POST",
3005
- headers: { "Authorization": `Basic ${twilioAuth}`, "Content-Type": "application/x-www-form-urlencoded" },
3006
- body: new URLSearchParams({ From: params.from, To: params.to, Body: params.message }).toString(),
3007
- };
3008
- }
3009
- case "assemblyai": {
3010
- if (action !== "transcribe") return null;
3011
- return {
3012
- url: "https://api.assemblyai.com/v2/transcript",
3013
- method: "POST",
3014
- headers: { "Authorization": apiKey, "Content-Type": "application/json" },
3015
- body: JSON.stringify({ audio_url: params.url || params.audio_url, ...params }),
3016
- };
3017
- }
3018
- case "anthropic": {
3019
- if (action === "chat" || action === "messages") {
3020
- return {
3021
- url: "https://api.anthropic.com/v1/messages",
3022
- method: "POST",
3023
- headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01", "Content-Type": "application/json" },
3024
- body: JSON.stringify(params),
3025
- };
3026
- }
3027
- return null;
3028
- }
3029
- case "cohere": {
3030
- if (action === "chat") {
3031
- return {
3032
- url: "https://api.cohere.com/v2/chat",
3033
- method: "POST",
3034
- headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
3035
- body: JSON.stringify(params),
3036
- };
3037
- }
3038
- if (action === "rerank") {
3039
- return {
3040
- url: "https://api.cohere.com/v2/rerank",
3041
- method: "POST",
3042
- headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
3043
- body: JSON.stringify(params),
3044
- };
3045
- }
3046
- return null;
3047
- }
3048
- case "apilayer": {
3049
- // 14 unified APILayer actions + popular legacy APIs — ported from src/execute.ts
3050
- // Reads product-specific env keys where APILayer requires them; falls back to unified.
3051
- const p = (params as Record<string, any>) || {};
3052
- const buildUrl = (base: string, qs?: Record<string, any>) => {
3053
- const u = new URL(base);
3054
- if (qs) for (const [k, v] of Object.entries(qs)) if (v !== undefined && v !== null && v !== "") u.searchParams.set(k, String(v));
3055
- return u.toString();
3056
- };
3057
- const envKey = (name: string) => process.env[name] || apiKey;
3058
-
3059
- switch (action) {
3060
- // Unified (apikey header)
3061
- case "exchange_rates": {
3062
- const endpoint = p.date ? "historical" : "latest";
3063
- return {
3064
- url: buildUrl(`https://api.apilayer.com/exchangerates_data/${endpoint}`, { base: p.base || "USD", symbols: p.symbols, date: p.date }),
3065
- method: "GET",
3066
- headers: { apikey: apiKey },
3067
- };
3068
- }
3069
- case "verify_email":
3070
- if (!p.email) return null;
3071
- return {
3072
- url: buildUrl("https://api.apilayer.com/email_verification/check", { email: p.email }),
3073
- method: "GET",
3074
- headers: { apikey: apiKey },
3075
- };
3076
- case "verify_number":
3077
- if (!p.number) return null;
3078
- return {
3079
- url: buildUrl("https://api.apilayer.com/number_verification/validate", { number: p.number }),
3080
- method: "GET",
3081
- headers: { apikey: apiKey },
3082
- };
3083
- case "world_news":
3084
- if (!p.url) return null;
3085
- return {
3086
- url: buildUrl("https://api.apilayer.com/world_news/extract-news", { url: p.url, analyze: p.analyze !== false ? "true" : "false" }),
3087
- method: "GET",
3088
- headers: { apikey: apiKey },
3089
- };
3090
- case "finance_news":
3091
- return {
3092
- url: buildUrl("https://api.apilayer.com/financelayer/news", { tickers: p.tickers, keywords: p.text, limit: p.number || 5 }),
3093
- method: "GET",
3094
- headers: { apikey: apiKey },
3095
- };
3096
- case "scrape":
3097
- if (!p.url) return null;
3098
- return {
3099
- url: buildUrl("https://api.apilayer.com/adv_scraper/scraper", { url: p.url }),
3100
- method: "GET",
3101
- headers: { apikey: apiKey },
3102
- };
3103
- case "skills":
3104
- if (!p.q) return null;
3105
- return {
3106
- url: buildUrl("https://api.promptapi.com/skills", { q: p.q, count: p.count }),
3107
- method: "GET",
3108
- headers: { apikey: apiKey },
3109
- };
3110
- case "image_crop": {
3111
- if (!p.url) return null;
3112
- const formData = new URLSearchParams();
3113
- formData.set("url", p.url);
3114
- if (p.width) formData.set("width", String(p.width));
3115
- if (p.height) formData.set("height", String(p.height));
3116
- return {
3117
- url: "https://api.apilayer.com/smart_crop/url",
3118
- method: "POST",
3119
- headers: { apikey: apiKey, "Content-Type": "application/x-www-form-urlencoded" },
3120
- body: formData.toString(),
3121
- };
3122
- }
3123
- case "form_submit": {
3124
- if (!p.endpoint) return null;
3125
- return {
3126
- url: `https://api.apilayer.com/form_api/${p.endpoint}`,
3127
- method: "POST",
3128
- headers: { apikey: apiKey, "Content-Type": "application/json" },
3129
- body: JSON.stringify(p.data || {}),
3130
- };
3131
- }
3132
-
3133
- // Product-specific access_key query (legacy domain) — binary response
3134
- case "pdf_generate": {
3135
- if (!p.document_url && !p.document_html) return null;
3136
- const pdfKey = envKey("PDFLAYER_API_KEY");
3137
- const url = buildUrl("https://api.pdflayer.com/api/convert", {
3138
- access_key: pdfKey,
3139
- page_size: p.page_size || "A4",
3140
- document_url: p.document_url,
3141
- });
3142
- if (p.document_html && !p.document_url) {
3143
- return {
3144
- url: buildUrl("https://api.pdflayer.com/api/convert", { access_key: pdfKey, page_size: p.page_size || "A4" }),
3145
- method: "POST",
3146
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
3147
- body: `document_html=${encodeURIComponent(p.document_html)}`,
3148
- };
3149
- }
3150
- return { url, method: "GET", headers: {} };
3151
- }
3152
- case "screenshot": {
3153
- if (!p.url) return null;
3154
- return {
3155
- url: buildUrl("https://api.screenshotlayer.com/api/capture", {
3156
- access_key: envKey("SCREENSHOTLAYER_API_KEY"),
3157
- url: p.url,
3158
- viewport: p.viewport || "1440x900",
3159
- fullpage: p.fullpage ? "1" : "0",
3160
- }),
3161
- method: "GET",
3162
- headers: {},
3163
- };
3164
- }
3165
- case "vat_check": {
3166
- if (!p.vat_number) return null;
3167
- return {
3168
- url: buildUrl("http://apilayer.net/api/validate", { access_key: envKey("VATLAYER_API_KEY"), vat_number: p.vat_number }),
3169
- method: "GET",
3170
- headers: {},
3171
- };
3172
- }
3173
-
3174
- // Legacy domains (each uses product-specific access_key query param)
3175
- case "market_data": {
3176
- if (!p.symbols) return null;
3177
- return {
3178
- url: buildUrl("http://api.marketstack.com/v1/eod", {
3179
- access_key: envKey("MARKETSTACK_API_KEY"), symbols: p.symbols, limit: p.limit || 10, date_from: p.date_from, date_to: p.date_to,
3180
- }),
3181
- method: "GET", headers: {},
3182
- };
3183
- }
3184
- case "aviation": {
3185
- return {
3186
- url: buildUrl("http://api.aviationstack.com/v1/flights", {
3187
- access_key: envKey("AVIATIONSTACK_API_KEY"), flight_iata: p.flight_iata, dep_iata: p.dep_iata, arr_iata: p.arr_iata, airline_iata: p.airline_iata,
3188
- }),
3189
- method: "GET", headers: {},
3190
- };
3191
- }
3192
- case "weatherstack_current":
3193
- case "weather": {
3194
- if (!p.query) return null;
3195
- return {
3196
- url: buildUrl("http://api.weatherstack.com/current", { access_key: envKey("WEATHERSTACK_API_KEY"), query: p.query, units: p.units || "m" }),
3197
- method: "GET", headers: {},
3198
- };
3199
- }
3200
- case "weatherstack_forecast": {
3201
- if (!p.query) return null;
3202
- return {
3203
- url: buildUrl("http://api.weatherstack.com/forecast", { access_key: envKey("WEATHERSTACK_API_KEY"), query: p.query, forecast_days: p.forecast_days || 3 }),
3204
- method: "GET", headers: {},
3205
- };
3206
- }
3207
- case "ipstack_lookup": {
3208
- if (!p.ip) return null;
3209
- return {
3210
- url: buildUrl(`http://api.ipstack.com/${encodeURIComponent(p.ip)}`, { access_key: envKey("IPSTACK_API_KEY") }),
3211
- method: "GET", headers: {},
3212
- };
3213
- }
3214
- case "ipapi_lookup": {
3215
- if (!p.ip) return null;
3216
- return {
3217
- url: buildUrl(`https://api.ipapi.com/api/${encodeURIComponent(p.ip)}`, { access_key: envKey("IPAPI_API_KEY") }),
3218
- method: "GET", headers: {},
3219
- };
3220
- }
3221
- case "currencylayer_live": {
3222
- return {
3223
- url: buildUrl("http://api.currencylayer.com/live", { access_key: envKey("CURRENCYLAYER_API_KEY"), source: p.source || "USD", currencies: p.currencies }),
3224
- method: "GET", headers: {},
3225
- };
3226
- }
3227
- case "currencylayer_convert": {
3228
- if (!p.from || !p.to || !p.amount) return null;
3229
- return {
3230
- url: buildUrl("http://api.currencylayer.com/convert", {
3231
- access_key: envKey("CURRENCYLAYER_API_KEY"), from: p.from, to: p.to, amount: p.amount, date: p.date,
3232
- }),
3233
- method: "GET", headers: {},
3234
- };
3235
- }
3236
- case "coinlayer_live": {
3237
- return {
3238
- url: buildUrl("http://api.coinlayer.com/live", { access_key: envKey("COINLAYER_API_KEY"), target: p.target || "USD", symbols: p.symbols }),
3239
- method: "GET", headers: {},
3240
- };
3241
- }
3242
- case "positionstack_forward": {
3243
- if (!p.query) return null;
3244
- return {
3245
- url: buildUrl("http://api.positionstack.com/v1/forward", { access_key: envKey("POSITIONSTACK_API_KEY"), query: p.query, limit: p.limit || 1 }),
3246
- method: "GET", headers: {},
3247
- };
3248
- }
3249
- case "positionstack_reverse": {
3250
- if (!p.query) return null;
3251
- return {
3252
- url: buildUrl("http://api.positionstack.com/v1/reverse", { access_key: envKey("POSITIONSTACK_API_KEY"), query: p.query, limit: p.limit || 1 }),
3253
- method: "GET", headers: {},
3254
- };
3255
- }
3256
- case "fixer_latest": {
3257
- return {
3258
- url: buildUrl("http://data.fixer.io/api/latest", { access_key: envKey("FIXER_API_KEY"), base: p.base || "EUR", symbols: p.symbols }),
3259
- method: "GET", headers: {},
3260
- };
3261
- }
3262
- case "fixer_convert": {
3263
- if (!p.from || !p.to || !p.amount) return null;
3264
- return {
3265
- url: buildUrl("http://data.fixer.io/api/convert", {
3266
- access_key: envKey("FIXER_API_KEY"), from: p.from, to: p.to, amount: p.amount, date: p.date,
3267
- }),
3268
- method: "GET", headers: {},
3269
- };
3270
- }
3271
- case "languagelayer_detect": {
3272
- if (!p.query) return null;
3273
- return {
3274
- url: buildUrl("http://api.languagelayer.com/detect", { access_key: envKey("LANGUAGELAYER_API_KEY"), query: p.query }),
3275
- method: "GET", headers: {},
3276
- };
3277
- }
3278
- case "scrapestack_scrape": {
3279
- if (!p.url) return null;
3280
- return {
3281
- url: buildUrl("http://api.scrapestack.com/scrape", { access_key: envKey("SCRAPESTACK_API_KEY"), url: p.url, render_js: p.render_js ? "1" : "0" }),
3282
- method: "GET", headers: {},
3283
- };
3284
- }
3285
- case "serpstack_search": {
3286
- if (!p.query) return null;
3287
- return {
3288
- url: buildUrl("http://api.serpstack.com/search", { access_key: envKey("SERPSTACK_API_KEY"), query: p.query, num: p.num || 10 }),
3289
- method: "GET", headers: {},
3290
- };
3291
- }
3292
- case "mediastack_news": {
3293
- return {
3294
- url: buildUrl("http://api.mediastack.com/v1/news", { access_key: envKey("MEDIASTACK_API_KEY"), keywords: p.keywords, categories: p.categories, limit: p.limit || 10 }),
3295
- method: "GET", headers: {},
3296
- };
3297
- }
3298
- case "userstack_detect": {
3299
- if (!p.ua) return null;
3300
- return {
3301
- url: buildUrl("http://api.userstack.com/detect", { access_key: envKey("USERSTACK_API_KEY"), ua: p.ua }),
3302
- method: "GET", headers: {},
3303
- };
3304
- }
3305
- case "exchangeratehost_latest": {
3306
- return {
3307
- url: buildUrl("https://api.exchangerate.host/live", { access_key: envKey("EXCHANGERATEHOST_API_KEY"), source: p.source || "USD", currencies: p.currencies }),
3308
- method: "GET", headers: {},
3309
- };
3310
- }
3311
- default:
3312
- return null;
3313
- }
3314
- }
3315
- default:
3316
- return null;
3317
- }
3318
- }
3319
-
3320
- // Resolve auth for /v1/execute: supports both sk-claw- keys and X-APIClaw-Internal
3321
- async function resolveExecuteAuth(
3322
- ctx: any,
3323
- request: Request
3324
- ): Promise<{ workspaceId?: string; keyId?: string; authMethod: "api-key" | "internal" | "anonymous" } | Response> {
3325
- // 1. Check internal server-to-server auth
3326
- const internalSecret = request.headers.get("X-APIClaw-Internal");
3327
- if (internalSecret) {
3328
- const expectedSecret = process.env.APICLAW_INTERNAL_SECRET;
3329
- if (!expectedSecret || internalSecret !== expectedSecret) {
3330
- return jsonResponse({ error: { message: "Invalid internal secret", type: "auth_error" } }, 401);
3331
- }
3332
- // Internal auth: extract workspace from body or header
3333
- const workspaceHeader = request.headers.get("X-APIClaw-Workspace");
3334
- return { workspaceId: workspaceHeader || undefined, authMethod: "internal" };
3335
- }
3336
-
3337
- // 2. Check for API key auth (Bearer sk-claw-...)
3338
- const auth = await resolveWorkspaceFromRequest(ctx, request);
3339
- if (auth.authMethod === "api-key" && auth.workspaceId && auth.keyId) {
3340
- // Verified-owner gate: API key alone isn't enough — workspace must be active+verified.
3341
- const verified = await resolveVerifiedOwnerByWorkspaceId(ctx, auth.workspaceId);
3342
- if (!verified.ok) {
3343
- ctx.runMutation(api.funnel.recordEvent, {
3344
- event: "call_api_blocked",
3345
- classification: "human",
3346
- workspaceId: auth.workspaceId as any,
3347
- props: { reason: verified.reason, channel: "http:execute" },
3348
- }).catch(() => {});
3349
- return jsonResponse(
3350
- { error: { message: verified.message, type: "verification_required", code: verified.reason } },
3351
- verified.reason === "quota_exceeded" ? 429 : 403
3352
- );
3353
- }
3354
- return { workspaceId: auth.workspaceId, keyId: auth.keyId, authMethod: "api-key" };
3355
- }
3356
-
3357
- // 3. No valid auth
3358
- return jsonResponse(
3359
- { error: { message: "Authentication required. Use Bearer sk-claw-... or X-APIClaw-Internal header.", type: "auth_error" } },
3360
- 401
3361
- );
3362
- }
3363
-
3364
- http.route({
3365
- path: "/v1/execute",
3366
- method: "POST",
3367
- handler: httpAction(async (ctx, request) => {
3368
- const startTime = Date.now();
3369
-
3370
- // Auth
3371
- const authResult = await resolveExecuteAuth(ctx, request);
3372
- if (authResult instanceof Response) return authResult;
3373
- const { workspaceId, authMethod } = authResult;
3374
-
3375
- // Parse body
3376
- let body: any;
3377
- try {
3378
- body = await request.json();
3379
- } catch {
3380
- return jsonResponse({ error: { message: "Invalid JSON body", type: "invalid_request" } }, 400);
3381
- }
3382
-
3383
- const { provider, action, params = {} } = body;
3384
- if (!provider) {
3385
- return jsonResponse({ error: { message: "provider is required", type: "invalid_request" } }, 400);
3386
- }
3387
- if (!action) {
3388
- return jsonResponse({ error: { message: "action is required", type: "invalid_request" } }, 400);
3389
- }
3390
-
3391
- const subagentId = request.headers.get("X-APIClaw-Subagent") || "main";
3392
-
3393
- // Determine execution path
3394
- let routeDetail = "";
3395
-
3396
- // Path 1: LLM routing (provider "auto" or known LLM provider with action "chat")
3397
- const isLLMRequest = action === "chat" && (
3398
- provider === "auto" ||
3399
- (PROVIDERS[provider]?.isLLM === true)
3400
- );
3401
-
3402
- if (isLLMRequest) {
3403
- // LLM routing path
3404
-
3405
- // Load workspace settings for routing
3406
- let settings = {
3407
- routingMode: "balanced",
3408
- defaultModel: null as string | null,
3409
- preferredProviders: [] as string[],
3410
- blockedProviders: [] as string[],
3411
- allowOpenRouterFallback: true,
3412
- };
3413
- if (workspaceId) {
3414
- try {
3415
- settings = await ctx.runQuery(internal.workspaceSettings.getForRouting, { workspaceId });
3416
- } catch { /* use defaults */ }
3417
- }
3418
-
3419
- const routeOverride = request.headers.get("X-APIClaw-Route");
3420
- const effectiveRoutingMode = routeOverride && ["best_price", "highest_quality", "fastest", "balanced"].includes(routeOverride)
3421
- ? routeOverride : settings.routingMode;
3422
- const effectivePreferred = routeOverride && PROVIDERS[routeOverride]?.isLLM
3423
- ? [routeOverride, ...settings.preferredProviders] : settings.preferredProviders;
3424
- // If a specific LLM provider is requested (not "auto"), prefer it
3425
- const finalPreferred = provider !== "auto" && PROVIDERS[provider]?.isLLM
3426
- ? [provider, ...effectivePreferred] : effectivePreferred;
3427
-
3428
- const effectiveModel = params.model || settings.defaultModel || "anthropic/claude-sonnet-4-6";
3429
-
3430
- const route = await routeLLMRequest(effectiveModel, {
3431
- routingMode: effectiveRoutingMode,
3432
- preferredProviders: finalPreferred,
3433
- blockedProviders: settings.blockedProviders,
3434
- allowOpenRouterFallback: settings.allowOpenRouterFallback,
3435
- }, params.messages);
3436
-
3437
- if (!route) {
3438
- return jsonResponse({ success: false, error: "No LLM provider available", _apiclaw: { latencyMs: Date.now() - startTime, route: "none", gateway: true } }, 503);
3439
- }
3440
-
3441
- routeDetail = route.reason;
3442
-
3443
- // Log usage
3444
- if (workspaceId) {
3445
- try {
3446
- await ctx.runMutation(api.analytics.log, {
3447
- event: "api_call", provider: "gateway", identifier: workspaceId,
3448
- workspaceId: workspaceId as any,
3449
- metadata: { action: "execute_chat", model: effectiveModel, routedTo: route.provider, routeReason: route.reason, authMethod },
3450
- });
3451
- await ctx.runMutation(api.logs.createProxyLog, {
3452
- workspaceId: workspaceId as any, provider: route.provider, action: "chat", subagentId,
3453
- });
3454
- await ctx.runMutation(api.workspaces.incrementUsage, { workspaceId: workspaceId as any });
3455
- } catch (e: any) { console.error("[Execute] LLM logging failed:", e.message); }
3456
- }
3457
-
3458
- // Forward to provider
3459
- try {
3460
- const { model: _m, ...restParams } = params;
3461
- const isAnthropic = route.provider === "anthropic";
3462
- let finalBody: any;
3463
- let headers: Record<string, string>;
3464
-
3465
- if (isAnthropic) {
3466
- const { body: anthropicBody } = openaiToAnthropicRequest(route.model, params.messages || [], restParams);
3467
- if (params.stream) anthropicBody.stream = true;
3468
- finalBody = anthropicBody;
3469
- headers = {
3470
- "x-api-key": route.apiKey,
3471
- "anthropic-version": "2023-06-01",
3472
- "Content-Type": "application/json",
3473
- ...(route.extraHeaders || {}),
3474
- };
3475
- } else {
3476
- finalBody = { model: route.model, messages: params.messages, stream: params.stream || false, ...restParams };
3477
- headers = {
3478
- "Authorization": `Bearer ${route.apiKey}`,
3479
- "Content-Type": "application/json",
3480
- ...(route.extraHeaders || {}),
3481
- };
3482
- }
3483
-
3484
- const response = await fetch(route.baseUrl, {
3485
- method: "POST", headers, body: JSON.stringify(finalBody),
3486
- });
3487
-
3488
- // Streaming
3489
- if (params.stream && response.body) {
3490
- return new Response(response.body, {
3491
- status: response.status,
3492
- headers: { "Content-Type": response.headers.get("Content-Type") || "text/event-stream", "Cache-Control": "no-cache", ...corsHeaders },
3493
- });
3494
- }
3495
-
3496
- let data = await response.json();
3497
-
3498
- // Translate Anthropic response to OpenAI format
3499
- if (isAnthropic && response.ok) {
3500
- data = anthropicToOpenaiResponse(data, route.model);
3501
- }
3502
- const latencyMs = Date.now() - startTime;
3503
-
3504
- // Calculate cost from token usage (parity with /v1/chat/completions)
3505
- const usage = (data as any)?.usage;
3506
- const { providerCost, apiclawCost } = calculateCallCost(route.model, usage);
3507
-
3508
- // Log cost to usage records
3509
- if (apiclawCost > 0 && workspaceId) {
3510
- ctx.runMutation(internal.billing.logCallCost, {
3511
- workspaceId: workspaceId as any,
3512
- provider: route.provider,
3513
- model: route.model,
3514
- providerCostUsd: providerCost,
3515
- apiclawCostUsd: apiclawCost,
3516
- inputTokens: usage?.prompt_tokens || 0,
3517
- outputTokens: usage?.completion_tokens || 0,
3518
- }).catch(() => {});
3519
- }
3520
-
3521
- return jsonResponse({
3522
- success: response.ok,
3523
- provider: route.provider,
3524
- action: "chat",
3525
- data,
3526
- _apiclaw: {
3527
- latencyMs, route: routeDetail, gateway: true, model: route.model,
3528
- cost: {
3529
- providerUsd: Math.round(providerCost * 1_000_000) / 1_000_000,
3530
- totalUsd: Math.round(apiclawCost * 1_000_000) / 1_000_000,
3531
- margin: "15%",
3532
- },
3533
- },
3534
- }, response.ok ? 200 : response.status);
3535
- } catch (e: any) {
3536
- return jsonResponse({ success: false, provider: provider, action, error: e.message, _apiclaw: { latencyMs: Date.now() - startTime, route: routeDetail, gateway: true } }, 500);
3537
- }
3538
- }
3539
-
3540
- // Path 2: Managed provider (known in PROVIDERS catalog)
3541
- if (PROVIDERS[provider]) {
3542
- // Managed provider path
3543
- routeDetail = `direct_${provider}`;
3544
-
3545
- const req = buildManagedRequest(provider, action, params);
3546
- if (!req) {
3547
- return jsonResponse({
3548
- success: false,
3549
- error: `Unknown action "${action}" for provider "${provider}"`,
3550
- _apiclaw: { latencyMs: Date.now() - startTime, route: routeDetail, gateway: true },
3551
- }, 400);
3552
- }
3553
-
3554
- // Log usage
3555
- if (workspaceId) {
3556
- try {
3557
- await ctx.runMutation(api.analytics.log, {
3558
- event: "api_call", provider, identifier: workspaceId,
3559
- workspaceId: workspaceId as any,
3560
- metadata: { action, subagentId, authMethod, via: "execute" },
3561
- });
3562
- await ctx.runMutation(api.logs.createProxyLog, {
3563
- workspaceId: workspaceId as any, provider, action, subagentId,
3564
- });
3565
- await ctx.runMutation(api.workspaces.incrementUsage, { workspaceId: workspaceId as any });
3566
- } catch (e: any) { console.error("[Execute] Managed logging failed:", e.message); }
3567
- }
3568
-
3569
- // Execute upstream call
3570
- try {
3571
- const fetchOpts: RequestInit = { method: req.method, headers: req.headers };
3572
- if (req.body) fetchOpts.body = req.body;
3573
-
3574
- const response = await fetch(req.url, fetchOpts);
3575
- const latencyMs = Date.now() - startTime;
3576
-
3577
- // Handle binary responses (audio, PDF, image, octet-stream)
3578
- const contentType = response.headers.get("Content-Type") || "";
3579
- const isBinary =
3580
- contentType.includes("audio/") ||
3581
- contentType.includes("image/") ||
3582
- contentType.includes("application/pdf") ||
3583
- contentType.includes("application/octet-stream");
3584
- if (isBinary) {
3585
- const buf = await response.arrayBuffer();
3586
- const bytes = new Uint8Array(buf);
3587
- let binary = "";
3588
- const chunk = 0x8000;
3589
- for (let i = 0; i < bytes.length; i += chunk) {
3590
- binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)) as any);
3591
- }
3592
- const base64 = btoa(binary);
3593
- return jsonResponse({
3594
- success: response.ok,
3595
- provider,
3596
- action,
3597
- data: {
3598
- message: response.ok ? "Binary asset returned" : "Binary error",
3599
- content_type: contentType,
3600
- size: buf.byteLength,
3601
- base64,
3602
- },
3603
- _apiclaw: { latencyMs, route: routeDetail, gateway: true },
3604
- }, response.ok ? 200 : response.status);
3605
- }
3606
-
3607
- // For text/json responses read once as text then try json parse
3608
- const raw = await response.text();
3609
- let data: any;
3610
- try {
3611
- data = JSON.parse(raw);
3612
- } catch {
3613
- data = { raw };
3614
- }
3615
-
3616
- return jsonResponse({
3617
- success: response.ok,
3618
- provider,
3619
- action,
3620
- data,
3621
- _apiclaw: { latencyMs, route: routeDetail, gateway: true },
3622
- }, response.ok ? 200 : response.status);
3623
- } catch (e: any) {
3624
- return jsonResponse({
3625
- success: false, provider, action, error: e.message,
3626
- _apiclaw: { latencyMs: Date.now() - startTime, route: routeDetail, gateway: true },
3627
- }, 500);
3628
- }
3629
- }
3630
-
3631
- // Path 3: Open API (generic HTTP proxy)
3632
- // Open API path
3633
- routeDetail = `open_${provider}`;
3634
-
3635
- const { baseUrl, method = "GET", headers: customHeaders = {}, body: customBody } = params;
3636
- if (!baseUrl) {
3637
- return jsonResponse({
3638
- success: false,
3639
- error: `Unknown provider "${provider}". For open APIs, include params.baseUrl.`,
3640
- _apiclaw: { latencyMs: Date.now() - startTime, route: "unknown", gateway: true },
3641
- }, 400);
3642
- }
3643
-
3644
- // Log usage
3645
- if (workspaceId) {
3646
- try {
3647
- await ctx.runMutation(api.analytics.log, {
3648
- event: "api_call", provider: `open:${provider}`, identifier: workspaceId,
3649
- workspaceId: workspaceId as any,
3650
- metadata: { action, subagentId, authMethod, baseUrl, via: "execute_open" },
3651
- });
3652
- await ctx.runMutation(api.logs.createProxyLog, {
3653
- workspaceId: workspaceId as any, provider: `open:${provider}`, action, subagentId,
3654
- });
3655
- await ctx.runMutation(api.workspaces.incrementUsage, { workspaceId: workspaceId as any });
3656
- } catch (e: any) { console.error("[Execute] Open API logging failed:", e.message); }
3657
- }
3658
-
3659
- // Execute open API call
3660
- try {
3661
- const fetchOpts: RequestInit = {
3662
- method: method.toUpperCase(),
3663
- headers: { "Content-Type": "application/json", ...customHeaders },
3664
- };
3665
- if (customBody && method.toUpperCase() !== "GET") {
3666
- fetchOpts.body = typeof customBody === "string" ? customBody : JSON.stringify(customBody);
3667
- }
3668
-
3669
- const response = await fetch(baseUrl, fetchOpts);
3670
- const latencyMs = Date.now() - startTime;
3671
-
3672
- let data: any;
3673
- const ct = response.headers.get("Content-Type") || "";
3674
- if (ct.includes("json")) {
3675
- try { data = await response.json(); } catch { data = { raw: await response.text() }; }
3676
- } else {
3677
- data = { raw: await response.text() };
3678
- }
3679
-
3680
- return jsonResponse({
3681
- success: response.ok,
3682
- provider,
3683
- action,
3684
- data,
3685
- _apiclaw: { latencyMs, route: routeDetail, gateway: true },
3686
- }, response.ok ? 200 : response.status);
3687
- } catch (e: any) {
3688
- return jsonResponse({
3689
- success: false, provider, action, error: e.message,
3690
- _apiclaw: { latencyMs: Date.now() - startTime, route: routeDetail, gateway: true },
3691
- }, 500);
3692
- }
3693
- }),
3694
- });
3695
-
3696
- http.route({
3697
- path: "/v1/execute",
3698
- method: "OPTIONS",
3699
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
3700
- });
3701
-
3702
- // /v1/models — List available models through APIClaw
3703
- http.route({
3704
- path: "/v1/models",
3705
- method: "GET",
3706
- handler: httpAction(async (ctx, request) => {
3707
- // API key auth optional for models listing
3708
- const models = [
3709
- // OpenRouter models (main LLM backbone)
3710
- // Anthropic direct models
3711
- { id: "anthropic/claude-sonnet-4-6", object: "model", owned_by: "anthropic", via: "direct" },
3712
- { id: "anthropic/claude-opus-4-6", object: "model", owned_by: "anthropic", via: "direct" },
3713
- { id: "anthropic/claude-3.5-sonnet", object: "model", owned_by: "anthropic", via: "direct" },
3714
- { id: "anthropic/claude-haiku-4-5", object: "model", owned_by: "anthropic", via: "direct" },
3715
- { id: "openai/gpt-4o", object: "model", owned_by: "openai", via: "openrouter" },
3716
- { id: "openai/gpt-4o-mini", object: "model", owned_by: "openai", via: "openrouter" },
3717
- { id: "openai/o3-mini", object: "model", owned_by: "openai", via: "openrouter" },
3718
- { id: "google/gemini-2.5-pro-preview", object: "model", owned_by: "google", via: "openrouter" },
3719
- { id: "google/gemini-2.5-flash-preview", object: "model", owned_by: "google", via: "openrouter" },
3720
- { id: "meta-llama/llama-3.3-70b-instruct", object: "model", owned_by: "meta", via: "openrouter" },
3721
- { id: "mistralai/mistral-large-latest", object: "model", owned_by: "mistral", via: "openrouter" },
3722
- { id: "deepseek/deepseek-r1", object: "model", owned_by: "deepseek", via: "openrouter" },
3723
- { id: "deepseek/deepseek-chat", object: "model", owned_by: "deepseek", via: "openrouter" },
3724
- { id: "qwen/qwen-2.5-72b-instruct", object: "model", owned_by: "qwen", via: "openrouter" },
3725
-
3726
- // Embedding models via /v1/embeddings
3727
- { id: "voyage/voyage-3-large", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
3728
- { id: "voyage/voyage-3", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
3729
- { id: "voyage/voyage-3-lite", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
3730
- { id: "voyage/voyage-code-3", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
3731
- { id: "voyage/voyage-multilingual-2", object: "model", owned_by: "voyage", via: "voyage", endpoint: "/v1/embeddings" },
3732
- { id: "mistral/mistral-embed", object: "model", owned_by: "mistral", via: "mistral", endpoint: "/v1/embeddings" },
3733
- { id: "openai/text-embedding-3-small", object: "model", owned_by: "openai", via: "openai", endpoint: "/v1/embeddings" },
3734
- { id: "openai/text-embedding-3-large", object: "model", owned_by: "openai", via: "openai", endpoint: "/v1/embeddings" },
3735
- { id: "openai/text-embedding-ada-002", object: "model", owned_by: "openai", via: "openai", endpoint: "/v1/embeddings" },
3736
- { id: "cohere/embed-v4.0", object: "model", owned_by: "cohere", via: "cohere", endpoint: "/v1/embeddings" },
3737
- { id: "cohere/embed-multilingual-v3", object: "model", owned_by: "cohere", via: "cohere", endpoint: "/v1/embeddings" },
3738
- ];
3739
-
3740
- return jsonResponse({
3741
- object: "list",
3742
- data: models,
3743
- _apiclaw: {
3744
- gateway: "v1",
3745
- note: "These models are available through APIClaw's unified gateway. All 800+ OpenRouter chat models + embedding models across Voyage, Mistral, OpenAI, and Cohere.",
3746
- non_llm_apis: Object.keys(PROVIDERS).length + " Direct Call providers (SMS, email, search, TTS, embeddings, code execution, scraping, and more)",
3747
- },
3748
- });
3749
- }),
3750
- });
3751
-
3752
- http.route({
3753
- path: "/v1/models",
3754
- method: "OPTIONS",
3755
- handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
3756
- });