@nordsym/apiclaw 1.8.5 → 1.8.7

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 (145) hide show
  1. package/README.md +4 -4
  2. package/apiclaw-README.md +12 -12
  3. package/convex/_generated/api.d.ts +16 -0
  4. package/convex/analytics.d.ts.map +1 -1
  5. package/convex/analytics.js.map +1 -1
  6. package/convex/analytics.ts +1 -0
  7. package/convex/apiKeys.ts +220 -0
  8. package/convex/backfillAnalytics.d.ts.map +1 -0
  9. package/convex/backfillAnalytics.js.map +1 -0
  10. package/convex/backfillAnalytics.ts +22 -0
  11. package/convex/backfillSearchLogs.d.ts.map +1 -0
  12. package/convex/backfillSearchLogs.js.map +1 -0
  13. package/convex/backfillSearchLogs.ts +35 -0
  14. package/convex/debugFilestackLogs.d.ts.map +1 -0
  15. package/convex/debugFilestackLogs.js.map +1 -0
  16. package/convex/debugFilestackLogs.ts +16 -0
  17. package/convex/debugGetToken.d.ts.map +1 -0
  18. package/convex/debugGetToken.js.map +1 -0
  19. package/convex/debugGetToken.ts +18 -0
  20. package/convex/directCall.ts +49 -14
  21. package/convex/email.ts +5 -5
  22. package/convex/http.d.ts.map +1 -1
  23. package/convex/http.js.map +1 -1
  24. package/convex/http.ts +611 -49
  25. package/convex/logs.ts +26 -22
  26. package/convex/migrateFilestack.d.ts.map +1 -1
  27. package/convex/migrateFilestack.js.map +1 -1
  28. package/convex/migrateFilestack.ts +65 -101
  29. package/convex/migratePartnersProd.d.ts.map +1 -0
  30. package/convex/migratePartnersProd.js.map +1 -0
  31. package/convex/migratePartnersProd.ts +174 -0
  32. package/convex/migrateProviderWorkspaces.d.ts.map +1 -0
  33. package/convex/migrateProviderWorkspaces.js.map +1 -0
  34. package/convex/migrateProviderWorkspaces.ts +175 -0
  35. package/convex/providers.js.map +1 -1
  36. package/convex/providers.ts +313 -203
  37. package/convex/schema.ts +50 -1
  38. package/convex/searchLogs.d.ts.map +1 -1
  39. package/convex/searchLogs.js.map +1 -1
  40. package/convex/searchLogs.ts +11 -3
  41. package/convex/seedPratham.ts +1 -1
  42. package/convex/spendAlerts.ts +2 -2
  43. package/convex/stripeActions.ts +1 -1
  44. package/convex/updateAPIStatus.ts +2 -3
  45. package/convex/workspaceSettings.ts +136 -0
  46. package/dist/bin-http.js +0 -0
  47. package/dist/bin.js +0 -0
  48. package/dist/cli/commands/demo.js +2 -2
  49. package/dist/cli/commands/doctor.js +1 -1
  50. package/dist/cli/commands/login.js +1 -1
  51. package/dist/cli/commands/setup.js +2 -2
  52. package/dist/discovery.js +1 -1
  53. package/dist/execute.js +3 -3
  54. package/dist/index.js +13 -13
  55. package/dist/registry/apis.json +1 -1
  56. package/dist/ui/errors.js +16 -16
  57. package/dist/ui/prompts.js +1 -1
  58. package/email-templates/filestack-provider-outreach.html +1 -1
  59. package/email-templates/partnership-template.html +1 -1
  60. package/email-templates/pratham-partnership-draft.html +2 -2
  61. package/package.json +2 -2
  62. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  63. package/reports/session-report-2026-04-05.html +433 -0
  64. package/src/cli/commands/demo.ts +2 -2
  65. package/src/cli/commands/doctor.ts +1 -1
  66. package/src/cli/commands/login.ts +1 -1
  67. package/src/cli/commands/setup.ts +2 -2
  68. package/src/discovery.ts +1 -1
  69. package/src/execute.ts +3 -3
  70. package/src/index.ts +14 -14
  71. package/src/registry/apis.json +1 -1
  72. package/src/ui/errors.ts +16 -16
  73. package/src/ui/prompts.ts +1 -1
  74. package/convex/adminActivate.d.ts +0 -3
  75. package/convex/adminActivate.js +0 -47
  76. package/convex/adminStats.d.ts +0 -9
  77. package/convex/adminStats.js +0 -280
  78. package/convex/agents.d.ts +0 -84
  79. package/convex/agents.js +0 -809
  80. package/convex/analytics.d.ts +0 -5
  81. package/convex/analytics.js +0 -166
  82. package/convex/billing.d.ts +0 -88
  83. package/convex/billing.js +0 -655
  84. package/convex/capabilities.d.ts +0 -9
  85. package/convex/capabilities.js +0 -145
  86. package/convex/chains.d.ts +0 -68
  87. package/convex/chains.js +0 -1105
  88. package/convex/credits.d.ts +0 -25
  89. package/convex/credits.js +0 -186
  90. package/convex/crons.d.ts +0 -3
  91. package/convex/crons.js +0 -17
  92. package/convex/directCall.d.ts +0 -72
  93. package/convex/directCall.js +0 -627
  94. package/convex/earnProgress.d.ts +0 -58
  95. package/convex/earnProgress.js +0 -649
  96. package/convex/email.d.ts +0 -14
  97. package/convex/email.js +0 -300
  98. package/convex/feedback.d.ts +0 -7
  99. package/convex/feedback.js +0 -227
  100. package/convex/http.d.ts +0 -3
  101. package/convex/http.js +0 -1405
  102. package/convex/inbound.d.ts +0 -2
  103. package/convex/inbound.js +0 -32
  104. package/convex/logs.d.ts +0 -48
  105. package/convex/logs.js +0 -621
  106. package/convex/migrateFilestack.d.ts +0 -2
  107. package/convex/migrateFilestack.js +0 -113
  108. package/convex/migratePratham.d.ts +0 -2
  109. package/convex/migratePratham.js +0 -121
  110. package/convex/mou.d.ts +0 -6
  111. package/convex/mou.js +0 -82
  112. package/convex/providerKeys.d.ts +0 -31
  113. package/convex/providerKeys.js +0 -257
  114. package/convex/providers.d.ts +0 -35
  115. package/convex/providers.js +0 -922
  116. package/convex/purchases.d.ts +0 -7
  117. package/convex/purchases.js +0 -157
  118. package/convex/ratelimit.d.ts +0 -4
  119. package/convex/ratelimit.js +0 -91
  120. package/convex/searchLogs.d.ts +0 -13
  121. package/convex/searchLogs.js +0 -232
  122. package/convex/seedAPILayerAPIs.d.ts +0 -7
  123. package/convex/seedAPILayerAPIs.js +0 -177
  124. package/convex/seedDirectCallConfigs.d.ts +0 -2
  125. package/convex/seedDirectCallConfigs.js +0 -324
  126. package/convex/seedPratham.d.ts +0 -6
  127. package/convex/seedPratham.js +0 -150
  128. package/convex/spendAlerts.d.ts +0 -36
  129. package/convex/spendAlerts.js +0 -380
  130. package/convex/stripeActions.d.ts +0 -19
  131. package/convex/stripeActions.js +0 -411
  132. package/convex/teams.d.ts +0 -21
  133. package/convex/teams.js +0 -215
  134. package/convex/telemetry.d.ts +0 -4
  135. package/convex/telemetry.js +0 -74
  136. package/convex/updateAPIStatus.d.ts +0 -6
  137. package/convex/updateAPIStatus.js +0 -40
  138. package/convex/usage.d.ts +0 -27
  139. package/convex/usage.js +0 -229
  140. package/convex/waitlist.d.ts +0 -4
  141. package/convex/waitlist.js +0 -49
  142. package/convex/webhooks.d.ts +0 -12
  143. package/convex/webhooks.js +0 -410
  144. package/convex/workspaces.d.ts +0 -33
  145. package/convex/workspaces.js +0 -991
