@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.
- package/README.md +4 -4
- package/apiclaw-README.md +12 -12
- package/convex/_generated/api.d.ts +16 -0
- package/convex/analytics.d.ts.map +1 -1
- package/convex/analytics.js.map +1 -1
- package/convex/analytics.ts +1 -0
- package/convex/apiKeys.ts +220 -0
- package/convex/backfillAnalytics.d.ts.map +1 -0
- package/convex/backfillAnalytics.js.map +1 -0
- package/convex/backfillAnalytics.ts +22 -0
- package/convex/backfillSearchLogs.d.ts.map +1 -0
- package/convex/backfillSearchLogs.js.map +1 -0
- package/convex/backfillSearchLogs.ts +35 -0
- package/convex/debugFilestackLogs.d.ts.map +1 -0
- package/convex/debugFilestackLogs.js.map +1 -0
- package/convex/debugFilestackLogs.ts +16 -0
- package/convex/debugGetToken.d.ts.map +1 -0
- package/convex/debugGetToken.js.map +1 -0
- package/convex/debugGetToken.ts +18 -0
- package/convex/directCall.ts +49 -14
- package/convex/email.ts +5 -5
- package/convex/http.d.ts.map +1 -1
- package/convex/http.js.map +1 -1
- package/convex/http.ts +611 -49
- package/convex/logs.ts +26 -22
- package/convex/migrateFilestack.d.ts.map +1 -1
- package/convex/migrateFilestack.js.map +1 -1
- package/convex/migrateFilestack.ts +65 -101
- package/convex/migratePartnersProd.d.ts.map +1 -0
- package/convex/migratePartnersProd.js.map +1 -0
- package/convex/migratePartnersProd.ts +174 -0
- package/convex/migrateProviderWorkspaces.d.ts.map +1 -0
- package/convex/migrateProviderWorkspaces.js.map +1 -0
- package/convex/migrateProviderWorkspaces.ts +175 -0
- package/convex/providers.js.map +1 -1
- package/convex/providers.ts +313 -203
- package/convex/schema.ts +50 -1
- package/convex/searchLogs.d.ts.map +1 -1
- package/convex/searchLogs.js.map +1 -1
- package/convex/searchLogs.ts +11 -3
- package/convex/seedPratham.ts +1 -1
- package/convex/spendAlerts.ts +2 -2
- package/convex/stripeActions.ts +1 -1
- package/convex/updateAPIStatus.ts +2 -3
- package/convex/workspaceSettings.ts +136 -0
- package/dist/bin-http.js +0 -0
- package/dist/bin.js +0 -0
- package/dist/cli/commands/demo.js +2 -2
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/login.js +1 -1
- package/dist/cli/commands/setup.js +2 -2
- package/dist/discovery.js +1 -1
- package/dist/execute.js +3 -3
- package/dist/index.js +13 -13
- package/dist/registry/apis.json +1 -1
- package/dist/ui/errors.js +16 -16
- package/dist/ui/prompts.js +1 -1
- package/email-templates/filestack-provider-outreach.html +1 -1
- package/email-templates/partnership-template.html +1 -1
- package/email-templates/pratham-partnership-draft.html +2 -2
- package/package.json +2 -2
- package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
- package/reports/session-report-2026-04-05.html +433 -0
- package/src/cli/commands/demo.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/login.ts +1 -1
- package/src/cli/commands/setup.ts +2 -2
- package/src/discovery.ts +1 -1
- package/src/execute.ts +3 -3
- package/src/index.ts +14 -14
- package/src/registry/apis.json +1 -1
- package/src/ui/errors.ts +16 -16
- package/src/ui/prompts.ts +1 -1
- package/convex/adminActivate.d.ts +0 -3
- package/convex/adminActivate.js +0 -47
- package/convex/adminStats.d.ts +0 -9
- package/convex/adminStats.js +0 -280
- package/convex/agents.d.ts +0 -84
- package/convex/agents.js +0 -809
- package/convex/analytics.d.ts +0 -5
- package/convex/analytics.js +0 -166
- package/convex/billing.d.ts +0 -88
- package/convex/billing.js +0 -655
- package/convex/capabilities.d.ts +0 -9
- package/convex/capabilities.js +0 -145
- package/convex/chains.d.ts +0 -68
- package/convex/chains.js +0 -1105
- package/convex/credits.d.ts +0 -25
- package/convex/credits.js +0 -186
- package/convex/crons.d.ts +0 -3
- package/convex/crons.js +0 -17
- package/convex/directCall.d.ts +0 -72
- package/convex/directCall.js +0 -627
- package/convex/earnProgress.d.ts +0 -58
- package/convex/earnProgress.js +0 -649
- package/convex/email.d.ts +0 -14
- package/convex/email.js +0 -300
- package/convex/feedback.d.ts +0 -7
- package/convex/feedback.js +0 -227
- package/convex/http.d.ts +0 -3
- package/convex/http.js +0 -1405
- package/convex/inbound.d.ts +0 -2
- package/convex/inbound.js +0 -32
- package/convex/logs.d.ts +0 -48
- package/convex/logs.js +0 -621
- package/convex/migrateFilestack.d.ts +0 -2
- package/convex/migrateFilestack.js +0 -113
- package/convex/migratePratham.d.ts +0 -2
- package/convex/migratePratham.js +0 -121
- package/convex/mou.d.ts +0 -6
- package/convex/mou.js +0 -82
- package/convex/providerKeys.d.ts +0 -31
- package/convex/providerKeys.js +0 -257
- package/convex/providers.d.ts +0 -35
- package/convex/providers.js +0 -922
- package/convex/purchases.d.ts +0 -7
- package/convex/purchases.js +0 -157
- package/convex/ratelimit.d.ts +0 -4
- package/convex/ratelimit.js +0 -91
- package/convex/searchLogs.d.ts +0 -13
- package/convex/searchLogs.js +0 -232
- package/convex/seedAPILayerAPIs.d.ts +0 -7
- package/convex/seedAPILayerAPIs.js +0 -177
- package/convex/seedDirectCallConfigs.d.ts +0 -2
- package/convex/seedDirectCallConfigs.js +0 -324
- package/convex/seedPratham.d.ts +0 -6
- package/convex/seedPratham.js +0 -150
- package/convex/spendAlerts.d.ts +0 -36
- package/convex/spendAlerts.js +0 -380
- package/convex/stripeActions.d.ts +0 -19
- package/convex/stripeActions.js +0 -411
- package/convex/teams.d.ts +0 -21
- package/convex/teams.js +0 -215
- package/convex/telemetry.d.ts +0 -4
- package/convex/telemetry.js +0 -74
- package/convex/updateAPIStatus.d.ts +0 -6
- package/convex/updateAPIStatus.js +0 -40
- package/convex/usage.d.ts +0 -27
- package/convex/usage.js +0 -229
- package/convex/waitlist.d.ts +0 -4
- package/convex/waitlist.js +0 -49
- package/convex/webhooks.d.ts +0 -12
- package/convex/webhooks.js +0 -410
- package/convex/workspaces.d.ts +0 -33
- 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
|
-
|
|
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
|
-
|
|
50
|
-
name: "
|
|
51
|
-
description: "
|
|
52
|
-
category: "
|
|
53
|
-
pricing: "
|
|
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: ["
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
140
|
-
|
|
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
|
|
149
|
-
if (
|
|
474
|
+
|
|
475
|
+
// If we have a workspace, log and increment usage
|
|
476
|
+
if (resolvedWorkspaceId) {
|
|
150
477
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|