Skip to content

Commit abd344d

Browse files
committed
agentHost: consume explicit BYOK continuations
1 parent 35d94d3 commit abd344d

2 files changed

Lines changed: 70 additions & 10 deletions

File tree

src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,14 @@ export class ByokLmProxyService extends LoopbackProxyServer<ByokLmProxyState> im
202202
}
203203

204204
const continuationScope = typeof body?.model === 'string' ? this._continuationScope(sessionId, vendor, body.model) : undefined;
205-
const recovered = continuationScope && body.previous_response_id === undefined
205+
const explicitResponseId = body?.previous_response_id;
206+
const explicit = continuationScope && explicitResponseId !== undefined
207+
? Array.from(runtime.state).find(pending => pending.scope === continuationScope && pending.responseId === explicitResponseId)
208+
: undefined;
209+
const recovered = continuationScope && explicitResponseId === undefined
206210
? this._findToolContinuation(runtime.state, continuationScope, body.input)
207211
: undefined;
212+
const consumed = recovered?.pending ?? explicit;
208213
const bridgeBody = recovered
209214
? { ...body, input: recovered.input, previous_response_id: recovered.pending.responseId }
210215
: body;
@@ -245,8 +250,8 @@ export class ByokLmProxyService extends LoopbackProxyServer<ByokLmProxyState> im
245250
this._writeJsonError(res, 502, result.error, 'api_error');
246251
return;
247252
}
248-
if (recovered) {
249-
runtime.state.delete(recovered.pending);
253+
if (consumed) {
254+
runtime.state.delete(consumed);
250255
}
251256
if (continuationScope) {
252257
this._addToolContinuation(runtime.state, continuationScope, result);

src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,59 @@ suite('ByokLmProxyService', () => {
380380
});
381381
});
382382

383+
test('consumes an explicit continuation only after a successful bridge result', async () => {
384+
const captured: IByokLmChatRequest[] = [];
385+
const output = { type: 'function_call_output', call_id: 'call_1', output: 'done' };
386+
const replayedInput = [
387+
{ type: 'function_call', call_id: 'call_1', name: 'tool', arguments: '{}' },
388+
output,
389+
];
390+
391+
await withProxy(
392+
async request => {
393+
captured.push(request);
394+
if (captured.length === 1) {
395+
return {
396+
responseId: 'resp_1',
397+
output: [{ type: 'function_call', callId: 'call_1', name: 'tool', argumentsJson: '{}' }],
398+
};
399+
}
400+
return captured.length === 2
401+
? { output: [], error: 'retryable failure' }
402+
: { output: [{ type: 'message', content: [{ type: 'text', text: 'done' }] }] };
403+
},
404+
async handle => {
405+
const post = (input: unknown, previousResponseId?: string) => fetch(responsesUrl(handle, 'acme'), {
406+
method: 'POST',
407+
headers: authHeaders(handle),
408+
body: JSON.stringify({ model: 'm', input, ...(previousResponseId ? { previous_response_id: previousResponseId } : {}) }),
409+
});
410+
411+
for (const [input, previousResponseId, expectedStatus] of [
412+
[[], undefined, 200],
413+
[[output], 'resp_1', 502],
414+
[[output], 'resp_1', 200],
415+
[replayedInput, undefined, 200],
416+
] as const) {
417+
const response = await post(input, previousResponseId);
418+
assert.strictEqual(response.status, expectedStatus);
419+
await response.text();
420+
}
421+
},
422+
);
423+
424+
assert.deepStrictEqual({
425+
previousResponseIds: captured.map(request => request.previousResponseId),
426+
finalInput: captured[3]?.input,
427+
}, {
428+
previousResponseIds: [undefined, 'resp_1', 'resp_1', undefined],
429+
finalInput: [
430+
{ type: 'function_call', callId: 'call_1', name: 'tool', argumentsJson: '{}' },
431+
{ type: 'function_call_output', callId: 'call_1', output: 'done' },
432+
],
433+
});
434+
});
435+
383436
test('preserves full stateless replay when no resumable provider state is reported', async () => {
384437
const captured: IByokLmChatRequest[] = [];
385438
const initialInput = [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Use the tool.' }] }];
@@ -644,16 +697,18 @@ suite('ByokLmProxyService', () => {
644697
);
645698
});
646699

647-
test('rejects a malformed JSON body with 400', async () => {
700+
test('rejects a malformed or null JSON body with 400', async () => {
648701
await withProxy(
649702
async () => ({ output: [] }),
650703
async (handle) => {
651-
const response = await fetch(responsesUrl(handle, 'acme'), {
652-
method: 'POST',
653-
headers: authHeaders(handle),
654-
body: 'not json',
655-
});
656-
assert.strictEqual(response.status, 400);
704+
for (const body of ['not json', 'null']) {
705+
const response = await fetch(responsesUrl(handle, 'acme'), {
706+
method: 'POST',
707+
headers: authHeaders(handle),
708+
body,
709+
});
710+
assert.strictEqual(response.status, 400);
711+
}
657712
},
658713
);
659714
});

0 commit comments

Comments
 (0)