Skip to content
Merged
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
13 changes: 11 additions & 2 deletions bin/muse-proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,18 @@ const server = http.createServer((req, res) => {
}
}

const failed = (code !== 0 && fullContent.length === 0) || (terminalState && terminalState !== 'completed' && fullContent.length === 0);
// Failure is judged by the run's authoritative terminal state when
// one was emitted, otherwise by the exit code (#672). Partial
// content must not mask a failure: a run that failed midway used to
// be reported as a successful 'stop' carrying the truncated text,
// because both clauses of the old predicate required empty content.
const failed = terminalState ? terminalState !== 'completed' : code !== 0;
if (failed) {
const detail = (stderrTail.trim().split('\n').pop() || `muse exec exited with code ${code}`).trim();
const stderrLine = stderrTail.trim().split('\n').pop();
const fallback = terminalState && terminalState !== 'completed'
? `muse run terminated with state '${terminalState}'`
: `muse exec exited with code ${code}`;
const detail = (stderrLine || fallback).trim();
const errorBody = { error: { message: `Muse execution failed: ${detail}` } };
if (stream) {
// The 200 + text/event-stream headers are already on the wire,
Expand Down
45 changes: 45 additions & 0 deletions bin/muse-proxy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ if grep -q FAILME "$prompt" 2>/dev/null; then
echo "stub muse: auth expired" >&2
exit 3
fi
if grep -q FAILPARTIAL "$prompt" 2>/dev/null; then
printf '%s\\n' '{"payload_type":"run.output.delta","payload":{"text":"partial answer"}}'
printf '%s\\n' '{"payload_type":"run.terminal.failed","payload":{"terminal":"failed","text":"partial answer"}}'
echo "stub muse: run failed midway" >&2
exit 0
fi
if grep -q CRASHPARTIAL "$prompt" 2>/dev/null; then
printf '%s\\n' '{"payload_type":"run.output.delta","payload":{"text":"partial answer"}}'
echo "stub muse: crashed midway" >&2
exit 3
fi
if grep -q DIVERGE "$prompt" 2>/dev/null; then
printf '%s\\n' '{"payload_type":"run.output.delta","payload":{"text":"abc"}}'
printf '%s\\n' '{"payload_type":"run.terminal.completed","payload":{"terminal":"completed","text":"xyz-longer"}}'
Expand Down Expand Up @@ -145,6 +156,40 @@ test('execution failure: stream gets an SSE-framed error + [DONE], non-stream ge
assert.equal(dataLines[dataLines.length - 1], 'data: [DONE]');
});

test('partial output then failure: masked success becomes an error in both modes (#672)', async (t) => {
const { port, stop } = await startProxy();
t.after(stop);

for (const marker of ['FAILPARTIAL', 'CRASHPARTIAL']) {
const messages = [{ role: 'user', content: `please ${marker} now` }];

const plainRes = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages })
});
assert.equal(plainRes.status, 500, `${marker}: non-stream must be a 500, not a 200 'stop'`);
const plainBody = JSON.parse(await plainRes.text());
assert.match(plainBody.error.message, /Muse execution failed: stub muse:/);

const streamRes = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages, stream: true })
});
assert.equal(streamRes.status, 200);
const sse = await streamRes.text();
const dataLines = sse.split('\n').filter(l => l.startsWith('data: '));
assert.equal(dataLines[dataLines.length - 1], 'data: [DONE]', `${marker}: stream must end with [DONE]`);
const events = dataLines.slice(0, -1).map(l => JSON.parse(l.slice('data: '.length)));
const errorEvent = events.find(e => e.error);
assert.ok(errorEvent, `${marker}: stream must carry an error event, got: ${JSON.stringify(sse)}`);
assert.match(errorEvent.error.message, /Muse execution failed: stub muse:/);
const stopChunk = events.find(e => e.choices && e.choices[0].finish_reason === 'stop');
assert.equal(stopChunk, undefined, `${marker}: no chunk may claim finish_reason 'stop'`);
}
});

test('divergent terminal text: non-stream uses it, stream keeps the deltas already sent', async (t) => {
const { port, stop } = await startProxy();
t.after(stop);
Expand Down
Loading