feat: v2.2.64 — LM Studio 모델 발견/에러 표시 + macOS 셸 호환성
- LM Studio 모델 dropdown을 SDK system.listDownloadedModels('llm') 으로
조회하도록 변경. REST /v1/models 는 JIT 옵션이 꺼져 있으면 로드된 모델만
반환하여 macOS 환경에서 dropdown 이 비거나 fallback 한 줄만 남던 문제 해결.
SDK 실패 시 REST 로 자동 fallback.
- LM Studio 로드/언로드 실패를 readyBar 의 영속 segment 로 표시. 모델을
다시 선택하면 clearLmStudioError() 로 해제.
- src/security.ts: PowerShell '&&' rewrite 를 win32 에서만 수행. macOS/Linux
에서는 'if (\$?) { ... }' 가 zsh/bash 문법 오류라 명령 자체가 깨졌음.
- src/utils.ts: system prompt 에 OS 별 [ENVIRONMENT] 블록 동적 주입
(셸/경로 스타일/체이닝 연산자). 'cd E:\\... ; ...' 같은 Windows 전용
예시를 macOS 에서 그대로 따라하던 회귀 차단.
- 테스트 mock 에 listDownloaded() 추가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+64
-15
@@ -60,6 +60,13 @@ export interface SidebarLmStudioDeps {
|
||||
activity: IActivityTracker;
|
||||
/** Returns the list of model identifiers currently loaded in LM Studio (cached). */
|
||||
loadedModels: () => Promise<string[]>;
|
||||
/**
|
||||
* Returns every model downloaded into LM Studio (modelKey form). Used by the
|
||||
* dropdown so it does not depend on LM Studio's JIT setting — REST
|
||||
* `/v1/models` only lists loaded models when JIT is off, which on macOS
|
||||
* commonly leaves the dropdown with just the fallback `defaultModel`.
|
||||
*/
|
||||
downloadedModels: () => Promise<string[]>;
|
||||
}
|
||||
|
||||
interface LastVisibleChatSnapshot {
|
||||
@@ -790,9 +797,24 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
});
|
||||
}
|
||||
|
||||
/** Latest LM Studio load/unload error — surfaced as a persistent segment in the readyBar
|
||||
* until the next successful model selection clears it. The transient chat toast alone
|
||||
* was getting missed (scrolled away, blended into chat noise). */
|
||||
private _lmStudioLastError: string | undefined;
|
||||
|
||||
/** Surface LM Studio lifecycle errors (load/unload failures) to the chat UI as a non-fatal toast. */
|
||||
public postLmStudioError(message: string): void {
|
||||
this._lmStudioLastError = message;
|
||||
this._view?.webview.postMessage({ type: 'lmStudioError', value: message });
|
||||
void this._sendReadyStatus();
|
||||
}
|
||||
|
||||
/** Clear the persistent LM Studio error segment. Called when the user picks a new
|
||||
* model so a stale failure does not haunt the next attempt. */
|
||||
public clearLmStudioError(): void {
|
||||
if (this._lmStudioLastError === undefined) return;
|
||||
this._lmStudioLastError = undefined;
|
||||
void this._sendReadyStatus();
|
||||
}
|
||||
|
||||
public resolveWebviewView(
|
||||
@@ -1221,6 +1243,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
contextLength: effectiveContextLength,
|
||||
nominalContextLength: config.contextLength,
|
||||
cappedForSmallModel,
|
||||
lmStudioError: this._lmStudioLastError ?? null,
|
||||
};
|
||||
} catch (err: any) {
|
||||
logError('Failed to build ready status.', { error: err?.message || String(err) });
|
||||
@@ -3915,24 +3938,50 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
} else {
|
||||
const engine = resolveEngine(url); // 단일 엔진만
|
||||
const modelsUrl = buildApiUrl(url, engine, 'models');
|
||||
try {
|
||||
logInfo('Model discovery started.', { engine, modelsUrl, force });
|
||||
const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) });
|
||||
const rawText = await res.text();
|
||||
if (!res.ok) {
|
||||
logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) });
|
||||
} else {
|
||||
const data = rawText ? JSON.parse(rawText) as any : {};
|
||||
models = engine === 'lmstudio'
|
||||
? (data.data || []).map((m: any) => m.id)
|
||||
: (data.models || []).map((m: any) => m.name);
|
||||
|
||||
if (models.length > 0) {
|
||||
logInfo('Model discovery succeeded.', { engine, count: models.length, modelsPreview: models.slice(0, 5) });
|
||||
// LM Studio: prefer the SDK's `system.listDownloadedModels('llm')` over the
|
||||
// REST `/v1/models` endpoint. REST only returns models that are currently
|
||||
// loaded when LM Studio's "Just-In-Time Model Loading" setting is off —
|
||||
// which on macOS leaves the dropdown with zero entries (or one fallback
|
||||
// entry that may not match any real `modelKey`). The SDK call enumerates
|
||||
// every downloaded LLM regardless of JIT and returns the exact `modelKey`
|
||||
// that `llm.load()` accepts. Falls back to REST if the SDK round-trip
|
||||
// throws (e.g. LM Studio app open but local server not enabled).
|
||||
if (engine === 'lmstudio' && this._lmStudio) {
|
||||
try {
|
||||
logInfo('Model discovery started (SDK).', { engine, force });
|
||||
const sdkModels = await this._lmStudio.downloadedModels();
|
||||
if (sdkModels.length > 0) {
|
||||
models = sdkModels;
|
||||
logInfo('Model discovery succeeded (SDK).', { engine, count: models.length, modelsPreview: models.slice(0, 5) });
|
||||
} else {
|
||||
logInfo('LM Studio SDK returned no downloaded models — falling back to REST /v1/models.', { engine });
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('LM Studio SDK model discovery failed — falling back to REST.', { engine, error: e?.message || String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
try {
|
||||
logInfo('Model discovery started (REST).', { engine, modelsUrl, force });
|
||||
const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) });
|
||||
const rawText = await res.text();
|
||||
if (!res.ok) {
|
||||
logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) });
|
||||
} else {
|
||||
const data = rawText ? JSON.parse(rawText) as any : {};
|
||||
models = engine === 'lmstudio'
|
||||
? (data.data || []).map((m: any) => m.id)
|
||||
: (data.models || []).map((m: any) => m.name);
|
||||
|
||||
if (models.length > 0) {
|
||||
logInfo('Model discovery succeeded (REST).', { engine, count: models.length, modelsPreview: models.slice(0, 5) });
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('Model discovery failed (REST).', { engine, modelsUrl, error: e?.message || String(e) });
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('Model discovery failed.', { engine, modelsUrl, error: e?.message || String(e) });
|
||||
}
|
||||
|
||||
online = models.length > 0;
|
||||
|
||||
Reference in New Issue
Block a user