mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-22 17:07:46 +08:00
Merge pull request #169 from CJackHwang/dev
Merge pull request #168 from CJackHwang/codex/fix-vercel-deployment-issue-with-api-calls fix(js): avoid false tool-call capture on plain tool_calls prose
This commit is contained in:
@@ -34,7 +34,8 @@ type toolCallDelta struct {
|
|||||||
Arguments string
|
Arguments string
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolSieveContextTailLimit = 256
|
// Keep in sync with JS TOOL_SIEVE_CONTEXT_TAIL_LIMIT.
|
||||||
|
const toolSieveContextTailLimit = 2048
|
||||||
|
|
||||||
func (s *toolStreamSieveState) resetIncrementalToolState() {
|
func (s *toolStreamSieveState) resetIncrementalToolState() {
|
||||||
s.disableDeltas = false
|
s.disableDeltas = false
|
||||||
|
|||||||
@@ -102,7 +102,10 @@ function extractToolCallObjects(text) {
|
|||||||
const obj = extractJSONObjectFrom(raw, start);
|
const obj = extractJSONObjectFrom(raw, start);
|
||||||
if (obj.ok) {
|
if (obj.ok) {
|
||||||
out.push(raw.slice(start, obj.end).trim());
|
out.push(raw.slice(start, obj.end).trim());
|
||||||
offset = obj.end;
|
// Ensure forward progress even when the matched keyword is outside
|
||||||
|
// the extracted JSON object (e.g. closing XML wrapper tags containing
|
||||||
|
// "tool_calls" after an earlier JSON arguments object).
|
||||||
|
offset = Math.max(obj.end, idx + matched.length);
|
||||||
idx = -1;
|
idx = -1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const TOOL_SIEVE_CONTEXT_TAIL_LIMIT = 4096;
|
// Keep in sync with Go toolSieveContextTailLimit.
|
||||||
|
const TOOL_SIEVE_CONTEXT_TAIL_LIMIT = 2048;
|
||||||
|
|
||||||
function createToolSieveState() {
|
function createToolSieveState() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -227,6 +227,24 @@ test('sieve flushes incomplete captured XML tool blocks without leaking raw tags
|
|||||||
assert.equal(leakedText.includes('<tool_call'), false);
|
assert.equal(leakedText.includes('<tool_call'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sieve captures XML wrapper tags with attributes without leaking wrapper text', () => {
|
||||||
|
const events = runSieve(
|
||||||
|
[
|
||||||
|
'前置正文H。',
|
||||||
|
'<tool_calls id="x"><tool_call><tool_name>read_file</tool_name><parameters>{"path":"README.MD"}</parameters></tool_call></tool_calls>',
|
||||||
|
'后置正文I。',
|
||||||
|
],
|
||||||
|
['read_file'],
|
||||||
|
);
|
||||||
|
const leakedText = collectText(events);
|
||||||
|
const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
|
||||||
|
assert.equal(hasToolCall, true);
|
||||||
|
assert.equal(leakedText.includes('前置正文H。'), true);
|
||||||
|
assert.equal(leakedText.includes('后置正文I。'), true);
|
||||||
|
assert.equal(leakedText.includes('<tool_calls id=\"x\">'), false);
|
||||||
|
assert.equal(leakedText.includes('</tool_calls>'), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('sieve still intercepts large tool json payloads over previous capture limit', () => {
|
test('sieve still intercepts large tool json payloads over previous capture limit', () => {
|
||||||
const large = 'a'.repeat(9000);
|
const large = 'a'.repeat(9000);
|
||||||
const payload = `{"tool_calls":[{"name":"read_file","input":{"path":"${large}"}}]}`;
|
const payload = `{"tool_calls":[{"name":"read_file","input":{"path":"${large}"}}]}`;
|
||||||
@@ -252,6 +270,46 @@ test('sieve keeps plain text intact in tool mode when no tool call appears', ()
|
|||||||
assert.equal(leakedText, '你好,这是普通文本回复。请继续。');
|
assert.equal(leakedText, '你好,这是普通文本回复。请继续。');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sieve keeps plain "tool_calls" prose as text when no valid payload follows', () => {
|
||||||
|
const events = runSieve(
|
||||||
|
['前置。', '这里提到 tool_calls 只是解释,不是调用。', '后置。'],
|
||||||
|
['read_file'],
|
||||||
|
);
|
||||||
|
const leakedText = collectText(events);
|
||||||
|
const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
|
||||||
|
assert.equal(hasToolCall, false);
|
||||||
|
assert.equal(leakedText.includes('tool_calls'), true);
|
||||||
|
assert.equal(leakedText, '前置。这里提到 tool_calls 只是解释,不是调用。后置。');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sieve keeps numbered planning prose before a real tool payload (mobile-chat style)', () => {
|
||||||
|
const events = runSieve(
|
||||||
|
[
|
||||||
|
'好的,我会依次测试每个工具,先把所有工具都调用一遍,然后汇总结果给你看。\n\n1. 获取当前时间\n',
|
||||||
|
'{"tool_calls":[{"name":"get_current_time","input":{}}]}',
|
||||||
|
],
|
||||||
|
['get_current_time'],
|
||||||
|
);
|
||||||
|
const leakedText = collectText(events);
|
||||||
|
const finalCalls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
|
||||||
|
assert.equal(finalCalls.length, 1);
|
||||||
|
assert.equal(finalCalls[0].name, 'get_current_time');
|
||||||
|
assert.equal(leakedText.includes('先把所有工具都调用一遍'), true);
|
||||||
|
assert.equal(leakedText.includes('1. 获取当前时间'), true);
|
||||||
|
assert.equal(leakedText.toLowerCase().includes('tool_calls'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sieve keeps numbered planning prose when no tool payload follows', () => {
|
||||||
|
const events = runSieve(
|
||||||
|
['好的,我会依次测试每个工具。\n\n1. 获取当前时间'],
|
||||||
|
['get_current_time'],
|
||||||
|
);
|
||||||
|
const leakedText = collectText(events);
|
||||||
|
const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
|
||||||
|
assert.equal(hasToolCall, false);
|
||||||
|
assert.equal(leakedText, '好的,我会依次测试每个工具。\n\n1. 获取当前时间');
|
||||||
|
});
|
||||||
|
|
||||||
test('sieve emits unknown tool payload (no args) as executable tool call', () => {
|
test('sieve emits unknown tool payload (no args) as executable tool call', () => {
|
||||||
const events = runSieve(
|
const events = runSieve(
|
||||||
['{"tool_calls":[{"name":"not_in_schema"}]}', '后置正文G。'],
|
['{"tool_calls":[{"name":"not_in_schema"}]}', '后置正文G。'],
|
||||||
|
|||||||
Reference in New Issue
Block a user