removed native tool_calls handling from OpenAIProvider.
This commit is contained in:
+11
-173
@@ -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, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
|
|
||||||
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, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
|
|
||||||
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'
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user