ai-agent-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.claude/commands/openspec/apply.md +23 -0
  2. package/.claude/commands/openspec/archive.md +27 -0
  3. package/.claude/commands/openspec/proposal.md +28 -0
  4. package/.claude/settings.local.json +12 -0
  5. package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
  6. package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
  7. package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
  8. package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
  9. package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
  10. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  11. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  12. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  13. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  14. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  15. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  16. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  17. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  18. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  19. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  20. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  21. package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
  22. package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
  23. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  24. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
  25. package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
  26. package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
  27. package/.cursor/commands/openspec-apply.md +23 -0
  28. package/.cursor/commands/openspec-archive.md +27 -0
  29. package/.cursor/commands/openspec-proposal.md +28 -0
  30. package/.cursor/commands/ui-ux-pro-max.md +226 -0
  31. package/.eslintrc.json +3 -0
  32. package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
  33. package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
  34. package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
  35. package/.shared/ui-ux-pro-max/data/products.csv +97 -0
  36. package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
  37. package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  38. package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  39. package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  40. package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  41. package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  42. package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  43. package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
  44. package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  45. package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  46. package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  47. package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
  48. package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
  49. package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  50. package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
  51. package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
  52. package/AGENTS.md +18 -0
  53. package/CLAUDE.md +18 -0
  54. package/IMPLEMENTATION.md +157 -0
  55. package/LICENSE +21 -0
  56. package/README.md +165 -0
  57. package/dist/.next/types/app/api/config/route.js +52 -0
  58. package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
  59. package/dist/.next/types/app/api/gateway/route.js +52 -0
  60. package/dist/.next/types/app/api/logs/route.js +52 -0
  61. package/dist/.next/types/app/api/models/route.js +52 -0
  62. package/dist/.next/types/app/api/providers/route.js +52 -0
  63. package/dist/.next/types/app/api/providers/test/route.js +52 -0
  64. package/dist/.next/types/app/api/service/start/route.js +52 -0
  65. package/dist/.next/types/app/api/service/status/route.js +52 -0
  66. package/dist/.next/types/app/api/service/stop/route.js +52 -0
  67. package/dist/.next/types/app/layout.js +22 -0
  68. package/dist/.next/types/app/logs/page.js +22 -0
  69. package/dist/.next/types/app/models/page.js +22 -0
  70. package/dist/.next/types/app/page.js +22 -0
  71. package/dist/.next/types/app/providers/page.js +22 -0
  72. package/dist/src/app/api/config/route.js +43 -0
  73. package/dist/src/app/api/gateway/[...path]/route.js +83 -0
  74. package/dist/src/app/api/gateway/route.js +63 -0
  75. package/dist/src/app/api/logs/route.js +34 -0
  76. package/dist/src/app/api/models/route.js +152 -0
  77. package/dist/src/app/api/providers/route.js +118 -0
  78. package/dist/src/app/api/providers/test/route.js +154 -0
  79. package/dist/src/app/api/service/start/route.js +55 -0
  80. package/dist/src/app/api/service/status/route.js +17 -0
  81. package/dist/src/app/api/service/stop/route.js +20 -0
  82. package/dist/src/app/components/ConfirmDialog.jsx +31 -0
  83. package/dist/src/app/components/Nav.jsx +45 -0
  84. package/dist/src/app/components/Toast.jsx +37 -0
  85. package/dist/src/app/components/ToastProvider.jsx +21 -0
  86. package/dist/src/app/layout.jsx +13 -0
  87. package/dist/src/app/logs/page.jsx +210 -0
  88. package/dist/src/app/models/page.jsx +291 -0
  89. package/dist/src/app/page.jsx +236 -0
  90. package/dist/src/app/providers/page.jsx +402 -0
  91. package/dist/src/cli/index.js +90 -0
  92. package/dist/src/db/database.js +69 -0
  93. package/dist/src/db/queries.js +261 -0
  94. package/dist/src/db/schema.js +67 -0
  95. package/dist/src/server/crypto.js +22 -0
  96. package/dist/src/server/gateway-server.js +200 -0
  97. package/dist/src/server/gateway.js +76 -0
  98. package/dist/src/server/logger.js +72 -0
  99. package/dist/src/server/providers/anthropic.js +52 -0
  100. package/dist/src/server/providers/gemini.js +64 -0
  101. package/dist/src/server/providers/index.js +16 -0
  102. package/dist/src/server/providers/openai.js +86 -0
  103. package/dist/src/server/providers/types.js +1 -0
  104. package/dist/src/server/service-manager.js +286 -0
  105. package/docs/TODO.md +19 -0
  106. package/next.config.js +7 -0
  107. package/openspec/AGENTS.md +456 -0
  108. package/openspec/changes/add-logging/proposal.md +18 -0
  109. package/openspec/changes/add-logging/specs/core/spec.md +21 -0
  110. package/openspec/changes/add-logging/tasks.md +16 -0
  111. package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
  112. package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
  113. package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
  114. package/openspec/changes/improve-gateway-startup/design.md +137 -0
  115. package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
  116. package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
  117. package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
  118. package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
  119. package/openspec/changes/init-api-gateway/design.md +185 -0
  120. package/openspec/changes/init-api-gateway/proposal.md +30 -0
  121. package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
  122. package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
  123. package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
  124. package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
  125. package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
  126. package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
  127. package/openspec/changes/init-api-gateway/tasks.md +84 -0
  128. package/openspec/project.md +58 -0
  129. package/package.json +51 -0
  130. package/postcss.config.js +6 -0
  131. package/src/app/api/config/route.ts +62 -0
  132. package/src/app/api/gateway/[...path]/route.ts +118 -0
  133. package/src/app/api/gateway/route.ts +77 -0
  134. package/src/app/api/logs/route.ts +48 -0
  135. package/src/app/api/models/route.ts +210 -0
  136. package/src/app/api/providers/route.ts +162 -0
  137. package/src/app/api/providers/test/route.ts +182 -0
  138. package/src/app/api/service/start/route.ts +73 -0
  139. package/src/app/api/service/status/route.ts +22 -0
  140. package/src/app/api/service/stop/route.ts +27 -0
  141. package/src/app/components/ConfirmDialog.tsx +63 -0
  142. package/src/app/components/Nav.tsx +66 -0
  143. package/src/app/components/Toast.tsx +61 -0
  144. package/src/app/components/ToastProvider.tsx +43 -0
  145. package/src/app/globals.css +71 -0
  146. package/src/app/layout.tsx +22 -0
  147. package/src/app/logs/page.tsx +261 -0
  148. package/src/app/models/page.tsx +500 -0
  149. package/src/app/page.tsx +742 -0
  150. package/src/app/providers/page.tsx +558 -0
  151. package/src/cli/index.ts +95 -0
  152. package/src/db/database.ts +125 -0
  153. package/src/db/queries.ts +339 -0
  154. package/src/db/schema.ts +117 -0
  155. package/src/server/crypto.ts +48 -0
  156. package/src/server/gateway-server.ts +306 -0
  157. package/src/server/gateway.ts +163 -0
  158. package/src/server/logger.ts +96 -0
  159. package/src/server/providers/anthropic.ts +121 -0
  160. package/src/server/providers/gemini.ts +112 -0
  161. package/src/server/providers/index.ts +20 -0
  162. package/src/server/providers/openai.ts +235 -0
  163. package/src/server/providers/types.ts +20 -0
  164. package/src/server/service-manager.ts +321 -0
  165. package/tailwind.config.js +16 -0
  166. package/tsconfig.json +29 -0
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Standalone Gateway Server
3
+ * This server only provides API gateway functionality for external clients
4
+ * It does NOT include the Web UI
5
+ */
6
+
7
+ import http from 'http';
8
+ import { getDatabase, closeDatabase } from '../db/database';
9
+ import { handleGatewayRequest } from './gateway';
10
+ import { getConfig } from '../db/queries';
11
+ import type { GatewayRequest, GatewayResponse } from './providers/types';
12
+
13
+ interface GatewayServerOptions {
14
+ port: number;
15
+ hostname?: string;
16
+ apiKey?: string;
17
+ }
18
+
19
+ export class GatewayServer {
20
+ private server: http.Server | null = null;
21
+ private port: number;
22
+ private hostname: string;
23
+ private apiKey: string | null;
24
+
25
+ constructor(options: GatewayServerOptions) {
26
+ this.port = options.port;
27
+ this.hostname = options.hostname || 'localhost';
28
+ this.apiKey = options.apiKey || null;
29
+ }
30
+
31
+ /**
32
+ * Validate API key if configured
33
+ */
34
+ private validateApiKey(authHeader: string | null): boolean {
35
+ if (!this.apiKey) {
36
+ return true; // No API key configured, allow all requests
37
+ }
38
+
39
+ if (!authHeader) {
40
+ return false;
41
+ }
42
+
43
+ // Support both "Bearer <key>" and direct key
44
+ const key = authHeader.startsWith('Bearer ')
45
+ ? authHeader.substring(7)
46
+ : authHeader;
47
+
48
+ return key === this.apiKey;
49
+ }
50
+
51
+ /**
52
+ * Parse request body
53
+ */
54
+ private async parseBody(req: http.IncomingMessage): Promise<any> {
55
+ return new Promise((resolve, reject) => {
56
+ let body = '';
57
+ req.on('data', (chunk) => {
58
+ body += chunk.toString();
59
+ });
60
+ req.on('end', () => {
61
+ if (!body) {
62
+ resolve(null);
63
+ return;
64
+ }
65
+ try {
66
+ resolve(JSON.parse(body));
67
+ } catch {
68
+ resolve(body);
69
+ }
70
+ });
71
+ req.on('error', reject);
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Handle gateway request
77
+ */
78
+ private async handleRequest(
79
+ req: http.IncomingMessage,
80
+ res: http.ServerResponse
81
+ ): Promise<void> {
82
+ const startTime = Date.now();
83
+ const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
84
+
85
+ console.log(`[Gateway Server ${requestId}] Incoming request:`, {
86
+ method: req.method,
87
+ url: req.url,
88
+ headers: {
89
+ 'content-type': req.headers['content-type'],
90
+ 'authorization': req.headers['authorization'] ? '***' : undefined,
91
+ },
92
+ });
93
+
94
+ // CORS headers
95
+ res.setHeader('Access-Control-Allow-Origin', '*');
96
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
97
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
98
+
99
+ // Handle preflight
100
+ if (req.method === 'OPTIONS') {
101
+ console.log(`[Gateway Server ${requestId}] Handling OPTIONS preflight`);
102
+ res.writeHead(200);
103
+ res.end();
104
+ return;
105
+ }
106
+
107
+ try {
108
+ // Validate API key if configured
109
+ const authHeader = req.headers.authorization || req.headers['x-api-key'] as string || null;
110
+ if (!this.validateApiKey(authHeader)) {
111
+ console.log(`[Gateway Server ${requestId}] API key validation failed`);
112
+ res.writeHead(401, { 'Content-Type': 'application/json' });
113
+ res.end(JSON.stringify({ error: { message: 'Invalid or missing API key' } }));
114
+ return;
115
+ }
116
+
117
+ // Parse URL
118
+ const url = new URL(req.url || '/', `http://${this.hostname}:${this.port}`);
119
+ const pathname = url.pathname;
120
+ console.log(`[Gateway Server ${requestId}] Parsed URL:`, {
121
+ pathname,
122
+ searchParams: Object.fromEntries(url.searchParams.entries()),
123
+ });
124
+
125
+ // Parse body first to get model ID
126
+ let body: any = null;
127
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
128
+ body = await this.parseBody(req);
129
+ console.log(`[Gateway Server ${requestId}] Parsed body:`, body ? JSON.stringify(body).substring(0, 200) : 'null');
130
+ }
131
+
132
+ // Get model ID from query, body, or URL path
133
+ // Priority: query param > body > path segment
134
+ let modelId = url.searchParams.get('model') ||
135
+ url.searchParams.get('model_id') ||
136
+ null;
137
+
138
+ // Get provider name (optional, for disambiguating models with same model_id)
139
+ const providerName = url.searchParams.get('provider') || null;
140
+
141
+ console.log(`[Gateway Server ${requestId}] Model ID from query:`, modelId);
142
+ console.log(`[Gateway Server ${requestId}] Provider from query:`, providerName);
143
+
144
+ // Try to get from body if not in query
145
+ if (!modelId && body) {
146
+ modelId = body.model || body.model_id || null;
147
+ console.log(`[Gateway Server ${requestId}] Model ID from body:`, modelId);
148
+ }
149
+ // Also get provider from body if not in query
150
+ const bodyProvider = body?.provider || null;
151
+ if (bodyProvider) {
152
+ console.log(`[Gateway Server ${requestId}] Provider from body:`, bodyProvider);
153
+ }
154
+
155
+ // Try to get from URL path (e.g., /v1/models/{model_id}/...)
156
+ if (!modelId && pathname) {
157
+ const pathMatch = pathname.match(/\/(?:v1|api\/gateway)\/models\/([^\/]+)/);
158
+ if (pathMatch) {
159
+ modelId = pathMatch[1];
160
+ console.log(`[Gateway Server ${requestId}] Model ID from path:`, modelId);
161
+ }
162
+ }
163
+
164
+ if (!modelId) {
165
+ console.log(`[Gateway Server ${requestId}] Error: Model ID not found`);
166
+ res.writeHead(400, { 'Content-Type': 'application/json' });
167
+ res.end(JSON.stringify({
168
+ error: {
169
+ message: 'Model ID not specified. Please provide model ID in query parameter (?model=xxx), request body, or URL path.'
170
+ }
171
+ }));
172
+ return;
173
+ }
174
+
175
+ console.log(`[Gateway Server ${requestId}] Processing request for model:`, modelId, 'provider:', providerName || bodyProvider || 'not specified');
176
+
177
+ // Build gateway request
178
+ const gatewayRequest: GatewayRequest = {
179
+ method: req.method || 'GET',
180
+ path: pathname,
181
+ headers: req.headers as Record<string, string>,
182
+ query: Object.fromEntries(url.searchParams.entries()),
183
+ body: body,
184
+ };
185
+
186
+ // Handle the request (pass provider name if specified)
187
+ const finalProviderName = providerName || bodyProvider;
188
+ console.log(`[Gateway Server ${requestId}] Calling handleGatewayRequest with model:`, modelId, 'provider:', finalProviderName || 'undefined');
189
+
190
+ const response = await handleGatewayRequest(modelId, gatewayRequest, finalProviderName || undefined);
191
+
192
+ console.log(`[Gateway Server ${requestId}] Response received:`, {
193
+ status: response.status,
194
+ bodyType: typeof response.body,
195
+ bodyPreview: typeof response.body === 'string'
196
+ ? response.body.substring(0, 200)
197
+ : JSON.stringify(response.body).substring(0, 200),
198
+ });
199
+
200
+ // Send response
201
+ // If body is already a string, send it directly; otherwise stringify it
202
+ let responseBody: string;
203
+ if (response.body === null || response.body === undefined) {
204
+ responseBody = '';
205
+ } else if (typeof response.body === 'string') {
206
+ responseBody = response.body;
207
+ } else {
208
+ responseBody = JSON.stringify(response.body);
209
+ }
210
+
211
+ const duration = Date.now() - startTime;
212
+ console.log(`[Gateway Server ${requestId}] Sending response (${duration}ms):`, {
213
+ status: response.status,
214
+ bodyLength: responseBody.length,
215
+ });
216
+
217
+ res.writeHead(response.status, response.headers);
218
+ res.end(responseBody);
219
+ } catch (error: any) {
220
+ const duration = Date.now() - startTime;
221
+ console.error(`[Gateway Server ${requestId}] Error (${duration}ms):`, {
222
+ message: error.message,
223
+ stack: error.stack,
224
+ });
225
+
226
+ if (!res.headersSent) {
227
+ res.writeHead(500, { 'Content-Type': 'application/json' });
228
+ res.end(JSON.stringify({
229
+ error: {
230
+ message: error.message || 'Internal server error',
231
+ type: 'gateway_error',
232
+ },
233
+ }));
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Start the gateway server
240
+ */
241
+ async start(): Promise<void> {
242
+ return new Promise((resolve, reject) => {
243
+ // Initialize database
244
+ try {
245
+ getDatabase();
246
+ } catch (error: any) {
247
+ reject(new Error(`Failed to initialize database: ${error.message}`));
248
+ return;
249
+ }
250
+
251
+ this.server = http.createServer((req, res) => {
252
+ console.log(`[Gateway Server] New connection from ${req.socket.remoteAddress}:${req.socket.remotePort}`);
253
+ this.handleRequest(req, res).catch((error) => {
254
+ console.error('[Gateway Server] Unhandled request error:', error);
255
+ if (!res.headersSent) {
256
+ res.writeHead(500, { 'Content-Type': 'application/json' });
257
+ res.end(JSON.stringify({ error: { message: 'Internal server error' } }));
258
+ }
259
+ });
260
+ });
261
+
262
+ this.server.listen(this.port, this.hostname, () => {
263
+ console.log(`✓ Gateway server ready on http://${this.hostname}:${this.port}`);
264
+ console.log(` API Gateway: http://${this.hostname}:${this.port}/api/gateway`);
265
+ if (this.apiKey) {
266
+ console.log(` API Key authentication: Enabled`);
267
+ }
268
+ resolve();
269
+ });
270
+
271
+ this.server.on('error', (error: any) => {
272
+ if (error.code === 'EADDRINUSE') {
273
+ reject(new Error(`Port ${this.port} is already in use`));
274
+ } else {
275
+ reject(error);
276
+ }
277
+ });
278
+
279
+ // Graceful shutdown
280
+ process.on('SIGTERM', () => {
281
+ this.stop();
282
+ });
283
+
284
+ process.on('SIGINT', () => {
285
+ this.stop();
286
+ });
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Stop the gateway server
292
+ */
293
+ async stop(): Promise<void> {
294
+ return new Promise((resolve) => {
295
+ if (this.server) {
296
+ this.server.close(() => {
297
+ console.log('Gateway server closed');
298
+ closeDatabase();
299
+ resolve();
300
+ });
301
+ } else {
302
+ resolve();
303
+ }
304
+ });
305
+ }
306
+ }
@@ -0,0 +1,163 @@
1
+ import { getAllProviders } from '@/db/queries';
2
+ import { getProviderAdapter } from './providers';
3
+ import { logRequest } from './logger';
4
+ import type { GatewayRequest, GatewayResponse } from './providers/types';
5
+
6
+ export async function handleGatewayRequest(
7
+ modelId: string,
8
+ request: GatewayRequest,
9
+ providerName?: string
10
+ ): Promise<GatewayResponse> {
11
+ const startTime = Date.now();
12
+ const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
13
+
14
+ console.log(`[Gateway ${requestId}] handleGatewayRequest called:`, {
15
+ modelId,
16
+ providerName: providerName || 'not specified',
17
+ method: request.method,
18
+ path: request.path,
19
+ });
20
+
21
+ try {
22
+ // Provider name is required for gateway requests
23
+ if (!providerName) {
24
+ console.log(`[Gateway ${requestId}] Provider name not specified`);
25
+ return {
26
+ status: 400,
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: {
29
+ error: {
30
+ message: 'Provider name is required. Please specify provider parameter.',
31
+ type: 'missing_provider',
32
+ }
33
+ },
34
+ };
35
+ }
36
+
37
+ // Find the provider by name
38
+ const providers = getAllProviders();
39
+ const provider = providers.find(p => p.name.toLowerCase() === providerName.toLowerCase());
40
+
41
+ if (!provider) {
42
+ console.log(`[Gateway ${requestId}] Provider not found:`, providerName);
43
+ const availableProviders = providers.map(p => p.name).join(', ');
44
+ return {
45
+ status: 404,
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: {
48
+ error: {
49
+ message: `Provider "${providerName}" not found. Available providers: ${availableProviders || 'none'}`,
50
+ type: 'provider_not_found',
51
+ }
52
+ },
53
+ };
54
+ }
55
+
56
+ console.log(`[Gateway ${requestId}] Provider found:`, {
57
+ id: provider.id,
58
+ name: provider.name,
59
+ protocol: provider.protocol,
60
+ base_url: provider.base_url,
61
+ hasApiKey: !!provider.api_key,
62
+ });
63
+
64
+ // Create a model object with the user-provided model_id and the provider configuration
65
+ // The model_id comes from the user request, not from the database
66
+ const model = {
67
+ id: 0, // Not used for gateway requests
68
+ provider_id: provider.id,
69
+ name: modelId, // Use model_id as name for logging
70
+ model_id: modelId, // User-provided model ID
71
+ enabled: true, // Always enabled for gateway requests
72
+ created_at: new Date().toISOString(),
73
+ updated_at: new Date().toISOString(),
74
+ provider: {
75
+ id: provider.id,
76
+ name: provider.name,
77
+ protocol: provider.protocol,
78
+ base_url: provider.base_url,
79
+ api_key: provider.api_key,
80
+ },
81
+ };
82
+
83
+ // Get provider adapter
84
+ const adapter = getProviderAdapter(provider.protocol);
85
+
86
+ // Forward the request using provider's config and user's model_id
87
+ console.log(`[Gateway ${requestId}] Forwarding request with:`, {
88
+ provider: provider.name,
89
+ protocol: provider.protocol,
90
+ modelId: modelId,
91
+ });
92
+
93
+ const response = await adapter.forwardRequest(model as any, request);
94
+
95
+ // Log the request (use NULL model_id for gateway requests)
96
+ const responseTimeMs = Date.now() - startTime;
97
+ try {
98
+ await logRequest(
99
+ {
100
+ modelId: null as any, // NULL for gateway requests (no model in database)
101
+ method: request.method,
102
+ path: request.path,
103
+ headers: request.headers,
104
+ query: request.query,
105
+ body: request.body,
106
+ },
107
+ {
108
+ status: response.status,
109
+ headers: response.headers,
110
+ body: response.body,
111
+ responseTimeMs,
112
+ }
113
+ );
114
+ console.log(`[Gateway ${requestId}] Request logged successfully`);
115
+ } catch (logError: any) {
116
+ // Log error but don't fail the request
117
+ console.error(`[Gateway ${requestId}] Failed to log request:`, logError.message);
118
+ }
119
+
120
+ return response;
121
+ } catch (error: any) {
122
+ const responseTimeMs = Date.now() - startTime;
123
+ console.error(`[Gateway ${requestId}] Error:`, {
124
+ message: error.message,
125
+ stack: error.stack,
126
+ });
127
+
128
+ const errorResponse: GatewayResponse = {
129
+ status: 500,
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: {
132
+ error: {
133
+ message: error.message || 'Internal server error',
134
+ type: 'gateway_error',
135
+ },
136
+ },
137
+ };
138
+
139
+ // Try to log error request (use NULL model_id)
140
+ try {
141
+ await logRequest(
142
+ {
143
+ modelId: null as any, // NULL for gateway requests
144
+ method: request.method,
145
+ path: request.path,
146
+ headers: request.headers,
147
+ query: request.query,
148
+ body: request.body,
149
+ },
150
+ {
151
+ status: errorResponse.status,
152
+ headers: errorResponse.headers,
153
+ body: errorResponse.body,
154
+ responseTimeMs,
155
+ }
156
+ );
157
+ } catch (logError: any) {
158
+ console.error(`[Gateway ${requestId}] Failed to log error request:`, logError.message);
159
+ }
160
+
161
+ return errorResponse;
162
+ }
163
+ }
@@ -0,0 +1,96 @@
1
+ import { createRequestLog } from '@/db/queries';
2
+ import { maskApiKey, maskToken } from './crypto';
3
+
4
+ export interface LogRequest {
5
+ modelId: number | null; // Allow null for gateway requests
6
+ method: string;
7
+ path: string;
8
+ headers: Record<string, string>;
9
+ query: Record<string, string>;
10
+ body: any;
11
+ }
12
+
13
+ export interface LogResponse {
14
+ status: number;
15
+ headers: Record<string, string>;
16
+ body: any;
17
+ responseTimeMs: number;
18
+ }
19
+
20
+ export async function logRequest(
21
+ request: LogRequest,
22
+ response: LogResponse
23
+ ): Promise<void> {
24
+ try {
25
+ // Mask sensitive information
26
+ const maskedHeaders = maskSensitiveHeaders(request.headers);
27
+ const maskedBody = maskSensitiveData(request.body);
28
+
29
+ await createRequestLog({
30
+ model_id: request.modelId,
31
+ request_method: request.method,
32
+ request_path: request.path,
33
+ request_headers: JSON.stringify(maskedHeaders),
34
+ request_query: JSON.stringify(request.query),
35
+ request_body: JSON.stringify(maskedBody),
36
+ response_status: response.status,
37
+ response_body: JSON.stringify(response.body),
38
+ response_time_ms: response.responseTimeMs,
39
+ });
40
+ } catch (error) {
41
+ console.error('Failed to log request:', error);
42
+ // Don't throw - logging failure shouldn't break the request
43
+ }
44
+ }
45
+
46
+ function maskSensitiveHeaders(headers: Record<string, string>): Record<string, string> {
47
+ const masked = { ...headers };
48
+
49
+ // Mask API keys
50
+ if (masked['authorization']) {
51
+ const auth = masked['authorization'];
52
+ if (auth.startsWith('Bearer ')) {
53
+ masked['authorization'] = `Bearer ${maskToken(auth.substring(7))}`;
54
+ } else {
55
+ masked['authorization'] = maskToken(auth);
56
+ }
57
+ }
58
+
59
+ if (masked['x-api-key']) {
60
+ masked['x-api-key'] = maskApiKey(masked['x-api-key']);
61
+ }
62
+
63
+ if (masked['api-key']) {
64
+ masked['api-key'] = maskApiKey(masked['api-key']);
65
+ }
66
+
67
+ return masked;
68
+ }
69
+
70
+ function maskSensitiveData(data: any): any {
71
+ if (!data || typeof data !== 'object') {
72
+ return data;
73
+ }
74
+
75
+ if (Array.isArray(data)) {
76
+ return data.map(maskSensitiveData);
77
+ }
78
+
79
+ const masked: any = {};
80
+ for (const [key, value] of Object.entries(data)) {
81
+ const lowerKey = key.toLowerCase();
82
+ if (lowerKey.includes('api') && lowerKey.includes('key')) {
83
+ masked[key] = maskApiKey(String(value));
84
+ } else if (lowerKey.includes('token')) {
85
+ masked[key] = maskToken(String(value));
86
+ } else if (lowerKey === 'authorization') {
87
+ masked[key] = maskToken(String(value));
88
+ } else if (typeof value === 'object' && value !== null) {
89
+ masked[key] = maskSensitiveData(value);
90
+ } else {
91
+ masked[key] = value;
92
+ }
93
+ }
94
+
95
+ return masked;
96
+ }
@@ -0,0 +1,121 @@
1
+ import type { ProviderAdapter, GatewayRequest, GatewayResponse } from './types';
2
+ import type { Model, Provider } from '@/db/schema';
3
+ import { decryptApiKey } from '@/server/crypto';
4
+
5
+ export class AnthropicAdapter implements ProviderAdapter {
6
+ async forwardRequest(
7
+ model: Model & { provider: Provider },
8
+ request: GatewayRequest
9
+ ): Promise<GatewayResponse> {
10
+ console.log('[Anthropic Adapter] Starting forwardRequest:', {
11
+ modelId: model.model_id,
12
+ modelName: model.name,
13
+ providerName: model.provider.name,
14
+ providerBaseUrl: model.provider.base_url,
15
+ hasEncryptedApiKey: !!model.provider.api_key,
16
+ encryptedApiKeyLength: model.provider.api_key?.length || 0,
17
+ });
18
+
19
+ let apiKey: string;
20
+ try {
21
+ apiKey = decryptApiKey(model.provider.api_key);
22
+ console.log('[Anthropic Adapter] API key decrypted successfully, length:', apiKey.length);
23
+ } catch (error: any) {
24
+ console.error('[Anthropic Adapter] Failed to decrypt API key:', error.message);
25
+ return {
26
+ status: 500,
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: { error: { message: 'Failed to decrypt API key', type: 'decryption_error' } },
29
+ };
30
+ }
31
+
32
+ let baseUrl = model.provider.base_url || 'https://api.anthropic.com/v1';
33
+
34
+ // Normalize baseUrl
35
+ baseUrl = baseUrl.trim().replace(/\/+$/, ''); // Remove trailing slashes
36
+
37
+ // Build the target URL
38
+ let targetPath = request.path;
39
+
40
+ // If path is root or empty, default to messages endpoint
41
+ if (!targetPath || targetPath === '/' || targetPath === '') {
42
+ targetPath = 'messages';
43
+ } else if (targetPath.startsWith('/v1/')) {
44
+ targetPath = targetPath.substring(4);
45
+ } else if (targetPath.startsWith('/')) {
46
+ targetPath = targetPath.substring(1);
47
+ }
48
+
49
+ // Ensure baseUrl ends with /v1
50
+ if (!baseUrl.endsWith('/v1')) {
51
+ baseUrl = baseUrl + '/v1';
52
+ }
53
+
54
+ const url = `${baseUrl}/${targetPath}`;
55
+
56
+ console.log('[Anthropic Adapter] Forwarding request:', {
57
+ baseUrl: model.provider.base_url,
58
+ normalizedBaseUrl: baseUrl,
59
+ originalPath: request.path,
60
+ targetPath,
61
+ url,
62
+ method: request.method,
63
+ hasApiKey: !!apiKey,
64
+ apiKeyPrefix: apiKey ? apiKey.substring(0, 10) + '...' : 'none',
65
+ });
66
+
67
+ // Prepare headers
68
+ const headers: Record<string, string> = {
69
+ 'x-api-key': apiKey,
70
+ 'anthropic-version': '2023-06-01',
71
+ 'Content-Type': 'application/json',
72
+ ...request.headers,
73
+ };
74
+ delete headers['host'];
75
+ delete headers['connection'];
76
+ delete headers['authorization'];
77
+
78
+ console.log('[Anthropic Adapter] Request headers:', {
79
+ 'Content-Type': headers['Content-Type'],
80
+ 'x-api-key': headers['x-api-key'] ? '***' : 'none',
81
+ 'anthropic-version': headers['anthropic-version'],
82
+ });
83
+
84
+ // Make the request
85
+ const response = await fetch(url, {
86
+ method: request.method,
87
+ headers,
88
+ body: request.method !== 'GET' && request.method !== 'HEAD' ? JSON.stringify(request.body) : undefined,
89
+ });
90
+
91
+ console.log('[Anthropic Adapter] Response status:', response.status, response.statusText);
92
+
93
+ const responseBody = await response.text();
94
+ console.log('[Anthropic Adapter] Response body preview:', responseBody.substring(0, 200));
95
+
96
+ let parsedBody: any;
97
+ try {
98
+ parsedBody = JSON.parse(responseBody);
99
+ } catch {
100
+ parsedBody = responseBody;
101
+ console.log('[Anthropic Adapter] Response body is not JSON, returning as string');
102
+ }
103
+
104
+ return {
105
+ status: response.status,
106
+ headers: Object.fromEntries(response.headers.entries()),
107
+ body: parsedBody,
108
+ };
109
+ }
110
+
111
+ async listModels(provider: Provider): Promise<Array<{ id: string; name: string }>> {
112
+ // Anthropic doesn't have a public models endpoint
113
+ // Return common models
114
+ return [
115
+ { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' },
116
+ { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus' },
117
+ { id: 'claude-3-sonnet-20240229', name: 'Claude 3 Sonnet' },
118
+ { id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku' },
119
+ ];
120
+ }
121
+ }