Update Astra: v2.80.19 - Refactoring Sidebar, LM Studio integration, and new tests
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { LMStudioClient as SDKClient } from '@lmstudio/sdk';
|
||||
import { logError, logInfo } from '../utils';
|
||||
|
||||
export interface ILMStudioClient {
|
||||
load(modelKey: string, signal?: AbortSignal): Promise<void>;
|
||||
unload(modelKey: string): Promise<void>;
|
||||
listLoaded(): Promise<string[]>;
|
||||
isReachable(): Promise<boolean>;
|
||||
setBaseUrl(httpBaseUrl: string): void;
|
||||
}
|
||||
|
||||
export class LMStudioLifecycleError extends Error {
|
||||
constructor(message: string, public readonly cause?: unknown) {
|
||||
super(message);
|
||||
this.name = 'LMStudioLifecycleError';
|
||||
}
|
||||
}
|
||||
|
||||
export function httpToWebSocketUrl(httpBaseUrl: string): string | undefined {
|
||||
const trimmed = (httpBaseUrl || '').trim();
|
||||
if (!trimmed) return undefined;
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
if (url.protocol === 'http:') url.protocol = 'ws:';
|
||||
else if (url.protocol === 'https:') url.protocol = 'wss:';
|
||||
else if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return undefined;
|
||||
if (url.pathname.endsWith('/v1')) url.pathname = url.pathname.slice(0, -3);
|
||||
if (url.pathname.endsWith('/api')) url.pathname = url.pathname.slice(0, -4);
|
||||
const out = url.toString().replace(/\/+$/, '');
|
||||
return out;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class LMStudioClient implements ILMStudioClient {
|
||||
private _sdk: SDKClient | undefined;
|
||||
private _wsUrl: string | undefined;
|
||||
|
||||
constructor(httpBaseUrl: string) {
|
||||
this.setBaseUrl(httpBaseUrl);
|
||||
}
|
||||
|
||||
setBaseUrl(httpBaseUrl: string): void {
|
||||
const ws = httpToWebSocketUrl(httpBaseUrl);
|
||||
if (ws !== this._wsUrl) {
|
||||
this._wsUrl = ws;
|
||||
this._sdk = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getSdk(): SDKClient {
|
||||
if (!this._sdk) {
|
||||
this._sdk = new SDKClient(this._wsUrl ? { baseUrl: this._wsUrl } : {});
|
||||
}
|
||||
return this._sdk;
|
||||
}
|
||||
|
||||
async load(modelKey: string, signal?: AbortSignal): Promise<void> {
|
||||
try {
|
||||
await this.getSdk().llm.load(modelKey, signal ? { signal } : undefined);
|
||||
logInfo('LM Studio model loaded.', { modelKey });
|
||||
} catch (e: any) {
|
||||
const msg = e?.message ?? String(e);
|
||||
throw new LMStudioLifecycleError(`Failed to load LM Studio model "${modelKey}": ${msg}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async unload(modelKey: string): Promise<void> {
|
||||
try {
|
||||
await this.getSdk().llm.unload(modelKey);
|
||||
logInfo('LM Studio model unloaded.', { modelKey });
|
||||
} catch (e: any) {
|
||||
const msg = e?.message ?? String(e);
|
||||
throw new LMStudioLifecycleError(`Failed to unload LM Studio model "${modelKey}": ${msg}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async listLoaded(): Promise<string[]> {
|
||||
try {
|
||||
const items: any[] = await this.getSdk().llm.listLoaded();
|
||||
return items
|
||||
.map((m) => m?.identifier ?? m?.modelKey ?? m?.path ?? null)
|
||||
.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
||||
} catch (e: any) {
|
||||
const msg = e?.message ?? String(e);
|
||||
throw new LMStudioLifecycleError(`Failed to list loaded LM Studio models: ${msg}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async isReachable(): Promise<boolean> {
|
||||
try {
|
||||
await this.getSdk().llm.listLoaded();
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
logError('LM Studio not reachable.', { error: e?.message ?? String(e) });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user