byok-relay 1.0.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.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Provider-specific request forwarding.
3
+ *
4
+ * Built-in providers: anthropic, openai, google, groq
5
+ *
6
+ * Generic OpenAI-compatible passthrough:
7
+ * Any provider registered as `openai-compatible:<base-url>` will be forwarded
8
+ * using Bearer token auth to the given base URL. This covers:
9
+ * OpenRouter, LiteLLM, Groq, Mistral, Ollama, etc.
10
+ *
11
+ * Adding a new built-in provider: add an entry to PROVIDERS below.
12
+ * Adding a custom OpenAI-compatible endpoint: no code change needed —
13
+ * the user stores their key under a name like `openrouter` and passes
14
+ * the base URL as a header `x-relay-base-url`.
15
+ */
16
+ const fetch = require('node-fetch');
17
+
18
+ const PROVIDERS = {
19
+ anthropic: {
20
+ baseUrl: 'https://api.anthropic.com',
21
+ buildHeaders: (apiKey, extraHeaders = {}) => ({
22
+ 'Content-Type': 'application/json',
23
+ 'x-api-key': apiKey,
24
+ 'anthropic-version': extraHeaders['anthropic-version'] || '2023-06-01',
25
+ ...Object.fromEntries(
26
+ Object.entries(extraHeaders).filter(([k]) =>
27
+ k.startsWith('anthropic-') && k !== 'anthropic-version'
28
+ )
29
+ ),
30
+ }),
31
+ },
32
+
33
+ openai: {
34
+ baseUrl: 'https://api.openai.com',
35
+ buildHeaders: (apiKey) => ({
36
+ 'Content-Type': 'application/json',
37
+ 'Authorization': `Bearer ${apiKey}`,
38
+ }),
39
+ },
40
+
41
+ google: {
42
+ // Gemini API — key is passed as query param; ?alt=sse required for SSE streaming
43
+ baseUrl: 'https://generativelanguage.googleapis.com',
44
+ buildHeaders: () => ({ 'Content-Type': 'application/json' }),
45
+ buildUrl: (baseUrl, path, apiKey) => {
46
+ // Add alt=sse for streaming endpoints, plus the API key
47
+ const isStreaming = path.includes('stream');
48
+ const params = new URLSearchParams({ key: apiKey });
49
+ if (isStreaming) params.set('alt', 'sse');
50
+ const sep = path.includes('?') ? '&' : '?';
51
+ return `${baseUrl}${path}${sep}${params.toString()}`;
52
+ },
53
+ },
54
+
55
+ groq: {
56
+ baseUrl: 'https://api.groq.com',
57
+ buildHeaders: (apiKey) => ({
58
+ 'Content-Type': 'application/json',
59
+ 'Authorization': `Bearer ${apiKey}`,
60
+ }),
61
+ },
62
+
63
+ openrouter: {
64
+ baseUrl: 'https://openrouter.ai',
65
+ buildHeaders: (apiKey, extraHeaders = {}) => ({
66
+ 'Content-Type': 'application/json',
67
+ 'Authorization': `Bearer ${apiKey}`,
68
+ // OpenRouter requires HTTP-Referer and optionally X-Title
69
+ 'HTTP-Referer': extraHeaders['http-referer'] || extraHeaders['x-relay-referer'] || 'https://github.com/avikalpg/byok-relay',
70
+ ...(extraHeaders['x-title'] ? { 'X-Title': extraHeaders['x-title'] } : {}),
71
+ }),
72
+ },
73
+
74
+ mistral: {
75
+ baseUrl: 'https://api.mistral.ai',
76
+ buildHeaders: (apiKey) => ({
77
+ 'Content-Type': 'application/json',
78
+ 'Authorization': `Bearer ${apiKey}`,
79
+ }),
80
+ },
81
+
82
+ /**
83
+ * Generic OpenAI-compatible passthrough.
84
+ * Client must pass `x-relay-base-url` header with the target base URL.
85
+ * Key name in storage can be anything (e.g. "my-ollama", "company-llm").
86
+ */
87
+ 'openai-compatible': {
88
+ baseUrl: null, // determined per-request from x-relay-base-url header
89
+ buildHeaders: (apiKey) => ({
90
+ 'Content-Type': 'application/json',
91
+ 'Authorization': `Bearer ${apiKey}`,
92
+ }),
93
+ },
94
+ };
95
+
96
+ /**
97
+ * Forward a request to the AI provider.
98
+ * Returns a node-fetch Response (streaming-capable).
99
+ *
100
+ * @param {string} provider - Provider name from PROVIDERS
101
+ * @param {string} path - URL path to forward (e.g. /v1/messages)
102
+ * @param {string} method - HTTP method
103
+ * @param {object} body - Request body
104
+ * @param {string} apiKey - Decrypted API key
105
+ * @param {object} extraHeaders - Additional headers from the original request
106
+ */
107
+ async function forwardRequest(provider, path, method, body, apiKey, extraHeaders = {}) {
108
+ const config = PROVIDERS[provider];
109
+ if (!config) throw new Error(`Unknown provider: ${provider}`);
110
+
111
+ let baseUrl = config.baseUrl;
112
+
113
+ // For openai-compatible, the base URL comes from the request header
114
+ if (provider === 'openai-compatible') {
115
+ baseUrl = extraHeaders['x-relay-base-url'];
116
+ if (!baseUrl) {
117
+ throw new Error('x-relay-base-url header required for openai-compatible provider');
118
+ }
119
+ // Strip trailing slash
120
+ baseUrl = baseUrl.replace(/\/$/, '');
121
+ }
122
+
123
+ const headers = config.buildHeaders(apiKey, extraHeaders);
124
+
125
+ // Some providers (Google) put the key in the URL
126
+ const url = config.buildUrl
127
+ ? config.buildUrl(baseUrl, path, apiKey)
128
+ : `${baseUrl}${path}`;
129
+
130
+ const response = await fetch(url, {
131
+ method,
132
+ headers,
133
+ body: method !== 'GET' ? JSON.stringify(body) : undefined,
134
+ });
135
+
136
+ return response;
137
+ }
138
+
139
+ const SUPPORTED_PROVIDERS = Object.keys(PROVIDERS);
140
+
141
+ module.exports = { forwardRequest, SUPPORTED_PROVIDERS };
package/vercel.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 2,
3
+ "builds": [
4
+ {
5
+ "src": "src/index.js",
6
+ "use": "@vercel/node"
7
+ }
8
+ ],
9
+ "routes": [
10
+ {
11
+ "src": "/(.*)",
12
+ "dest": "src/index.js"
13
+ }
14
+ ]
15
+ }