Merge pull request #1616 from bilawalsidhu/fix/chat-tool-schema-patterns

fix(chat): send OpenAI chat tool schemas Chat Completions accepts
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-06 22:21:05 -08:00
commit c0a5d4141d
3 files changed
+65 -2

No files matched your search

@@ -0,0 +1,4 @@
- Typed chat on OpenAI answers again: OpenAI refused every message because
`resolve_area`'s area-id pattern uses Unicode classes Chat Completions does
not accept. Chat leaves those patterns out; area ids are still checked
before the tool runs (samehkhamis, #1616).
+30 -1
View File
@@ -64,6 +64,30 @@ function validMessages(messages) {
return !pending.length;
}
/**
* A tool schema without the `pattern`s Chat Completions refuses: it answers
* 400 for a whole request whose tool uses Unicode property classes (`\p{L}`),
* as `resolve_area`'s area ids do. Every argument is still checked against
* the action's own contract before the tool runs, so the model loses a hint,
* not a rule. Realtime voice accepts these patterns and keeps them.
*/
export function chatSchema(schema) {
if (Array.isArray(schema)) return schema.map(chatSchema);
if (!object(schema)) return schema;
return Object.fromEntries(
Object.entries(schema)
.filter(
([key, value]) =>
!(
key === 'pattern' &&
typeof value === 'string' &&
/\\[pP]\{/.test(value)
),
)
.map(([key, value]) => [key, chatSchema(value)]),
);
}
export function createOpenAiChatHandler({
annotationGuidance,
tools = GEV_REALTIME_TOOLS,
@@ -126,7 +150,12 @@ export function createOpenAiChatHandler({
tools: offeredTools(tools, { overpass }).map(
({ name, description, parameters }) => ({
type: 'function',
function: { name, description, parameters, strict: false },
function: {
name,
description,
parameters: chatSchema(parameters),
strict: false,
},
}),
),
},
+31 -1
View File
@@ -3,7 +3,10 @@ import assert from 'node:assert/strict';
import http from 'node:http';
import { once } from 'node:events';
import { createGeminiChatHandler } from '../server/providers/gemini/chat.js';
import { createOpenAiChatHandler } from '../server/providers/openai/chat.js';
import {
chatSchema,
createOpenAiChatHandler,
} from '../server/providers/openai/chat.js';
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
import {
realtimeDirectives,
@@ -534,6 +537,11 @@ for (const [name, provider] of Object.entries(providers)) {
assert.equal(offered.includes('osm_query'), overpass);
assert.equal(prompt.includes('osm_query'), overpass);
assert.ok(offered.includes('resolve_area'));
// Chat Completions answers 400 invalid_function_parameters for a
// pattern with Unicode property classes, as resolve_area's area ids
// use; OpenAI chat leaves those patterns out.
if (name === 'openai')
assert.doesNotMatch(JSON.stringify(body.tools), /\\\\[pP]\{/);
}
});
test(`${name} throttled replies say they are rate limited`, async (t) => {
@@ -697,3 +705,25 @@ test('OpenAI empty tool arrays are removed before history is returned', async (t
const body = await (await f.send()).json();
assert.equal(body.content.tool_calls, undefined);
});
test('OpenAI chat schemas drop only the patterns Chat Completions refuses', () => {
assert.match(JSON.stringify(GEV_REALTIME_TOOLS), /\\\\[pP]\{/);
assert.deepEqual(
chatSchema({
type: 'object',
properties: {
id: { type: 'string', pattern: '^ne:[\\p{L}]+$', maxLength: 80 },
code: { type: 'string', pattern: '^[A-Z]{2}$' },
list: { type: 'array', items: [{ pattern: '\\P{N}' }] },
},
}),
{
type: 'object',
properties: {
id: { type: 'string', maxLength: 80 },
code: { type: 'string', pattern: '^[A-Z]{2}$' },
list: { type: 'array', items: [{}] },
},
},
);
});