1414 * accepts OAuth tokens, not API keys.
1515 */
1616
17- const { normalizeApiTarget, normalizeBasePath, createAdapterMethods } = require ( '../proxy-utils' ) ;
17+ const { normalizeApiTarget, normalizeBasePath, createAdapterMethods, composeBodyTransforms } = require ( '../proxy-utils' ) ;
1818const { URL } = require ( 'url' ) ;
1919
2020/**
@@ -137,6 +137,51 @@ function deriveGitHubApiBasePath(env = process.env) {
137137 }
138138}
139139
140+ /**
141+ * Normalize OpenAI-style tool calls that omit the required type field.
142+ *
143+ * Some Copilot/OpenAI-compatible responses can echo tool_calls entries with a
144+ * null/undefined `type`, which later causes upstream validation failures when
145+ * the same message history is sent back. This transform patches only
146+ * function-style tool calls by setting `type: "function"`.
147+ *
148+ * @param {Buffer } body - Raw request body
149+ * @returns {Buffer|null } Updated body or null when no changes are needed
150+ */
151+ function normalizeNullTypeToolCalls ( body ) {
152+ let parsed ;
153+ try {
154+ parsed = JSON . parse ( body . toString ( 'utf8' ) ) ;
155+ } catch {
156+ return null ;
157+ }
158+
159+ if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed ) || ! Array . isArray ( parsed . messages ) ) {
160+ return null ;
161+ }
162+
163+ let changed = false ;
164+ for ( const message of parsed . messages ) {
165+ if ( ! message || typeof message !== 'object' || Array . isArray ( message ) || ! Array . isArray ( message . tool_calls ) ) {
166+ continue ;
167+ }
168+
169+ for ( const toolCall of message . tool_calls ) {
170+ if ( ! toolCall || typeof toolCall !== 'object' || Array . isArray ( toolCall ) ) continue ;
171+ const hasFunctionPayload =
172+ toolCall . function &&
173+ typeof toolCall . function === 'object' &&
174+ ! Array . isArray ( toolCall . function ) ;
175+ if ( toolCall . type == null && hasFunctionPayload ) {
176+ toolCall . type = 'function' ;
177+ changed = true ;
178+ }
179+ }
180+ }
181+
182+ return changed ? Buffer . from ( JSON . stringify ( parsed ) ) : null ;
183+ }
184+
140185/**
141186 * Create the GitHub Copilot provider adapter.
142187 *
@@ -152,7 +197,10 @@ function createCopilotAdapter(env, deps = {}) {
152197 const rawTarget = deriveCopilotApiTarget ( env ) ;
153198 const basePath = normalizeBasePath ( env . COPILOT_API_BASE_PATH ) ;
154199
155- const bodyTransform = deps . bodyTransform || null ;
200+ const bodyTransform = composeBodyTransforms (
201+ deps . bodyTransform || null ,
202+ normalizeNullTypeToolCalls
203+ ) ;
156204
157205 // Pre-computed models path used by getModelsFetchConfig and getReflectionInfo.
158206 // For BYOK/custom providers the base path prefix is included (e.g. /api/v1/models
@@ -334,5 +382,6 @@ module.exports = {
334382 deriveCopilotApiTarget,
335383 deriveGitHubApiTarget,
336384 deriveGitHubApiBasePath,
385+ normalizeNullTypeToolCalls,
337386 } ,
338387} ;
0 commit comments