mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 14:52:19 +00:00
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:
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).
|
||||||
@@ -64,6 +64,30 @@ function validMessages(messages) {
|
|||||||
return !pending.length;
|
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({
|
export function createOpenAiChatHandler({
|
||||||
annotationGuidance,
|
annotationGuidance,
|
||||||
tools = GEV_REALTIME_TOOLS,
|
tools = GEV_REALTIME_TOOLS,
|
||||||
@@ -126,7 +150,12 @@ export function createOpenAiChatHandler({
|
|||||||
tools: offeredTools(tools, { overpass }).map(
|
tools: offeredTools(tools, { overpass }).map(
|
||||||
({ name, description, parameters }) => ({
|
({ name, description, parameters }) => ({
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: { name, description, parameters, strict: false },
|
function: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
parameters: chatSchema(parameters),
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import assert from 'node:assert/strict';
|
|||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { once } from 'node:events';
|
import { once } from 'node:events';
|
||||||
import { createGeminiChatHandler } from '../server/providers/gemini/chat.js';
|
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 { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
|
||||||
import {
|
import {
|
||||||
realtimeDirectives,
|
realtimeDirectives,
|
||||||
@@ -534,6 +537,11 @@ for (const [name, provider] of Object.entries(providers)) {
|
|||||||
assert.equal(offered.includes('osm_query'), overpass);
|
assert.equal(offered.includes('osm_query'), overpass);
|
||||||
assert.equal(prompt.includes('osm_query'), overpass);
|
assert.equal(prompt.includes('osm_query'), overpass);
|
||||||
assert.ok(offered.includes('resolve_area'));
|
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) => {
|
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();
|
const body = await (await f.send()).json();
|
||||||
assert.equal(body.content.tool_calls, undefined);
|
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: [{}] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user