@nordsym/apiclaw 1.8.6 → 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 (125) hide show
  1. package/README.md +4 -4
  2. package/apiclaw-README.md +12 -12
  3. package/convex/_generated/api.d.ts +4 -0
  4. package/convex/apiKeys.ts +220 -0
  5. package/convex/directCall.ts +49 -14
  6. package/convex/email.ts +5 -5
  7. package/convex/http.ts +598 -40
  8. package/convex/logs.ts +26 -22
  9. package/convex/migrateProviderWorkspaces.ts +154 -35
  10. package/convex/providers.ts +313 -203
  11. package/convex/schema.ts +50 -1
  12. package/convex/searchLogs.ts +2 -6
  13. package/convex/seedPratham.ts +1 -1
  14. package/convex/spendAlerts.ts +2 -2
  15. package/convex/stripeActions.ts +1 -1
  16. package/convex/updateAPIStatus.ts +2 -3
  17. package/convex/workspaceSettings.ts +136 -0
  18. package/dist/cli/commands/demo.js +2 -2
  19. package/dist/cli/commands/doctor.js +1 -1
  20. package/dist/cli/commands/login.js +1 -1
  21. package/dist/cli/commands/setup.js +2 -2
  22. package/dist/discovery.js +1 -1
  23. package/dist/execute.js +3 -3
  24. package/dist/index.js +13 -13
  25. package/dist/ui/errors.js +16 -16
  26. package/dist/ui/prompts.js +1 -1
  27. package/email-templates/filestack-provider-outreach.html +1 -1
  28. package/email-templates/partnership-template.html +1 -1
  29. package/email-templates/pratham-partnership-draft.html +2 -2
  30. package/package.json +2 -2
  31. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  32. package/reports/session-report-2026-04-05.html +433 -0
  33. package/src/cli/commands/demo.ts +2 -2
  34. package/src/cli/commands/doctor.ts +1 -1
  35. package/src/cli/commands/login.ts +1 -1
  36. package/src/cli/commands/setup.ts +2 -2
  37. package/src/discovery.ts +1 -1
  38. package/src/execute.ts +3 -3
  39. package/src/index.ts +14 -14
  40. package/src/ui/errors.ts +16 -16
  41. package/src/ui/prompts.ts +1 -1
  42. package/convex/adminActivate.d.ts +0 -3
  43. package/convex/adminActivate.js +0 -47
  44. package/convex/adminStats.d.ts +0 -9
  45. package/convex/adminStats.js +0 -280
  46. package/convex/agents.d.ts +0 -84
  47. package/convex/agents.js +0 -809
  48. package/convex/analytics.d.ts +0 -5
  49. package/convex/analytics.js +0 -167
  50. package/convex/backfillAnalytics.d.ts +0 -2
  51. package/convex/backfillAnalytics.js +0 -20
  52. package/convex/backfillSearchLogs.d.ts +0 -2
  53. package/convex/backfillSearchLogs.js +0 -29
  54. package/convex/billing.d.ts +0 -88
  55. package/convex/billing.js +0 -655
  56. package/convex/capabilities.d.ts +0 -9
  57. package/convex/capabilities.js +0 -145
  58. package/convex/chains.d.ts +0 -68
  59. package/convex/chains.js +0 -1105
  60. package/convex/credits.d.ts +0 -25
  61. package/convex/credits.js +0 -186
  62. package/convex/crons.d.ts +0 -3
  63. package/convex/crons.js +0 -17
  64. package/convex/debugFilestackLogs.d.ts +0 -2
  65. package/convex/debugFilestackLogs.js +0 -17
  66. package/convex/debugGetToken.d.ts +0 -2
  67. package/convex/debugGetToken.js +0 -18
  68. package/convex/directCall.d.ts +0 -72
  69. package/convex/directCall.js +0 -627
  70. package/convex/earnProgress.d.ts +0 -58
  71. package/convex/earnProgress.js +0 -649
  72. package/convex/email.d.ts +0 -14
  73. package/convex/email.js +0 -300
  74. package/convex/feedback.d.ts +0 -7
  75. package/convex/feedback.js +0 -227
  76. package/convex/http.d.ts +0 -3
  77. package/convex/http.js +0 -1408
  78. package/convex/inbound.d.ts +0 -2
  79. package/convex/inbound.js +0 -32
  80. package/convex/logs.d.ts +0 -48
  81. package/convex/logs.js +0 -621
  82. package/convex/migrateFilestack.d.ts +0 -2
  83. package/convex/migrateFilestack.js +0 -74
  84. package/convex/migratePartnersProd.d.ts +0 -8
  85. package/convex/migratePartnersProd.js +0 -165
  86. package/convex/migratePratham.d.ts +0 -2
  87. package/convex/migratePratham.js +0 -121
  88. package/convex/migrateProviderWorkspaces.d.ts +0 -2
  89. package/convex/migrateProviderWorkspaces.js +0 -55
  90. package/convex/mou.d.ts +0 -6
  91. package/convex/mou.js +0 -82
  92. package/convex/providerKeys.d.ts +0 -31
  93. package/convex/providerKeys.js +0 -257
  94. package/convex/providers.d.ts +0 -35
  95. package/convex/providers.js +0 -922
  96. package/convex/purchases.d.ts +0 -7
  97. package/convex/purchases.js +0 -157
  98. package/convex/ratelimit.d.ts +0 -4
  99. package/convex/ratelimit.js +0 -91
  100. package/convex/searchLogs.d.ts +0 -13
  101. package/convex/searchLogs.js +0 -246
  102. package/convex/seedAPILayerAPIs.d.ts +0 -7
  103. package/convex/seedAPILayerAPIs.js +0 -177
  104. package/convex/seedDirectCallConfigs.d.ts +0 -2
  105. package/convex/seedDirectCallConfigs.js +0 -324
  106. package/convex/seedPratham.d.ts +0 -6
  107. package/convex/seedPratham.js +0 -150
  108. package/convex/spendAlerts.d.ts +0 -36
  109. package/convex/spendAlerts.js +0 -380
  110. package/convex/stripeActions.d.ts +0 -19
  111. package/convex/stripeActions.js +0 -411
  112. package/convex/teams.d.ts +0 -21
  113. package/convex/teams.js +0 -215
  114. package/convex/telemetry.d.ts +0 -4
  115. package/convex/telemetry.js +0 -74
  116. package/convex/updateAPIStatus.d.ts +0 -6
  117. package/convex/updateAPIStatus.js +0 -40
  118. package/convex/usage.d.ts +0 -27
  119. package/convex/usage.js +0 -229
  120. package/convex/waitlist.d.ts +0 -4
  121. package/convex/waitlist.js +0 -49
  122. package/convex/webhooks.d.ts +0 -12
  123. package/convex/webhooks.js +0 -410
  124. package/convex/workspaces.d.ts +0 -33
  125. 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,40 +403,76 @@ 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
-
134
- // Resolve workspaceId from identifier if possible
135
- let resolvedWorkspaceId: string | undefined;
136
- if (identifier && !identifier.startsWith("anon:") && identifier !== "unknown" && identifier.length > 20) {
137
- resolvedWorkspaceId = identifier;
138
- }
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 });
139
460
 
140
461
  // ALWAYS log to analytics (even if identifier is missing)
141
462
  try {
142
463
  const result = await ctx.runMutation(api.analytics.log, {
143
464
  event: "api_call",
144
465
  provider,
145
- identifier: identifier || "unknown",
466
+ identifier: identifier,
146
467
  workspaceId: resolvedWorkspaceId as any,
147
- metadata: { action, subagentId },
468
+ metadata: { action, subagentId, authMethod: auth.authMethod },
148
469
  });
149
470
  console.log("[Proxy] Analytics logged:", result);
150
471
  } catch (e: any) {
151
472
  console.error("[Proxy] Analytics logging failed:", e.message, e.stack);
152
- // Continue even if analytics fails
153
473
  }
154
-
155
- // If we have an identifier and it's a workspace ID (not anon:), log to workspace
474
+
475
+ // If we have a workspace, log and increment usage
156
476
  if (resolvedWorkspaceId) {
157
477
  try {
158
478
  await ctx.runMutation(api.logs.createProxyLog, {
@@ -161,22 +481,20 @@ async function validateAndLogProxyCall(
161
481
  action,
162
482
  subagentId,
163
483
  });
164
-
165
- // Increment workspace usage
484
+
166
485
  await ctx.runMutation(api.workspaces.incrementUsage, {
167
486
  workspaceId: resolvedWorkspaceId as any,
168
487
  });
169
-
488
+
170
489
  console.log("[Proxy] Workspace logged for:", resolvedWorkspaceId);
171
- return { valid: true, workspaceId: resolvedWorkspaceId, subagentId };
490
+ return { valid: true, workspaceId: resolvedWorkspaceId, subagentId, authMethod: auth.authMethod };
172
491
  } catch (e: any) {
173
492
  console.error("[Proxy] Workspace logging failed:", e.message);
174
- // Continue even if workspace logging fails
175
493
  }
176
494
  }
177
-
495
+
178
496
  // Return success regardless (don't block API calls)
179
- return { valid: true, subagentId };
497
+ return { valid: true, subagentId, authMethod: auth.authMethod };
180
498
  }
