removed native tool_calls handling from OpenAIProvider.

This commit is contained in:
Xiaohan-Tian
2025-08-16 15:49:41 -07:00
parent e69abfd916
commit cf4f3edb30
+11 -173
View File
@@ -66,30 +66,15 @@ export class OpenAIProvider extends LLMProvider {
/** /**
* Parse Ollama's raw JSON chunk format * Parse Ollama's raw JSON chunk format
*/ */
private parseOllamaChunk(chunk: string): { thinking?: string; content?: string; isDone?: boolean; toolCalls?: Array<{ name: string; arguments: Record<string, unknown> }> } { private parseOllamaChunk(chunk: string): { thinking?: string; content?: string; isDone?: boolean } {
try { try {
const json = JSON.parse(chunk.trim()); const json = JSON.parse(chunk.trim());
const thinking: string | undefined = json.message?.thinking; const thinking: string | undefined = json.message?.thinking;
const content: string | undefined = json.message?.content || json.response; // Handle both chat and completion formats const content: string | undefined = json.message?.content || json.response; // Handle both chat and completion formats
type WireToolCall = { function?: { name?: unknown; arguments?: unknown } };
const toolCallsRaw: unknown[] | undefined = json.message?.tool_calls as unknown[] | undefined;
const toolCalls = Array.isArray(toolCallsRaw)
? (toolCallsRaw
.map((tc: unknown) => {
const wire = tc as WireToolCall;
const fn = wire?.function;
if (!fn || typeof fn.name !== 'string') return null;
const args = fn.arguments;
if (args === null || typeof args !== 'object' || Array.isArray(args)) return null;
return { name: fn.name, arguments: args as Record<string, unknown> };
})
.filter((v): v is { name: string; arguments: Record<string, unknown> } => v !== null))
: undefined;
return { return {
thinking, thinking,
content, content,
isDone: json.done === true, isDone: json.done === true
toolCalls
}; };
} catch { } catch {
return {}; // Invalid JSON, return empty object return {}; // Invalid JSON, return empty object
@@ -99,7 +84,7 @@ export class OpenAIProvider extends LLMProvider {
/** /**
* Parse OpenAI's SSE format chunk * Parse OpenAI's SSE format chunk
*/ */
private parseOpenAIChunk(line: string): { thinking?: string; content?: string; isDone?: boolean; toolCallDelta?: Array<{ index?: number; function?: { name?: string; arguments?: string } }> } { private parseOpenAIChunk(line: string): { thinking?: string; content?: string; isDone?: boolean } {
if (!line.startsWith('data: ')) { if (!line.startsWith('data: ')) {
return {}; return {};
} }
@@ -114,11 +99,7 @@ export class OpenAIProvider extends LLMProvider {
const delta = json.choices?.[0]?.delta; const delta = json.choices?.[0]?.delta;
const thinking: string | undefined = delta?.thinking; // Some providers may stream "thinking" const thinking: string | undefined = delta?.thinking; // Some providers may stream "thinking"
const content: string | undefined = delta?.content; const content: string | undefined = delta?.content;
const tcd = delta?.tool_calls; return { thinking, content, isDone: false };
const toolCallDelta: Array<{ index?: number; function?: { name?: string; arguments?: string } }> | undefined = Array.isArray(tcd)
? (tcd as Array<{ index?: number; function?: { name?: string; arguments?: string } }>)
: undefined;
return { thinking, content, isDone: false, toolCallDelta };
} catch { } catch {
return {}; // Skip invalid JSON lines return {}; // Skip invalid JSON lines
} }
@@ -126,8 +107,7 @@ export class OpenAIProvider extends LLMProvider {
async *generateStream( async *generateStream(
messages: Message[], messages: Message[],
systemPrompt?: string, systemPrompt?: string
tools?: Record<string, unknown>[]
): AsyncIterableIterator<StreamChunk> { ): AsyncIterableIterator<StreamChunk> {
// Get fresh config values // Get fresh config values
const config = this.getCurrentConfig(); const config = this.getCurrentConfig();
@@ -157,7 +137,6 @@ export class OpenAIProvider extends LLMProvider {
...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}), ...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}),
messages: openAIMessages, messages: openAIMessages,
stream: true, stream: true,
tools: tools || undefined
}), }),
}); });
@@ -174,28 +153,7 @@ export class OpenAIProvider extends LLMProvider {
let buffer = ''; let buffer = '';
let firstChunkProcessed = false; let firstChunkProcessed = false;
let lastSegmentType: 'thinking' | 'content' | null = null; let lastSegmentType: 'thinking' | 'content' | null = null;
const pendingFunctionCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
const openAIToolCallBuilders: Record<number, { name?: string; argumentsText: string }> = {};
const escapeXml = (text: string): string =>
String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
const functionCallToXml = (name: string, args: Record<string, unknown>): string => {
const keys = Object.keys(args);
const inner = keys
.map((k) => {
const value = (args as Record<string, unknown>)[k];
const text = typeof value === 'string' ? value : JSON.stringify(value);
return `<${k}>${escapeXml(text)}</${k}>`;
})
.join('\n');
return `<${name}>\n${inner}\n</${name}>`;
};
try { try {
while (true) { while (true) {
@@ -220,19 +178,8 @@ export class OpenAIProvider extends LLMProvider {
} }
if (this.isOllamaFormat) { if (this.isOllamaFormat) {
const { thinking, content, isDone, toolCalls } = this.parseOllamaChunk(trimmedLine); const { thinking, content, isDone } = this.parseOllamaChunk(trimmedLine);
if (isDone) { if (isDone) {
// Append any pending or current tool calls as XML before finishing
const allToolCalls = [
...pendingFunctionCalls,
...(toolCalls || [])
];
if (allToolCalls.length > 0) {
const xmlBlocks = allToolCalls
.map((tc) => `\n${functionCallToXml(tc.name, tc.arguments)}\n`)
.join('');
yield { type: 'text', content: xmlBlocks };
}
yield { type: 'done', content: '' }; yield { type: 'done', content: '' };
return; return;
} }
@@ -253,41 +200,11 @@ export class OpenAIProvider extends LLMProvider {
lastSegmentType = 'content'; lastSegmentType = 'content';
} }
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
// Accumulate and append at the end of stream
pendingFunctionCalls.push(...toolCalls);
}
continue; continue;
} }
const { thinking, content, isDone, toolCallDelta } = this.parseOpenAIChunk(trimmedLine); const { thinking, content, isDone } = this.parseOpenAIChunk(trimmedLine);
if (isDone) { if (isDone) {
// Finalize any accumulated OpenAI tool calls and emit XML
const finalizedToolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
for (const indexStr of Object.keys(openAIToolCallBuilders)) {
const idx = Number(indexStr);
const builder = openAIToolCallBuilders[idx];
if (!builder || !builder.name) continue;
let argsObj: Record<string, unknown> | null = null;
if (builder.argumentsText) {
try {
argsObj = JSON.parse(builder.argumentsText);
} catch {
argsObj = null;
}
}
if (argsObj) {
finalizedToolCalls.push({ name: builder.name, arguments: argsObj });
}
}
const allToolCalls = [...pendingFunctionCalls, ...finalizedToolCalls];
if (allToolCalls.length > 0) {
const xmlBlocks = allToolCalls
.map((tc) => `\n${functionCallToXml(tc.name, tc.arguments)}\n`)
.join('');
yield { type: 'text', content: xmlBlocks };
}
yield { type: 'done', content: '' }; yield { type: 'done', content: '' };
return; return;
} }
@@ -307,24 +224,6 @@ export class OpenAIProvider extends LLMProvider {
yield { type: 'text', content }; yield { type: 'text', content };
lastSegmentType = 'content'; lastSegmentType = 'content';
} }
if (Array.isArray(toolCallDelta) && toolCallDelta.length > 0) {
for (const tc of toolCallDelta) {
const index: number = typeof tc.index === 'number' ? tc.index : 0;
if (!openAIToolCallBuilders[index]) {
openAIToolCallBuilders[index] = { argumentsText: '' };
}
const fn = tc.function;
if (fn) {
if (typeof fn.name === 'string') {
openAIToolCallBuilders[index].name = fn.name;
}
if (typeof fn.arguments === 'string') {
openAIToolCallBuilders[index].argumentsText += fn.arguments;
}
}
}
}
} }
} }
} finally { } finally {
@@ -334,8 +233,7 @@ export class OpenAIProvider extends LLMProvider {
async generateCompletion( async generateCompletion(
messages: Message[], messages: Message[],
systemPrompt?: string, systemPrompt?: string
tools?: Record<string, unknown>[]
): Promise<LLMResponse> { ): Promise<LLMResponse> {
// Get fresh config values // Get fresh config values
const config = this.getCurrentConfig(); const config = this.getCurrentConfig();
@@ -365,7 +263,6 @@ export class OpenAIProvider extends LLMProvider {
...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}), ...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}),
messages: openAIMessages, messages: openAIMessages,
stream: false, stream: false,
tools: tools || undefined
}), }),
}); });
@@ -376,25 +273,6 @@ export class OpenAIProvider extends LLMProvider {
const data = await response.json(); const data = await response.json();
// Handle different response formats // Handle different response formats
const escapeXml = (text: string): string =>
String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
const functionCallToXml = (name: string, args: Record<string, unknown>): string => {
const keys = Object.keys(args);
const inner = keys
.map((k) => {
const value = (args as Record<string, unknown>)[k];
const text = typeof value === 'string' ? value : JSON.stringify(value);
return `<${k}>${escapeXml(text)}</${k}>`;
})
.join('\n');
return `<${name}>\n${inner}\n</${name}>`;
};
if (data.message) { if (data.message) {
// Ollama/compatible format // Ollama/compatible format
@@ -402,28 +280,8 @@ export class OpenAIProvider extends LLMProvider {
const contentText: string = data.message.content || data.response || ''; const contentText: string = data.message.content || data.response || '';
const textCombined = thinking && contentText ? `${thinking}\n\n${contentText}` : (thinking || contentText); const textCombined = thinking && contentText ? `${thinking}\n\n${contentText}` : (thinking || contentText);
type WireToolCall = { function?: { name?: unknown; arguments?: unknown } };
const toolCallsRaw: unknown[] | undefined = data.message.tool_calls as unknown[] | undefined;
const toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = Array.isArray(toolCallsRaw)
? (toolCallsRaw
.map((tc: unknown) => {
const wire = tc as WireToolCall;
const fn = wire?.function;
if (!fn || typeof fn.name !== 'string') return null;
const args = fn.arguments;
if (args === null || typeof args !== 'object' || Array.isArray(args)) return null;
return { name: fn.name, arguments: args as Record<string, unknown> };
})
.filter((v): v is { name: string; arguments: Record<string, unknown> } => v !== null))
: [];
const xmlBlocks = toolCalls.length > 0
? toolCalls.map((tc) => `\n${functionCallToXml(tc.name, tc.arguments)}\n`).join('')
: '';
return { return {
content: `${textCombined}${xmlBlocks}`, content: textCombined,
toolCalls: data.message.tool_calls || undefined,
finished: data.done === true || data.done_reason === 'stop' finished: data.done === true || data.done_reason === 'stop'
}; };
} else { } else {
@@ -433,29 +291,9 @@ export class OpenAIProvider extends LLMProvider {
const contentText: string = choice?.message?.content || ''; const contentText: string = choice?.message?.content || '';
const textCombined = thinking && contentText ? `${thinking}\n\n${contentText}` : (thinking || contentText); const textCombined = thinking && contentText ? `${thinking}\n\n${contentText}` : (thinking || contentText);
type WireToolCall2 = { function?: { name?: unknown; arguments?: unknown } };
const toolCallsRaw: unknown[] | undefined = choice?.message?.tool_calls as unknown[] | undefined;
const toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = Array.isArray(toolCallsRaw)
? (toolCallsRaw
.map((tc: unknown) => {
const wire = tc as WireToolCall2;
const fn = wire?.function;
if (!fn || typeof fn.name !== 'string') return null;
const args = fn.arguments;
if (args === null || typeof args !== 'object' || Array.isArray(args)) return null;
return { name: fn.name, arguments: args as Record<string, unknown> };
})
.filter((v): v is { name: string; arguments: Record<string, unknown> } => v !== null))
: [];
const xmlBlocks = toolCalls.length > 0
? toolCalls.map((tc) => `\n${functionCallToXml(tc.name, tc.arguments)}\n`).join('')
: '';
return { return {
content: `${textCombined}${xmlBlocks}`, content: textCombined,
toolCalls: choice?.message?.tool_calls || undefined, finished: choice?.finish_reason === 'stop'
finished: choice?.finish_reason === 'stop' || choice?.finish_reason === 'tool_calls'
}; };
} }
} }