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.
- package/.env.example +14 -0
- package/.github/workflows/deploy.yml +28 -0
- package/LICENSE +201 -0
- package/README.md +234 -0
- package/deploy/byok-relay.service +21 -0
- package/examples/react-vite/package-lock.json +1714 -0
- package/llms.txt +80 -0
- package/package.json +23 -0
- package/skills/byok-relay/SKILL.md +162 -0
- package/src/db.js +126 -0
- package/src/index.js +217 -0
- package/src/providers.js +141 -0
- package/vercel.json +15 -0
package/src/providers.js
ADDED
|
@@ -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 };
|