@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.
- package/README.md +4 -4
- package/apiclaw-README.md +12 -12
- package/convex/_generated/api.d.ts +4 -0
- package/convex/apiKeys.ts +220 -0
- package/convex/directCall.ts +49 -14
- package/convex/email.ts +5 -5
- package/convex/http.ts +598 -40
- package/convex/logs.ts +26 -22
- package/convex/migrateProviderWorkspaces.ts +154 -35
- package/convex/providers.ts +313 -203
- package/convex/schema.ts +50 -1
- package/convex/searchLogs.ts +2 -6
- 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/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/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/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 -167
- package/convex/backfillAnalytics.d.ts +0 -2
- package/convex/backfillAnalytics.js +0 -20
- package/convex/backfillSearchLogs.d.ts +0 -2
- package/convex/backfillSearchLogs.js +0 -29
- 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/debugFilestackLogs.d.ts +0 -2
- package/convex/debugFilestackLogs.js +0 -17
- package/convex/debugGetToken.d.ts +0 -2
- package/convex/debugGetToken.js +0 -18
- 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 -1408
- 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 -74
- package/convex/migratePartnersProd.d.ts +0 -8
- package/convex/migratePartnersProd.js +0 -165
- package/convex/migratePratham.d.ts +0 -2
- package/convex/migratePratham.js +0 -121
- package/convex/migrateProviderWorkspaces.d.ts +0 -2
- package/convex/migrateProviderWorkspaces.js +0 -55
- 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 -246
- 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,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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|