mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-29 18:11:40 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afcf201367 | ||
|
|
45b4dc6aa4 |
No files matched your search
@@ -1234,3 +1234,74 @@ test('re-injects the remembered tool name when the SDK missed the naming chunk',
|
||||
assert.deepEqual(executedCommands, ['pwd']);
|
||||
assert.equal(text, 'tool completed');
|
||||
});
|
||||
|
||||
for (const indexes of [[0], [1], [1, 4], [4, 1]]) {
|
||||
test(`completes terminal_execute with streamed tool indexes ${indexes.join(',')} (#3274)`, async (t) => {
|
||||
const globals = globalThis as unknown as { window?: unknown };
|
||||
const originalWindow = globals.window;
|
||||
t.after(() => { globals.window = originalWindow; });
|
||||
const dataHandlers = new Map<string, (data: string) => void>();
|
||||
const endHandlers = new Map<string, () => void>();
|
||||
const sentBodies: Array<Record<string, unknown>> = [];
|
||||
globals.window = {
|
||||
netcatty: {
|
||||
aiChatCancel: async () => true,
|
||||
onAiStreamData: (id: string, cb: (data: string) => void) => {
|
||||
dataHandlers.set(id, cb);
|
||||
return () => dataHandlers.delete(id);
|
||||
},
|
||||
onAiStreamEnd: (id: string, cb: () => void) => {
|
||||
endHandlers.set(id, cb);
|
||||
return () => endHandlers.delete(id);
|
||||
},
|
||||
onAiStreamError: () => () => undefined,
|
||||
aiChatStream: async (id: string, _url: string, _headers: unknown, body: string) => {
|
||||
sentBodies.push(JSON.parse(body));
|
||||
const emit = (delta: unknown, finishReason: string | null = null) => dataHandlers.get(id)?.(JSON.stringify({
|
||||
id: 'chatcmpl-sparse', object: 'chat.completion.chunk', created: 0, model: 'test',
|
||||
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
||||
}));
|
||||
if (sentBodies.length === 1) {
|
||||
emit({ content: 'Checking.' });
|
||||
for (const index of indexes) {
|
||||
emit({ tool_calls: [{ index, id: `call_${index}`, type: 'function',
|
||||
function: { name: 'terminal_execute', arguments: '{"command":' } }] });
|
||||
}
|
||||
// Interleave continuations in reverse order to verify stable correlation.
|
||||
for (const index of [...indexes].reverse()) {
|
||||
emit({ tool_calls: [{ index, function: { arguments: `"echo ${index}"}` } }] });
|
||||
}
|
||||
emit({}, 'tool_calls');
|
||||
} else {
|
||||
emit({ content: 'Done.' });
|
||||
emit({}, 'stop');
|
||||
}
|
||||
endHandlers.get(id)?.();
|
||||
return { ok: true, statusCode: 200, statusText: 'OK' };
|
||||
},
|
||||
},
|
||||
};
|
||||
const executed: string[] = [];
|
||||
const result = streamText({
|
||||
model: createModelFromConfig({ id: 'sparse-test', providerId: 'custom', name: 'Test',
|
||||
apiKey: 'test', baseURL: 'https://example.test/v1', defaultModel: 'test', enabled: true }),
|
||||
messages: [{ role: 'user', content: 'Check the terminal' }],
|
||||
tools: { terminal_execute: tool({ inputSchema: z.object({ command: z.string() }),
|
||||
execute: async ({ command }) => { executed.push(command); return { ok: true }; } }) },
|
||||
stopWhen: isStepCount(2),
|
||||
});
|
||||
const errors: unknown[] = [];
|
||||
let text = '';
|
||||
for await (const part of result.stream) {
|
||||
if (part.type === 'error') errors.push(part.error);
|
||||
if (part.type === 'text-delta') text += part.text;
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(executed.sort(), indexes.map(index => `echo ${index}`).sort());
|
||||
assert.equal(text, 'Checking.Done.');
|
||||
assert.equal(sentBodies.length, 2);
|
||||
const messages = sentBodies[1].messages as Array<{ role: string; tool_call_id?: string }>;
|
||||
assert.deepEqual(messages.filter(message => message.role === 'tool').map(message => message.tool_call_id).sort(),
|
||||
indexes.map(index => `call_${index}`).sort());
|
||||
});
|
||||
}
|
||||
@@ -136,6 +136,7 @@ function createOpenAIChatToolCallNormalizer(requestId: string): (data: string) =
|
||||
// Keys forwarded inside choices array position 0 — the only element the
|
||||
// AI SDK reads. Tool calls living at position > 0 are invisible to it.
|
||||
const sdkVisibleKeys = new Set<string>();
|
||||
const sdkIndexesByKey = new Map<string, number>();
|
||||
const requestIdToken = requestId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
|
||||
return (data: string): string => {
|
||||
@@ -165,6 +166,23 @@ function createOpenAIChatToolCallNormalizer(requestId: string): (data: string) =
|
||||
const choiceIndex = typeof choiceRecord.index === 'number' ? choiceRecord.index : choicePosition;
|
||||
let deltaChanged = false;
|
||||
const normalizedToolCalls: unknown[] = [];
|
||||
const forwardToolCall = (toolCall: Record<string, unknown>, key: string) => {
|
||||
// The SDK reads only choices[0] and stores calls in a dense array.
|
||||
// Assign indexes only when forwarding a named call to that array.
|
||||
if (choicePosition === 0) {
|
||||
let sdkIndex = sdkIndexesByKey.get(key);
|
||||
if (sdkIndex === undefined) {
|
||||
sdkIndex = sdkIndexesByKey.size;
|
||||
sdkIndexesByKey.set(key, sdkIndex);
|
||||
}
|
||||
if (toolCall.index !== sdkIndex) {
|
||||
toolCall = { ...toolCall, index: sdkIndex };
|
||||
changed = true;
|
||||
deltaChanged = true;
|
||||
}
|
||||
}
|
||||
normalizedToolCalls.push(toolCall);
|
||||
};
|
||||
for (const [toolCallPosition, toolCall] of deltaRecord.tool_calls.entries()) {
|
||||
if (!toolCall || typeof toolCall !== 'object') {
|
||||
normalizedToolCalls.push(toolCall);
|
||||
@@ -209,11 +227,11 @@ function createOpenAIChatToolCallNormalizer(requestId: string): (data: string) =
|
||||
normalizedToolCall.function === toolCallRecord.function &&
|
||||
(!rememberedName || hasFunctionName(toolCallRecord))
|
||||
) {
|
||||
normalizedToolCalls.push(toolCall);
|
||||
forwardToolCall(toolCallRecord, key);
|
||||
} else {
|
||||
changed = true;
|
||||
deltaChanged = true;
|
||||
normalizedToolCalls.push(normalizedToolCall);
|
||||
forwardToolCall(normalizedToolCall, key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -254,13 +272,13 @@ function createOpenAIChatToolCallNormalizer(requestId: string): (data: string) =
|
||||
normalizedToolCall.type === toolCallRecord.type &&
|
||||
normalizedToolCall.function === toolCallRecord.function
|
||||
) {
|
||||
normalizedToolCalls.push(toolCall);
|
||||
forwardToolCall(toolCallRecord, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
deltaChanged = true;
|
||||
normalizedToolCalls.push(normalizedToolCall);
|
||||
forwardToolCall(normalizedToolCall, key);
|
||||
}
|
||||
|
||||
if (!deltaChanged) return choice;
|
||||
|
||||
Reference in new issue
Block a user