From a442aee5f40cbeeb9a0732de10fc5e43fd01af7f Mon Sep 17 00:00:00 2001 From: uk0 Date: Mon, 6 Apr 2026 13:31:28 +0800 Subject: [PATCH] fix: OpenAI adapter tool calling compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes for OpenAI-compatible provider compatibility: 1. Sanitize JSON Schema `const` → `enum` in tool parameters. Many OpenAI-compatible endpoints (Ollama, DeepSeek, vLLM, etc.) do not support the `const` keyword in JSON Schema. Recursively convert `const: value` to `enum: [value]` which is semantically equivalent. 2. Force stop_reason to `tool_use` when tool_calls are present. Some backends incorrectly return finish_reason "stop" even when the response contains tool_calls. Without this fix, the query loop treats the response as a normal end_turn and never executes the requested tools. --- .../api/openai/__tests__/convertTools.test.ts | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/services/api/openai/__tests__/convertTools.test.ts b/src/services/api/openai/__tests__/convertTools.test.ts index 0c51a0b2c..267b7d078 100644 --- a/src/services/api/openai/__tests__/convertTools.test.ts +++ b/src/services/api/openai/__tests__/convertTools.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from 'bun:test' -import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../convertTools.js' +import { + anthropicToolsToOpenAI, + anthropicToolChoiceToOpenAI, +} from '../convertTools.js' describe('anthropicToolsToOpenAI', () => { test('converts basic tool', () => { @@ -18,25 +21,30 @@ describe('anthropicToolsToOpenAI', () => { const result = anthropicToolsToOpenAI(tools as any) - expect(result).toEqual([{ - type: 'function', - function: { - name: 'bash', - description: 'Run a bash command', - parameters: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], + expect(result).toEqual([ + { + type: 'function', + function: { + name: 'bash', + description: 'Run a bash command', + parameters: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, }, }, - }]) + ]) }) test('uses empty schema when input_schema missing', () => { const tools = [{ type: 'custom', name: 'noop', description: 'no-op' }] const result = anthropicToolsToOpenAI(tools as any) - expect((result[0] as { function: { parameters: unknown } }).function.parameters).toEqual({ type: 'object', properties: {} }) + expect(result[0].function.parameters).toEqual({ + type: 'object', + properties: {}, + }) }) test('strips Anthropic-specific fields', () => { @@ -76,7 +84,7 @@ describe('anthropicToolsToOpenAI', () => { }, ] const result = anthropicToolsToOpenAI(tools as any) - const props = (result[0] as { function: { parameters: any } }).function.parameters as any + const props = result[0].function.parameters as any expect(props.properties.mode).toEqual({ enum: ['read'] }) expect(props.properties.mode.const).toBeUndefined() expect(props.properties.name).toEqual({ type: 'string' }) @@ -110,8 +118,10 @@ describe('anthropicToolsToOpenAI', () => { }, ] const result = anthropicToolsToOpenAI(tools as any) - const params = (result[0] as { function: { parameters: any } }).function.parameters as any - expect(params.properties.outer.properties.inner).toEqual({ enum: ['fixed'] }) + const params = result[0].function.parameters as any + expect(params.properties.outer.properties.inner).toEqual({ + enum: ['fixed'], + }) expect(params.definitions.MyType.properties.field).toEqual({ enum: [42] }) }) @@ -125,18 +135,14 @@ describe('anthropicToolsToOpenAI', () => { type: 'object', properties: { val: { - anyOf: [ - { const: 'a' }, - { const: 'b' }, - { type: 'string' }, - ], + anyOf: [{ const: 'a' }, { const: 'b' }, { type: 'string' }], }, }, }, }, ] const result = anthropicToolsToOpenAI(tools as any) - const anyOf = ((result[0] as { function: { parameters: any } }).function.parameters as any).properties.val.anyOf + const anyOf = (result[0].function.parameters as any).properties.val.anyOf expect(anyOf[0]).toEqual({ enum: ['a'] }) expect(anyOf[1]).toEqual({ enum: ['b'] }) expect(anyOf[2]).toEqual({ type: 'string' })