package/convex/http.ts CHANGED
@@ -12,8 +12,86 @@ import {
12
12
 
13
13
  const http = httpRouter();
14
14
 
15
- // Provider catalog
16
- const PROVIDERS = {
15
+ // Provider catalog — all 19 Direct Call providers
16
+ interface ProviderMeta {
17
+ name: string;
18
+ description: string;
19
+ category: string;
20
+ pricing: string;
21
+ regions: string[];
22
+ tags: string[];
23
+ isLLM: boolean; // can serve /v1/chat/completions
24
+ envKey?: string; // env var name for API key
25
+ baseUrl?: string; // chat completions base URL (LLM providers only)
26
+ speed: "fast" | "medium" | "slow"; // latency tier
27
+ costTier: "free" | "cheap" | "medium" | "expensive"; // relative cost
28
+ }
29
+
30
+ const PROVIDERS: Record<string, ProviderMeta> = {
31
+ openrouter: {
32
+ name: "OpenRouter",
33
+ description: "Multi-model LLM API. Access GPT, Claude, Llama, Gemini, and 800+ models.",
34
+ category: "llm",
35
+ pricing: "Varies by model",
36
+ regions: ["Global"],
37
+ tags: ["llm", "ai", "gpt", "claude", "gemini", "llama"],
38
+ isLLM: true,
39
+ envKey: "OPENROUTER_API_KEY",
40
+ baseUrl: "https://openrouter.ai/api/v1/chat/completions",
41
+ speed: "medium",
42
+ costTier: "medium",
43
+ },
44
+ groq: {
45
+ name: "Groq",
46
+ description: "Ultra-fast LLM inference. Llama, Mixtral, Gemma at lightning speed.",
47
+ category: "llm",
48
+ pricing: "~$0.05-0.27/M tokens",
49
+ regions: ["Global"],
50
+ tags: ["llm", "fast", "llama", "mixtral", "gemma"],
51
+ isLLM: true,
52
+ envKey: "GROQ_API_KEY",
53
+ baseUrl: "https://api.groq.com/openai/v1/chat/completions",
54
+ speed: "fast",
55
+ costTier: "cheap",
56
+ },
57
+ mistral: {
58
+ name: "Mistral",
59
+ description: "Mistral AI models. Efficient European LLMs with strong coding.",
60
+ category: "llm",
61
+ pricing: "~$0.10-2.00/M tokens",
62
+ regions: ["EU", "Global"],
63
+ tags: ["llm", "mistral", "eu", "coding", "embeddings"],
64
+ isLLM: true,
65
+ envKey: "MISTRAL_API_KEY",
66
+ baseUrl: "https://api.mistral.ai/v1/chat/completions",
67
+ speed: "fast",
68
+ costTier: "cheap",
69
+ },
70
+ together: {
71
+ name: "Together AI",
72
+ description: "Open-source model inference. Llama, Qwen, DeepSeek at scale.",
73
+ category: "llm",
74
+ pricing: "~$0.10-0.90/M tokens",
75
+ regions: ["Global"],
76
+ tags: ["llm", "open-source", "llama", "qwen", "deepseek"],
77
+ isLLM: true,
78
+ envKey: "TOGETHER_API_KEY",
79
+ baseUrl: "https://api.together.xyz/v1/chat/completions",
80
+ speed: "fast",
81
+ costTier: "cheap",
82
+ },
83
+ cohere: {
84
+ name: "Cohere",
85
+ description: "Enterprise LLM with strong RAG and reranking capabilities.",
86
+ category: "llm",
87
+ pricing: "~$0.15-2.50/M tokens",
88
+ regions: ["Global"],
89
+ tags: ["llm", "rag", "rerank", "enterprise", "embeddings"],
90
+ isLLM: false, // Cohere uses non-OpenAI-compatible API format
91
+ envKey: "COHERE_API_KEY",
92
+ speed: "medium",
93
+ costTier: "medium",
94
+ },
17
95
  "46elks": {
18
96
  name: "46elks",
19
97
  description: "SMS API for EU/Nordics. GDPR compliant.",
@@ -21,6 +99,10 @@ const PROVIDERS = {
21
99
  pricing: "~$0.035/SMS",
22
100
  regions: ["EU", "Nordic"],
23
101
  tags: ["sms", "eu", "gdpr", "nordic"],
102
+ isLLM: false,
103
+ envKey: "ELKS_API_KEY",
104
+ speed: "fast",
105
+ costTier: "cheap",
24
106
  },
25
107
  twilio: {
26
108
  name: "Twilio",
@@ -29,6 +111,10 @@ const PROVIDERS = {
29
111
  pricing: "~$0.04/SMS, ~$0.01/min voice",
30
112
  regions: ["Global"],
31
113
  tags: ["sms", "voice", "global"],
114
+ isLLM: false,
115
+ envKey: "TWILIO_AUTH_TOKEN",
116
+ speed: "fast",
117
+ costTier: "cheap",
32
118
  },
33
119
  resend: {
34
120
  name: "Resend",
@@ -37,6 +123,10 @@ const PROVIDERS = {
37
123
  pricing: "~$0.001/email",
38
124
  regions: ["Global"],
39
125
  tags: ["email", "transactional"],
126
+ isLLM: false,
127
+ envKey: "RESEND_API_KEY",
128
+ speed: "fast",
129
+ costTier: "free",
40
130
  },
41
131
  brave_search: {
42
132
  name: "Brave Search",
@@ -45,30 +135,82 @@ const PROVIDERS = {
45
135
  pricing: "~$0.005/search",
46
136
  regions: ["Global"],
47
137
  tags: ["search", "web", "privacy"],
138
+ isLLM: false,
139
+ envKey: "BRAVE_API_KEY",
140
+ speed: "fast",
141
+ costTier: "cheap",
48
142
  },
49
- openrouter: {
50
- name: "OpenRouter",
51
- description: "Multi-model LLM API. Access GPT, Claude, Llama, etc.",
52
- category: "llm",
53
- pricing: "Varies by model",
143
+ serper: {
144
+ name: "Serper",
145
+ description: "Google Search API. Fast SERP results for AI agents.",
146
+ category: "search",
147
+ pricing: "~$0.001/search",
54
148
  regions: ["Global"],
55
- tags: ["llm", "ai", "gpt", "claude"],
149
+ tags: ["search", "google", "serp"],
150
+ isLLM: false,
151
+ envKey: "SERPER_API_KEY",
152
+ speed: "fast",
153
+ costTier: "cheap",
56
154
  },
57
155
  elevenlabs: {
58
156
  name: "ElevenLabs",
59
- description: "Text-to-speech API. High quality voices.",
157
+ description: "Text-to-speech API. High quality AI voices.",
60
158
  category: "tts",
61
159
  pricing: "~$0.0003/char",
62
160
  regions: ["Global"],
63
- tags: ["tts", "voice", "audio"],
161
+ tags: ["tts", "voice", "audio", "speech"],
162
+ isLLM: false,
163
+ envKey: "ELEVENLABS_API_KEY",
164
+ speed: "medium",
165
+ costTier: "medium",
166
+ },
167
+ deepgram: {
168
+ name: "Deepgram",
169
+ description: "Speech-to-text API. Fast, accurate transcription with Nova-3.",
170
+ category: "stt",
171
+ pricing: "~$0.0043/min",
172
+ regions: ["Global"],
173
+ tags: ["stt", "transcription", "voice", "audio"],
174
+ isLLM: false,
175
+ envKey: "DEEPGRAM_API_KEY",
176
+ speed: "fast",
177
+ costTier: "cheap",
178
+ },
179
+ assemblyai: {
180
+ name: "AssemblyAI",
181
+ description: "Speech-to-text with speaker diarization, summarization, and sentiment.",
182
+ category: "stt",
183
+ pricing: "~$0.01/min",
184
+ regions: ["Global"],
185
+ tags: ["stt", "transcription", "diarization", "sentiment"],
186
+ isLLM: false,
187
+ envKey: "ASSEMBLYAI_API_KEY",
188
+ speed: "medium",
189
+ costTier: "cheap",
64
190
  },
65
191
  replicate: {
66
192
  name: "Replicate",
67
- description: "Run AI models (Whisper, SDXL, Llama, etc). Pay per prediction.",
193
+ description: "Run AI models (Whisper, SDXL, Llama, Flux, etc). Pay per prediction.",
68
194
  category: "ai",
69
195
  pricing: "Varies by model",
70
196
  regions: ["Global"],
71
197
  tags: ["ai", "ml", "whisper", "image", "audio", "transcription"],
198
+ isLLM: false,
199
+ envKey: "REPLICATE_API_TOKEN",
200
+ speed: "slow",
201
+ costTier: "medium",
202
+ },
203
+ stability: {
204
+ name: "Stability AI",
205
+ description: "Image generation API. Stable Diffusion 3, SDXL.",
206
+ category: "image",
207
+ pricing: "~$0.03/image",
208
+ regions: ["Global"],
209
+ tags: ["image", "generation", "stable-diffusion", "sdxl"],
210
+ isLLM: false,
211
+ envKey: "STABILITY_API_KEY",
212
+ speed: "slow",
213
+ costTier: "medium",
72
214
  },
73
215
  firecrawl: {
74
216
  name: "Firecrawl",
@@ -77,6 +219,10 @@ const PROVIDERS = {
77
219
  pricing: "~$0.001/page",
78
220
  regions: ["Global"],
79
221
  tags: ["scraping", "web", "crawl", "extract"],
222
+ isLLM: false,
223
+ envKey: "FIRECRAWL_API_KEY",
224
+ speed: "medium",
225
+ costTier: "cheap",
80
226
  },
81
227
  github: {
82
228
  name: "GitHub",
@@ -85,14 +231,22 @@ const PROVIDERS = {
85
231
  pricing: "Free tier available",
86
232
  regions: ["Global"],
87
233
  tags: ["github", "code", "repos", "developer"],
234
+ isLLM: false,
235
+ envKey: "GITHUB_TOKEN",
236
+ speed: "fast",
237
+ costTier: "free",
88
238
  },
89
239
  e2b: {
90
240
  name: "E2B",
91
- description: "Secure code sandbox for AI agents. Run Python, shell commands in isolated environments.",
241
+ description: "Secure code sandbox for AI agents. Run Python, shell in isolated environments.",
92
242
  category: "sandbox",
93
243
  pricing: "$0.000028/s (2 vCPU)",
94
244
  regions: ["Global"],
95
245
  tags: ["sandbox", "code", "python", "execution", "ai", "agents"],
246
+ isLLM: false,
247
+ envKey: "E2B_API_KEY",
248
+ speed: "medium",
249
+ costTier: "cheap",
96
250
  },
97
251
  apilayer: {
98
252
  name: "APILayer",
@@ -101,8 +255,138 @@ const PROVIDERS = {
101
255
  pricing: "Free tier available, paid plans per API",
102
256
  regions: ["Global"],
103
257
  tags: ["exchange", "stocks", "aviation", "pdf", "screenshot", "verification", "vat", "news", "scraping"],
258
+ isLLM: false,
259
+ envKey: "APILAYER_API_KEY",
260
+ speed: "medium",
261
+ costTier: "cheap",
104
262
  },
105
- } as const;
263
+ };
264
+
265
+ // ==============================================
266
+ // INTELLIGENT LLM ROUTER
267
+ // ==============================================
268
+
269
+ // Model-to-provider mapping: which direct providers can serve which model patterns
270
+ const MODEL_PROVIDER_MAP: { pattern: RegExp; provider: string; nativeModel: string }[] = [
271
+ // Groq-native models
272
+ { pattern: /^(groq\/)?llama-3\.3-70b/i, provider: "groq", nativeModel: "llama-3.3-70b-versatile" },
273
+ { pattern: /^(groq\/)?llama-3\.1-8b/i, provider: "groq", nativeModel: "llama-3.1-8b-instant" },
274
+ { pattern: /^(groq\/)?gemma2?-9b/i, provider: "groq", nativeModel: "gemma2-9b-it" },
275
+ { pattern: /^(groq\/)?mixtral-8x7b/i, provider: "groq", nativeModel: "mixtral-8x7b-32768" },
276
+ // Mistral-native models
277
+ { pattern: /^(mistralai\/)?mistral-small/i, provider: "mistral", nativeModel: "mistral-small-latest" },
278
+ { pattern: /^(mistralai\/)?mistral-large/i, provider: "mistral", nativeModel: "mistral-large-latest" },
279
+ { pattern: /^(mistralai\/)?mistral-medium/i, provider: "mistral", nativeModel: "mistral-medium-latest" },
280
+ { pattern: /^(mistralai\/)?codestral/i, provider: "mistral", nativeModel: "codestral-latest" },
281
+ { pattern: /^(mistralai\/)?pixtral/i, provider: "mistral", nativeModel: "pixtral-large-latest" },
282
+ // Together-native models
283
+ { pattern: /^(together\/)?meta-llama\/Llama-3\.3-70B/i, provider: "together", nativeModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo" },
284
+ { pattern: /^(together\/)?Qwen\/Qwen2\.5-72B/i, provider: "together", nativeModel: "Qwen/Qwen2.5-72B-Instruct-Turbo" },
285
+ { pattern: /^(together\/)?deepseek-ai\/DeepSeek-R1/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-R1" },
286
+ { pattern: /^(together\/)?deepseek-ai\/DeepSeek-V3/i, provider: "together", nativeModel: "deepseek-ai/DeepSeek-V3" },
287
+ ];
288
+
289
+ interface RoutingDecision {
290
+ provider: string;
291
+ model: string;
292
+ baseUrl: string;
293
+ apiKey: string;
294
+ reason: string;
295
+ extraHeaders?: Record<string, string>;
296
+ }
297
+
298
+ function routeLLMRequest(
299
+ requestedModel: string,
300
+ settings: {
301
+ routingMode: string;
302
+ preferredProviders: string[];
303
+ blockedProviders: string[];
304
+ allowOpenRouterFallback: boolean;
305
+ }
306
+ ): RoutingDecision | null {
307
+ // 1. Check direct provider matches for the requested model
308
+ for (const mapping of MODEL_PROVIDER_MAP) {
309
+ if (!mapping.pattern.test(requestedModel)) continue;
310
+ if (settings.blockedProviders.includes(mapping.provider)) continue;
311
+
312
+ const providerMeta = PROVIDERS[mapping.provider];
313
+ if (!providerMeta?.isLLM || !providerMeta.envKey || !providerMeta.baseUrl) continue;
314
+
315
+ const apiKey = process.env[providerMeta.envKey];
316
+ if (!apiKey) continue;
317
+
318
+ // For "highest_quality" mode, prefer OpenRouter (more model options)
319
+ if (settings.routingMode === "highest_quality" && !settings.preferredProviders.includes(mapping.provider)) {
320
+ continue;
321
+ }
322
+
323
+ return {
324
+ provider: mapping.provider,
325
+ model: mapping.nativeModel,
326
+ baseUrl: providerMeta.baseUrl,
327
+ apiKey,
328
+ reason: `direct_${mapping.provider}`,
329
+ };
330
+ }
331
+
332
+ // 2. Routing mode preferences for unknown models
333
+ if (settings.routingMode === "fastest") {
334
+ // Try Groq first (fastest inference), then Together, then Mistral
335
+ for (const fastProvider of ["groq", "together", "mistral"]) {
336
+ if (settings.blockedProviders.includes(fastProvider)) continue;
337
+ const meta = PROVIDERS[fastProvider];
338
+ if (!meta?.isLLM || !meta.envKey || !meta.baseUrl) continue;
339
+ const key = process.env[meta.envKey];
340
+ if (!key) continue;
341
+ // Only route if the model looks like it belongs to this provider
342
+ // Don't send anthropic/claude to groq
343
+ if (requestedModel.includes("anthropic/") || requestedModel.includes("openai/") || requestedModel.includes("google/")) break;
344
+ return {
345
+ provider: fastProvider,
346
+ model: requestedModel,
347
+ baseUrl: meta.baseUrl,
348
+ apiKey: key,
349
+ reason: `fastest_mode_${fastProvider}`,
350
+ };
351
+ }
352
+ }
353
+
354
+ // 3. Preferred providers check
355
+ for (const preferred of settings.preferredProviders) {
356
+ if (settings.blockedProviders.includes(preferred)) continue;
357
+ const meta = PROVIDERS[preferred];
358
+ if (!meta?.isLLM || !meta.envKey || !meta.baseUrl) continue;
359
+ const key = process.env[meta.envKey];
360
+ if (!key) continue;
361
+ return {
362
+ provider: preferred,
363
+ model: requestedModel,
364
+ baseUrl: meta.baseUrl,
365
+ apiKey: key,
366
+ reason: `preferred_${preferred}`,
367
+ };
368
+ }
369
+
370
+ // 4. Fallback to OpenRouter
371
+ if (!settings.blockedProviders.includes("openrouter") && settings.allowOpenRouterFallback !== false) {
372
+ const orKey = process.env.OPENROUTER_API_KEY;
373
+ if (orKey) {
374
+ return {
375
+ provider: "openrouter",
376
+ model: requestedModel,
377
+ baseUrl: "https://openrouter.ai/api/v1/chat/completions",
378
+ apiKey: orKey,
379
+ reason: "openrouter_fallback",
380
+ extraHeaders: {
381
+ "HTTP-Referer": "https://apiclaw.cloud",
382
+ "X-Title": "APIClaw Gateway",
383
+ },
384
+ };
385
+ }
386
+ }
387
+
388
+ return null; // No provider available
389
+ }
106
390
 
107
391
  // CORS headers
108
392
  const corsHeaders = {
@@ -119,60 +403,98 @@ function jsonResponse(data: unknown, status = 200) {
119
403
  });
120
404
  }
121
405
 
406
+ // ============================================
407
+ // UNIFIED AUTH: resolves workspace from any auth method
408
+ // Priority: 1) Authorization: Bearer sk-claw-... (API key)
409
+ // 2) X-APIClaw-Identifier (legacy MCP workspace ID)
410
+ // 3) Anonymous (still allowed, just untracked)
411
+ // ============================================
412
+
413
+ async function resolveWorkspaceFromRequest(
414
+ ctx: any,
415
+ request: Request
416
+ ): Promise<{ workspaceId?: string; keyId?: string; authMethod: "api-key" | "identifier" | "anonymous" }> {
417
+ // 1. Check for API key auth (Bearer sk-claw-...)
418
+ const authHeader = request.headers.get("Authorization");
419
+ if (authHeader?.startsWith("Bearer sk-claw-")) {
420
+ const rawKey = authHeader.slice(7); // Remove "Bearer "
421
+ try {
422
+ const resolved = await ctx.runQuery(internal.apiKeys.resolveKey, { rawKey });
423
+ if (resolved) {
424
+ // Touch lastUsedAt (fire and forget)
425
+ ctx.runMutation(api.apiKeys.touchKey, { keyId: resolved.keyId }).catch(() => {});
426
+ return { workspaceId: resolved.workspaceId, keyId: resolved.keyId, authMethod: "api-key" };
427
+ }
428
+ } catch (e: any) {
429
+ console.error("[Auth] API key resolution failed:", e.message);
430
+ }
431
+ // Invalid key - don't fall through to anonymous
432
+ return { authMethod: "anonymous" };
433
+ }
434
+
435
+ // 2. Check for legacy identifier
436
+ const identifier = request.headers.get("X-APIClaw-Identifier");
437
+ if (identifier && !identifier.startsWith("anon:") && identifier !== "unknown" && identifier.length > 20) {
438
+ return { workspaceId: identifier, authMethod: "identifier" };
439
+ }
440
+
441
+ // 3. Anonymous
442
+ return { authMethod: "anonymous" };
443
+ }
444
+
122
445
  // Helper to validate session and log API usage
123
446
  async function validateAndLogProxyCall(
124
447
  ctx: any,
125
448
  request: Request,
126
449
  provider: string,
127
450
  action: string
128
- ): Promise<{ valid: boolean; workspaceId?: string; subagentId?: string; error?: string }> {
129
- const identifier = request.headers.get("X-APIClaw-Identifier");
451
+ ): Promise<{ valid: boolean; workspaceId?: string; subagentId?: string; error?: string; authMethod?: string }> {
130
452
  const subagentId = request.headers.get("X-APIClaw-Subagent") || "main";
131
-
132
- console.log("[Proxy] Call received", { provider, action, identifier, subagentId });
133
-
453
+
454
+ // Resolve workspace from any auth method
455
+ const auth = await resolveWorkspaceFromRequest(ctx, request);
456
+ const resolvedWorkspaceId = auth.workspaceId;
457
+ const identifier = request.headers.get("X-APIClaw-Identifier") || auth.workspaceId || "unknown";
458
+
459
+ console.log("[Proxy] Call received", { provider, action, authMethod: auth.authMethod, workspaceId: resolvedWorkspaceId, subagentId });
460
+
134
461
  // ALWAYS log to analytics (even if identifier is missing)
135
462
  try {
136
463
  const result = await ctx.runMutation(api.analytics.log, {
137
464
  event: "api_call",
138
465
  provider,
139
- identifier: identifier || "unknown",
140
- metadata: { action, subagentId },
466
+ identifier: identifier,
467
+ workspaceId: resolvedWorkspaceId as any,
468
+ metadata: { action, subagentId, authMethod: auth.authMethod },
141
469
  });
142
470
  console.log("[Proxy] Analytics logged:", result);
143
471
  } catch (e: any) {
144
472
  console.error("[Proxy] Analytics logging failed:", e.message, e.stack);
145
- // Continue even if analytics fails
146
473
  }
147
-
148
- // If we have an identifier and it's a workspace ID (not anon:), log to workspace
149
- if (identifier && !identifier.startsWith("anon:") && identifier !== "unknown") {
474
+
475
+ // If we have a workspace, log and increment usage
476
+ if (resolvedWorkspaceId) {
150
477
  try {
151
- // Validate it's actually a workspace ID by checking format
152
- if (identifier.length > 20) {
153
- await ctx.runMutation(api.logs.createProxyLog, {
154
- workspaceId: identifier as any,
155
- provider,
156
- action,
157
- subagentId,
158
- });
159
-
160
- // Increment workspace usage
161
- await ctx.runMutation(api.workspaces.incrementUsage, {
162
- workspaceId: identifier as any,
163
- });
164
-
165
- console.log("[Proxy] Workspace logged for:", identifier);
166
- return { valid: true, workspaceId: identifier, subagentId };
167
- }
478
+ await ctx.runMutation(api.logs.createProxyLog, {
479
+ workspaceId: resolvedWorkspaceId as any,
480
+ provider,
481
+ action,
482
+ subagentId,
483
+ });
484
+
485
+ await ctx.runMutation(api.workspaces.incrementUsage, {
486
+ workspaceId: resolvedWorkspaceId as any,
487
+ });
488
+
489
+ console.log("[Proxy] Workspace logged for:", resolvedWorkspaceId);
490
+ return { valid: true, workspaceId: resolvedWorkspaceId, subagentId, authMethod: auth.authMethod };
168
491
  } catch (e: any) {
169
492
  console.error("[Proxy] Workspace logging failed:", e.message);
170
- // Continue even if workspace logging fails
171
493
  }
172
494
  }
173
-
495
+
174
496
  // Return success regardless (don't block API calls)
175
- return { valid: true, subagentId };
497
+ return { valid: true, subagentId, authMethod: auth.authMethod };
176
498
  }
177
499
 
178
500
  // OPTIONS handler for CORS
@@ -472,7 +794,7 @@ http.route({
472
794
  headers: {
473
795
  "Authorization": `Bearer ${OPENROUTER_KEY}`,
474
796
  "Content-Type": "application/json",
475
- "HTTP-Referer": "https://apiclaw.nordsym.com",
797
+ "HTTP-Referer": "https://apiclaw.cloud",
476
798
  "X-Title": "APIClaw",
477
799
  },
478
800
  body: JSON.stringify(body),
@@ -1337,7 +1659,7 @@ http.route({
1337
1659
  });
1338
1660
 
1339
1661
  // Send email directly - SIMPLE HTML (complex tables get stripped by Gmail)
1340
- const verifyUrl = `https://apiclaw.nordsym.com/auth/verify?token=${result.token}`;
1662
+ const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${result.token}`;
1341
1663
  const html = `<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;">
1342
1664
  <h1>🦞 APIClaw</h1>
1343
1665
  <h2>An AI Agent Wants to Connect</h2>
@@ -1360,7 +1682,7 @@ http.route({
1360
1682
  "Content-Type": "application/json",
1361
1683
  },
1362
1684
  body: JSON.stringify({
1363
- from: "APIClaw <noreply@apiclaw.nordsym.com>",
1685
+ from: "APIClaw <noreply@apiclaw.cloud>",
1364
1686
  to: email.toLowerCase(),
1365
1687
  subject: "🦞 Verify Your Email — APIClaw",
1366
1688
  html: html,
@@ -1551,7 +1873,7 @@ http.route({
1551
1873
  method: "POST",
1552
1874
  handler: httpAction(async (ctx, request) => {
1553
1875
  const identifier = request.headers.get("X-APIClaw-Identifier");
1554
-
1876
+
1555
1877
  try {
1556
1878
  const logId = await ctx.runMutation(api.analytics.log, {
1557
1879
  event: "test_endpoint",
@@ -1559,7 +1881,7 @@ http.route({
1559
1881
  identifier: identifier || "test",
1560
1882
  metadata: { test: true },
1561
1883
  });
1562
-
1884
+
1563
1885
  return jsonResponse({
1564
1886
  success: true,
1565
1887
  identifier,
@@ -1575,3 +1897,243 @@ http.route({
1575
1897
  }
1576
1898
  }),
1577
1899
  });
1900
+
1901
+ // ==============================================
1902
+ // GATEWAY v1 — Unified API Layer for AI Agents
1903
+ // ==============================================
1904
+ // OpenAI-compatible /v1/chat/completions endpoint.
1905
+ // Accepts: Authorization: Bearer sk-claw-...
1906
+ // Routes to the best available LLM provider (OpenRouter by default).
1907
+ // This is what OpenClaw and any agent configures as their API endpoint.
1908
+ // ==============================================
1909
+
1910
+ // Helper: extract Bearer token from Authorization header
1911
+ function extractBearerToken(request: Request): string | null {
1912
+ const auth = request.headers.get("Authorization");
1913
+ if (!auth?.startsWith("Bearer ")) return null;
1914
+ return auth.slice(7);
1915
+ }
1916
+
1917
+ // Helper: require API key auth, return 401 if missing
1918
+ async function requireApiKeyAuth(
1919
+ ctx: any,
1920
+ request: Request
1921
+ ): Promise<{ workspaceId: string; keyId: string } | Response> {
1922
+ const auth = await resolveWorkspaceFromRequest(ctx, request);
1923
+ if (auth.authMethod !== "api-key" || !auth.workspaceId || !auth.keyId) {
1924
+ return jsonResponse(
1925
+ {
1926
+ error: {
1927
+ message: "Invalid API key. Generate one at https://apiclaw.cloud/workspace?tab=api-keys",
1928
+ type: "invalid_api_key",
1929
+ code: "invalid_api_key",
1930
+ },
1931
+ },
1932
+ 401
1933
+ );
1934
+ }
1935
+ return { workspaceId: auth.workspaceId, keyId: auth.keyId };
1936
+ }
1937
+
1938
+ // /v1/chat/completions — OpenAI-compatible LLM gateway with intelligent routing
1939
+ http.route({
1940
+ path: "/v1/chat/completions",
1941
+ method: "POST",
1942
+ handler: httpAction(async (ctx, request) => {
1943
+ const startTime = Date.now();
1944
+
1945
+ // Require API key auth
1946
+ const authResult = await requireApiKeyAuth(ctx, request);
1947
+ if (authResult instanceof Response) return authResult;
1948
+ const { workspaceId } = authResult;
1949
+
1950
+ // Parse body
1951
+ let body: any;
1952
+ try {
1953
+ body = await request.json();
1954
+ } catch {
1955
+ return jsonResponse({ error: { message: "Invalid JSON body", type: "invalid_request_error" } }, 400);
1956
+ }
1957
+
1958
+ const { model, messages, stream, ...rest } = body;
1959
+ if (!messages || !Array.isArray(messages)) {
1960
+ return jsonResponse({ error: { message: "messages array is required", type: "invalid_request_error" } }, 400);
1961
+ }
1962
+
1963
+ // Request-level overrides (X-APIClaw-Route header)
1964
+ const routeOverride = request.headers.get("X-APIClaw-Route"); // e.g. "fastest" or "groq"
1965
+
1966
+ // Load workspace settings
1967
+ let settings: {
1968
+ routingMode: string;
1969
+ defaultModel: string | null;
1970
+ preferredProviders: string[];
1971
+ blockedProviders: string[];
1972
+ allowOpenRouterFallback: boolean;
1973
+ };
1974
+ try {
1975
+ settings = await ctx.runQuery(internal.workspaceSettings.getForRouting, { workspaceId });
1976
+ } catch {
1977
+ settings = {
1978
+ routingMode: "balanced",
1979
+ defaultModel: null,
1980
+ preferredProviders: [],
1981
+ blockedProviders: [],
1982
+ allowOpenRouterFallback: true,
1983
+ };
1984
+ }
1985
+
1986
+ // Apply request-level overrides
1987
+ const effectiveRoutingMode = routeOverride && ["best_price", "highest_quality", "fastest", "balanced"].includes(routeOverride)
1988
+ ? routeOverride
1989
+ : settings.routingMode;
1990
+
1991
+ // If routeOverride is a provider name, add it as preferred
1992
+ const effectivePreferred = routeOverride && PROVIDERS[routeOverride]?.isLLM
1993
+ ? [routeOverride, ...settings.preferredProviders]
1994
+ : settings.preferredProviders;
1995
+
1996
+ const effectiveModel = model || settings.defaultModel || "anthropic/claude-sonnet-4-6";
1997
+
1998
+ // Route the request
1999
+ const route = routeLLMRequest(effectiveModel, {
2000
+ routingMode: effectiveRoutingMode,
2001
+ preferredProviders: effectivePreferred,
2002
+ blockedProviders: settings.blockedProviders,
2003
+ allowOpenRouterFallback: settings.allowOpenRouterFallback,
2004
+ });
2005
+
2006
+ if (!route) {
2007
+ return jsonResponse({ error: { message: "No LLM provider available. Check workspace settings.", type: "server_error" } }, 503);
2008
+ }
2009
+
2010
+ // Log usage
2011
+ try {
2012
+ await ctx.runMutation(api.analytics.log, {
2013
+ event: "api_call",
2014
+ provider: "gateway",
2015
+ identifier: workspaceId,
2016
+ workspaceId: workspaceId as any,
2017
+ metadata: {
2018
+ action: "chat_completions",
2019
+ model: effectiveModel,
2020
+ routedTo: route.provider,
2021
+ routeReason: route.reason,
2022
+ authMethod: "api-key",
2023
+ },
2024
+ });
2025
+ await ctx.runMutation(api.logs.createProxyLog, {
2026
+ workspaceId: workspaceId as any,
2027
+ provider: route.provider,
2028
+ action: "chat_completions",
2029
+ subagentId: request.headers.get("X-APIClaw-Subagent") || "main",
2030
+ });
2031
+ await ctx.runMutation(api.workspaces.incrementUsage, {
2032
+ workspaceId: workspaceId as any,
2033
+ });
2034
+ } catch (e: any) {
2035
+ console.error("[Gateway] Logging failed:", e.message);
2036
+ }
2037
+
2038
+ // Forward to the chosen provider
2039
+ try {
2040
+ const requestBody = {
2041
+ model: route.model,
2042
+ messages,
2043
+ stream: stream || false,
2044
+ ...rest,
2045
+ };
2046
+
2047
+ const headers: Record<string, string> = {
2048
+ "Authorization": `Bearer ${route.apiKey}`,
2049
+ "Content-Type": "application/json",
2050
+ ...(route.extraHeaders || {}),
2051
+ };
2052
+
2053
+ const response = await fetch(route.baseUrl, {
2054
+ method: "POST",
2055
+ headers,
2056
+ body: JSON.stringify(requestBody),
2057
+ });
2058
+
2059
+ // For streaming responses, proxy the stream directly
2060
+ if (stream && response.body) {
2061
+ return new Response(response.body, {
2062
+ status: response.status,
2063
+ headers: {
2064
+ "Content-Type": response.headers.get("Content-Type") || "text/event-stream",
2065
+ "Cache-Control": "no-cache",
2066
+ "Connection": "keep-alive",
2067
+ ...corsHeaders,
2068
+ },
2069
+ });
2070
+ }
2071
+
2072
+ // Non-streaming: return JSON
2073
+ const data = await response.json();
2074
+ const latencyMs = Date.now() - startTime;
2075
+
2076
+ // Add APIClaw metadata
2077
+ if (data && typeof data === "object") {
2078
+ (data as any)._apiclaw = {
2079
+ latencyMs,
2080
+ provider: route.provider,
2081
+ routeReason: route.reason,
2082
+ model: route.model,
2083
+ gateway: "v1",
2084
+ };
2085
+ }
2086
+
2087
+ return jsonResponse(data, response.status);
2088
+ } catch (e: any) {
2089
+ return jsonResponse({ error: { message: e.message, type: "server_error" } }, 500);
2090
+ }
2091
+ }),
2092
+ });
2093
+
2094
+ http.route({
2095
+ path: "/v1/chat/completions",
2096
+ method: "OPTIONS",
2097
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2098
+ });
2099
+
2100
+ // /v1/models — List available models through APIClaw
2101
+ http.route({
2102
+ path: "/v1/models",
2103
+ method: "GET",
2104
+ handler: httpAction(async (ctx, request) => {
2105
+ // API key auth optional for models listing
2106
+ const models = [
2107
+ // OpenRouter models (main LLM backbone)
2108
+ { id: "anthropic/claude-sonnet-4-6", object: "model", owned_by: "anthropic", via: "openrouter" },
2109
+ { id: "anthropic/claude-haiku-3.5", object: "model", owned_by: "anthropic", via: "openrouter" },
2110
+ { id: "anthropic/claude-opus-4", object: "model", owned_by: "anthropic", via: "openrouter" },
2111
+ { id: "openai/gpt-4o", object: "model", owned_by: "openai", via: "openrouter" },
2112
+ { id: "openai/gpt-4o-mini", object: "model", owned_by: "openai", via: "openrouter" },
2113
+ { id: "openai/o3-mini", object: "model", owned_by: "openai", via: "openrouter" },
2114
+ { id: "google/gemini-2.5-pro-preview", object: "model", owned_by: "google", via: "openrouter" },
2115
+ { id: "google/gemini-2.5-flash-preview", object: "model", owned_by: "google", via: "openrouter" },
2116
+ { id: "meta-llama/llama-3.3-70b-instruct", object: "model", owned_by: "meta", via: "openrouter" },
2117
+ { id: "mistralai/mistral-large-latest", object: "model", owned_by: "mistral", via: "openrouter" },
2118
+ { id: "deepseek/deepseek-r1", object: "model", owned_by: "deepseek", via: "openrouter" },
2119
+ { id: "deepseek/deepseek-chat", object: "model", owned_by: "deepseek", via: "openrouter" },
2120
+ { id: "qwen/qwen-2.5-72b-instruct", object: "model", owned_by: "qwen", via: "openrouter" },
2121
+ ];
2122
+
2123
+ return jsonResponse({
2124
+ object: "list",
2125
+ data: models,
2126
+ _apiclaw: {
2127
+ gateway: "v1",
2128
+ note: "These models are available through APIClaw's unified gateway. All 800+ OpenRouter models are accessible by ID.",
2129
+ non_llm_apis: Object.keys(PROVIDERS).length + " additional APIs available (SMS, email, search, TTS, code execution, scraping, and more)",
2130
+ },
2131
+ });
2132
+ }),
2133
+ });
2134
+
2135
+ http.route({
2136
+ path: "/v1/models",
2137
+ method: "OPTIONS",
2138
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
2139
+ });