diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index a7bc2998..30860822 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -21,6 +21,7 @@ const { createAdapterMethods, composeBodyTransforms, } = require('../proxy-utils'); +const { sanitizeNullToolCallTypes } = require('../body-transform'); const { URL } = require('url'); // AWF injects this sentinel value into the agent environment for credential isolation. @@ -173,51 +174,6 @@ function deriveGitHubApiBasePath(env = process.env) { } } -/** - * Normalize OpenAI-style tool calls that omit the required type field. - * - * Some Copilot/OpenAI-compatible responses can echo tool_calls entries with a - * null/undefined `type`, which later causes upstream validation failures when - * the same message history is sent back. This transform patches only - * function-style tool calls by setting `type: "function"`. - * - * @param {Buffer} body - Raw request body - * @returns {Buffer|null} Updated body or null when no changes are needed - */ -function normalizeNullTypeToolCalls(body) { - let parsed; - try { - parsed = JSON.parse(body.toString('utf8')); - } catch { - return null; - } - - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Array.isArray(parsed.messages)) { - return null; - } - - let changed = false; - for (const message of parsed.messages) { - if (!message || typeof message !== 'object' || Array.isArray(message) || !Array.isArray(message.tool_calls)) { - continue; - } - - for (const toolCall of message.tool_calls) { - if (!toolCall || typeof toolCall !== 'object' || Array.isArray(toolCall)) continue; - const hasFunctionPayload = - toolCall.function && - typeof toolCall.function === 'object' && - !Array.isArray(toolCall.function); - if (toolCall.type == null && hasFunctionPayload) { - toolCall.type = 'function'; - changed = true; - } - } - } - - return changed ? Buffer.from(JSON.stringify(parsed)) : null; -} - /** * Create the GitHub Copilot provider adapter. * @@ -236,7 +192,7 @@ function createCopilotAdapter(env, deps = {}) { const bodyTransform = composeBodyTransforms( deps.bodyTransform || null, - normalizeNullTypeToolCalls + (body) => { const result = sanitizeNullToolCallTypes(body); return result ? result.body : null; } ); // Pre-computed models path used by getModelsFetchConfig and getReflectionInfo. @@ -410,7 +366,6 @@ module.exports = { deriveCopilotApiTarget, deriveGitHubApiTarget, deriveGitHubApiBasePath, - normalizeNullTypeToolCalls, COPILOT_PLACEHOLDER_TOKEN, }, }; diff --git a/containers/api-proxy/server.auth.test.js b/containers/api-proxy/server.auth.test.js index f2f59787..4a51ae1a 100644 --- a/containers/api-proxy/server.auth.test.js +++ b/containers/api-proxy/server.auth.test.js @@ -6,9 +6,10 @@ const { shouldStripHeader } = require('./proxy-utils'); const { - _testing: { resolveCopilotAuthToken, resolveApiKey, stripBearerPrefix, normalizeNullTypeToolCalls, COPILOT_PLACEHOLDER_TOKEN }, + _testing: { resolveCopilotAuthToken, resolveApiKey, stripBearerPrefix, COPILOT_PLACEHOLDER_TOKEN }, createCopilotAdapter, } = require('./providers/copilot'); +const { sanitizeNullToolCallTypes } = require('./body-transform'); describe('shouldStripHeader', () => { it('should strip authorization header', () => { @@ -170,7 +171,7 @@ describe('resolveApiKey', () => { }); }); -describe('normalizeNullTypeToolCalls', () => { +describe('sanitizeNullToolCallTypes (via copilot body transform)', () => { it('normalizes null tool_call type to "function" in outgoing message history', () => { const input = Buffer.from(JSON.stringify({ model: 'gpt-5.4', @@ -188,9 +189,9 @@ describe('normalizeNullTypeToolCalls', () => { ], })); - const transformed = normalizeNullTypeToolCalls(input); - expect(transformed).not.toBeNull(); - const parsed = JSON.parse(transformed.toString('utf8')); + const result = sanitizeNullToolCallTypes(input); + expect(result).not.toBeNull(); + const parsed = JSON.parse(result.body.toString('utf8')); expect(parsed.messages[0].tool_calls[0].type).toBe('function'); }); @@ -210,7 +211,7 @@ describe('normalizeNullTypeToolCalls', () => { ], })); - expect(normalizeNullTypeToolCalls(input)).toBeNull(); + expect(sanitizeNullToolCallTypes(input)).toBeNull(); }); });