Skip to content

Commit 308136b

Browse files
sjbermansalonichf5
authored andcommitted
Basic NJS module to extract model name (#3877)
Problem: To support the full Gateway API Inference Extension, we need to be able to extract the model name from the client request body in certain situations. Solution: Add a basic NJS module to extract the model name. This module will be enhanced (I've added notes) to be included in the full solution. On its own, it is not yet used.
1 parent f47862e commit 308136b

File tree

9 files changed

+90
-6
lines changed

9 files changed

+90
-6
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
22
1+
24

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ GEN_CRD_API_REFERENCE_DOCS_VERSION = v0.3.0
3333
# renovate: datasource=go depName=sigs.k8s.io/controller-tools
3434
CONTROLLER_TOOLS_VERSION = v0.19.0
3535
# renovate: datasource=docker depName=node
36-
NODE_VERSION = 22
36+
NODE_VERSION = 24
3737
# renovate: datasource=docker depName=quay.io/helmpack/chart-testing
3838
CHART_TESTING_VERSION = v3.14.0
3939
# renovate: datasource=github-tags depName=dadav/helm-schema

build/Dockerfile.nginx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ RUN apk add --no-cache bash \
2323
&& ln -sf /dev/stderr /var/log/nginx/error.log
2424

2525
COPY build/entrypoint.sh /agent/entrypoint.sh
26-
COPY ${NJS_DIR}/httpmatches.js /usr/lib/nginx/modules/njs/httpmatches.js
26+
COPY ${NJS_DIR}/ /usr/lib/nginx/modules/njs/
2727
COPY ${NGINX_CONF_DIR}/nginx.conf /etc/nginx/nginx.conf
2828
COPY ${NGINX_CONF_DIR}/grpc-error-locations.conf /etc/nginx/grpc-error-locations.conf
2929
COPY ${NGINX_CONF_DIR}/grpc-error-pages.conf /etc/nginx/grpc-error-pages.conf

build/Dockerfile.nginxplus

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ RUN apk add --no-cache bash \
2929
&& ln -sf /dev/stderr /var/log/nginx/error.log
3030

3131
COPY build/entrypoint.sh /agent/entrypoint.sh
32-
COPY ${NJS_DIR}/httpmatches.js /usr/lib/nginx/modules/njs/httpmatches.js
32+
COPY ${NJS_DIR}/ /usr/lib/nginx/modules/njs/
3333
COPY ${NGINX_CONF_DIR}/nginx-plus.conf /etc/nginx/nginx.conf
3434
COPY ${NGINX_CONF_DIR}/grpc-error-locations.conf /etc/nginx/grpc-error-locations.conf
3535
COPY ${NGINX_CONF_DIR}/grpc-error-pages.conf /etc/nginx/grpc-error-pages.conf

internal/controller/nginx/conf/nginx-plus.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ events {
1212
http {
1313
include /etc/nginx/conf.d/*.conf;
1414
include /etc/nginx/mime.types;
15-
js_import modules/njs/httpmatches.js;
15+
js_import /usr/lib/nginx/modules/njs/httpmatches.js;
16+
js_import /usr/lib/nginx/modules/njs/epp.js;
1617

1718
default_type application/octet-stream;
1819

internal/controller/nginx/conf/nginx.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ events {
1212
http {
1313
include /etc/nginx/conf.d/*.conf;
1414
include /etc/nginx/mime.types;
15-
js_import modules/njs/httpmatches.js;
15+
js_import /usr/lib/nginx/modules/njs/httpmatches.js;
16+
js_import /usr/lib/nginx/modules/njs/epp.js;
1617

1718
default_type application/octet-stream;
1819

internal/controller/nginx/modules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies.
2222

2323
- [httpmatches](./src/httpmatches.js): a location handler for HTTP requests. It redirects requests to an internal
2424
location block based on the request's headers, arguments, and method.
25+
- [epp](./src/epp.js): handles communication with the EndpointPicker (EPP) component. This is for acquiring a specific AI endpoint to route client traffic to when using the Gateway API Inference Extension.
2526

2627
### Helpful Resources for Module Development
2728

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// This file contains the methods to get an AI workload endpoint from the EndpointPicker (EPP).
2+
3+
// TODO (sberman): this module will need to be enhanced to include the following:
4+
// - function that sends the subrequest to the Go middleware application (to get the endpoint from EPP)
5+
// - if a user has specified an Exact matching condition for a model name, extract the model name from
6+
// the request body, and if it matches that condition, set the proper value in the X-Gateway-Model-Name header
7+
// (based on if we do a redirect or traffic split (see design doc)) in the subrequest. If the client request
8+
// already has this header set, then I don't think we need to extract the model from the body, just pass
9+
// through the existing header.
10+
// I believe we have to use js_content to call the NJS functionality. Because this takes over
11+
// the request, we will likely have to finish the NJS functionality with an internalRedirect to an internal
12+
// location that proxy_passes to the chosen endpoint.
13+
14+
// extractModel extracts the model name from the request body.
15+
function extractModel(r) {
16+
try {
17+
var body = JSON.parse(r.requestText);
18+
if (body && body.model !== undefined) {
19+
return String(body.model);
20+
}
21+
} catch (e) {
22+
r.error(`error parsing request body for model name: ${e.message}`);
23+
return '';
24+
}
25+
r.error('request body does not contain model parameter');
26+
return '';
27+
}
28+
29+
export default { extractModel };
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { default as epp } from '../src/epp.js';
2+
import { expect, describe, it } from 'vitest';
3+
4+
function makeRequest(body) {
5+
let r = {
6+
// Test mocks
7+
error(msg) {
8+
r.variables.error = msg;
9+
},
10+
requestText: body,
11+
variables: {},
12+
};
13+
14+
return r;
15+
}
16+
17+
describe('extractModel', () => {
18+
const tests = [
19+
{
20+
name: 'returns the model value',
21+
body: '{"model":"gpt-4"}',
22+
model: 'gpt-4',
23+
error: undefined,
24+
},
25+
{
26+
name: 'returns empty string if model is missing',
27+
body: '{"foo":1}',
28+
model: '',
29+
error: 'request body does not contain model parameter',
30+
},
31+
{
32+
name: 'returns empty string for invalid JSON',
33+
body: 'not-json',
34+
model: '',
35+
error: `error parsing request body for model name: Unexpected token 'o', "not-json" is not valid JSON`,
36+
},
37+
{
38+
name: 'empty request body',
39+
body: '',
40+
model: '',
41+
error: 'error parsing request body for model name: Unexpected end of JSON input',
42+
},
43+
];
44+
45+
tests.forEach((test) => {
46+
it(test.name, () => {
47+
let r = makeRequest(test.body);
48+
expect(epp.extractModel(r)).to.equal(test.model);
49+
expect(r.variables.error).to.equal(test.error);
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)