| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| let passed = 0; |
| let failed = 0; |
|
|
| function test(name, fn) { |
| try { |
| fn(); |
| console.log(` โ
${name}`); |
| passed++; |
| } catch (e) { |
| console.error(` โ ${name}`); |
| console.error(` ${e.message}`); |
| failed++; |
| } |
| } |
|
|
| function assert(condition, msg) { |
| if (!condition) throw new Error(msg || 'Assertion failed'); |
| } |
|
|
| function assertEqual(a, b, msg) { |
| const as = JSON.stringify(a), bs = JSON.stringify(b); |
| if (as !== bs) throw new Error(msg || `Expected ${bs}, got ${as}`); |
| } |
|
|
| function stringifyUnknownContent(value) { |
| if (value === null || value === undefined) return ''; |
| if (typeof value === 'string') return value; |
| if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { |
| return String(value); |
| } |
| try { |
| return JSON.stringify(value); |
| } catch { |
| return String(value); |
| } |
| } |
|
|
| function extractOpenAIContentBlocks(msg) { |
| if (msg.content === null || msg.content === undefined) return ''; |
| if (typeof msg.content === 'string') return msg.content; |
| if (Array.isArray(msg.content)) { |
| const blocks = []; |
| for (const p of msg.content) { |
| if ((p.type === 'text' || p.type === 'input_text') && p.text) { |
| blocks.push({ type: 'text', text: p.text }); |
| } else if (p.type === 'image_url' && p.image_url?.url) { |
| blocks.push({ |
| type: 'image', |
| source: { type: 'url', media_type: 'image/jpeg', data: p.image_url.url }, |
| }); |
| } else if (p.type === 'input_image' && p.image_url?.url) { |
| blocks.push({ |
| type: 'image', |
| source: { type: 'url', media_type: 'image/jpeg', data: p.image_url.url }, |
| }); |
| } |
| } |
| return blocks.length > 0 ? blocks : ''; |
| } |
| return stringifyUnknownContent(msg.content); |
| } |
|
|
| function extractOpenAIContent(msg) { |
| const blocks = extractOpenAIContentBlocks(msg); |
| if (typeof blocks === 'string') return blocks; |
| return blocks.filter(b => b.type === 'text').map(b => b.text).join('\n'); |
| } |
|
|
| |
| function toBlocks(content) { |
| if (typeof content === 'string') { |
| return content ? [{ type: 'text', text: content }] : []; |
| } |
| return content || []; |
| } |
|
|
| function mergeConsecutiveRoles(messages) { |
| if (messages.length <= 1) return messages; |
| const merged = []; |
| for (const msg of messages) { |
| const last = merged[merged.length - 1]; |
| if (last && last.role === msg.role) { |
| const lastBlocks = toBlocks(last.content); |
| const newBlocks = toBlocks(msg.content); |
| last.content = [...lastBlocks, ...newBlocks]; |
| } else { |
| merged.push({ ...msg }); |
| } |
| } |
| return merged; |
| } |
|
|
| |
| function responsesToChatCompletions(body) { |
| const messages = []; |
|
|
| if (body.instructions && typeof body.instructions === 'string') { |
| messages.push({ role: 'system', content: body.instructions }); |
| } |
|
|
| const input = body.input; |
| if (typeof input === 'string') { |
| messages.push({ role: 'user', content: input }); |
| } else if (Array.isArray(input)) { |
| for (const item of input) { |
| |
| if (item.type === 'function_call_output') { |
| messages.push({ |
| role: 'tool', |
| content: stringifyUnknownContent(item.output), |
| tool_call_id: item.call_id || '', |
| }); |
| continue; |
| } |
| const role = item.role || 'user'; |
| if (role === 'system' || role === 'developer') { |
| const text = extractOpenAIContent({ |
| role: 'system', |
| content: item.content ?? null, |
| }); |
| messages.push({ role: 'system', content: text }); |
| } else if (role === 'user') { |
| const rawContent = item.content ?? null; |
| const normalizedContent = typeof rawContent === 'string' |
| ? rawContent |
| : Array.isArray(rawContent) && rawContent.every(b => b.type === 'input_text') |
| ? rawContent.map(b => b.text || '').join('\n') |
| : rawContent; |
| messages.push({ |
| role: 'user', |
| content: normalizedContent, |
| }); |
| } else if (role === 'assistant') { |
| const blocks = Array.isArray(item.content) ? item.content : []; |
| const text = blocks.filter(b => b.type === 'output_text').map(b => b.text).join('\n'); |
| const toolCallBlocks = blocks.filter(b => b.type === 'function_call'); |
| const toolCalls = toolCallBlocks.map(b => ({ |
| id: b.call_id || `call_${Math.random().toString(36).slice(2)}`, |
| type: 'function', |
| function: { |
| name: b.name || '', |
| arguments: b.arguments || '{}', |
| }, |
| })); |
| messages.push({ |
| role: 'assistant', |
| content: text || null, |
| ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}), |
| }); |
| } |
| } |
| } |
|
|
| const tools = Array.isArray(body.tools) |
| ? body.tools.map(t => ({ |
| type: 'function', |
| function: { |
| name: t.name || '', |
| description: t.description, |
| parameters: t.parameters, |
| }, |
| })) |
| : undefined; |
|
|
| return { |
| model: body.model || 'gpt-4', |
| messages, |
| stream: body.stream ?? true, |
| temperature: body.temperature, |
| max_tokens: body.max_output_tokens || 8192, |
| tools, |
| }; |
| } |
|
|
| |
| |
| |
| console.log('\n๐ฆ [1] responsesToChatCompletions โ ๅบๆฌ่ฝฌๆข\n'); |
|
|
| test('็ฎๅๅญ็ฌฆไธฒ input โ user ๆถๆฏ', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: 'Hello, how are you?', |
| }); |
| assertEqual(result.model, 'gpt-4'); |
| assertEqual(result.messages.length, 1); |
| assertEqual(result.messages[0].role, 'user'); |
| assertEqual(result.messages[0].content, 'Hello, how are you?'); |
| }); |
|
|
| test('ๅธฆ instructions โ system ๆถๆฏ', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| instructions: 'You are a helpful assistant.', |
| input: 'Hello', |
| }); |
| assertEqual(result.messages.length, 2); |
| assertEqual(result.messages[0].role, 'system'); |
| assertEqual(result.messages[0].content, 'You are a helpful assistant.'); |
| assertEqual(result.messages[1].role, 'user'); |
| }); |
|
|
| test('ๅค่ฝฎๅฏน่ฏ input ๆฐ็ป', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: [ |
| { role: 'user', content: 'What is 2+2?' }, |
| { role: 'assistant', content: [{ type: 'output_text', text: '4' }] }, |
| { role: 'user', content: 'And 3+3?' }, |
| ], |
| }); |
| assertEqual(result.messages.length, 3); |
| assertEqual(result.messages[0].role, 'user'); |
| assertEqual(result.messages[1].role, 'assistant'); |
| assertEqual(result.messages[1].content, '4'); |
| assertEqual(result.messages[2].role, 'user'); |
| }); |
|
|
| test('developer ่ง่ฒ โ system', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: [ |
| { role: 'developer', content: 'You are a coding assistant.' }, |
| { role: 'user', content: 'Write hello world' }, |
| ], |
| }); |
| assertEqual(result.messages[0].role, 'system'); |
| assertEqual(result.messages[0].content, 'You are a coding assistant.'); |
| }); |
|
|
| test('function_call_output โ tool ๆถๆฏ', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: [ |
| { role: 'user', content: 'List files' }, |
| { |
| role: 'assistant', |
| content: [{ |
| type: 'function_call', |
| call_id: 'call_123', |
| name: 'list_dir', |
| arguments: '{"path":"."}' |
| }] |
| }, |
| { |
| type: 'function_call_output', |
| call_id: 'call_123', |
| output: 'file1.ts\nfile2.ts' |
| }, |
| ], |
| }); |
| assertEqual(result.messages.length, 3); |
| assertEqual(result.messages[2].role, 'tool'); |
| assertEqual(result.messages[2].content, 'file1.ts\nfile2.ts'); |
| assertEqual(result.messages[2].tool_call_id, 'call_123'); |
| }); |
|
|
| test('function_call_output ๅฏน่ฑก โ JSON ๅญ็ฌฆไธฒ', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: [ |
| { role: 'user', content: 'Summarize tool output' }, |
| { |
| type: 'function_call_output', |
| call_id: 'call_obj', |
| output: { files: ['a.ts', 'b.ts'], count: 2 } |
| }, |
| ], |
| }); |
| assertEqual(result.messages.length, 2); |
| assertEqual(result.messages[1].role, 'tool'); |
| assertEqual(result.messages[1].content, '{"files":["a.ts","b.ts"],"count":2}'); |
| assertEqual(result.messages[1].tool_call_id, 'call_obj'); |
| }); |
|
|
| test('ๅฉๆๆถๆฏๅธฆ function_call โ tool_calls', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: [ |
| { role: 'user', content: 'Read file' }, |
| { |
| role: 'assistant', |
| content: [{ |
| type: 'function_call', |
| call_id: 'call_abc', |
| name: 'read_file', |
| arguments: '{"path":"index.ts"}' |
| }] |
| }, |
| ], |
| }); |
| assertEqual(result.messages[1].role, 'assistant'); |
| assert(result.messages[1].tool_calls, 'should have tool_calls'); |
| assertEqual(result.messages[1].tool_calls.length, 1); |
| assertEqual(result.messages[1].tool_calls[0].function.name, 'read_file'); |
| assertEqual(result.messages[1].tool_calls[0].function.arguments, '{"path":"index.ts"}'); |
| }); |
|
|
| test('ๅทฅๅ
ทๅฎไน่ฝฌๆข', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: 'hello', |
| tools: [ |
| { |
| type: 'function', |
| name: 'read_file', |
| description: 'Read a file', |
| parameters: { type: 'object', properties: { path: { type: 'string' } } }, |
| } |
| ], |
| }); |
| assert(result.tools, 'should have tools'); |
| assertEqual(result.tools.length, 1); |
| assertEqual(result.tools[0].function.name, 'read_file'); |
| }); |
|
|
| test('input_text content ๆฐ็ป', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: [ |
| { |
| role: 'user', |
| content: [ |
| { type: 'input_text', text: 'Part 1' }, |
| { type: 'input_text', text: 'Part 2' }, |
| ] |
| }, |
| ], |
| }); |
| assertEqual(result.messages[0].content, 'Part 1\nPart 2'); |
| }); |
|
|
| test('Responses user input_image ไธๅบไธขๅคฑ', () => { |
| const result = responsesToChatCompletions({ |
| model: 'gpt-4', |
| input: [ |
| { |
| role: 'user', |
| content: [ |
| { type: 'input_text', text: '่ฏทๆ่ฟฐ่ฟๅผ ๅพ' }, |
| { type: 'input_image', image_url: { url: 'https://example.com/image.jpg' } }, |
| ] |
| }, |
| ], |
| }); |
| assertEqual(result.messages.length, 1); |
| assert(Array.isArray(result.messages[0].content), 'content should remain multimodal blocks'); |
| assertEqual(result.messages[0].content[0], { type: 'input_text', text: '่ฏทๆ่ฟฐ่ฟๅผ ๅพ' }); |
| assertEqual(result.messages[0].content[1], { type: 'input_image', image_url: { url: 'https://example.com/image.jpg' } }); |
| }); |
|
|
| test('stream ้ป่ฎคไธบ true', () => { |
| const result = responsesToChatCompletions({ model: 'gpt-4', input: 'hi' }); |
| assertEqual(result.stream, true); |
| }); |
|
|
| test('stream ๆพๅผ่ฎพไธบ false', () => { |
| const result = responsesToChatCompletions({ model: 'gpt-4', input: 'hi', stream: false }); |
| assertEqual(result.stream, false); |
| }); |
|
|
| test('max_output_tokens ่ฝฌๆข', () => { |
| const result = responsesToChatCompletions({ model: 'gpt-4', input: 'hi', max_output_tokens: 4096 }); |
| assertEqual(result.max_tokens, 4096); |
| }); |
|
|
| |
| |
| |
| console.log('\n๐ฆ [2] mergeConsecutiveRoles โ ๆถๆฏๅๅนถ\n'); |
|
|
| test('ไบคๆฟ่ง่ฒไธๅๅนถ', () => { |
| const msgs = [ |
| { role: 'user', content: 'Hello' }, |
| { role: 'assistant', content: 'Hi' }, |
| { role: 'user', content: 'Bye' }, |
| ]; |
| const result = mergeConsecutiveRoles(msgs); |
| assertEqual(result.length, 3); |
| }); |
|
|
| test('่ฟ็ปญ user ๆถๆฏๅๅนถ', () => { |
| const msgs = [ |
| { role: 'user', content: 'Message 1' }, |
| { role: 'user', content: 'Message 2' }, |
| { role: 'assistant', content: 'Response' }, |
| ]; |
| const result = mergeConsecutiveRoles(msgs); |
| assertEqual(result.length, 2); |
| assertEqual(result[0].role, 'user'); |
| |
| assert(Array.isArray(result[0].content), 'merged content should be array'); |
| assertEqual(result[0].content.length, 2); |
| assertEqual(result[0].content[0].text, 'Message 1'); |
| assertEqual(result[0].content[1].text, 'Message 2'); |
| }); |
|
|
| test('่ฟ็ปญ assistant ๆถๆฏๅๅนถ', () => { |
| const msgs = [ |
| { role: 'user', content: 'Hello' }, |
| { role: 'assistant', content: 'Part 1' }, |
| { role: 'assistant', content: 'Part 2' }, |
| ]; |
| const result = mergeConsecutiveRoles(msgs); |
| assertEqual(result.length, 2); |
| assertEqual(result[1].role, 'assistant'); |
| assert(Array.isArray(result[1].content)); |
| assertEqual(result[1].content.length, 2); |
| }); |
|
|
| test('tool result + text user ๆถๆฏๅๅนถ', () => { |
| const msgs = [ |
| { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'id1', content: 'output' }] }, |
| { role: 'user', content: 'Follow up question' }, |
| ]; |
| const result = mergeConsecutiveRoles(msgs); |
| assertEqual(result.length, 1); |
| assert(Array.isArray(result[0].content)); |
| assertEqual(result[0].content.length, 2); |
| }); |
|
|
| test('็ฉบๆถๆฏๅ่กจ', () => { |
| assertEqual(mergeConsecutiveRoles([]).length, 0); |
| }); |
|
|
| test('ๅๆกๆถๆฏไธๅๅนถ', () => { |
| const result = mergeConsecutiveRoles([{ role: 'user', content: 'solo' }]); |
| assertEqual(result.length, 1); |
| }); |
|
|
| test('ไธๆก่ฟ็ปญ user ๅ
จ้จๅๅนถ', () => { |
| const msgs = [ |
| { role: 'user', content: 'A' }, |
| { role: 'user', content: 'B' }, |
| { role: 'user', content: 'C' }, |
| ]; |
| const result = mergeConsecutiveRoles(msgs); |
| assertEqual(result.length, 1); |
| assert(Array.isArray(result[0].content)); |
| assertEqual(result[0].content.length, 3); |
| }); |
|
|
| |
| |
| |
| console.log('\n๐ฆ [3] Cursor ๆๅนณๆ ผๅผๅทฅๅ
ทๅ
ผๅฎน\n'); |
|
|
| function convertTools(tools) { |
| return tools.map(t => { |
| if ('function' in t && t.function) { |
| return { |
| name: t.function.name, |
| description: t.function.description, |
| input_schema: t.function.parameters || { type: 'object', properties: {} }, |
| }; |
| } |
| return { |
| name: t.name || '', |
| description: t.description, |
| input_schema: t.input_schema || { type: 'object', properties: {} }, |
| }; |
| }); |
| } |
|
|
| test('ๆ ๅ OpenAI ๆ ผๅผๅทฅๅ
ท', () => { |
| const tools = convertTools([{ |
| type: 'function', |
| function: { |
| name: 'read_file', |
| description: 'Read file contents', |
| parameters: { type: 'object', properties: { path: { type: 'string' } } }, |
| }, |
| }]); |
| assertEqual(tools[0].name, 'read_file'); |
| assertEqual(tools[0].description, 'Read file contents'); |
| assert(tools[0].input_schema.properties.path); |
| }); |
|
|
| test('Cursor ๆๅนณๆ ผๅผๅทฅๅ
ท', () => { |
| const tools = convertTools([{ |
| name: 'write_file', |
| description: 'Write file', |
| input_schema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } } }, |
| }]); |
| assertEqual(tools[0].name, 'write_file'); |
| assertEqual(tools[0].description, 'Write file'); |
| assert(tools[0].input_schema.properties.path); |
| assert(tools[0].input_schema.properties.content); |
| }); |
|
|
| test('ๆททๅๆ ผๅผๅทฅๅ
ทๅ่กจ', () => { |
| const tools = convertTools([ |
| { |
| type: 'function', |
| function: { name: 'tool_a', description: 'A', parameters: {} }, |
| }, |
| { |
| name: 'tool_b', |
| description: 'B', |
| input_schema: {}, |
| }, |
| ]); |
| assertEqual(tools.length, 2); |
| assertEqual(tools[0].name, 'tool_a'); |
| assertEqual(tools[1].name, 'tool_b'); |
| }); |
|
|
| test('็ผบๅฐ input_schema ็ๆๅนณๆ ผๅผ', () => { |
| const tools = convertTools([{ name: 'simple_tool' }]); |
| assertEqual(tools[0].name, 'simple_tool'); |
| assert(tools[0].input_schema, 'should have default input_schema'); |
| assertEqual(tools[0].input_schema.type, 'object'); |
| }); |
|
|
| |
| |
| |
| console.log('\n๐ฆ [4] ๅข้ๆตๅผๅทฅๅ
ท่ฐ็จ้ช่ฏ\n'); |
|
|
| test('128 ๅญ่ๅๅ๏ผshort arguments', () => { |
| const args = '{"path":"src/index.ts"}'; |
| const CHUNK_SIZE = 128; |
| const chunks = []; |
| for (let j = 0; j < args.length; j += CHUNK_SIZE) { |
| chunks.push(args.slice(j, j + CHUNK_SIZE)); |
| } |
| |
| assertEqual(chunks.length, 1); |
| assertEqual(chunks[0], args); |
| }); |
|
|
| test('128 ๅญ่ๅๅ๏ผlong arguments', () => { |
| const longContent = 'A'.repeat(400); |
| const args = JSON.stringify({ path: 'test.ts', content: longContent }); |
| const CHUNK_SIZE = 128; |
| const chunks = []; |
| for (let j = 0; j < args.length; j += CHUNK_SIZE) { |
| chunks.push(args.slice(j, j + CHUNK_SIZE)); |
| } |
| |
| assertEqual(chunks.join(''), args); |
| |
| assert(chunks.length > 1, `Expected multiple chunks, got ${chunks.length}`); |
| |
| for (const c of chunks) { |
| assert(c.length <= CHUNK_SIZE, `Chunk too long: ${c.length}`); |
| } |
| }); |
|
|
| test('็ฉบ arguments ้ถๅธง', () => { |
| const args = ''; |
| const CHUNK_SIZE = 128; |
| const chunks = []; |
| for (let j = 0; j < args.length; j += CHUNK_SIZE) { |
| chunks.push(args.slice(j, j + CHUNK_SIZE)); |
| } |
| assertEqual(chunks.length, 0); |
| }); |
|
|
| |
| |
| |
| console.log('\n' + 'โ'.repeat(55)); |
| console.log(` ็ปๆ: ${passed} ้่ฟ / ${failed} ๅคฑ่ดฅ / ${passed + failed} ๆป่ฎก`); |
| console.log('โ'.repeat(55) + '\n'); |
|
|
| if (failed > 0) process.exit(1); |
|
|