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.
- package/.claude/commands/openspec/apply.md +23 -0
- package/.claude/commands/openspec/archive.md +27 -0
- package/.claude/commands/openspec/proposal.md +28 -0
- package/.claude/settings.local.json +12 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
- package/.cursor/commands/openspec-apply.md +23 -0
- package/.cursor/commands/openspec-archive.md +27 -0
- package/.cursor/commands/openspec-proposal.md +28 -0
- package/.cursor/commands/ui-ux-pro-max.md +226 -0
- package/.eslintrc.json +3 -0
- package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
- package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
- package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
- package/.shared/ui-ux-pro-max/data/products.csv +97 -0
- package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
- package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
- package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
- package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
- package/AGENTS.md +18 -0
- package/CLAUDE.md +18 -0
- package/IMPLEMENTATION.md +157 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/.next/types/app/api/config/route.js +52 -0
- package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
- package/dist/.next/types/app/api/gateway/route.js +52 -0
- package/dist/.next/types/app/api/logs/route.js +52 -0
- package/dist/.next/types/app/api/models/route.js +52 -0
- package/dist/.next/types/app/api/providers/route.js +52 -0
- package/dist/.next/types/app/api/providers/test/route.js +52 -0
- package/dist/.next/types/app/api/service/start/route.js +52 -0
- package/dist/.next/types/app/api/service/status/route.js +52 -0
- package/dist/.next/types/app/api/service/stop/route.js +52 -0
- package/dist/.next/types/app/layout.js +22 -0
- package/dist/.next/types/app/logs/page.js +22 -0
- package/dist/.next/types/app/models/page.js +22 -0
- package/dist/.next/types/app/page.js +22 -0
- package/dist/.next/types/app/providers/page.js +22 -0
- package/dist/src/app/api/config/route.js +43 -0
- package/dist/src/app/api/gateway/[...path]/route.js +83 -0
- package/dist/src/app/api/gateway/route.js +63 -0
- package/dist/src/app/api/logs/route.js +34 -0
- package/dist/src/app/api/models/route.js +152 -0
- package/dist/src/app/api/providers/route.js +118 -0
- package/dist/src/app/api/providers/test/route.js +154 -0
- package/dist/src/app/api/service/start/route.js +55 -0
- package/dist/src/app/api/service/status/route.js +17 -0
- package/dist/src/app/api/service/stop/route.js +20 -0
- package/dist/src/app/components/ConfirmDialog.jsx +31 -0
- package/dist/src/app/components/Nav.jsx +45 -0
- package/dist/src/app/components/Toast.jsx +37 -0
- package/dist/src/app/components/ToastProvider.jsx +21 -0
- package/dist/src/app/layout.jsx +13 -0
- package/dist/src/app/logs/page.jsx +210 -0
- package/dist/src/app/models/page.jsx +291 -0
- package/dist/src/app/page.jsx +236 -0
- package/dist/src/app/providers/page.jsx +402 -0
- package/dist/src/cli/index.js +90 -0
- package/dist/src/db/database.js +69 -0
- package/dist/src/db/queries.js +261 -0
- package/dist/src/db/schema.js +67 -0
- package/dist/src/server/crypto.js +22 -0
- package/dist/src/server/gateway-server.js +200 -0
- package/dist/src/server/gateway.js +76 -0
- package/dist/src/server/logger.js +72 -0
- package/dist/src/server/providers/anthropic.js +52 -0
- package/dist/src/server/providers/gemini.js +64 -0
- package/dist/src/server/providers/index.js +16 -0
- package/dist/src/server/providers/openai.js +86 -0
- package/dist/src/server/providers/types.js +1 -0
- package/dist/src/server/service-manager.js +286 -0
- package/docs/TODO.md +19 -0
- package/next.config.js +7 -0
- package/openspec/AGENTS.md +456 -0
- package/openspec/changes/add-logging/proposal.md +18 -0
- package/openspec/changes/add-logging/specs/core/spec.md +21 -0
- package/openspec/changes/add-logging/tasks.md +16 -0
- package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
- package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
- package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
- package/openspec/changes/improve-gateway-startup/design.md +137 -0
- package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
- package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
- package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
- package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
- package/openspec/changes/init-api-gateway/design.md +185 -0
- package/openspec/changes/init-api-gateway/proposal.md +30 -0
- package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
- package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
- package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
- package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
- package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
- package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
- package/openspec/changes/init-api-gateway/tasks.md +84 -0
- package/openspec/project.md +58 -0
- package/package.json +51 -0
- package/postcss.config.js +6 -0
- package/src/app/api/config/route.ts +62 -0
- package/src/app/api/gateway/[...path]/route.ts +118 -0
- package/src/app/api/gateway/route.ts +77 -0
- package/src/app/api/logs/route.ts +48 -0
- package/src/app/api/models/route.ts +210 -0
- package/src/app/api/providers/route.ts +162 -0
- package/src/app/api/providers/test/route.ts +182 -0
- package/src/app/api/service/start/route.ts +73 -0
- package/src/app/api/service/status/route.ts +22 -0
- package/src/app/api/service/stop/route.ts +27 -0
- package/src/app/components/ConfirmDialog.tsx +63 -0
- package/src/app/components/Nav.tsx +66 -0
- package/src/app/components/Toast.tsx +61 -0
- package/src/app/components/ToastProvider.tsx +43 -0
- package/src/app/globals.css +71 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/logs/page.tsx +261 -0
- package/src/app/models/page.tsx +500 -0
- package/src/app/page.tsx +742 -0
- package/src/app/providers/page.tsx +558 -0
- package/src/cli/index.ts +95 -0
- package/src/db/database.ts +125 -0
- package/src/db/queries.ts +339 -0
- package/src/db/schema.ts +117 -0
- package/src/server/crypto.ts +48 -0
- package/src/server/gateway-server.ts +306 -0
- package/src/server/gateway.ts +163 -0
- package/src/server/logger.ts +96 -0
- package/src/server/providers/anthropic.ts +121 -0
- package/src/server/providers/gemini.ts +112 -0
- package/src/server/providers/index.ts +20 -0
- package/src/server/providers/openai.ts +235 -0
- package/src/server/providers/types.ts +20 -0
- package/src/server/service-manager.ts +321 -0
- package/tailwind.config.js +16 -0
- 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
|
+
}
|