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:
parent
70d6b28853
commit
a442aee5f4
|
|
@ -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' })
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user