cdsa-harness 0.22.2 → 0.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/builtins.js +1 -1
- package/src/cli.js +10 -0
- package/src/llm.js +50 -9
package/package.json
CHANGED
package/src/builtins.js
CHANGED
package/src/cli.js
CHANGED
|
@@ -549,6 +549,12 @@ async function runSetup(ask, cfg) {
|
|
|
549
549
|
return false;
|
|
550
550
|
}
|
|
551
551
|
cfg.provider = provider;
|
|
552
|
+
// ollama 설정이 남긴 base_url 잔재는 다른 provider 의 요청까지 그 주소로 보내버린다.
|
|
553
|
+
// provider 를 새로 고르면 지운다(ollama 는 아래에서 다시 채워진다).
|
|
554
|
+
if (provider !== "ollama" && cfg.base_url) {
|
|
555
|
+
console.log(c.dim(`(이전 설정의 base_url(${cfg.base_url}) 을 지웠어요 — ${provider} 공식 엔드포인트를 사용합니다)`));
|
|
556
|
+
cfg.base_url = "";
|
|
557
|
+
}
|
|
552
558
|
|
|
553
559
|
if (provider === "mock") {
|
|
554
560
|
cfg.model = "mock-agent";
|
|
@@ -1263,6 +1269,10 @@ export async function main(argv = []) {
|
|
|
1263
1269
|
const p = user.split(/\s+/)[1];
|
|
1264
1270
|
if (!PROVIDERS.includes(p)) { console.log(c.yellow(`provider 는 ${PROVIDERS.join("/")} 중 하나.`)); continue; }
|
|
1265
1271
|
cfg.provider = p;
|
|
1272
|
+
if (p !== "ollama" && cfg.base_url) {
|
|
1273
|
+
console.log(c.dim(`(이전 설정의 base_url(${cfg.base_url}) 을 지웠어요 — ${p} 공식 엔드포인트를 사용합니다)`));
|
|
1274
|
+
cfg.base_url = "";
|
|
1275
|
+
}
|
|
1266
1276
|
if (SUGGESTED_MODELS[p]?.length) cfg.model = SUGGESTED_MODELS[p][0];
|
|
1267
1277
|
loop.client = makeClient(cfg);
|
|
1268
1278
|
loop.reset();
|
package/src/llm.js
CHANGED
|
@@ -46,6 +46,18 @@ export class LLMClient {
|
|
|
46
46
|
throw new LLMError(`지원하지 않는 provider 입니다: ${this.provider}`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// _post/_openStream 을 감싸, 샘플링 파라미터 거부(400)를 만나면 빼고 1회 재시도.
|
|
50
|
+
async _sendWithSamplingRetry(send, body) {
|
|
51
|
+
if (this._noSampling) this._stripSampling(body);
|
|
52
|
+
try {
|
|
53
|
+
return await send(body);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (!this._shouldRetryWithoutSampling(e, body)) throw e;
|
|
56
|
+
this._stripSampling(body);
|
|
57
|
+
return await send(body);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
// 스트리밍용: 응답 객체(본문 스트림)를 그대로 받는다.
|
|
50
62
|
async _openStream(url, headers, body) {
|
|
51
63
|
const json = JSON.stringify(body);
|
|
@@ -57,7 +69,7 @@ export class LLMClient {
|
|
|
57
69
|
res = await fetch(url, { method: "POST", headers, body: json, signal: ctrl.signal });
|
|
58
70
|
} catch (e) {
|
|
59
71
|
clearTimeout(timer);
|
|
60
|
-
throw new LLMError(this._netErrorMessage(e));
|
|
72
|
+
throw new LLMError(this._netErrorMessage(e, url));
|
|
61
73
|
}
|
|
62
74
|
if (!res.ok) {
|
|
63
75
|
clearTimeout(timer);
|
|
@@ -67,8 +79,24 @@ export class LLMClient {
|
|
|
67
79
|
return { res, started, timer, bodyBytes: Buffer.byteLength(json, "utf8") };
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
_netErrorMessage(e) {
|
|
71
|
-
return describeNetError(e, this.provider, this.timeout);
|
|
82
|
+
_netErrorMessage(e, url = "") {
|
|
83
|
+
return describeNetError(e, this.provider, this.timeout, url);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 최신 Claude 모델(Sonnet 5, Opus 4.7+ 등)은 temperature/top_p/top_k 샘플링 파라미터를
|
|
87
|
+
// 거부한다(400: "`temperature` is deprecated for this model."). 모델 목록을 하드코딩하는
|
|
88
|
+
// 대신 그 400 을 감지하면 파라미터를 빼고 1회 재시도하고, 세션 동안 기억해 다시 안 보낸다.
|
|
89
|
+
_shouldRetryWithoutSampling(e, body) {
|
|
90
|
+
if (this._noSampling) return false;
|
|
91
|
+
if (!("temperature" in body || "top_p" in body || "top_k" in body)) return false;
|
|
92
|
+
return e instanceof LLMError && /API 오류 400/.test(e.message) && /temperature|top_p|top_k/.test(e.message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_stripSampling(body) {
|
|
96
|
+
this._noSampling = true;
|
|
97
|
+
delete body.temperature;
|
|
98
|
+
delete body.top_p;
|
|
99
|
+
delete body.top_k;
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
async _post(url, headers, body) {
|
|
@@ -80,7 +108,7 @@ export class LLMClient {
|
|
|
80
108
|
try {
|
|
81
109
|
res = await fetch(url, { method: "POST", headers, body: json, signal: ctrl.signal });
|
|
82
110
|
} catch (e) {
|
|
83
|
-
throw new LLMError(this._netErrorMessage(e));
|
|
111
|
+
throw new LLMError(this._netErrorMessage(e, url));
|
|
84
112
|
} finally {
|
|
85
113
|
clearTimeout(timer);
|
|
86
114
|
}
|
|
@@ -108,7 +136,7 @@ export class LLMClient {
|
|
|
108
136
|
headers["HTTP-Referer"] = "https://github.com/cdsassj00/miniharness";
|
|
109
137
|
headers["X-Title"] = "CDSA Harness";
|
|
110
138
|
}
|
|
111
|
-
const { payload, latencyMs, bodyBytes } = await this._post(url, headers, body);
|
|
139
|
+
const { payload, latencyMs, bodyBytes } = await this._sendWithSamplingRetry((b) => this._post(url, headers, b), body);
|
|
112
140
|
const parsed = parseOpenAiReply(payload);
|
|
113
141
|
return {
|
|
114
142
|
...parsed,
|
|
@@ -126,7 +154,7 @@ export class LLMClient {
|
|
|
126
154
|
"anthropic-version": "2023-06-01",
|
|
127
155
|
"Content-Type": "application/json",
|
|
128
156
|
};
|
|
129
|
-
const { payload, latencyMs, bodyBytes } = await this._post(url, headers, body);
|
|
157
|
+
const { payload, latencyMs, bodyBytes } = await this._sendWithSamplingRetry((b) => this._post(url, headers, b), body);
|
|
130
158
|
const parsed = parseAnthropicReply(payload);
|
|
131
159
|
return {
|
|
132
160
|
...parsed,
|
|
@@ -148,7 +176,7 @@ export class LLMClient {
|
|
|
148
176
|
headers["HTTP-Referer"] = "https://github.com/cdsassj00/miniharness";
|
|
149
177
|
headers["X-Title"] = "CDSA Harness";
|
|
150
178
|
}
|
|
151
|
-
const { res, started, timer, bodyBytes } = await this._openStream(url, headers, body);
|
|
179
|
+
const { res, started, timer, bodyBytes } = await this._sendWithSamplingRetry((b) => this._openStream(url, headers, b), body);
|
|
152
180
|
let content = "";
|
|
153
181
|
const tcMap = new Map(); // index -> {id,name,args}
|
|
154
182
|
let usage = null;
|
|
@@ -183,7 +211,7 @@ export class LLMClient {
|
|
|
183
211
|
const body = toAnthropicBody(messages, tools, this.model, this.temperature, this.maxTokens);
|
|
184
212
|
body.stream = true;
|
|
185
213
|
const headers = { "x-api-key": this.apiKey, "anthropic-version": "2023-06-01", "Content-Type": "application/json" };
|
|
186
|
-
const { res, started, timer, bodyBytes } = await this._openStream(url, headers, body);
|
|
214
|
+
const { res, started, timer, bodyBytes } = await this._sendWithSamplingRetry((b) => this._openStream(url, headers, b), body);
|
|
187
215
|
let content = "";
|
|
188
216
|
const blocks = new Map(); // index -> {type,name,id,json}
|
|
189
217
|
let usage = { input: null, output: null, total: 0 };
|
|
@@ -231,7 +259,7 @@ export class LLMClient {
|
|
|
231
259
|
// Node 내장 fetch(undici)는 실패하면 겉면 메시지가 "fetch failed" 하나뿐이고,
|
|
232
260
|
// 진짜 원인(DNS 실패·프록시·인증서 등)은 e.cause 체인 안에 숨어 있다.
|
|
233
261
|
// 여기서 원인을 끝까지 파고들어 사용자에게 코드와 해결 힌트까지 보여준다.
|
|
234
|
-
export function describeNetError(e, provider = "", timeout = 0) {
|
|
262
|
+
export function describeNetError(e, provider = "", timeout = 0, url = "") {
|
|
235
263
|
// 자체 타임아웃(AbortController)에 걸린 경우: fetch 는 AbortError 를 던진다.
|
|
236
264
|
if (e?.name === "AbortError" || e?.name === "TimeoutError") {
|
|
237
265
|
return `네트워크 오류: 응답 시간 초과(${timeout}ms). 네트워크가 느리거나 요청이 너무 클 수 있어요.`;
|
|
@@ -257,6 +285,19 @@ export function describeNetError(e, provider = "", timeout = 0) {
|
|
|
257
285
|
else if (codes.length) msg += `\n ↳ 원인 코드: ${codes.join(", ")}`;
|
|
258
286
|
|
|
259
287
|
const has = (...cs) => cs.some((c) => codes.includes(c));
|
|
288
|
+
|
|
289
|
+
// provider 는 클라우드(anthropic 등)인데 요청이 로컬 주소로 가는 경우:
|
|
290
|
+
// 이전에 ollama 설정 등이 남긴 config.json 의 base_url 잔재가 원인이다.
|
|
291
|
+
// 이때 방화벽/프록시 힌트는 오답이므로 이 안내만 보여주고 끝낸다.
|
|
292
|
+
const localUrl = /^https?:\/\/(localhost|127\.0\.0\.1|\[?::1\]?)([:/]|$)/i.test(url);
|
|
293
|
+
if (localUrl && provider && provider !== "ollama") {
|
|
294
|
+
msg +=
|
|
295
|
+
`\n ↳ provider 는 '${provider}' 인데 요청이 로컬 주소(${url})로 가고 있어요. ` +
|
|
296
|
+
"설정 파일(config.json)에 이전 ollama 설정의 base_url 이 남아 있을 가능성이 큽니다.\n" +
|
|
297
|
+
" ↳ 해결: /setup 을 다시 실행하거나, config.json 에서 \"base_url\" 값을 지우고 재시작하세요.";
|
|
298
|
+
return msg;
|
|
299
|
+
}
|
|
300
|
+
|
|
260
301
|
if (has("ENOTFOUND", "EAI_AGAIN")) {
|
|
261
302
|
msg += "\n ↳ DNS 조회 실패예요. 인터넷 연결을 확인하세요. 사내망이라면 프록시 뒤일 가능성이 큽니다(아래 프록시 안내 참고).";
|
|
262
303
|
}
|