Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"desktop:package:win": "npm run dist:win --workspace @blockrun/franklin-desktop",
"dev": "tsc --watch",
"start": "node dist/index.js",
"test": "npm run build && node --test --test-concurrency=4 --test-reporter=spec test/local.mjs test/api-key.local.mjs test/solana-migration.local.mjs test/serve-security.local.mjs test/skills.local.mjs test/skills-management.local.mjs test/glob-responsive.local.mjs test/desktop-auto-routing.local.mjs test/desktop-questions.local.mjs test/desktop-question-flow.local.mjs test/desktop-input.local.mjs test/desktop-image-flow.local.mjs test/repair.mjs test/model-resolver.mjs test/exa.local.mjs test/audit-batch.local.mjs test/polymarket.local.mjs test/market.local.mjs test/hooks.local.mjs test/scheduler.local.mjs test/trade-plan.local.mjs test/goal.local.mjs test/memory.local.mjs test/agent-host.local.mjs test/reservation.local.mjs test/model-catalog.local.mjs test/arc.local.mjs test/subprocess-env.local.mjs test/withdraw.local.mjs test/orders.local.mjs test/payment-hold.local.mjs test/experience.local.mjs",
"test": "npm run build && node --test --test-concurrency=4 --test-reporter=spec test/local.mjs test/api-key.local.mjs test/solana-migration.local.mjs test/serve-security.local.mjs test/skills.local.mjs test/skills-management.local.mjs test/glob-responsive.local.mjs test/desktop-auto-routing.local.mjs test/desktop-questions.local.mjs test/desktop-question-flow.local.mjs test/desktop-input.local.mjs test/desktop-image-flow.local.mjs test/repair.mjs test/model-resolver.mjs test/exa.local.mjs test/audit-batch.local.mjs test/polymarket.local.mjs test/market.local.mjs test/hooks.local.mjs test/scheduler.local.mjs test/trade-plan.local.mjs test/goal.local.mjs test/memory.local.mjs test/agent-host.local.mjs test/reservation.local.mjs test/model-catalog.local.mjs test/arc.local.mjs test/subprocess-env.local.mjs test/withdraw.local.mjs test/orders.local.mjs test/payment-hold.local.mjs test/experience.local.mjs test/sse-translator.local.mjs",
"test:e2e": "npm run build && node --test --test-reporter=spec test/e2e.mjs",
"e2e:polymarket:readonly": "npm run build && node scripts/polymarket-e2e-readonly.mjs",
"test:free-models": "npm run build && node --test --test-reporter=spec test/free-model-matrix.mjs",
Expand Down
21 changes: 14 additions & 7 deletions src/proxy/sse-translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface TranslatorState {
thinkingBlockActive: boolean;
textBlockActive: boolean;
messageStarted: boolean;
stopReasonEmitted: boolean;
inputTokens: number;
outputTokens: number;
}
Expand All @@ -38,6 +39,7 @@ export class SSETranslator {
thinkingBlockActive: false,
textBlockActive: false,
messageStarted: false,
stopReasonEmitted: false,
inputTokens: 0,
outputTokens: 0,
};
Expand Down Expand Up @@ -69,13 +71,17 @@ export class SSETranslator {
for (const event of events) {
if (event.data === '[DONE]') {
translated.push(...this.closeActiveBlocks());
translated.push(
this.formatSSE('message_delta', {
type: 'message_delta',
delta: { stop_reason: 'end_turn', stop_sequence: null },
usage: { output_tokens: this.state.outputTokens },
})
);
// A finish_reason chunk already emitted the real stop_reason (e.g.
// tool_use); a second message_delta here would overwrite it.
if (!this.state.stopReasonEmitted) {
translated.push(
this.formatSSE('message_delta', {
type: 'message_delta',
delta: { stop_reason: 'end_turn', stop_sequence: null },
usage: { output_tokens: this.state.outputTokens },
})
);
}
translated.push(this.formatSSE('message_stop', { type: 'message_stop' }));
continue;
}
Expand Down Expand Up @@ -230,6 +236,7 @@ export class SSETranslator {
: choice.finish_reason === 'stop'
? 'end_turn'
: choice.finish_reason;
this.state.stopReasonEmitted = true;
translated.push(
this.formatSSE('message_delta', {
type: 'message_delta',
Expand Down
62 changes: 62 additions & 0 deletions test/sse-translator.local.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { SSETranslator } from '../dist/proxy/sse-translator.js';

function sse(obj) {
return `data: ${typeof obj === 'string' ? obj : JSON.stringify(obj)}\n\n`;
}

function stopReasons(output) {
return output
.split('\n\n')
.filter((e) => e.startsWith('event: message_delta'))
.map((e) => JSON.parse(e.split('data: ')[1]).delta.stop_reason);
}

function run(finishReason) {
const t = new SSETranslator('test-model');
const out = [];
const feed = (c) => out.push(t.processChunk(c) ?? '');
feed(sse({ choices: [{ index: 0, delta: { role: 'assistant', content: 'hi' } }] }));
if (finishReason === 'tool_calls') {
feed(
sse({
choices: [
{
index: 0,
delta: {
tool_calls: [
{ index: 0, id: 'call_1', type: 'function', function: { name: 'Read', arguments: '{}' } },
],
},
},
],
})
);
}
feed(sse({ choices: [{ index: 0, delta: {}, finish_reason: finishReason }] }));
feed(sse('[DONE]'));
return out.join('');
}

describe('SSETranslator stop_reason', () => {
it('keeps tool_use when [DONE] follows a tool_calls finish', () => {
const output = run('tool_calls');
assert.deepEqual(stopReasons(output), ['tool_use']);
assert.ok(output.includes('event: message_stop'));
});

it('emits a single end_turn for a normal stop', () => {
const output = run('stop');
assert.deepEqual(stopReasons(output), ['end_turn']);
assert.ok(output.includes('event: message_stop'));
});

it('falls back to end_turn when [DONE] arrives without a finish_reason', () => {
const t = new SSETranslator('test-model');
t.processChunk(sse({ choices: [{ index: 0, delta: { content: 'hi' } }] }));
const output = t.processChunk(sse('[DONE]')) ?? '';
assert.deepEqual(stopReasons(output), ['end_turn']);
assert.ok(output.includes('event: message_stop'));
});
});