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,48 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDatabase } from '@/db/database';
3
+ import { getRequestLogs, getRequestLogById, getRequestLogCount } from '@/db/queries';
4
+
5
+ // Ensure Node.js runtime (required for SQLite)
6
+ export const runtime = 'nodejs';
7
+
8
+ export async function GET(request: NextRequest) {
9
+ try {
10
+ getDatabase();
11
+ const { searchParams } = new URL(request.url);
12
+ const id = searchParams.get('id');
13
+ const limit = parseInt(searchParams.get('limit') || '100');
14
+ const offset = parseInt(searchParams.get('offset') || '0');
15
+ const modelId = searchParams.get('model_id');
16
+
17
+ if (id) {
18
+ const log = getRequestLogById(parseInt(id));
19
+ if (!log) {
20
+ return NextResponse.json(
21
+ { error: 'Log not found' },
22
+ { status: 404 }
23
+ );
24
+ }
25
+ return NextResponse.json(log);
26
+ }
27
+
28
+ const logs = getRequestLogs(
29
+ limit,
30
+ offset,
31
+ modelId ? parseInt(modelId) : undefined
32
+ );
33
+ const total = getRequestLogCount(modelId ? parseInt(modelId) : undefined);
34
+
35
+ return NextResponse.json({
36
+ logs,
37
+ total,
38
+ limit,
39
+ offset,
40
+ });
41
+ } catch (error: any) {
42
+ console.error('Logs API error:', error);
43
+ return NextResponse.json(
44
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
45
+ { status: 500 }
46
+ );
47
+ }
48
+ }
@@ -0,0 +1,210 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDatabase } from '@/db/database';
3
+ import {
4
+ getAllModels,
5
+ createModel,
6
+ updateModel,
7
+ deleteModel,
8
+ getModelsByProvider,
9
+ getProviderById,
10
+ } from '@/db/queries';
11
+ import { getProviderAdapter } from '@/server/providers';
12
+
13
+ // Ensure Node.js runtime (required for SQLite)
14
+ export const runtime = 'nodejs';
15
+
16
+ export async function GET(request: NextRequest) {
17
+ try {
18
+ getDatabase();
19
+ const { searchParams } = new URL(request.url);
20
+ const providerId = searchParams.get('provider_id');
21
+
22
+ if (providerId) {
23
+ const models = getModelsByProvider(parseInt(providerId));
24
+ return NextResponse.json(models);
25
+ } else {
26
+ const models = getAllModels();
27
+ return NextResponse.json(models);
28
+ }
29
+ } catch (error: any) {
30
+ console.error('Models API error:', error);
31
+ return NextResponse.json(
32
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
33
+ { status: 500 }
34
+ );
35
+ }
36
+ }
37
+
38
+ export async function POST(request: NextRequest) {
39
+ try {
40
+ getDatabase();
41
+ const body = await request.json();
42
+ const { provider_id, name, model_id, enabled } = body;
43
+
44
+ if (!provider_id || !name || !model_id) {
45
+ return NextResponse.json(
46
+ { error: 'Missing required fields' },
47
+ { status: 400 }
48
+ );
49
+ }
50
+
51
+ const model = createModel({
52
+ provider_id,
53
+ name,
54
+ model_id,
55
+ enabled: enabled !== undefined ? enabled : true,
56
+ });
57
+
58
+ return NextResponse.json(model);
59
+ } catch (error: any) {
60
+ console.error('Models API error:', error);
61
+ return NextResponse.json(
62
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
63
+ { status: 500 }
64
+ );
65
+ }
66
+ }
67
+
68
+ export async function PUT(request: NextRequest) {
69
+ try {
70
+ getDatabase();
71
+ const body = await request.json();
72
+ const { id, name, model_id, enabled, provider_id } = body;
73
+
74
+ if (!id) {
75
+ return NextResponse.json(
76
+ { error: 'Model ID is required' },
77
+ { status: 400 }
78
+ );
79
+ }
80
+
81
+ const updateData: any = {};
82
+ if (name !== undefined) updateData.name = name;
83
+ if (model_id !== undefined) updateData.model_id = model_id;
84
+ if (enabled !== undefined) updateData.enabled = enabled;
85
+ if (provider_id !== undefined) updateData.provider_id = provider_id;
86
+
87
+ const model = updateModel(id, updateData);
88
+ if (!model) {
89
+ return NextResponse.json(
90
+ { error: 'Model not found' },
91
+ { status: 404 }
92
+ );
93
+ }
94
+
95
+ return NextResponse.json(model);
96
+ } catch (error: any) {
97
+ console.error('Models API error:', error);
98
+ return NextResponse.json(
99
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
100
+ { status: 500 }
101
+ );
102
+ }
103
+ }
104
+
105
+ export async function DELETE(request: NextRequest) {
106
+ try {
107
+ getDatabase();
108
+ const { searchParams } = new URL(request.url);
109
+ const id = searchParams.get('id');
110
+
111
+ if (!id) {
112
+ return NextResponse.json(
113
+ { error: 'Model ID is required' },
114
+ { status: 400 }
115
+ );
116
+ }
117
+
118
+ const success = deleteModel(parseInt(id));
119
+ if (!success) {
120
+ return NextResponse.json(
121
+ { error: 'Model not found' },
122
+ { status: 404 }
123
+ );
124
+ }
125
+
126
+ return NextResponse.json({ success: true });
127
+ } catch (error: any) {
128
+ console.error('Models API error:', error);
129
+ return NextResponse.json(
130
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
131
+ { status: 500 }
132
+ );
133
+ }
134
+ }
135
+
136
+ // Fetch models from provider
137
+ export async function PATCH(request: NextRequest) {
138
+ try {
139
+ getDatabase();
140
+ const body = await request.json();
141
+ const { provider_id } = body;
142
+
143
+ if (!provider_id) {
144
+ return NextResponse.json(
145
+ { error: 'Provider ID is required' },
146
+ { status: 400 }
147
+ );
148
+ }
149
+
150
+ const provider = getProviderById(provider_id);
151
+ if (!provider) {
152
+ return NextResponse.json(
153
+ { error: 'Provider not found' },
154
+ { status: 404 }
155
+ );
156
+ }
157
+
158
+ console.log('[Models API] Fetching models for provider:', {
159
+ providerId: provider_id,
160
+ providerName: provider.name,
161
+ protocol: provider.protocol,
162
+ baseUrl: provider.base_url,
163
+ });
164
+
165
+ const adapter = getProviderAdapter(provider.protocol);
166
+ const models = await adapter.listModels(provider);
167
+
168
+ console.log('[Models API] Fetched models:', models.length);
169
+
170
+ // Create or update models
171
+ const results = [];
172
+ for (const modelInfo of models) {
173
+ try {
174
+ const existing = getAllModels().find(
175
+ m => m.provider_id === provider_id && m.model_id === modelInfo.id
176
+ );
177
+
178
+ if (existing) {
179
+ updateModel(existing.id, {
180
+ name: modelInfo.name,
181
+ model_id: modelInfo.id,
182
+ });
183
+ results.push({ ...existing, name: modelInfo.name });
184
+ } else {
185
+ const newModel = createModel({
186
+ provider_id,
187
+ name: modelInfo.name,
188
+ model_id: modelInfo.id,
189
+ enabled: false, // 默认关闭
190
+ });
191
+ results.push(newModel);
192
+ }
193
+ } catch (error: any) {
194
+ console.error(`Failed to create/update model ${modelInfo.id}:`, error);
195
+ }
196
+ }
197
+
198
+ return NextResponse.json({
199
+ success: true,
200
+ models: results,
201
+ count: results.length,
202
+ });
203
+ } catch (error: any) {
204
+ console.error('Models API error:', error);
205
+ return NextResponse.json(
206
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
207
+ { status: 500 }
208
+ );
209
+ }
210
+ }
@@ -0,0 +1,162 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDatabase } from '@/db/database';
3
+ import { getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider } from '@/db/queries';
4
+ import { encryptApiKey, decryptApiKey } from '@/server/crypto';
5
+
6
+ // Ensure Node.js runtime (required for SQLite)
7
+ export const runtime = 'nodejs';
8
+
9
+ export async function GET(request: NextRequest) {
10
+ try {
11
+ getDatabase();
12
+ const { searchParams } = new URL(request.url);
13
+ const id = searchParams.get('id');
14
+ const includeKey = searchParams.get('includeKey') === 'true';
15
+
16
+ // If ID is provided, return single provider (for editing)
17
+ if (id) {
18
+ const provider = getProviderById(parseInt(id));
19
+ if (!provider) {
20
+ return NextResponse.json(
21
+ { error: 'Provider not found' },
22
+ { status: 404 }
23
+ );
24
+ }
25
+ // Return with decrypted API key if requested (for editing)
26
+ return NextResponse.json({
27
+ ...provider,
28
+ api_key: includeKey ? decryptApiKey(provider.api_key) : '***',
29
+ });
30
+ }
31
+
32
+ // Return all providers
33
+ const providers = getAllProviders();
34
+ // Don't return API keys in list view
35
+ const sanitized = providers.map(p => ({
36
+ ...p,
37
+ api_key: '***',
38
+ }));
39
+ return NextResponse.json(sanitized);
40
+ } catch (error: any) {
41
+ console.error('Providers API error:', error);
42
+ return NextResponse.json(
43
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
44
+ { status: 500 }
45
+ );
46
+ }
47
+ }
48
+
49
+ export async function POST(request: NextRequest) {
50
+ try {
51
+ getDatabase();
52
+ const body = await request.json();
53
+ const { name, protocol, base_url, api_key } = body;
54
+
55
+ if (!name || !protocol || !base_url || !api_key) {
56
+ return NextResponse.json(
57
+ { error: 'Missing required fields' },
58
+ { status: 400 }
59
+ );
60
+ }
61
+
62
+ const encryptedKey = encryptApiKey(api_key.trim());
63
+ const provider = createProvider({
64
+ name,
65
+ protocol,
66
+ base_url,
67
+ api_key: encryptedKey,
68
+ });
69
+
70
+ return NextResponse.json({
71
+ ...provider,
72
+ api_key: '***',
73
+ });
74
+ } catch (error: any) {
75
+ console.error('Providers API error:', error);
76
+ return NextResponse.json(
77
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
78
+ { status: 500 }
79
+ );
80
+ }
81
+ }
82
+
83
+ export async function PUT(request: NextRequest) {
84
+ try {
85
+ getDatabase();
86
+ const body = await request.json();
87
+ const { id, name, protocol, base_url, api_key } = body;
88
+
89
+ if (!id) {
90
+ return NextResponse.json(
91
+ { error: 'Provider ID is required' },
92
+ { status: 400 }
93
+ );
94
+ }
95
+
96
+ const existingProvider = getProviderById(id);
97
+ if (!existingProvider) {
98
+ return NextResponse.json(
99
+ { error: 'Provider not found' },
100
+ { status: 404 }
101
+ );
102
+ }
103
+
104
+ const updateData: any = {};
105
+ if (name !== undefined) updateData.name = name;
106
+ if (protocol !== undefined) updateData.protocol = protocol;
107
+ if (base_url !== undefined) updateData.base_url = base_url;
108
+ if (api_key !== undefined && api_key !== null && api_key.trim() !== '') {
109
+ updateData.api_key = encryptApiKey(api_key.trim());
110
+ }
111
+
112
+ const provider = updateProvider(id, updateData);
113
+ if (!provider) {
114
+ return NextResponse.json(
115
+ { error: 'Provider not found' },
116
+ { status: 404 }
117
+ );
118
+ }
119
+
120
+ return NextResponse.json({
121
+ ...provider,
122
+ api_key: '***',
123
+ });
124
+ } catch (error: any) {
125
+ console.error('Providers API error:', error);
126
+ return NextResponse.json(
127
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
128
+ { status: 500 }
129
+ );
130
+ }
131
+ }
132
+
133
+ export async function DELETE(request: NextRequest) {
134
+ try {
135
+ getDatabase();
136
+ const { searchParams } = new URL(request.url);
137
+ const id = searchParams.get('id');
138
+
139
+ if (!id) {
140
+ return NextResponse.json(
141
+ { error: 'Provider ID is required' },
142
+ { status: 400 }
143
+ );
144
+ }
145
+
146
+ const success = deleteProvider(parseInt(id));
147
+ if (!success) {
148
+ return NextResponse.json(
149
+ { error: 'Provider not found' },
150
+ { status: 404 }
151
+ );
152
+ }
153
+
154
+ return NextResponse.json({ success: true });
155
+ } catch (error: any) {
156
+ console.error('Providers API error:', error);
157
+ return NextResponse.json(
158
+ { error: error.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined },
159
+ { status: 500 }
160
+ );
161
+ }
162
+ }
@@ -0,0 +1,182 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDatabase } from '@/db/database';
3
+ import { getProviderById, getModelById } from '@/db/queries';
4
+ import { getProviderAdapter } from '@/server/providers';
5
+ import type { GatewayRequest } from '@/server/providers/types';
6
+
7
+ // Ensure Node.js runtime (required for SQLite)
8
+ export const runtime = 'nodejs';
9
+
10
+ export async function POST(request: NextRequest) {
11
+ try {
12
+ getDatabase();
13
+ const body = await request.json();
14
+ const { provider_id, model_id } = body;
15
+
16
+ if (!provider_id || !model_id) {
17
+ return NextResponse.json(
18
+ { error: 'Provider ID and Model ID are required' },
19
+ { status: 400 }
20
+ );
21
+ }
22
+
23
+ // Get provider and model
24
+ const provider = getProviderById(provider_id);
25
+ if (!provider) {
26
+ return NextResponse.json(
27
+ { error: 'Provider not found' },
28
+ { status: 404 }
29
+ );
30
+ }
31
+
32
+ const model = getModelById(model_id);
33
+ if (!model || model.provider_id !== provider_id) {
34
+ return NextResponse.json(
35
+ { error: 'Model not found or does not belong to this provider' },
36
+ { status: 404 }
37
+ );
38
+ }
39
+
40
+ // Get provider adapter
41
+ const adapter = getProviderAdapter(provider.protocol);
42
+
43
+ // Create a test request based on protocol
44
+ const testRequest = createTestRequest(provider.protocol, model.model_id);
45
+
46
+ // Send test request with timeout
47
+ const timeoutPromise = new Promise((_, reject) => {
48
+ setTimeout(() => reject(new Error('连接超时,请检查网络或稍后重试')), 10000);
49
+ });
50
+
51
+ const testPromise = adapter.forwardRequest(
52
+ { ...model, provider } as any,
53
+ testRequest
54
+ );
55
+
56
+ const response = await Promise.race([testPromise, timeoutPromise]) as any;
57
+
58
+ // Check response status
59
+ if (response.status >= 200 && response.status < 300) {
60
+ return NextResponse.json({
61
+ success: true,
62
+ message: '连接成功,模型可用',
63
+ });
64
+ } else {
65
+ // Parse error message
66
+ let errorMessage = '连接失败';
67
+ if (response.body?.error?.message) {
68
+ errorMessage = response.body.error.message;
69
+ } else if (typeof response.body === 'string') {
70
+ errorMessage = response.body;
71
+ }
72
+
73
+ // Map common error codes to friendly messages
74
+ if (response.status === 401 || response.status === 403) {
75
+ errorMessage = 'API Key 无效,请检查配置';
76
+ } else if (response.status === 404) {
77
+ errorMessage = '模型不存在或不可用';
78
+ } else if (response.status >= 500) {
79
+ errorMessage = '服务器错误,请稍后重试';
80
+ }
81
+
82
+ return NextResponse.json(
83
+ {
84
+ success: false,
85
+ error: errorMessage,
86
+ status: response.status,
87
+ },
88
+ { status: 200 } // Return 200 so frontend can handle the error
89
+ );
90
+ }
91
+ } catch (error: any) {
92
+ console.error('Test connection error:', error);
93
+
94
+ // Handle specific error types
95
+ let errorMessage = '连接失败';
96
+ if (error.message.includes('超时')) {
97
+ errorMessage = error.message;
98
+ } else if (error.message.includes('fetch failed') || error.message.includes('ECONNREFUSED')) {
99
+ errorMessage = '无法连接到服务器,请检查 Base URL 和网络连接';
100
+ } else if (error.message.includes('ENOTFOUND') || error.message.includes('DNS')) {
101
+ errorMessage = 'DNS 解析失败,请检查 Base URL';
102
+ } else {
103
+ errorMessage = error.message || '连接失败,请检查配置';
104
+ }
105
+
106
+ return NextResponse.json(
107
+ {
108
+ success: false,
109
+ error: errorMessage,
110
+ },
111
+ { status: 200 } // Return 200 so frontend can handle the error
112
+ );
113
+ }
114
+ }
115
+
116
+ function createTestRequest(protocol: string, modelId: string): GatewayRequest {
117
+ switch (protocol) {
118
+ case 'openai':
119
+ return {
120
+ method: 'POST',
121
+ path: '/v1/chat/completions',
122
+ headers: {
123
+ 'Content-Type': 'application/json',
124
+ },
125
+ query: {},
126
+ body: {
127
+ model: modelId,
128
+ messages: [
129
+ {
130
+ role: 'user',
131
+ content: 'test',
132
+ },
133
+ ],
134
+ max_tokens: 5,
135
+ },
136
+ };
137
+
138
+ case 'anthropic':
139
+ return {
140
+ method: 'POST',
141
+ path: '/v1/messages',
142
+ headers: {
143
+ 'Content-Type': 'application/json',
144
+ },
145
+ query: {},
146
+ body: {
147
+ model: modelId,
148
+ max_tokens: 5,
149
+ messages: [
150
+ {
151
+ role: 'user',
152
+ content: 'test',
153
+ },
154
+ ],
155
+ },
156
+ };
157
+
158
+ case 'gemini':
159
+ return {
160
+ method: 'POST',
161
+ path: `/v1/models/${modelId}:generateContent`,
162
+ headers: {
163
+ 'Content-Type': 'application/json',
164
+ },
165
+ query: {},
166
+ body: {
167
+ contents: [
168
+ {
169
+ parts: [
170
+ {
171
+ text: 'test',
172
+ },
173
+ ],
174
+ },
175
+ ],
176
+ },
177
+ };
178
+
179
+ default:
180
+ throw new Error(`Unsupported protocol: ${protocol}`);
181
+ }
182
+ }
@@ -0,0 +1,73 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDatabase } from '@/db/database';
3
+ import { serviceManager } from '@/server/service-manager';
4
+ import { getConfig } from '@/db/queries';
5
+ import net from 'net';
6
+
7
+ // Ensure Node.js runtime (required for service manager)
8
+ export const runtime = 'nodejs';
9
+
10
+ export async function POST(request: NextRequest) {
11
+ try {
12
+ // Initialize database
13
+ getDatabase();
14
+
15
+ const body = await request.json().catch(() => ({}));
16
+ const port = body.port ? parseInt(body.port, 10) : null;
17
+
18
+ // Get port from config if not provided
19
+ let targetPort = port;
20
+ if (!targetPort) {
21
+ const portConfig = getConfig('port');
22
+ targetPort = portConfig ? parseInt(portConfig.value, 10) : 3000;
23
+ }
24
+
25
+ // In development, if port is 3000 (default dev server port), check for conflict
26
+ const isDev = process.env.NODE_ENV !== 'production';
27
+ if (isDev && targetPort === 3000) {
28
+ // Check if port 3000 is in use (likely by the dev server)
29
+ const testServer = net.createServer();
30
+ const portInUse = await new Promise<boolean>((resolve) => {
31
+ testServer.listen(3000, () => {
32
+ testServer.close(() => resolve(false));
33
+ });
34
+ testServer.on('error', () => resolve(true));
35
+ setTimeout(() => {
36
+ testServer.close(() => resolve(false));
37
+ }, 100);
38
+ });
39
+
40
+ if (portInUse) {
41
+ return NextResponse.json(
42
+ {
43
+ status: 'stopped',
44
+ error: 'Port 3000 is already in use by the development server. Please configure a different port (e.g., 3001) in the settings above.'
45
+ },
46
+ { status: 400 }
47
+ );
48
+ }
49
+ }
50
+
51
+ // Validate port
52
+ if (isNaN(targetPort) || targetPort < 1 || targetPort > 65535) {
53
+ return NextResponse.json(
54
+ { status: 'stopped', error: 'Invalid port number' },
55
+ { status: 400 }
56
+ );
57
+ }
58
+
59
+ const result = await serviceManager.start(targetPort);
60
+
61
+ if (result.error) {
62
+ return NextResponse.json(result, { status: 400 });
63
+ }
64
+
65
+ return NextResponse.json(result);
66
+ } catch (error: any) {
67
+ console.error('Service start API error:', error);
68
+ return NextResponse.json(
69
+ { status: 'stopped', error: error.message || 'Failed to start service' },
70
+ { status: 500 }
71
+ );
72
+ }
73
+ }
@@ -0,0 +1,22 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getDatabase } from '@/db/database';
3
+ import { serviceManager } from '@/server/service-manager';
4
+
5
+ // Ensure Node.js runtime (required for service manager)
6
+ export const runtime = 'nodejs';
7
+
8
+ export async function GET(request: NextRequest) {
9
+ try {
10
+ // Initialize database
11
+ getDatabase();
12
+
13
+ const status = await serviceManager.getStatus();
14
+ return NextResponse.json(status);
15
+ } catch (error: any) {
16
+ console.error('Service status API error:', error);
17
+ return NextResponse.json(
18
+ { status: 'stopped', error: error.message || 'Failed to get service status' },
19
+ { status: 500 }
20
+ );
21
+ }
22
+ }