Skip to content

Commit 63eb3a2

Browse files
committed
allow paths other than /v1
1 parent 720a32d commit 63eb3a2

File tree

4 files changed

+48
-14
lines changed

4 files changed

+48
-14
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Manages Nginx for reverse proxy to multiple LLMs, with TLS & Bearer Auth tokens. Deployed with docker.
44

5-
- Aggregates multiple OpenAI-type LLM APIs (all routes must be prefixed with "/v1")
5+
- Aggregates multiple OpenAI-type LLM APIs
66
- Supports cloudflare domains
77
- Uses Let's Encrypt for TLS certificates
88
- Uses certbot for certificate issuance and renewal
@@ -33,7 +33,7 @@ version: '3.6'
3333

3434
services:
3535
llmp:
36-
image: ghcr.io/j4ys0n/llm-proxy:1.5.0
36+
image: ghcr.io/j4ys0n/llm-proxy:1.5.1
3737
container_name: llmp
3838
hostname: llmp
3939
restart: unless-stopped
@@ -51,7 +51,7 @@ services:
5151
Here's what your `.env` file should look like:
5252
```bash
5353
PORT=8080 # node.js listen port. right now nginx is hard coded, so don't change this.
54-
TARGET_URLS=http://localhost:1234,http://192.168.1.100:1234|api-key-here # list of api endpoints (don't add /v1)
54+
TARGET_URLS=http://localhost:1234,http://192.168.1.100:1234|api-key-here # list of api endpoints (/v1 is optional)
5555
JWT_SECRET=randomly_generated_secret # secret for JWT token generation, change this!
5656
AUTH_USERNAME=admin
5757
AUTH_PASSWORD=secure_password # super basic auth credentials for the admin interface

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: '3.6'
22

33
services:
44
llmp:
5-
image: ghcr.io/j4ys0n/llm-proxy:1.5.0
5+
image: ghcr.io/j4ys0n/llm-proxy:1.5.1
66
container_name: llmp
77
hostname: llmp
88
restart: unless-stopped

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "llm-proxy",
3-
"version": "1.5.0",
3+
"version": "1.5.1",
44
"description": "Manages Nginx for reverse proxy to multiple LLMs, with TLS & Bearer Auth tokens",
55
"main": "dist/index.js",
66
"scripts": {

src/controllers/llm.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,43 @@ export interface ModelMap {
1515

1616
const defaultContentType = 'application/json'
1717

18+
function getPath(url: string): { path: string, base: string, apiKey?: string } {
19+
try {
20+
const urlParts = url.split('|')
21+
const apiKey = urlParts.length > 1 ? urlParts[1] : undefined
22+
const urlObject = new URL(url)
23+
return {
24+
path: urlObject.pathname || '/v1',
25+
base: urlObject.origin,
26+
apiKey
27+
}
28+
} catch (error) {
29+
// Return the input if it's already a path starting with '/'
30+
if (url.startsWith('/')) return { path: url, base: 'http://localhost' }
31+
// Return '/v1' for invalid URLs
32+
return { path: '/v1', base: 'http://localhost' }
33+
}
34+
}
35+
1836
async function fetchModels(targetUrls: string[]): Promise<ModelMap> {
1937
const tmp: ModelMap = {}
2038
for (const urlAndToken of targetUrls) {
2139
const [url, apiKey] = urlAndToken.split('|').map(s => s.trim())
22-
const reqHeaders: { [key: string]: string } = {
40+
const { path, base } = getPath(url)
41+
const headers: { [key: string]: string } = {
2342
accept: defaultContentType,
2443
'Content-Type': defaultContentType
2544
}
2645
if (apiKey != null && apiKey !== '') {
27-
reqHeaders['Authorization'] = `Bearer ${apiKey}`
46+
headers['Authorization'] = `Bearer ${apiKey}`
47+
}
48+
const params = {
49+
method: 'GET',
50+
url: `${base}/${path}/models`,
51+
headers
2852
}
2953
try {
30-
const response = await axios.get(`${url}/v1/models`)
54+
const response = await axios(params)
3155
const models = response.data.data || []
3256
const hostId = extractDomainName(url)
3357
models.forEach((model: Model) => {
@@ -85,26 +109,36 @@ export class LLMController {
85109
public async forwardPostRequest(req: Request, res: Response, next: NextFunction) {
86110
if (
87111
req.method === 'POST' &&
88-
(req.path.startsWith('v1/') || req.path.startsWith('/v1/')) &&
112+
(req.path.startsWith('v1') || req.path.startsWith('/v1')) &&
89113
req.body != null &&
90114
req.body.model != null &&
91115
this.targetUrls.length > 0
92116
) {
93117
const { model: modelId } = req.body
94-
let targetUrl = this.targetUrls[0] // Default to first URL if no matching model found
118+
const { base: firstBaseUrl, path: firstPath, apiKey: firstApiKey } = getPath(this.targetUrls[0])
119+
let targetUrl = firstBaseUrl // Default to first URL if no matching model found
120+
let targetPath = firstPath
121+
let targetApiKey = firstApiKey
95122

96123
const hash = md5(modelId)
97124
if (modelId && this.modelCache[hash]) {
98-
targetUrl = this.modelCache[hash].url
125+
const { path, base, apiKey } = getPath(this.modelCache[hash].url)
126+
targetUrl = base
127+
targetPath = path
128+
targetApiKey = apiKey
99129
}
100-
const fullUrl = new URL(req.path, targetUrl).toString()
130+
const reqPath = req.path.startsWith('/v1/') ? req.path.replace('/v1', targetPath) : `${targetPath}${req.path}`
131+
const fullUrl = new URL(reqPath, targetUrl).toString()
101132
log('info', `Forwarding request to: ${fullUrl} -> ${modelId}`)
102-
133+
const headers = { ...req.headers }
134+
if (targetApiKey) {
135+
headers['Authorization'] = `Bearer ${targetApiKey}`
136+
}
103137
try {
104138
const axiosConfig: AxiosRequestConfig = {
105139
method: req.method,
106140
url: fullUrl,
107-
headers: { ...req.headers },
141+
headers,
108142
data: req.body,
109143
responseType: 'stream'
110144
}

0 commit comments

Comments
 (0)