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,112 @@
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 GeminiAdapter implements ProviderAdapter {
6
+ async forwardRequest(
7
+ model: Model & { provider: Provider },
8
+ request: GatewayRequest
9
+ ): Promise<GatewayResponse> {
10
+ const apiKey = decryptApiKey(model.provider.api_key);
11
+ let baseUrl = model.provider.base_url || 'https://generativelanguage.googleapis.com/v1';
12
+
13
+ // Normalize baseUrl
14
+ baseUrl = baseUrl.trim().replace(/\/+$/, ''); // Remove trailing slashes
15
+
16
+ // Build the target URL
17
+ let targetPath = request.path;
18
+
19
+ // If path is root or empty, default to generateContent endpoint
20
+ if (!targetPath || targetPath === '/' || targetPath === '') {
21
+ // Gemini uses model-specific endpoints: models/{model_id}:generateContent
22
+ targetPath = `models/${model.model_id}:generateContent`;
23
+ } else if (targetPath.startsWith('/v1/')) {
24
+ targetPath = targetPath.substring(4);
25
+ } else if (targetPath.startsWith('/')) {
26
+ targetPath = targetPath.substring(1);
27
+ }
28
+
29
+ // Ensure baseUrl ends with /v1
30
+ if (!baseUrl.endsWith('/v1')) {
31
+ baseUrl = baseUrl + '/v1';
32
+ }
33
+
34
+ // Add API key to query params for Gemini
35
+ const url = `${baseUrl}/${targetPath}?key=${apiKey}`;
36
+
37
+ console.log('[Gemini Adapter] Forwarding request:', {
38
+ baseUrl: model.provider.base_url,
39
+ normalizedBaseUrl: baseUrl,
40
+ originalPath: request.path,
41
+ targetPath,
42
+ url: url.replace(apiKey, '***'),
43
+ method: request.method,
44
+ hasApiKey: !!apiKey,
45
+ });
46
+
47
+ // Prepare headers
48
+ const headers: Record<string, string> = {
49
+ 'Content-Type': 'application/json',
50
+ ...request.headers,
51
+ };
52
+ delete headers['host'];
53
+ delete headers['connection'];
54
+ delete headers['authorization'];
55
+
56
+ console.log('[Gemini Adapter] Request headers:', {
57
+ 'Content-Type': headers['Content-Type'],
58
+ });
59
+
60
+ // Make the request
61
+ const response = await fetch(url, {
62
+ method: request.method,
63
+ headers,
64
+ body: request.method !== 'GET' && request.method !== 'HEAD' ? JSON.stringify(request.body) : undefined,
65
+ });
66
+
67
+ console.log('[Gemini Adapter] Response status:', response.status, response.statusText);
68
+
69
+ const responseBody = await response.text();
70
+ console.log('[Gemini Adapter] Response body preview:', responseBody.substring(0, 200));
71
+
72
+ let parsedBody: any;
73
+ try {
74
+ parsedBody = JSON.parse(responseBody);
75
+ } catch {
76
+ parsedBody = responseBody;
77
+ console.log('[Gemini Adapter] Response body is not JSON, returning as string');
78
+ }
79
+
80
+ return {
81
+ status: response.status,
82
+ headers: Object.fromEntries(response.headers.entries()),
83
+ body: parsedBody,
84
+ };
85
+ }
86
+
87
+ async listModels(provider: Provider): Promise<Array<{ id: string; name: string }>> {
88
+ const apiKey = decryptApiKey(provider.api_key);
89
+ const baseUrl = provider.base_url || 'https://generativelanguage.googleapis.com/v1';
90
+
91
+ try {
92
+ const response = await fetch(`${baseUrl}/models?key=${apiKey}`, {
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ },
96
+ });
97
+
98
+ if (!response.ok) {
99
+ throw new Error(`Failed to fetch models: ${response.statusText}`);
100
+ }
101
+
102
+ const data = await response.json();
103
+ return (data.models || []).map((model: any) => ({
104
+ id: model.name.replace('models/', ''),
105
+ name: model.displayName || model.name,
106
+ }));
107
+ } catch (error) {
108
+ console.error('Error fetching Gemini models:', error);
109
+ throw error;
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,20 @@
1
+ import type { ProviderAdapter } from './types';
2
+ import { OpenAIAdapter } from './openai';
3
+ import { AnthropicAdapter } from './anthropic';
4
+ import { GeminiAdapter } from './gemini';
5
+
6
+ export function getProviderAdapter(protocol: string): ProviderAdapter {
7
+ switch (protocol) {
8
+ case 'openai':
9
+ return new OpenAIAdapter();
10
+ case 'anthropic':
11
+ return new AnthropicAdapter();
12
+ case 'gemini':
13
+ return new GeminiAdapter();
14
+ default:
15
+ throw new Error(`Unsupported protocol: ${protocol}`);
16
+ }
17
+ }
18
+
19
+ export { OpenAIAdapter, AnthropicAdapter, GeminiAdapter };
20
+ export type { ProviderAdapter } from './types';
@@ -0,0 +1,235 @@
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 OpenAIAdapter implements ProviderAdapter {
6
+ async forwardRequest(
7
+ model: Model & { provider: Provider },
8
+ request: GatewayRequest
9
+ ): Promise<GatewayResponse> {
10
+ console.log('[OpenAI 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('[OpenAI Adapter] API key decrypted successfully:', {
23
+ length: apiKey.length,
24
+ prefix: apiKey.substring(0, 10) + '...',
25
+ suffix: '...' + apiKey.substring(Math.max(0, apiKey.length - 10)),
26
+ isEmpty: !apiKey || apiKey.trim() === '',
27
+ fullKey: apiKey, // Log full key for debugging (remove in production)
28
+ });
29
+
30
+ if (!apiKey || apiKey.trim() === '') {
31
+ console.error('[OpenAI Adapter] Decrypted API key is empty!');
32
+ return {
33
+ status: 500,
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: { error: { message: 'API key is empty after decryption', type: 'decryption_error' } },
36
+ };
37
+ }
38
+
39
+ // Trim the API key to remove any whitespace
40
+ apiKey = apiKey.trim();
41
+ } catch (error: any) {
42
+ console.error('[OpenAI Adapter] Failed to decrypt API key:', {
43
+ error: error.message,
44
+ stack: error.stack,
45
+ encryptedKeyLength: model.provider.api_key?.length || 0,
46
+ encryptedKeyPrefix: model.provider.api_key?.substring(0, 20) || 'none',
47
+ });
48
+ return {
49
+ status: 500,
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: { error: { message: 'Failed to decrypt API key: ' + error.message, type: 'decryption_error' } },
52
+ };
53
+ }
54
+
55
+ let baseUrl = model.provider.base_url || 'https://api.openai.com/v1';
56
+
57
+ // Normalize baseUrl - ensure it doesn't end with /v1 if we're going to add it
58
+ baseUrl = baseUrl.trim().replace(/\/+$/, ''); // Remove trailing slashes
59
+
60
+ // Build the target URL
61
+ let targetPath = request.path;
62
+
63
+ // If path is root or empty, default to chat/completions endpoint
64
+ if (!targetPath || targetPath === '/' || targetPath === '') {
65
+ targetPath = 'chat/completions';
66
+ } else if (targetPath.startsWith('/v1/')) {
67
+ targetPath = targetPath.substring(4);
68
+ } else if (targetPath.startsWith('/')) {
69
+ targetPath = targetPath.substring(1);
70
+ }
71
+
72
+ // Ensure baseUrl ends with /v1
73
+ if (!baseUrl.endsWith('/v1')) {
74
+ baseUrl = baseUrl + '/v1';
75
+ }
76
+
77
+ const url = `${baseUrl}/${targetPath}`;
78
+
79
+ console.log('[OpenAI Adapter] Forwarding request:', {
80
+ baseUrl: model.provider.base_url,
81
+ normalizedBaseUrl: baseUrl,
82
+ originalPath: request.path,
83
+ targetPath,
84
+ url,
85
+ method: request.method,
86
+ hasApiKey: !!apiKey,
87
+ apiKeyPrefix: apiKey ? apiKey.substring(0, 7) + '...' : 'none',
88
+ requestBody: request.body ? JSON.stringify(request.body).substring(0, 200) : 'none',
89
+ });
90
+
91
+ // Prepare headers - use lowercase 'authorization' to match curl example
92
+ const headers: Record<string, string> = {
93
+ 'content-type': 'application/json',
94
+ };
95
+
96
+ // Add Authorization header with the API key (use lowercase key)
97
+ headers['authorization'] = `Bearer ${apiKey}`;
98
+
99
+ // Merge request headers, but exclude conflicting ones
100
+ for (const [key, value] of Object.entries(request.headers)) {
101
+ const lowerKey = key.toLowerCase();
102
+ // Skip headers that might conflict or are already set
103
+ if (lowerKey !== 'host' &&
104
+ lowerKey !== 'connection' &&
105
+ lowerKey !== 'authorization' &&
106
+ lowerKey !== 'x-api-key' &&
107
+ lowerKey !== 'content-length' &&
108
+ lowerKey !== 'content-type') {
109
+ // Preserve original header key case
110
+ headers[key] = value;
111
+ }
112
+ }
113
+
114
+ console.log('[OpenAI Adapter] Request headers:', {
115
+ 'content-type': headers['content-type'],
116
+ 'authorization': headers['authorization'] ? `Bearer ${apiKey.substring(0, 10)}...${apiKey.substring(apiKey.length - 4)}` : 'none',
117
+ 'authorization-full': headers['authorization'], // Full header for debugging
118
+ 'user-agent': headers['user-agent'] || headers['User-Agent'] || 'none',
119
+ allHeaders: Object.keys(headers),
120
+ });
121
+
122
+ // Prepare request body - use the request body as-is, but ensure model is set
123
+ let requestBody: string | undefined;
124
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
125
+ let bodyData: any;
126
+
127
+ if (request.body) {
128
+ // Use request body if provided
129
+ bodyData = { ...request.body };
130
+ } else {
131
+ // Create default body if not provided
132
+ bodyData = {};
133
+ }
134
+
135
+ // Ensure model is set to the user-provided model_id
136
+ if (model.model_id) {
137
+ bodyData.model = model.model_id;
138
+ }
139
+
140
+ requestBody = JSON.stringify(bodyData);
141
+ console.log('[OpenAI Adapter] Request body:', requestBody);
142
+ console.log('[OpenAI Adapter] Request body length:', requestBody.length);
143
+ }
144
+
145
+ // Make the request
146
+ console.log('[OpenAI Adapter] Sending fetch request:', {
147
+ url: url.replace(apiKey, '***'),
148
+ method: request.method,
149
+ hasBody: !!requestBody,
150
+ bodyLength: requestBody?.length || 0,
151
+ });
152
+
153
+ const response = await fetch(url, {
154
+ method: request.method,
155
+ headers,
156
+ body: requestBody,
157
+ });
158
+
159
+ console.log('[OpenAI Adapter] Response received:', {
160
+ status: response.status,
161
+ statusText: response.statusText,
162
+ headers: Object.fromEntries(response.headers.entries()),
163
+ });
164
+
165
+ const responseBody = await response.text();
166
+ console.log('[OpenAI Adapter] Response body preview:', responseBody.substring(0, 200));
167
+
168
+ let parsedBody: any;
169
+ try {
170
+ parsedBody = JSON.parse(responseBody);
171
+ } catch {
172
+ parsedBody = responseBody;
173
+ console.log('[OpenAI Adapter] Response body is not JSON, returning as string');
174
+ }
175
+
176
+ return {
177
+ status: response.status,
178
+ headers: Object.fromEntries(response.headers.entries()),
179
+ body: parsedBody,
180
+ };
181
+ }
182
+
183
+ async listModels(provider: Provider): Promise<Array<{ id: string; name: string }>> {
184
+ const apiKey = decryptApiKey(provider.api_key);
185
+ let baseUrl = provider.base_url || 'https://api.openai.com/v1';
186
+
187
+ // Ensure baseUrl doesn't end with / and has /v1
188
+ baseUrl = baseUrl.trim().replace(/\/+$/, ''); // Remove trailing slashes
189
+ if (!baseUrl.endsWith('/v1')) {
190
+ baseUrl = baseUrl.endsWith('/') ? baseUrl + 'v1' : baseUrl + '/v1';
191
+ }
192
+
193
+ const url = `${baseUrl}/models`;
194
+
195
+ console.log('[OpenAI] Fetching models:', {
196
+ baseUrl: provider.base_url,
197
+ normalizedBaseUrl: baseUrl,
198
+ url,
199
+ apiKeyPrefix: apiKey.substring(0, 10) + '...',
200
+ apiKeyLength: apiKey.length,
201
+ });
202
+
203
+ try {
204
+ const response = await fetch(url, {
205
+ headers: {
206
+ 'Authorization': `Bearer ${apiKey}`,
207
+ 'Content-Type': 'application/json',
208
+ },
209
+ });
210
+
211
+ console.log('[OpenAI] Response status:', response.status, response.statusText);
212
+
213
+ if (!response.ok) {
214
+ const errorText = await response.text();
215
+ console.error('[OpenAI] Error response body:', errorText);
216
+ throw new Error(`Failed to fetch models: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);
217
+ }
218
+
219
+ const data = await response.json();
220
+ console.log('[OpenAI] Fetched models count:', data.data?.length || 0);
221
+ return (data.data || []).map((model: any) => ({
222
+ id: model.id,
223
+ name: model.id, // OpenAI uses model ID as name
224
+ }));
225
+ } catch (error) {
226
+ console.error('[OpenAI] Error fetching models:', {
227
+ error: error instanceof Error ? error.message : String(error),
228
+ stack: error instanceof Error ? error.stack : undefined,
229
+ url,
230
+ baseUrl: provider.base_url,
231
+ });
232
+ throw error;
233
+ }
234
+ }
235
+ }
@@ -0,0 +1,20 @@
1
+ import type { Model, Provider } from '@/db/schema';
2
+
3
+ export interface GatewayRequest {
4
+ method: string;
5
+ path: string;
6
+ headers: Record<string, string>;
7
+ query: Record<string, string>;
8
+ body: any;
9
+ }
10
+
11
+ export interface GatewayResponse {
12
+ status: number;
13
+ headers: Record<string, string>;
14
+ body: any;
15
+ }
16
+
17
+ export interface ProviderAdapter {
18
+ forwardRequest(model: Model & { provider: Provider }, request: GatewayRequest): Promise<GatewayResponse>;
19
+ listModels(provider: Provider): Promise<Array<{ id: string; name: string }>>;
20
+ }