지서버(ollama_local)에 muse-glimmer를 추가로 올려두고 gemma4:26b와 골라 쓰려 했으나 설정 화면에서 지서버 모델은 자유입력 텍스트 박스라 모델명을 손으로 정확히 적어야 했다. 정작 refreshProviderModels는 ollama_local일 때도 목록을 가져오고 있었는데, 그 결과를 항상 ollama 패널의 select에 써넣어서 — 지서버를 고르면 그 패널이 숨겨지므로 — 가져온 목록이 화면에 한 번도 안 보이고 버려지고 있었다. - 지서버 모델 필드를 select로 바꾸고 "모델 새로고침" 버튼 추가 - refreshProviderModels가 provider별로 맞는 select와 상태줄을 쓰도록 수정 - lm_studio/llama_cpp의 모델 필드는 아직 input이라, SELECT일 때만 option을 채운다 (input에 <option>을 넣으면 사용자가 입력한 값이 날아간다) 여기서 새로 생기는 함정 하나를 같이 막았다. onProviderChange는 ollama_local만 자동 새로고침에서 빼두는데, 드롭다운을 고른 것만으로 WOL 패킷을 쏘고 45초를 기다리면 안 되기 때문이다. 그러면 새로고침 없이 저장할 때 select가 placeholder(값 "")뿐이라 설정된 모델명이 빈 값으로 덮어써진다. seedModelSelect로 저장된 모델을 항상 실제 option으로 심어둬서 막았다(빈 select에 .value를 넣으면 조용히 무시된다는 점도 이유). buildThinkCandidates: 명시적 think:false는 재시도 사다리를 오르지 않는다. 사다리는 glm-5.1:cloud처럼 think를 생략하면 생각만 하고 content를 안 내는 모델을 건지려고 만든 것인데, false는 성격이 다르다. "이 모델은 생각에 예산을 낭비한다"는 운영자의 결정이라, 빈 응답 한 번에 thinking을 도로 켜면 그 결정이 조용히 뒤집힌다. 08-12 muse-glimmer 실측: 생각비중 85%, 체감 3.1 tok/s → think:false로 20.9 tok/s(7배)에 답변은 오히려 길어지고 잘림도 사라짐(20문항 빈 답변 0건). 사다리가 이걸 되돌리면 의도한 7배가 말없이 느린 경로로 돌아간다. 이제 false는 모델 기본값까지만 물러선다 — 빈 응답이 막다른 길이 되지는 않게. 테스트용으로 모듈 레벨 export로 추출(346개 통과). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
/**
|
|
* buildThinkCandidates — think 재시도 사다리
|
|
*
|
|
* 빈 응답이 오면 다른 think 값으로 재시도한다. glm-5.1:cloud처럼 think를 생략하면 생각만 하고
|
|
* content를 안 내놓는 모델을 건지려고 만든 장치다.
|
|
*
|
|
* 문제는 `think:false`였다. 2026-08-12 muse-glimmer(생각비중 85%, 체감 3.1 tok/s) 측정 후
|
|
* 생각을 끄기로 했는데, 사다리가 빈 응답 한 번에 thinking을 도로 켜버리면 그 결정이 조용히
|
|
* 뒤집힌다. 명시적 false는 모델 기본값까지만 물러선다.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { buildThinkCandidates } from '../src/providers/ollama-adapter';
|
|
|
|
describe('think:false는 사다리를 오르지 않는다', () => {
|
|
test('false → [false, undefined]까지만', () => {
|
|
assert.deepEqual(buildThinkCandidates(false), [false, undefined]);
|
|
});
|
|
|
|
test('생각을 켜는 값이 하나도 없다', () => {
|
|
const c = buildThinkCandidates(false);
|
|
for (const v of [true, 'low', 'medium', 'high']) {
|
|
assert.ok(!c.includes(v as any), `${v}가 포함되면 안 됨`);
|
|
}
|
|
});
|
|
|
|
test('빈 응답이 막다른 길이 되지 않게 기본값 폴백은 남긴다', () => {
|
|
assert.ok(buildThinkCandidates(false).includes(undefined));
|
|
});
|
|
});
|
|
|
|
describe('나머지 경로는 그대로', () => {
|
|
test('요청값이 맨 앞에 온다', () => {
|
|
assert.equal(buildThinkCandidates(true)[0], true);
|
|
assert.equal(buildThinkCandidates('low')[0], 'low');
|
|
assert.equal(buildThinkCandidates(undefined)[0], undefined);
|
|
});
|
|
|
|
test('true 요청은 여전히 여러 모드로 재시도한다', () => {
|
|
const c = buildThinkCandidates(true);
|
|
assert.ok(c.includes('low'));
|
|
assert.ok(c.includes(undefined));
|
|
assert.ok(c.includes('medium'));
|
|
});
|
|
|
|
test('중복이 없다', () => {
|
|
for (const req of [undefined, true, false, 'low', 'medium', 'high'] as const) {
|
|
const c = buildThinkCandidates(req);
|
|
assert.equal(new Set(c).size, c.length, `중복 발생: ${JSON.stringify(c)}`);
|
|
}
|
|
});
|
|
});
|