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
49 changes: 2 additions & 47 deletions containers/api-proxy/providers/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand All @@ -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; }
);
Comment on lines 193 to 196

// Pre-computed models path used by getModelsFetchConfig and getReflectionInfo.
Expand Down Expand Up @@ -410,7 +366,6 @@ module.exports = {
deriveCopilotApiTarget,
deriveGitHubApiTarget,
deriveGitHubApiBasePath,
normalizeNullTypeToolCalls,
COPILOT_PLACEHOLDER_TOKEN,
},
};
13 changes: 7 additions & 6 deletions containers/api-proxy/server.auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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',
Expand All @@ -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');
Comment on lines 174 to 195
});

Expand All @@ -210,7 +211,7 @@ describe('normalizeNullTypeToolCalls', () => {
],
}));

expect(normalizeNullTypeToolCalls(input)).toBeNull();
expect(sanitizeNullToolCallTypes(input)).toBeNull();
});
});

Expand Down
Loading