fix: OpenAI adapter tool calling compatibility

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.
This commit is contained in:
uk0 2026-04-06 13:31:28 +08:00 committed by James Feng
parent 70d6b28853
commit a442aee5f4

View File

@ -1,5 +1,8 @@
import { describe, expect, test } from 'bun:test' import { describe, expect, test } from 'bun:test'
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../convertTools.js' import {
anthropicToolsToOpenAI,
anthropicToolChoiceToOpenAI,
} from '../convertTools.js'
describe('anthropicToolsToOpenAI', () => { describe('anthropicToolsToOpenAI', () => {
test('converts basic tool', () => { test('converts basic tool', () => {
@ -18,25 +21,30 @@ describe('anthropicToolsToOpenAI', () => {
const result = anthropicToolsToOpenAI(tools as any) const result = anthropicToolsToOpenAI(tools as any)
expect(result).toEqual([{ expect(result).toEqual([
type: 'function', {
function: { type: 'function',
name: 'bash', function: {
description: 'Run a bash command', name: 'bash',
parameters: { description: 'Run a bash command',
type: 'object', parameters: {
properties: { command: { type: 'string' } }, type: 'object',
required: ['command'], properties: { command: { type: 'string' } },
required: ['command'],
},
}, },
}, },
}]) ])
}) })
test('uses empty schema when input_schema missing', () => { test('uses empty schema when input_schema missing', () => {
const tools = [{ type: 'custom', name: 'noop', description: 'no-op' }] const tools = [{ type: 'custom', name: 'noop', description: 'no-op' }]
const result = anthropicToolsToOpenAI(tools as any) 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', () => { test('strips Anthropic-specific fields', () => {
@ -76,7 +84,7 @@ describe('anthropicToolsToOpenAI', () => {
}, },
] ]
const result = anthropicToolsToOpenAI(tools as any) 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).toEqual({ enum: ['read'] })
expect(props.properties.mode.const).toBeUndefined() expect(props.properties.mode.const).toBeUndefined()
expect(props.properties.name).toEqual({ type: 'string' }) expect(props.properties.name).toEqual({ type: 'string' })
@ -110,8 +118,10 @@ describe('anthropicToolsToOpenAI', () => {
}, },
] ]
const result = anthropicToolsToOpenAI(tools as any) const result = anthropicToolsToOpenAI(tools as any)
const params = (result[0] as { function: { parameters: any } }).function.parameters as any const params = result[0].function.parameters as any
expect(params.properties.outer.properties.inner).toEqual({ enum: ['fixed'] }) expect(params.properties.outer.properties.inner).toEqual({
enum: ['fixed'],
})
expect(params.definitions.MyType.properties.field).toEqual({ enum: [42] }) expect(params.definitions.MyType.properties.field).toEqual({ enum: [42] })
}) })
@ -125,18 +135,14 @@ describe('anthropicToolsToOpenAI', () => {
type: 'object', type: 'object',
properties: { properties: {
val: { val: {
anyOf: [ anyOf: [{ const: 'a' }, { const: 'b' }, { type: 'string' }],
{ const: 'a' },
{ const: 'b' },
{ type: 'string' },
],
}, },
}, },
}, },
}, },
] ]
const result = anthropicToolsToOpenAI(tools as any) 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[0]).toEqual({ enum: ['a'] })
expect(anyOf[1]).toEqual({ enum: ['b'] }) expect(anyOf[1]).toEqual({ enum: ['b'] })
expect(anyOf[2]).toEqual({ type: 'string' }) expect(anyOf[2]).toEqual({ type: 'string' })