Skip to content

feat: add support for Claude structured output#57

Merged
bbbugg merged 11 commits intomainfrom
fix/claude-api
Feb 3, 2026
Merged

feat: add support for Claude structured output#57
bbbugg merged 11 commits intomainfrom
fix/claude-api

Conversation

@bbbugg
Copy link
Member

@bbbugg bbbugg commented Feb 3, 2026

Chinese

  • 支持 Claude 接口的结构化输出
  • 修复在 Claude Code 和 OpenCode 中,不支持的工具调用参数

English

  • Added support for structured output in the Claude API.
  • Fixed unsupported tool call arguments in Claude Code and OpenCode.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_format and output_config parameters, converting them to Gemini's responseSchema
  • Implemented comprehensive schema conversion method _convertSchemaToGemini that 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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bbbugg bbbugg merged commit 5c8cd90 into main Feb 3, 2026
6 checks passed
@github-actions
Copy link

github-actions bot commented Feb 5, 2026

🎉 此 PR 的修改已在版本 v0.6.0 中发布。
🎉 The changes in this PR have been released in version v0.6.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant