chore: version up to 2.80.34 and package

This commit is contained in:
g1nation
2026-05-12 22:54:21 +09:00
parent 148bfb070b
commit 065e598cca
26 changed files with 2023 additions and 139 deletions
+39 -4
View File
@@ -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);