181
499
 
182
500
  // OPTIONS handler for CORS
@@ -476,7 +794,7 @@ http.route({
476
794
  headers: {
477
795
  "Authorization": `Bearer ${OPENROUTER_KEY}`,
478
796
  "Content-Type": "application/json",
479
- "HTTP-Referer": "https://apiclaw.nordsym.com",
797
+ "HTTP-Referer": "https://apiclaw.cloud",
480
798
  "X-Title": "APIClaw",
481
799
  },
482
800
  body: JSON.stringify(body),
@@ -1341,7 +1659,7 @@ http.route({
1341
1659
  });
1342
1660
 
1343
1661
  // Send email directly - SIMPLE HTML (complex tables get stripped by Gmail)
1344
- const verifyUrl = `https://apiclaw.nordsym.com/auth/verify?token=${result.token}`;
1662
+ const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${result.token}`;
1345
1663
  const html = `<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;">
1346
1664
  <h1>🦞 APIClaw</h1>
1347
1665
  <h2>An AI Agent Wants to Connect</h2>
@@ -1364,7 +1682,7 @@ http.route({
1364
1682
  "Content-Type": "application/json",
1365
1683
  },
1366
1684
  body: JSON.stringify({
1367
- from: "APIClaw <noreply@apiclaw.nordsym.com>",
1685
+ from: "APIClaw <noreply@apiclaw.cloud>",
1368
1686
  to: email.toLowerCase(),
1369
1687
  subject: "🦞 Verify Your Email — APIClaw",
1370
1688
  html: html,
@@ -1555,7 +1873,7 @@ http.route({
1555
1873
  method: "POST",
1556
1874
  handler: httpAction(async (ctx, request) => {
1557
1875
  const identifier = request.headers.get("X-APIClaw-Identifier");
1558
-
1876
+
1559
1877
  try {
1560
1878
  const logId = await ctx.runMutation(api.analytics.log, {
1561
1879
  event: "test_endpoint",
@@ -1563,7 +1881,7 @@ http.route({
1563
1881
  identifier: identifier || "test",
1564
1882
  metadata: { test: true },
1565
1883
  });
1566
-
1884
+
1567
1885
  return jsonResponse({
1568
1886
  success: true,
1569
1887
  identifier,
@@ -1579,3 +1897,243 @@ http.route({
1579
1897
  }
1580
1898
  }),
1581
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
+ });