Skip to content

[Duplicate Code] Duplicate null-tool-call normalization logic in copilot.js and body-transform.js #3269

@github-actions

Description

@github-actions

Duplicate Code Opportunity

Summary

  • Pattern: Near-identical function for normalizing null type fields in OpenAI-style tool_calls arrays
  • Locations: containers/api-proxy/providers/copilot.js:187–221 and containers/api-proxy/body-transform.js:12–65
  • Impact: ~35 lines of near-duplicate request-body transformation logic; a bug fixed in one function won't automatically be caught in the other

Evidence

copilot.js lines 187–221normalizeNullTypeToolCalls:

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;
}

body-transform.js lines 12–65sanitizeNullToolCallTypes (already an exported shared utility):

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

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

  let changed = false;
  let normalizedCount = 0;
  let droppedCount = 0;

  for (const message of parsed.messages) {
    if (!message || typeof message !== 'object' || !Array.isArray(message.tool_calls)) continue;
    const nextToolCalls = [];
    for (const toolCall of message.tool_calls) {
      if (toolCall && typeof toolCall === 'object' && Object.hasOwn(toolCall, 'type') && toolCall.type === null) {
        if (toolCall.function && typeof toolCall.function === 'object') {
          nextToolCalls.push({ ...toolCall, type: 'function' });
          normalizedCount += 1;
        } else {
          droppedCount += 1;
        }
        changed = true;
        continue;
      }
      nextToolCalls.push(toolCall);
    }
    message.tool_calls = nextToolCalls;
  }

  if (!changed) return null;
  return { body: Buffer.from(JSON.stringify(parsed)), normalizedCount, droppedCount };
}

The two functions diverge slightly:

  • body-transform.js also drops malformed entries (no function payload) and returns { body, normalizedCount, droppedCount }.
  • copilot.js skips malformed entries silently and returns Buffer|null.

Suggested Refactoring

body-transform.js is already the shared utilities module (module.exports exposes sanitizeNullToolCallTypes). The copilot.js version should be removed and the adapter should delegate to the shared function:

// In copilot.js — remove normalizeNullTypeToolCalls entirely and instead:
const { sanitizeNullToolCallTypes } = require('../body-transform');

// In createCopilotAdapter's bodyTransforms array, replace the local function reference:
(body) => {
  const result = sanitizeNullToolCallTypes(body);
  return result ? result.body : null;
}

The return-type difference (counted result vs plain Buffer) can be bridged with a one-line wrapper as shown above.

Affected Files

  • containers/api-proxy/providers/copilot.js — lines 187–221 (function definition), 239 (usage), 413 (export)
  • containers/api-proxy/body-transform.js — lines 12–65

Effort Estimate

Low


Detected by Duplicate Code Detector workflow. Run date: 2026-05-16

Generated by Duplicate Code Detector · ● 9.9M ·

  • expires on Jun 15, 2026, 9:58 PM UTC

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions