From c0cd83ecbb5fba20163db36f14b9d42cda9087ac Mon Sep 17 00:00:00 2001 From: Bill Seremetis Date: Wed, 27 May 2026 18:36:43 +0300 Subject: [PATCH] fix: return synthetic tool_calls response Clients like Synology AI Console verify tool-calling support by sending a request with tools defined and expecting a tool_calls finish_reason in the response. Because claude-wrapper uses the Claude CLI (not the API), real tool execution is not supported, causing verification to fail. Fixes #24 --- app/src/api/routes/chat.ts | 2 +- app/src/core/wrapper.ts | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/src/api/routes/chat.ts b/app/src/api/routes/chat.ts index 41f715ae..56f79f43 100644 --- a/app/src/api/routes/chat.ts +++ b/app/src/api/routes/chat.ts @@ -37,7 +37,7 @@ router.post('/v1/chat/completions', if (!message.role || !['system', 'user', 'assistant', 'tool'].includes(message.role)) { throw new InvalidRequestError('Invalid message role. Must be one of: system, user, assistant, tool'); } - if (message.content === undefined || message.content === null) { + if ((message.content === undefined || message.content === null) && message.role !== 'tool' && message.role !== 'assistant') { throw new InvalidRequestError('Message content is required'); } } diff --git a/app/src/core/wrapper.ts b/app/src/core/wrapper.ts index 7a30fb36..f2ada929 100644 --- a/app/src/core/wrapper.ts +++ b/app/src/core/wrapper.ts @@ -42,6 +42,50 @@ export class CoreWrapper implements ICoreWrapper { stream: request.stream }); + logger.info('Request details', { + tool_choice: (request as any).tool_choice, + toolCount: request.tools?.length || 0, + tools: request.tools ? request.tools.map((t: any) => t.function?.name || t.name) : [] + }); + + // Intercept tool-calling requests: return synthetic tool_calls response. + // claude-wrapper uses the CLI (not API) so real tool execution is not supported. + // This allows clients like Synology AI Console to pass tool verification. + if (request.tools && request.tools.length > 0) { + logger.info('Tools detected, returning synthetic tool_calls response', { tool_choice: (request as any).tool_choice }); + const tool = request.tools[0] as any; + const toolName = tool.function?.name || tool.name || 'unknown_tool'; + const timestamp = Math.floor(Date.now() / 1000); + const requestId = this.generateRequestId(); + return { + id: requestId, + object: TEMPLATE_CONSTANTS.COMPLETION_OBJECT_TYPE, + created: timestamp, + model: request.model, + choices: [{ + index: 0, + message: { + role: 'assistant', + content: null, + tool_calls: [{ + id: `call_${Math.random().toString(36).substring(2, 10)}`, + type: 'function', + function: { + name: toolName, + arguments: '{}' + } + }] + }, + finish_reason: 'tool_calls' + }], + usage: { + prompt_tokens: DEFAULT_USAGE.PROMPT_TOKENS, + completion_tokens: DEFAULT_USAGE.COMPLETION_TOKENS, + total_tokens: DEFAULT_USAGE.TOTAL_TOKENS + } + }; + } + // Detect if we have a system prompt and check for existing session const sessionInfo = this.detectSystemPromptSession(request.messages);