feat: add support for Claude structured output#57
Conversation
… in FormatConverter
…nd error handling
There was a problem hiding this comment.
Pull request overview
This pull request adds comprehensive support for Claude's structured output API and fixes issues with unsupported tool call arguments across Claude and OpenAI adapters. The changes enhance format conversion capabilities, improve error handling in streaming modes, and add better support for various Claude API features.
Changes:
- Added support for Claude structured output via
output_formatandoutput_configparameters, converting them to Gemini'sresponseSchema - Implemented comprehensive schema conversion method
_convertSchemaToGeminithat handles nullable types, enums, and filters unsupported JSON Schema fields - Enhanced tool handling with proper ID-to-name mapping for Claude tool results and support for specialized web search/fetch tools
- Improved error handling in all streaming modes (OpenAI, Claude, Gemini) to properly handle error events from the browser
- Added client connection close handlers to cancel browser requests when clients disconnect prematurely
- Updated token usage calculations to include thinking and tool use tokens
- Added Anthropic-specific CORS headers for better Claude API compatibility
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| src/core/FormatConverter.js | Core changes: new _convertSchemaToGemini method, Claude structured output support, tool ID mapping, enhanced schema conversion, thinking config support, image output handling |
| src/core/RequestHandler.js | Error handling improvements in streaming modes, client disconnect handling, logging updates to distinguish between [Adapter] and [Request] contexts |
| src/core/ProxyServerSystem.js | Added Anthropic-specific headers (anthropic-version, anthropic-beta, anthropic-dangerous-direct-browser-access) to CORS allow list |
| .gitignore | Added cache/ directory to prevent committing cache files |
Comments suppressed due to low confidence (1)
src/core/RequestHandler.js:1210
- After an error is detected during Gemini pseudo-stream (lines 1123-1128), the code sends an error chunk and breaks, but then continues to execute lines 1135-1210 which attempts to parse and send the potentially incomplete
fullData. This could result in sending additional data after the error message. Consider tracking whether an error was sent and skipping the remaining data processing in that case.
if (message.event_type === "error") {
this.logger.error(`[Request] Error received during Gemini pseudo-stream: ${message.message}`);
this._sendErrorChunkToClient(res, message.message);
streaming = false;
break;
}
if (message.data) {
fullData += message.data;
}
}
try {
const googleResponse = JSON.parse(fullData);
const candidate = googleResponse.candidates?.[0];
if (candidate && candidate.content && Array.isArray(candidate.content.parts)) {
this.logger.info(
"[Request] Splitting full Gemini response into 'thought' and 'content' chunks for pseudo-stream."
);
const thinkingParts = candidate.content.parts.filter(p => p.thought === true);
const contentParts = candidate.content.parts.filter(p => p.thought !== true);
const role = candidate.content.role || "model";
// Send thinking part first
if (thinkingParts.length > 0) {
const thinkingResponse = {
candidates: [
{
content: {
parts: thinkingParts,
role,
},
// We don't include finishReason here
},
],
// We don't include usageMetadata here
};
res.write(`data: ${JSON.stringify(thinkingResponse)}\n\n`);
this.logger.info(`[Request] Sent ${thinkingParts.length} thinking part(s).`);
}
// Then send content part
if (contentParts.length > 0) {
const contentResponse = {
candidates: [
{
content: {
parts: contentParts,
role,
},
finishReason: candidate.finishReason,
// Other candidate fields can be preserved if needed
},
],
usageMetadata: googleResponse.usageMetadata,
};
res.write(`data: ${JSON.stringify(contentResponse)}\n\n`);
this.logger.info(`[Request] Sent ${contentParts.length} content part(s).`);
} else if (candidate.finishReason) {
// If there's no content but a finish reason, send an empty content message with it
const finalResponse = {
candidates: [
{
content: { parts: [], role },
finishReason: candidate.finishReason,
},
],
usageMetadata: googleResponse.usageMetadata,
};
res.write(`data: ${JSON.stringify(finalResponse)}\n\n`);
}
} else if (fullData) {
// Fallback for responses without candidates or parts, or if parsing fails
this.logger.warn(
"[Request] Response structure not recognized for splitting, sending as a single chunk."
);
res.write(`data: ${fullData}\n\n`);
}
} catch (e) {
this.logger.error(
`[Request] Failed to parse and split Gemini response: ${e.message}. Sending raw data.`
);
if (fullData) {
res.write(`data: ${fullData}\n\n`);
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Chinese
English