Skip to content

Commit 777c0b2

Browse files
authored
refactor(api-proxy): deduplicate null tool_call type normalization (#3272)
* Initial plan * refactor(api-proxy): remove duplicate normalizeNullTypeToolCalls, delegate to sanitizeNullToolCallTypes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent babcbfa commit 777c0b2

2 files changed

Lines changed: 9 additions & 53 deletions

File tree

containers/api-proxy/providers/copilot.js

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
createAdapterMethods,
2222
composeBodyTransforms,
2323
} = require('../proxy-utils');
24+
const { sanitizeNullToolCallTypes } = require('../body-transform');
2425
const { URL } = require('url');
2526

2627
// AWF injects this sentinel value into the agent environment for credential isolation.
@@ -173,51 +174,6 @@ function deriveGitHubApiBasePath(env = process.env) {
173174
}
174175
}
175176

176-
/**
177-
* Normalize OpenAI-style tool calls that omit the required type field.
178-
*
179-
* Some Copilot/OpenAI-compatible responses can echo tool_calls entries with a
180-
* null/undefined `type`, which later causes upstream validation failures when
181-
* the same message history is sent back. This transform patches only
182-
* function-style tool calls by setting `type: "function"`.
183-
*
184-
* @param {Buffer} body - Raw request body
185-
* @returns {Buffer|null} Updated body or null when no changes are needed
186-
*/
187-
function normalizeNullTypeToolCalls(body) {
188-
let parsed;
189-
try {
190-
parsed = JSON.parse(body.toString('utf8'));
191-
} catch {
192-
return null;
193-
}
194-
195-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Array.isArray(parsed.messages)) {
196-
return null;
197-
}
198-
199-
let changed = false;
200-
for (const message of parsed.messages) {
201-
if (!message || typeof message !== 'object' || Array.isArray(message) || !Array.isArray(message.tool_calls)) {
202-
continue;
203-
}
204-
205-
for (const toolCall of message.tool_calls) {
206-
if (!toolCall || typeof toolCall !== 'object' || Array.isArray(toolCall)) continue;
207-
const hasFunctionPayload =
208-
toolCall.function &&
209-
typeof toolCall.function === 'object' &&
210-
!Array.isArray(toolCall.function);
211-
if (toolCall.type == null && hasFunctionPayload) {
212-
toolCall.type = 'function';
213-
changed = true;
214-
}
215-
}
216-
}
217-
218-
return changed ? Buffer.from(JSON.stringify(parsed)) : null;
219-
}
220-
221177
/**
222178
* Create the GitHub Copilot provider adapter.
223179
*
@@ -236,7 +192,7 @@ function createCopilotAdapter(env, deps = {}) {
236192

237193
const bodyTransform = composeBodyTransforms(
238194
deps.bodyTransform || null,
239-
normalizeNullTypeToolCalls
195+
(body) => { const result = sanitizeNullToolCallTypes(body); return result ? result.body : null; }
240196
);
241197

242198
// Pre-computed models path used by getModelsFetchConfig and getReflectionInfo.
@@ -410,7 +366,6 @@ module.exports = {
410366
deriveCopilotApiTarget,
411367
deriveGitHubApiTarget,
412368
deriveGitHubApiBasePath,
413-
normalizeNullTypeToolCalls,
414369
COPILOT_PLACEHOLDER_TOKEN,
415370
},
416371
};

containers/api-proxy/server.auth.test.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77
const { shouldStripHeader } = require('./proxy-utils');
88
const {
9-
_testing: { resolveCopilotAuthToken, resolveApiKey, stripBearerPrefix, normalizeNullTypeToolCalls, COPILOT_PLACEHOLDER_TOKEN },
9+
_testing: { resolveCopilotAuthToken, resolveApiKey, stripBearerPrefix, COPILOT_PLACEHOLDER_TOKEN },
1010
createCopilotAdapter,
1111
} = require('./providers/copilot');
12+
const { sanitizeNullToolCallTypes } = require('./body-transform');
1213

1314
describe('shouldStripHeader', () => {
1415
it('should strip authorization header', () => {
@@ -170,7 +171,7 @@ describe('resolveApiKey', () => {
170171
});
171172
});
172173

173-
describe('normalizeNullTypeToolCalls', () => {
174+
describe('sanitizeNullToolCallTypes (via copilot body transform)', () => {
174175
it('normalizes null tool_call type to "function" in outgoing message history', () => {
175176
const input = Buffer.from(JSON.stringify({
176177
model: 'gpt-5.4',
@@ -188,9 +189,9 @@ describe('normalizeNullTypeToolCalls', () => {
188189
],
189190
}));
190191

191-
const transformed = normalizeNullTypeToolCalls(input);
192-
expect(transformed).not.toBeNull();
193-
const parsed = JSON.parse(transformed.toString('utf8'));
192+
const result = sanitizeNullToolCallTypes(input);
193+
expect(result).not.toBeNull();
194+
const parsed = JSON.parse(result.body.toString('utf8'));
194195
expect(parsed.messages[0].tool_calls[0].type).toBe('function');
195196
});
196197

@@ -210,7 +211,7 @@ describe('normalizeNullTypeToolCalls', () => {
210211
],
211212
}));
212213

213-
expect(normalizeNullTypeToolCalls(input)).toBeNull();
214+
expect(sanitizeNullToolCallTypes(input)).toBeNull();
214215
});
215216
});
216217

0 commit comments

Comments
 (0)