Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion containers/api-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ COPY server.js logging.js metrics.js rate-limiter.js \
oidc-token-provider-base.js \
github-oidc.js aws-oidc-token-provider.js gcp-oidc-token-provider.js \
anthropic-oidc-token-provider.js \
oidc-refresh-utils.js body-transform.js rate-limit.js websocket-proxy.js \
oidc-refresh-utils.js body-transform.js body-utils.js rate-limit.js websocket-proxy.js \
deprecated-header-tracker.js billing-headers.js upstream-response.js \
anthropic-cache.js otel.js ./
COPY guards/ ./guards/
Expand Down
31 changes: 8 additions & 23 deletions containers/api-proxy/body-transform.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const { parseBodyAsObject } = require('./body-utils');

/**
* Sanitize OpenAI-compatible request history where tool_calls[].type is null.
*
Expand All @@ -10,14 +12,8 @@
* @returns {{ body: Buffer, normalizedCount: number, droppedCount: number }|null}
*/
function sanitizeNullToolCallTypes(body) {
let parsed;
try {
parsed = JSON.parse(body.toString('utf8'));
} catch {
return null;
}

if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.messages)) {
const parsed = parseBodyAsObject(body);
if (!parsed || !Array.isArray(parsed.messages)) {
return null;
}

Expand Down Expand Up @@ -82,13 +78,8 @@ function sanitizeNullToolCallTypes(body) {
* @returns {Buffer|null}
*/
function injectSteeringMessage(body, provider, message) {
let parsed;
try {
parsed = JSON.parse(body.toString());
} catch {
return null;
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
let parsed = parseBodyAsObject(body);
if (!parsed) return null;

if (provider === 'anthropic') {
if (typeof parsed.system === 'string') {
Expand Down Expand Up @@ -148,14 +139,8 @@ function injectStreamOptions(body, provider, requestPath = '') {
const pathOnly = typeof requestPath === 'string' ? requestPath.split('?')[0] : '';
if (/^\/?(?:v\d+\/)?responses(?:\/|$)/.test(pathOnly)) return null;

let parsed;
try {
parsed = JSON.parse(body.toString('utf8'));
} catch {
return null;
}

if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
const parsed = parseBodyAsObject(body);
if (!parsed) return null;
if (!parsed.stream) return null;
if (parsed.stream_options) return null;

Expand Down
25 changes: 25 additions & 0 deletions containers/api-proxy/body-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';

/**
* Parse a request/response body as a plain JSON object.
*
* Returns null on parse error, non-object, or array input.
*
* @param {Buffer|string} body
* @returns {Record<string, unknown>|null}
*/
function parseBodyAsObject(body) {
let parsed;
try {
parsed = JSON.parse(body.toString('utf8'));
} catch {
return null;
}

if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
return parsed;
}

module.exports = {
parseBodyAsObject,
};
17 changes: 17 additions & 0 deletions containers/api-proxy/body-utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { parseBodyAsObject } = require('./body-utils');

describe('parseBodyAsObject', () => {
it('parses a JSON object from a buffer', () => {
const parsed = parseBodyAsObject(Buffer.from(JSON.stringify({ model: 'gpt-5.4' })));
expect(parsed).toEqual({ model: 'gpt-5.4' });
});

it('returns null for invalid JSON', () => {
expect(parseBodyAsObject(Buffer.from('not-json'))).toBeNull();
});

it('returns null for arrays and non-objects', () => {
expect(parseBodyAsObject(Buffer.from(JSON.stringify([])))).toBeNull();
expect(parseBodyAsObject(Buffer.from(JSON.stringify('text')))).toBeNull();
});
});
11 changes: 3 additions & 8 deletions containers/api-proxy/model-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/

const { getTierSortedModels } = require('./model-discovery');
const { parseBodyAsObject } = require('./body-utils');

const DEFAULT_MODEL_FALLBACK = Object.freeze({
enabled: true,
Expand Down Expand Up @@ -418,14 +419,8 @@ function rewriteModelInBody(body, provider, aliases, availableModels, modelFallb
// Only attempt rewrite for non-empty bodies
if (!body || body.length === 0) return null;

let parsed;
try {
parsed = JSON.parse(body.toString('utf8'));
} catch {
return null; // Non-JSON body — skip
}

if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
const parsed = parseBodyAsObject(body);
if (!parsed) return null; // Non-JSON body — skip

// Determine the requested model. If absent, try the default alias ("").
const originalModel = typeof parsed.model === 'string' ? parsed.model : '';
Expand Down
10 changes: 3 additions & 7 deletions containers/api-proxy/providers/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
composeBodyTransforms,
} = require('../proxy-utils');
const { sanitizeNullToolCallTypes } = require('../body-transform');
const { parseBodyAsObject } = require('../body-utils');
const { URL } = require('url');

/**
Expand Down Expand Up @@ -151,13 +152,8 @@ function parseByokExtraBodyFields(raw) {
function injectByokExtraBodyFields(body, fields) {
if (!fields || Object.keys(fields).length === 0) return null;

let parsed;
try {
parsed = JSON.parse(body.toString('utf8'));
} catch {
return null;
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
const parsed = parseBodyAsObject(body);
if (!parsed) return null;

let changed = false;
for (const [field, value] of Object.entries(fields)) {
Expand Down
Loading