Skip to content

Commit b0b0a3a

Browse files
committed
fix: utf8 decoder, finish_reason override, and chunk boundary handling
Fix: streaming proxy - Use utf8.decoder.bind() instead of per-chunk utf8.decode() to avoid FormatException on incomplete UTF-8 sequences across TCP chunks Fix: finish_reason consistency - Override finish_reason to tool_calls when toolCalls is non-empty even if upstream reports finishReason stop - Prevents OpenCode from treating a tool-call response as conversation end and starting extra rounds Fix: non-streaming handler - Add leftover buffer for chunk boundary handling, matching streaming handler behavior
1 parent 8875321 commit b0b0a3a

1 file changed

Lines changed: 14 additions & 3 deletions

File tree

lib/src/server/proxy.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,7 @@ class ProxyServer {
189189
bool sentAssistantRole = false;
190190
final toolCalls = <Map<String, dynamic>>[];
191191

192-
await for (final raw in upstreamRes) {
193-
final chunk = utf8.decode(raw);
192+
await for (final chunk in utf8.decoder.bind(upstreamRes)) {
194193
final parts = (leftover + chunk).split('\n');
195194
leftover = parts.removeLast();
196195

@@ -280,6 +279,10 @@ class ProxyServer {
280279
}
281280
}
282281

282+
if (toolCalls.isNotEmpty && finishReason == 'stop') {
283+
finishReason = 'tool_calls';
284+
}
285+
283286
_sendSSE(response, {
284287
'id': 'cmpl-${DateTime.now().millisecondsSinceEpoch}',
285288
'object': 'chat.completion.chunk',
@@ -394,10 +397,14 @@ class ProxyServer {
394397
int inputTokens = 0;
395398
int outputTokens = 0;
396399
final toolCalls = <Map<String, dynamic>>[];
400+
String leftover = '';
397401

398402
await for (final raw in upstreamRes) {
399403
final chunk = utf8.decode(raw);
400-
for (final line in chunk.split('\n')) {
404+
final parts = (leftover + chunk).split('\n');
405+
leftover = parts.removeLast();
406+
407+
for (final line in parts) {
401408
if (line.trim().isEmpty) continue;
402409
try {
403410
final event = jsonDecode(line) as Map<String, dynamic>;
@@ -424,6 +431,10 @@ class ProxyServer {
424431
}
425432
}
426433

434+
if (toolCalls.isNotEmpty && finishReason == 'stop') {
435+
finishReason = 'tool_calls';
436+
}
437+
427438
final choice = <String, dynamic>{
428439
'index': 0,
429440
'message': {

0 commit comments

Comments
 (0)