Skip to content

Commit 7ea2aa2

Browse files
committed
🤖 fix: handle null citations in Anthropic API proxy responses
Some API proxies 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.
1 parent d06a4a2 commit 7ea2aa2

File tree

1 file changed

+81
-3
lines changed

1 file changed

+81
-3
lines changed

src/node/services/aiService.ts

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,82 @@ 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 API proxies normalize the response
178+
* and set `citations: null` on all content blocks, which causes validation errors:
179+
* "Invalid input: expected array, received null"
180+
*
181+
* This wrapper intercepts the response, parses the JSON, removes null citations
182+
* fields from content blocks, and returns a fixed response.
183+
*/
184+
function wrapFetchWithCitationsNullFix(baseFetch: typeof fetch): typeof fetch {
185+
const fixingFetch = async (
186+
input: Parameters<typeof fetch>[0],
187+
init?: Parameters<typeof fetch>[1]
188+
): Promise<Response> => {
189+
const response = await baseFetch(input, init);
190+
191+
// Only fix successful JSON responses
192+
const contentType = response.headers.get("content-type") ?? "";
193+
if (!response.ok || !contentType.includes("application/json")) {
194+
return response;
195+
}
196+
197+
try {
198+
const json = (await response.json()) as {
199+
content?: Array<{ citations?: unknown }>;
200+
};
201+
202+
// Fix citations: null in content blocks
203+
if (Array.isArray(json.content)) {
204+
let modified = false;
205+
for (const block of json.content) {
206+
if (
207+
block &&
208+
typeof block === "object" &&
209+
"citations" in block &&
210+
block.citations === null
211+
) {
212+
delete block.citations;
213+
modified = true;
214+
}
215+
}
216+
217+
if (modified) {
218+
const fixedBody = JSON.stringify(json);
219+
const fixedHeaders = new Headers(response.headers);
220+
fixedHeaders.set("content-length", String(fixedBody.length));
221+
return new Response(fixedBody, {
222+
status: response.status,
223+
statusText: response.statusText,
224+
headers: fixedHeaders,
225+
});
226+
}
227+
}
228+
229+
// No fix needed, but we already consumed the body - reconstruct
230+
return new Response(JSON.stringify(json), {
231+
status: response.status,
232+
statusText: response.statusText,
233+
headers: response.headers,
234+
});
235+
} catch {
236+
// Can't fix - response body was consumed but failed to parse
237+
// This shouldn't happen for valid JSON, but return a failed response
238+
return new Response(null, {
239+
status: 500,
240+
statusText: "Failed to parse response JSON",
241+
});
242+
}
243+
};
244+
245+
return Object.assign(fixingFetch, baseFetch) as typeof fetch;
246+
}
247+
172248
/**
173249
* Get fetch function for provider - use custom if provided, otherwise unlimited timeout default
174250
*/
@@ -429,11 +505,13 @@ export class AIService extends EventEmitter {
429505

430506
// Lazy-load Anthropic provider to reduce startup time
431507
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)
434508
// Use getProviderFetch to preserve any user-configured custom fetch (e.g., proxies)
509+
// Then chain wrappers:
510+
// 1. citationsFix: removes `citations: null` from proxy responses (breaks SDK validation)
511+
// 2. cacheControl: injects cache_control on tools/messages (SDK doesn't translate providerOptions)
435512
const baseFetch = getProviderFetch(providerConfig);
436-
const fetchWithCacheControl = wrapFetchWithAnthropicCacheControl(baseFetch);
513+
const fetchWithCitationsFix = wrapFetchWithCitationsNullFix(baseFetch);
514+
const fetchWithCacheControl = wrapFetchWithAnthropicCacheControl(fetchWithCitationsFix);
437515
const provider = createAnthropic({
438516
...normalizedConfig,
439517
headers,

0 commit comments

Comments
 (0)