chore: version up to 2.80.34 and package
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { LMStudioStreamer } from '../src/lmstudio/streamer';
|
||||
import type { ChatStreamEvent } from '../src/lmstudio/streamer';
|
||||
import type { ILMStudioClient } from '../src/lmstudio/client';
|
||||
|
||||
class FakeModel {
|
||||
@@ -15,14 +16,16 @@ class FakeModel {
|
||||
public failNext: Error | null = null;
|
||||
public chunks: string[] = [];
|
||||
|
||||
constructor(opts: { chunks?: string[]; failAfter?: number; throwOnRespond?: Error } = {}) {
|
||||
constructor(opts: { chunks?: string[]; failAfter?: number; throwOnRespond?: Error; stopReason?: string } = {}) {
|
||||
this.chunks = opts.chunks ?? ['Hel', 'lo, ', 'world'];
|
||||
this._failAfter = opts.failAfter;
|
||||
this._throwOnRespond = opts.throwOnRespond;
|
||||
this.stopReason = opts.stopReason;
|
||||
}
|
||||
|
||||
private _failAfter?: number;
|
||||
private _throwOnRespond?: Error;
|
||||
public stopReason: string | undefined;
|
||||
|
||||
respond(chat: any, opts: any) {
|
||||
if (this._throwOnRespond) {
|
||||
@@ -32,10 +35,15 @@ class FakeModel {
|
||||
this.lastOpts = opts;
|
||||
const chunks = this.chunks;
|
||||
const failAfter = this._failAfter;
|
||||
const stopReason = this.stopReason;
|
||||
let i = 0;
|
||||
const self = this;
|
||||
return {
|
||||
// Real OngoingPrediction is both async-iterable AND a thenable resolving to a
|
||||
// PredictionResult with `.stats.stopReason`. Mirror that shape so the streamer
|
||||
// can read the stop reason after the stream drains.
|
||||
const prediction: any = {
|
||||
cancel: async () => { self.cancelCount++; },
|
||||
then(resolve: (v: any) => void) { resolve({ stats: { stopReason } }); },
|
||||
[Symbol.asyncIterator]() {
|
||||
return {
|
||||
async next() {
|
||||
@@ -54,6 +62,7 @@ class FakeModel {
|
||||
};
|
||||
},
|
||||
};
|
||||
return prediction;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +87,19 @@ class FakeClient implements ILMStudioClient {
|
||||
}
|
||||
}
|
||||
|
||||
async function collect(stream: AsyncIterable<{ token: string }>): Promise<string[]> {
|
||||
// The streamer emits a trailing { token: '', stopReason } event on normal completion;
|
||||
// `collect` returns just the non-empty content tokens (what every real consumer uses).
|
||||
async function collect(stream: AsyncIterable<ChatStreamEvent>): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
for await (const { token } of stream) out.push(token);
|
||||
for await (const { token } of stream) {
|
||||
if (token) out.push(token);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function collectEvents(stream: AsyncIterable<ChatStreamEvent>): Promise<ChatStreamEvent[]> {
|
||||
const out: ChatStreamEvent[] = [];
|
||||
for await (const ev of stream) out.push(ev);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -98,6 +117,22 @@ describe('LMStudioStreamer', () => {
|
||||
expect(client.model.lastOpts.temperature).toBe(0.4);
|
||||
});
|
||||
|
||||
test('emits a trailing stopReason event from prediction stats', async () => {
|
||||
const client = new FakeClient(new FakeModel({ chunks: ['hi'], stopReason: 'maxPredictedTokensReached' }));
|
||||
const streamer = new LMStudioStreamer(client);
|
||||
const events = await collectEvents(streamer.stream({
|
||||
modelName: 'm1',
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
temperature: 0.1,
|
||||
maxTokens: 64,
|
||||
}));
|
||||
expect(events.map(e => e.token)).toEqual(['hi', '']);
|
||||
expect(events[events.length - 1].stopReason).toBe('maxPredictedTokensReached');
|
||||
// maxTokens / contextOverflowPolicy are forwarded to the SDK
|
||||
expect(client.model.lastOpts.maxTokens).toBe(64);
|
||||
expect(client.model.lastOpts.contextOverflowPolicy).toBe('stopAtLimit');
|
||||
});
|
||||
|
||||
test('passes signal through to the SDK', async () => {
|
||||
const client = new FakeClient();
|
||||
const streamer = new LMStudioStreamer(client);
|
||||
|
||||
Reference in New Issue
Block a user