refactor: pre seed responses

This commit is contained in:
cogwheel0
2025-09-05 11:15:39 +05:30
parent 6af18c97b2
commit 58d5cf076b
7 changed files with 149 additions and 118 deletions

View File

@@ -1130,7 +1130,7 @@ class ApiService {
if (msg.role == 'assistant' && msg.model != null)
'modelName': msg.model,
if (msg.role == 'assistant') 'modelIdx': 0,
if (msg.role == 'assistant') 'done': true,
if (msg.role == 'assistant') 'done': !msg.isStreaming,
if (msg.role == 'user' && model != null) 'models': [model],
if (combinedFilesMap.isNotEmpty) 'files': combinedFilesMap,
};
@@ -1166,7 +1166,7 @@ class ApiService {
if (msg.role == 'assistant' && msg.model != null)
'modelName': msg.model,
if (msg.role == 'assistant') 'modelIdx': 0,
if (msg.role == 'assistant') 'done': true,
if (msg.role == 'assistant') 'done': !msg.isStreaming,
if (msg.role == 'user' && model != null) 'models': [model],
if (combinedFilesArray.isNotEmpty) 'files': combinedFilesArray,
});
@@ -2652,11 +2652,14 @@ class ApiService {
String? sessionIdOverride,
List<Map<String, dynamic>>? toolServers,
Map<String, dynamic>? backgroundTasks,
String? responseMessageId,
}) {
final streamController = StreamController<String>();
// Generate unique IDs
final messageId = const Uuid().v4();
final messageId = (responseMessageId != null && responseMessageId.isNotEmpty)
? responseMessageId
: const Uuid().v4();
final sessionId =
(sessionIdOverride != null && sessionIdOverride.isNotEmpty)
? sessionIdOverride
@@ -2809,6 +2812,8 @@ class ApiService {
// Always use task-based background flow for unified pipeline.
// When a dynamic channel (session_id) is not provided, this method falls
// back to polling and streams deltas to the UI.
// Always use background task flow (matches web client) to ensure
// server maintains correct history with pre-seeded assistant id.
final bool useBackgroundTasks = true;
// Use background flow only when required; otherwise prefer SSE even with chat_id.

View File

@@ -236,6 +236,71 @@ class ToolCallsParser {
return raw.length > max ? '${raw.substring(0, max)}' : raw;
}
}
/// Sanitize assistant/user content before sending to the API, mirroring
/// the web client's `processDetails` behavior:
/// - Remove <details type="reasoning"> and <details type="code_interpreter"> blocks
/// - Replace <details type="tool_calls" ...>...</details> blocks with the
/// JSON-serialized `result` attribute (as a quoted string) when available;
/// otherwise replace with an empty string.
static String sanitizeForApi(String content) {
if (content.isEmpty) return content;
// Remove blocks we never want to include in conversation context
final removeTypes = ['reasoning', 'code_interpreter'];
for (final t in removeTypes) {
content = content.replaceAll(
RegExp(
'<details\\s+type=\"${t}\"[^>]*>[\\s\\S]*?<\\/details>',
multiLine: true,
dotAll: true,
),
'',
);
}
if (!content.contains('<details')) return content.trim();
// Replace tool_calls blocks in-order with their results
final segs = segments(content);
if (segs == null || segs.isEmpty) return content.trim();
final buf = StringBuffer();
for (final seg in segs) {
if (seg.isToolCall && seg.entry != null) {
final entry = seg.entry!;
dynamic res = entry.result;
String out;
if (res == null) {
out = '';
} else {
try {
out = json.encode(res);
} catch (_) {
out = res.toString();
}
}
// Match web behavior: wrap in quotes so it's clearly a string payload
if (out.isNotEmpty && !(out.startsWith('"') && out.endsWith('"'))) {
out = '"$out"';
}
buf.write(out);
} else {
// Keep the raw text, but also remove any stray non-tool_calls details blocks
final t = (seg.text ?? '').replaceAll(
RegExp(
r'<details(?!\s+type=\"tool_calls\")[^>]*>[\s\S]*?<\/details>',
multiLine: true,
dotAll: true,
),
'',
);
if (t.isNotEmpty) buf.write(t);
}
}
return buf.toString().trim();
}
}
/// Ordered piece of content: either plain text or a tool-call entry