Skip to content

Commit a259434

Browse files
committed
🤖 fix: handle null citations in Anthropic API proxy responses
Some AI bridge proxies (e.g., Coder's aibridge) normalize Anthropic API responses and incorrectly set 'citations: null' on all content blocks. The Vercel AI SDK's Anthropic provider has strict validation that expects citations to be an array when present, causing validation errors: 'Invalid input: expected array, received null' This adds a fetch wrapper that intercepts responses and removes null citations fields from content blocks before the SDK validates them. The fix is applied only to the Anthropic provider since that's where the validation issue occurs.
1 parent d06a4a2 commit a259434

File tree

1 file changed

+82
-3
lines changed

1 file changed

+82
-3
lines changed

src/node/services/aiService.ts

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,83 @@ function wrapFetchWithAnthropicCacheControl(baseFetch: typeof fetch): typeof fet
169169
return Object.assign(cachingFetch, baseFetch) as typeof fetch;
170170
}
171171

172+
/**
173+
* Wrap fetch to fix Anthropic API responses from proxies that incorrectly
174+
* return `citations: null` instead of omitting the field or returning an array.
175+
*
176+
* The Vercel AI SDK's Anthropic provider has strict validation that expects
177+
* `citations` to be an array when present. Some AI bridge proxies (e.g., Coder's
178+
* aibridge) normalize the response and set `citations: null` on all content blocks,
179+
* which causes validation errors like:
180+
* "Invalid input: expected array, received null"
181+
*
182+
* This wrapper intercepts the response, parses the JSON, removes null citations
183+
* fields from content blocks, and returns a fixed response.
184+
*/
185+
function wrapFetchWithCitationsNullFix(baseFetch: typeof fetch): typeof fetch {
186+
const fixingFetch = async (
187+
input: Parameters<typeof fetch>[0],
188+
init?: Parameters<typeof fetch>[1]
189+
): Promise<Response> => {
190+
const response = await baseFetch(input, init);
191+
192+
// Only fix successful JSON responses
193+
const contentType = response.headers.get("content-type") ?? "";
194+
if (!response.ok || !contentType.includes("application/json")) {
195+
return response;
196+
}
197+
198+
try {
199+
const json = (await response.json()) as {
200+
content?: Array<{ citations?: unknown }>;
201+
};
202+
203+
// Fix citations: null in content blocks
204+
if (Array.isArray(json.content)) {
205+
let modified = false;
206+
for (const block of json.content) {
207+
if (
208+
block &&
209+
typeof block === "object" &&
210+
"citations" in block &&
211+
block.citations === null
212+
) {
213+
delete block.citations;
214+
modified = true;
215+
}
216+
}
217+
218+
if (modified) {
219+
const fixedBody = JSON.stringify(json);
220+
const fixedHeaders = new Headers(response.headers);
221+
fixedHeaders.set("content-length", String(fixedBody.length));
222+
return new Response(fixedBody, {
223+
status: response.status,
224+
statusText: response.statusText,
225+
headers: fixedHeaders,
226+
});
227+
}
228+
}
229+
230+
// No fix needed, but we already consumed the body - reconstruct
231+
return new Response(JSON.stringify(json), {
232+
status: response.status,
233+
statusText: response.statusText,
234+
headers: response.headers,
235+
});
236+
} catch {
237+
// Can't fix - response body was consumed but failed to parse
238+
// This shouldn't happen for valid JSON, but return a failed response
239+
return new Response(null, {
240+
status: 500,
241+
statusText: "Failed to parse response JSON",
242+
});
243+
}
244+
};
245+
246+
return Object.assign(fixingFetch, baseFetch) as typeof fetch;
247+
}
248+
172249
/**
173250
* Get fetch function for provider - use custom if provided, otherwise unlimited timeout default
174251
*/
@@ -429,11 +506,13 @@ export class AIService extends EventEmitter {
429506

430507
// Lazy-load Anthropic provider to reduce startup time
431508
const { createAnthropic } = await PROVIDER_REGISTRY.anthropic();
432-
// Wrap fetch to inject cache_control on tools and messages
433-
// (SDK doesn't translate providerOptions to cache_control for these)
434509
// Use getProviderFetch to preserve any user-configured custom fetch (e.g., proxies)
510+
// Then chain wrappers:
511+
// 1. citationsFix: removes `citations: null` from API proxy responses (breaks SDK validation)
512+
// 2. cacheControl: injects cache_control on tools/messages (SDK doesn't translate providerOptions)
435513
const baseFetch = getProviderFetch(providerConfig);
436-
const fetchWithCacheControl = wrapFetchWithAnthropicCacheControl(baseFetch);
514+
const fetchWithCitationsFix = wrapFetchWithCitationsNullFix(baseFetch);
515+
const fetchWithCacheControl = wrapFetchWithAnthropicCacheControl(fetchWithCitationsFix);
437516
const provider = createAnthropic({
438517
...normalizedConfig,
439518
headers,

0 commit comments

Comments
 (0)