first commit
This commit is contained in:
19
test/fixtures/ollama-toolcalls-response.json
vendored
Executable file
19
test/fixtures/ollama-toolcalls-response.json
vendored
Executable file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"model": "Huihui-Qwen3.5-27B-Claude-4.6-Opus-abliterated-4bit",
|
||||
"created_at": "2024-05-01T10:00:00.000000Z",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read",
|
||||
"arguments": "{\"path\":\"/tmp/test.txt\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"done": true
|
||||
}
|
||||
9
test/fixtures/ollama-xml-response.json
vendored
Executable file
9
test/fixtures/ollama-xml-response.json
vendored
Executable file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"model": "Huihui-Qwen3.5-27B-Claude-4.6-Opus-abliterated-4bit",
|
||||
"created_at": "2024-05-01T10:00:00.000000Z",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I will read the file for you.\n<tool_call>\n<function=read>\n<parameter=path>\n/tmp/test.txt\n</parameter>\n</function>\n</tool_call>"
|
||||
},
|
||||
"done": true
|
||||
}
|
||||
32
test/fixtures/openclaw-like-request.json
vendored
Executable file
32
test/fixtures/openclaw-like-request.json
vendored
Executable file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"model": "hotwa/qwen35-9b-agent:latest",
|
||||
"stream": false,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "请读取 /tmp/test.txt 的内容"
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read",
|
||||
"description": "Read a file from disk",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
78
test/integration.proxy.test.ts
Executable file
78
test/integration.proxy.test.ts
Executable file
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { buildServer } from '../src/server';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Proxy Integration Test', () => {
|
||||
let server: FastifyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
server = buildServer();
|
||||
// In vitest we can mock the global fetch
|
||||
global.fetch = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.close();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('proxies request and rewrites XML response to tool_calls', async () => {
|
||||
// Read fixtures
|
||||
const requestFixturePath = path.join(__dirname, 'fixtures', 'openclaw-like-request.json');
|
||||
const responseFixturePath = path.join(__dirname, 'fixtures', 'ollama-xml-response.json');
|
||||
|
||||
const requestJson = JSON.parse(fs.readFileSync(requestFixturePath, 'utf8'));
|
||||
const responseJson = JSON.parse(fs.readFileSync(responseFixturePath, 'utf8'));
|
||||
|
||||
// Mock fetch to return the ollama-xml-response.json
|
||||
(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => responseJson
|
||||
});
|
||||
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/chat',
|
||||
payload: requestJson
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
|
||||
// Verify proxy forwarded it
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
const fetchArgs = (global.fetch as any).mock.calls[0];
|
||||
expect(fetchArgs[0]).toContain('/api/chat');
|
||||
|
||||
const upstreamBody = JSON.parse(fetchArgs[1].body);
|
||||
expect(upstreamBody.model).toBe('Huihui-Qwen3.5-27B-Claude-4.6-Opus-abliterated-4bit');
|
||||
|
||||
// Verify response was rewritten
|
||||
expect(body.message.content).toBe("");
|
||||
expect(body.message.tool_calls).toBeDefined();
|
||||
expect(body.message.tool_calls).toHaveLength(1);
|
||||
expect(body.message.tool_calls[0].function.name).toBe('read');
|
||||
expect(JSON.parse(body.message.tool_calls[0].function.arguments)).toEqual({
|
||||
path: "/tmp/test.txt"
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects streaming requests cleanly', async () => {
|
||||
const requestFixturePath = path.join(__dirname, 'fixtures', 'openclaw-like-request.json');
|
||||
const requestJson = JSON.parse(fs.readFileSync(requestFixturePath, 'utf8'));
|
||||
requestJson.stream = true;
|
||||
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/chat',
|
||||
payload: requestJson
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = JSON.parse(response.payload);
|
||||
expect(body.error).toContain('Streaming is not supported');
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
52
test/response-rewriter.test.ts
Executable file
52
test/response-rewriter.test.ts
Executable file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { rewriteResponse } from '../src/proxy/response-rewriter';
|
||||
import { OllamaChatResponse } from '../src/types/ollama';
|
||||
|
||||
describe('Response Rewriter', () => {
|
||||
it('rewrites XML tool call in content into structured tool_calls', () => {
|
||||
const inputResponse: OllamaChatResponse = {
|
||||
model: "test-model",
|
||||
done: true,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "<function=read>\n<parameter=path>\n/tmp/test.txt\n</parameter>\n</function>"
|
||||
}
|
||||
};
|
||||
|
||||
const result = rewriteResponse(inputResponse);
|
||||
|
||||
expect(result.message.content).toBe("");
|
||||
expect(result.message.tool_calls).toBeDefined();
|
||||
expect(result.message.tool_calls).toHaveLength(1);
|
||||
|
||||
const toolCall = result.message.tool_calls![0];
|
||||
expect(toolCall.type).toBe('function');
|
||||
expect(toolCall.function.name).toBe('read');
|
||||
|
||||
const argsObject = JSON.parse(toolCall.function.arguments);
|
||||
expect(argsObject).toEqual({ path: '/tmp/test.txt' });
|
||||
});
|
||||
|
||||
it('does not touch response that already has tool_calls', () => {
|
||||
const inputResponse: OllamaChatResponse = {
|
||||
model: "test-model",
|
||||
done: true,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "Here are the calls",
|
||||
tool_calls: [
|
||||
{
|
||||
id: "123",
|
||||
type: "function",
|
||||
function: { name: "read", arguments: "{}" }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const result = rewriteResponse(inputResponse);
|
||||
|
||||
expect(result.message.content).toBe("Here are the calls"); // not cleared
|
||||
expect(result.message.tool_calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
48
test/xml-toolcall.test.ts
Executable file
48
test/xml-toolcall.test.ts
Executable file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseXmlToolCalls } from '../src/parsers/xml-toolcall';
|
||||
|
||||
describe('XML ToolCall Parser', () => {
|
||||
it('parses basic XML tool call correctly', () => {
|
||||
const content = `
|
||||
<tool_call>
|
||||
<function=read>
|
||||
<parameter=path>
|
||||
/tmp/test.txt
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>
|
||||
`;
|
||||
|
||||
const result = parseXmlToolCalls(content);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('read');
|
||||
expect(result[0].args).toEqual({ path: '/tmp/test.txt' });
|
||||
});
|
||||
|
||||
it('parses multiple parameters correctly', () => {
|
||||
const content = `
|
||||
<function=write>
|
||||
<parameter=path>
|
||||
/tmp/a.txt
|
||||
</parameter>
|
||||
<parameter=content>
|
||||
hello
|
||||
</parameter>
|
||||
</function>
|
||||
`;
|
||||
|
||||
const result = parseXmlToolCalls(content);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('write');
|
||||
expect(result[0].args).toEqual({
|
||||
path: '/tmp/a.txt',
|
||||
content: 'hello'
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores normal text', () => {
|
||||
const content = `I will read the file. Let me check the system.`;
|
||||
const result = parseXmlToolCalls(content);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user