deel-local-cli 1.20.13 → 2.0.1
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/README.ko.md +97 -78
- package/README.md +120 -81
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +23 -3
- package/src/acp/serve.js +333 -25
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +253 -26
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +299 -0
- package/src/commands/model.js +851 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +454 -41
- package/src/configexplain.js +127 -15
- package/src/doctor.js +164 -27
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +531 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +36 -4
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +501 -38
- package/src/tools/fig.js +21 -4
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1083 -148
- package/src/tools/jobs.js +328 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +367 -59
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +328 -45
- package/src/tools/webfetch.js +194 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/backend/azure.js
CHANGED
|
@@ -58,8 +58,19 @@ export function 애저인가(input) {
|
|
|
58
58
|
if (!s) return false;
|
|
59
59
|
const u = 뜯기(s);
|
|
60
60
|
if (!u) return /\/openai\/deployments(\/|\?|$)/i.test(s);
|
|
61
|
+
/*
|
|
62
|
+
* 배포 주소부터 본다 — `/v1` 로 끝나는지보다 **앞**이다.
|
|
63
|
+
*
|
|
64
|
+
* 배포 이름이 하필 `v1` 인 자원이 있다. 그 주소
|
|
65
|
+
* (`…/openai/deployments/v1`)를 `/v1` 로 끝난다는 이유로 OpenAI 규격 창구로
|
|
66
|
+
* 보고 Azure 에서 빼 버렸다. 길에 `/openai/deployments/` 가 이미 적혀 있으면
|
|
67
|
+
* 마지막 한 칸은 **배포 이름**이지 판 번호가 아니다 — 애저풀기 는 이 주소에서
|
|
68
|
+
* 배포 `v1` 을 제대로 뽑는다. 두 함수의 답이 갈리면 한쪽만 맞는 자리가 된다.
|
|
69
|
+
*/
|
|
70
|
+
const 배포길 = /\/openai\/deployments(\/|$)/i.test(u.pathname);
|
|
71
|
+
if (배포길) return true;
|
|
61
72
|
if (/\/v\d+\/?$/.test(u.pathname)) return false;
|
|
62
|
-
return 애저호스트.test(u.hostname)
|
|
73
|
+
return 애저호스트.test(u.hostname);
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
function 뜯기(s) {
|
package/src/backend/cachemark.js
CHANGED
|
@@ -123,7 +123,8 @@ export const 조각표 = Symbol.for('deel.시스템조각');
|
|
|
123
123
|
const 붙일수있나 = (b) => {
|
|
124
124
|
const t = b?.type;
|
|
125
125
|
if (t === 'thinking' || t === 'redacted_thinking') return false;
|
|
126
|
-
|
|
126
|
+
// 빈칸만 든 글도 빈 글이다 — 서버는 그런 글 블록 자체를 거절한다 (2.0.0 6회차 캐시표식6).
|
|
127
|
+
if (t === 'text') return String(b.text ?? '').trim().length > 0;
|
|
127
128
|
return t === 'tool_result' || t === 'tool_use' || t === 'image' || t === 'document';
|
|
128
129
|
};
|
|
129
130
|
|
|
@@ -159,8 +160,18 @@ export const 닻블록 = 14;
|
|
|
159
160
|
/** 대화가 이 블록 수를 넘으면 닻을 하나 더 박는다. 그 아래면 꼬리 하나로 닿는다. */
|
|
160
161
|
export const 닻문턱 = 20;
|
|
161
162
|
|
|
162
|
-
/**
|
|
163
|
-
|
|
163
|
+
/**
|
|
164
|
+
* 메시지 하나가 몇 블록인가. 글 한 덩이짜리는 1블록이다.
|
|
165
|
+
*
|
|
166
|
+
* OpenAI 꼴에서 도구를 부른 차례는 부름이 content 가 아니라 `tool_calls` 에 있고, 서버 쪽에서는
|
|
167
|
+
* 부름 하나하나가 블록이 된다. 1블록으로 세면 부름이 셋인 대화에서 닻→꼬리가 24블록이 되어
|
|
168
|
+
* 창(20) 밖에 박혔다 (2.0.0 6회차 캐시표식6). 글과 부름을 같이 센다.
|
|
169
|
+
*/
|
|
170
|
+
const 블록수 = (m) => {
|
|
171
|
+
if (Array.isArray(m?.content)) return m.content.length;
|
|
172
|
+
const 부름 = Array.isArray(m?.tool_calls) ? m.tool_calls.length : 0;
|
|
173
|
+
return (m?.content ? 1 : 0) + 부름 || 1;
|
|
174
|
+
};
|
|
164
175
|
|
|
165
176
|
/** 이 배열이 통틀어 몇 블록인가. */
|
|
166
177
|
const 통블록 = (ms) => ms.reduce((n, m) => n + 블록수(m), 0);
|
|
@@ -197,7 +208,16 @@ export function 블록에붙이기(msg, 칸 = 'cache_control') {
|
|
|
197
208
|
* @param {boolean} 표식쓰나
|
|
198
209
|
*/
|
|
199
210
|
export function 시스템블록(조각들, 표식쓰나, 칸 = 'cache_control', { 긴수명 = true } = {}) {
|
|
200
|
-
|
|
211
|
+
/*
|
|
212
|
+
* 빈칸만 든 조각은 **없는 것으로 친다.**
|
|
213
|
+
*
|
|
214
|
+
* 잣대가 `x.length` 였다. 그러면 `' '` · `'\n'` 이 그대로 지나 빈칸만 든 글
|
|
215
|
+
* 블록이 되는데, Anthropic 은 그것을 400 으로 튕긴다 — 화면에서는 열쇠가 틀린 것과
|
|
216
|
+
* 구별이 안 되는 400 이다. 조각이 전부 빈칸이면 **그 첫 조각에 캐시 표식까지** 붙어
|
|
217
|
+
* 나갔다. 조각 안의 빈칸은 안 건드린다 — 글자를 한 자라도 바꾸면 앞머리가 통째로
|
|
218
|
+
* 새로 엮인다(아래 굳은끝 머리말).
|
|
219
|
+
*/
|
|
220
|
+
const 있는것 = (조각들 ?? []).map((x) => String(x ?? '')).filter((x) => x.trim().length);
|
|
201
221
|
if (!있는것.length) return null;
|
|
202
222
|
if (!표식쓰나) return 있는것.join('');
|
|
203
223
|
const 굳은끝 = 0; // 첫 조각이 굳은 부분이다
|
|
@@ -281,7 +301,13 @@ function 닻자리(ms, 꼬리) {
|
|
|
281
301
|
let 센것 = 0;
|
|
282
302
|
for (let i = 꼬리; i >= 0; i--) {
|
|
283
303
|
센것 += 블록수(ms[i]);
|
|
284
|
-
|
|
304
|
+
/*
|
|
305
|
+
* 꼬리 **한 메시지**가 벌써 닻블록을 넘으면(병렬 도구 결과 수십 개) 꼬리를 고르면 안 된다 —
|
|
306
|
+
* 표식은 메시지의 마지막 블록에만 붙으니 그건 꼬리 표식과 같은 자리라 닻이 사라지고, 꼬리의
|
|
307
|
+
* 20블록 창은 그 메시지 안에서 끝나 앞 요청이 굳힌 자리에 못 닿는다 (2.0.0 6회차 캐시표식6).
|
|
308
|
+
* 한 칸 앞에 박으면 그 닻이 앞 요청의 꼬리를 읽어 온다.
|
|
309
|
+
*/
|
|
310
|
+
if (센것 >= 닻블록) return i === 꼬리 ? Math.max(0, i - 1) : i;
|
|
285
311
|
}
|
|
286
312
|
return 0;
|
|
287
313
|
}
|
|
@@ -65,7 +65,12 @@ export function 인증서설정(prof) {
|
|
|
65
65
|
* 하나뿐이었다 — 그건 프로필마다 다르게 못 준다. 사내 게이트웨이와 로컬
|
|
66
66
|
* 모델을 오가는 사람에게는 그게 곧 못 쓰는 것이다.
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
/*
|
|
69
|
+
* 개인키만 적은 것도 **버리지 않는다** (사냥5 B5-09). 여기서 null 로 돌리면 사람이 적은
|
|
70
|
+
* key 가 조용히 사라지고, 게이트웨이는 「인증서를 내라」 는 악수 실패만 준다. 들고 가서
|
|
71
|
+
* 인증서찾기 가 「반쪽이다」 라고 말하게 한다.
|
|
72
|
+
*/
|
|
73
|
+
if (!편것.cert && !편것.pfx && !편것.ca && !편것.key) return null;
|
|
69
74
|
return 편것;
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -127,6 +132,23 @@ export function 인증서찾기(주소) {
|
|
|
127
132
|
const o = 집(주소);
|
|
128
133
|
const 설정 = o ? 등록부.get(o) : null;
|
|
129
134
|
if (!설정) return null;
|
|
135
|
+
/*
|
|
136
|
+
* 반쪽만 적었으면 붙기 전에 말한다 (사냥5 B5-09).
|
|
137
|
+
*
|
|
138
|
+
* 인증서만 적고 개인키를 안 적으면 Node 는 인증서를 **아예 안 낸다.** 게이트웨이는
|
|
139
|
+
* 「인증서를 내라」 는 경보로 끊고, 그 화면으로는 무엇이 빠졌는지 알 길이 없다. 개인키만
|
|
140
|
+
* 적은 것도 같다. pfx 는 한 덩이에 둘 다 들었으므로 따지지 않는다. 경로가 틀린 것과 같은
|
|
141
|
+
* 「설정을 고칠 일」 이라 같은 코드로 던진다 — 화면(http.js normalizeError)이 말을 그대로 싣고,
|
|
142
|
+
* 다시 불러 봐야 소용없다는 것도 같다.
|
|
143
|
+
*/
|
|
144
|
+
if (!설정.pfx && !!설정.cert !== !!설정.key) {
|
|
145
|
+
const 있는것 = 설정.cert ? '인증서(cert)' : '개인키(key)';
|
|
146
|
+
const 빠진것 = 설정.cert ? '개인키(key)' : '인증서(cert)';
|
|
147
|
+
throw Object.assign(
|
|
148
|
+
new Error(`클라이언트 ${있는것}만 적혀 있고 ${빠진것}가 없습니다 — 프로필 "인증서" 에 둘을 함께 적거나 pfx 한 덩이로 적으세요`),
|
|
149
|
+
{ code: 'CERT_READ' },
|
|
150
|
+
);
|
|
151
|
+
}
|
|
130
152
|
const 옵션 = {};
|
|
131
153
|
if (설정.pfx) 옵션.pfx = 파일읽기(설정.pfx, '클라이언트 인증서(pfx)');
|
|
132
154
|
if (설정.cert) 옵션.cert = 파일읽기(설정.cert, '클라이언트 인증서');
|
package/src/backend/ctxsize.js
CHANGED
|
@@ -62,6 +62,20 @@ const 출력이름 = [
|
|
|
62
62
|
'num_predict', 'n_predict', 'max_output_length',
|
|
63
63
|
];
|
|
64
64
|
|
|
65
|
+
/*
|
|
66
|
+
* 이름 **앞에 뭐가 붙어 와도** 같은 뜻으로 본다 (`qwen3.context_length` · `llm.n_ctx`).
|
|
67
|
+
*
|
|
68
|
+
* 잣대를 목록마다 따로 둔다. 전에는 최대 쪽 잣대 하나뿐이라 `llm.n_ctx` 가
|
|
69
|
+
* 「모델 최대」 로만 걸리고 「올린 길이」 에서는 안 걸렸다 — 숫자 하나가
|
|
70
|
+
* 「최대 8,192 · 올린 길이 모름」 이 되고, /ctx 자세히 가 그대로 찍었다.
|
|
71
|
+
* 글자 덩어리 쪽(글에서찾기)은 원래 앞에 붙은 것을 넘기고 읽는다. 두 잣대가
|
|
72
|
+
* 갈리면 '객체로 오면 못 찾고 글로 오면 찾는' 설명 못 할 상태가 된다.
|
|
73
|
+
*/
|
|
74
|
+
const 꼬리잣대 = new Map([
|
|
75
|
+
[최대이름, /(^|[._])(context_length|context_window|n_ctx)$/i],
|
|
76
|
+
[올린이름, /(^|[._])(loaded_context_length|num_ctx|n_ctx)$/i],
|
|
77
|
+
]);
|
|
78
|
+
|
|
65
79
|
// 사람이 알아볼 수 없는 값은 안 받는다. 512 미만은 오독, 1000만 초과는 단위 착각.
|
|
66
80
|
const 최소 = 512;
|
|
67
81
|
const 최대허용 = 10_000_000;
|
|
@@ -114,9 +128,10 @@ export function 파보기(obj, 이름들, { 글도 = true, depth = 0 } = {}) {
|
|
|
114
128
|
const v = 성한수(obj[key]);
|
|
115
129
|
if (v) return { value: v, key };
|
|
116
130
|
}
|
|
131
|
+
const 꼬리 = 꼬리잣대.get(이름들);
|
|
117
132
|
for (const [k, v] of Object.entries(obj)) {
|
|
118
|
-
// 이름이 정확히 안 맞으면 '…context_length' 처럼 끝나는 것도 본다 (Ollama).
|
|
119
|
-
if (
|
|
133
|
+
// 이름이 정확히 안 맞으면 '…context_length' 처럼 끝나는 것도 본다 (Ollama · 꼬리잣대 머리말).
|
|
134
|
+
if (꼬리 && 꼬리.test(k)) {
|
|
120
135
|
const n = 성한수(v);
|
|
121
136
|
if (n) return { value: n, key: k };
|
|
122
137
|
}
|
|
@@ -229,7 +244,9 @@ export async function probeCtx(conn, { timeout = 6000 } = {}) {
|
|
|
229
244
|
const 답들 = await Promise.all(자리.map(([label, url, opts]) => 본다(label, url, opts)));
|
|
230
245
|
|
|
231
246
|
let max = null; let loaded = null; let out = null;
|
|
232
|
-
let
|
|
247
|
+
let outSource = null; let maxKey = null; let loadedKey = null;
|
|
248
|
+
// 값마다 어느 창구에서 왔는지 따로 든다 — 아래에서 **채택한 값**의 창구만 내보낸다.
|
|
249
|
+
let 최대창구 = null; let 올린창구 = null;
|
|
233
250
|
|
|
234
251
|
for (const [i, [label]] of 자리.entries()) {
|
|
235
252
|
const json = 답들[i].json;
|
|
@@ -241,15 +258,24 @@ export async function probeCtx(conn, { timeout = 6000 } = {}) {
|
|
|
241
258
|
const m = 파보기(대상, 최대이름);
|
|
242
259
|
const l = 파보기(대상, 올린이름);
|
|
243
260
|
const o = 파보기(대상, 출력이름);
|
|
244
|
-
if (m && !max) { max = m.value; maxKey = m.key;
|
|
245
|
-
if (l && !loaded) { loaded = l.value; loadedKey = l.key; }
|
|
261
|
+
if (m && !max) { max = m.value; maxKey = m.key; 최대창구 = label; }
|
|
262
|
+
if (l && !loaded) { loaded = l.value; loadedKey = l.key; 올린창구 = label; }
|
|
246
263
|
// llama.cpp 의 n_ctx 는 '지금 올린 길이' 다. 그 서버에서는 최대도 그 값으로 본다.
|
|
247
|
-
if (올린것도 && m && !loaded) { loaded = m.value; loadedKey = m.key; }
|
|
264
|
+
if (올린것도 && m && !loaded) { loaded = m.value; loadedKey = m.key; 올린창구 = label; }
|
|
248
265
|
if (o && !out) { out = o.value; outSource = label; }
|
|
249
266
|
}
|
|
250
267
|
|
|
251
268
|
// 실제로 쓸 값: 올려 둔 길이가 있으면 그것. 없으면 모델 최대.
|
|
252
269
|
const value = loaded ?? max ?? null;
|
|
270
|
+
/*
|
|
271
|
+
* source 는 **채택한 값**이 어디서 왔는지다.
|
|
272
|
+
*
|
|
273
|
+
* 여태는 최대를 준 창구 이름을 적었다. 모델 상세가 655,360(최대)을, LM Studio 가
|
|
274
|
+
* 8,192(올린 길이)를 주면 쓰는 값은 8,192 인데 화면은 「모델 상세에서 읽음」 이라
|
|
275
|
+
* 적었다 (repl.js · commands/model.js 가 이 칸을 그대로 찍는다). 값이 이상할 때
|
|
276
|
+
* 사람이 뒤지러 가는 자리가 바로 이 이름이라, 틀리면 엉뚱한 창구를 판다.
|
|
277
|
+
*/
|
|
278
|
+
const source = loaded ? 올린창구 : (max ? 최대창구 : null);
|
|
253
279
|
/*
|
|
254
280
|
* 못 알아낸 까닭. **서버 탓으로 뭉개지 않는다.**
|
|
255
281
|
*
|
|
@@ -260,10 +286,20 @@ export async function probeCtx(conn, { timeout = 6000 } = {}) {
|
|
|
260
286
|
* 이상한 줄 알고 서버를 뒤진다.
|
|
261
287
|
*/
|
|
262
288
|
const 문지기가막음 = tried.length > 0 && tried.every((t) => t.막힘);
|
|
289
|
+
/*
|
|
290
|
+
* 404 로 **답한** 것과 아무 말도 없는 것도 다르다.
|
|
291
|
+
*
|
|
292
|
+
* 여섯 자리가 전부 404 를 돌려줬는데 이 자리는 「아무 응답도 못 받았습니다」 였다.
|
|
293
|
+
* 서버는 또박또박 「그런 문은 없다」 고 말했는데, 사람은 서버가 죽었거나
|
|
294
|
+
* 방화벽이 삼킨 줄 알고 서버를 뒤지러 간다. 받은 코드를 그대로 적으면
|
|
295
|
+
* 「길이를 알려 주는 문이 없는 서버」 라는 것이 한눈에 보인다.
|
|
296
|
+
*/
|
|
297
|
+
const 답한코드 = [...new Set(tried.filter((t) => t.status > 0).map((t) => t.status))];
|
|
263
298
|
const why = value ? null
|
|
264
299
|
: 문지기가막음 ? '이 주소로 나갈 허락이 아직 없어서 물어보지도 못했습니다'
|
|
265
300
|
: tried.some((t) => t.ok) ? '서버가 응답은 했지만 길이를 안 알려 줍니다'
|
|
266
|
-
:
|
|
301
|
+
: 답한코드.length ? `서버는 답했지만 길이를 알려 주는 문이 없습니다 (HTTP ${답한코드.join('·')})`
|
|
302
|
+
: '두드린 자리에서 아무 응답도 못 받았습니다';
|
|
267
303
|
|
|
268
304
|
return { value, max, loaded, out, source, outSource, maxKey, loadedKey, tried, why };
|
|
269
305
|
}
|
|
@@ -281,7 +317,7 @@ export function parseSize(text) {
|
|
|
281
317
|
return 성한수(Number(m[1]) * mult);
|
|
282
318
|
}
|
|
283
319
|
|
|
284
|
-
/**
|
|
320
|
+
/** 32k · 640k · 1.0M — 화면에 넣을 짧은 표기 */
|
|
285
321
|
export function fmtSize(n) {
|
|
286
322
|
const v = Number(n) || 0;
|
|
287
323
|
if (v < 1000) return String(v);
|
|
@@ -289,5 +325,16 @@ export function fmtSize(n) {
|
|
|
289
325
|
return (v / (1024 * 1024)).toFixed(1) + 'M';
|
|
290
326
|
}
|
|
291
327
|
|
|
292
|
-
|
|
328
|
+
/*
|
|
329
|
+
* 서버가 끝내 안 알려 줄 때 쓰는 값.
|
|
330
|
+
*
|
|
331
|
+
* **더 안 올린다.** 요즘 로컬 창구가 실제로 **올려 두는** 길이는 이보다 작다 —
|
|
332
|
+
* Ollama 의 num_ctx, llama.cpp 서버의 --ctx-size, LM Studio 가 처음 올릴 때 잡는
|
|
333
|
+
* 값이 다 4,096 언저리다. 모델이 655,360 까지 된다는 것과 서버가 그만큼 올려
|
|
334
|
+
* 뒀다는 것은 다른 말이다(맨 위 머리말).
|
|
335
|
+
*
|
|
336
|
+
* 여기는 **아무것도 못 알아냈을 때** 쓰는 값이라 큰 쪽으로 틀리면 매 요청이
|
|
337
|
+
* 거절당하고, 작은 쪽으로 틀리면 덜 쓸 뿐이다. 그리고 덜 쓰는 쪽은 3겹
|
|
338
|
+
* (backend/learn.js)이 서버의 거절문에서 곧 배워 올린다 — 반대쪽은 못 배운다.
|
|
339
|
+
*/
|
|
293
340
|
export const 기본값 = 32768;
|
package/src/backend/detect.js
CHANGED
|
@@ -49,12 +49,16 @@ async function tryAzure(input, key) {
|
|
|
49
49
|
let 닿음 = false; // 서버가 HTTP 로 대답을 하기는 했나
|
|
50
50
|
const 막힌것 = []; // 거절당한 방식들 — 다 해 보고 고른다
|
|
51
51
|
let 마지막오류 = null;
|
|
52
|
+
// 200 인데 JSON 이 아닌 답 — 사내 로그인 페이지·프록시 안내면이 가로챈 것 (아래 끝 머리말).
|
|
53
|
+
let 글로답함 = false;
|
|
52
54
|
|
|
53
55
|
for (const style of 차례) {
|
|
54
56
|
if (style.id !== 'none' && !key) continue;
|
|
55
57
|
const r = await req(푼것.목록주소, { headers: headersFor(style.id, key), timeout: 12000 });
|
|
56
58
|
if (r.status) 닿음 = true;
|
|
57
59
|
else 마지막오류 = r.error ?? 마지막오류;
|
|
60
|
+
// 아무도 답한 적 없이 시간이 다 됐으면 머리를 바꿔 12초씩 더 기다리지 않는다 (시간다됨 머리말).
|
|
61
|
+
if (!닿음 && 시간다됨(r)) break;
|
|
58
62
|
|
|
59
63
|
if (r.ok && r.json) {
|
|
60
64
|
const models = 배포목록(r.json);
|
|
@@ -68,7 +72,20 @@ async function tryAzure(input, key) {
|
|
|
68
72
|
+ ' Azure 포털에서 모델을 배포한 뒤 다시 해 보세요.',
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
|
-
|
|
75
|
+
/*
|
|
76
|
+
* 거절당한 것은 **코드를 가리지 않고** 적어 둔다.
|
|
77
|
+
*
|
|
78
|
+
* 여태는 401·403·404 만 담았다. 그래서 앞단이 아파서 낸 500·502 는 담기는
|
|
79
|
+
* 자리가 없었고, 아래에서 「무엇을 봤는지」 를 말할 때 그 자리가 통째로
|
|
80
|
+
* 비어 있었다 — 서버가 아픈 것이 주소를 잘못 적은 것으로 읽혔다.
|
|
81
|
+
*
|
|
82
|
+
* 200 인데 JSON 이 아닌 것(사내 로그인 페이지)은 여기 안 들어온다. 그건
|
|
83
|
+
* 거절이 아니라 **다른 서버가 답한 것**이라, 인증 방식을 고르는 표에
|
|
84
|
+
* 섞이면 안 된다.
|
|
85
|
+
*/
|
|
86
|
+
if (r.status && !r.ok) 막힌것.push({ auth: style.id, status: r.status, ms: r.ms });
|
|
87
|
+
// 그래도 **본 것**이다. 아래 끝에서 이 한 가지만은 말해 준다 (사냥6 막판-뒷단).
|
|
88
|
+
else if (r.ok && !r.json) 글로답함 = true;
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
// 아무도 못 닿았다. 이건 목록 권한 문제가 아니라 연결 문제다.
|
|
@@ -101,10 +118,93 @@ async function tryAzure(input, key) {
|
|
|
101
118
|
warn: 다막힘 && key
|
|
102
119
|
? `배포 목록도 인증도 확인 못 했습니다 (해 본 방식마다 HTTP ${막힌것.map((x) => x.status).join('·')})`
|
|
103
120
|
+ ' — 주소에 적힌 배포로 그냥 씁니다. 첫 한마디에서 걸리면 /model 로 인증 방식을 바꾸세요.'
|
|
104
|
-
|
|
121
|
+
/*
|
|
122
|
+
* 열쇠 없이 두드려 401·403 을 받았으면 그렇다고 말한다. 「목록을 못 봤다」 만 적으면 첫
|
|
123
|
+
* 한마디의 401 을 사람이 짐작해야 한다 — 서버는 이미 열쇠가 없다고 말했다 (6회차 Gemini 알아내기6 D1).
|
|
124
|
+
*/
|
|
125
|
+
: !key && (고른것.status === 401 || 고른것.status === 403)
|
|
126
|
+
? `열쇠 없이 두드렸더니 배포 목록이 HTTP ${고른것.status} 입니다 — 주소에 적힌 배포로 그냥 씁니다.`
|
|
127
|
+
+ ' 첫 한마디에서도 막히면 열쇠(api-key)를 넣으세요.'
|
|
128
|
+
: `배포 목록을 못 봤습니다 (HTTP ${고른것.status}) — 주소에 적힌 배포로 그냥 씁니다.`,
|
|
105
129
|
};
|
|
106
130
|
}
|
|
107
|
-
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* ── 여기까지 왔다는 것은 **배포를 못 집었다**는 뜻이다 ──────────────────
|
|
134
|
+
*
|
|
135
|
+
* 사람이 본 것: 포털에서 자원 주소만(배포 이름 없이) 복사해 넣고 열쇠가
|
|
136
|
+
* 틀렸을 때, 화면은 「Azure 주소로 보이는데 배포를 못 찾았습니다 — 주소에
|
|
137
|
+
* /openai/deployments/<배포이름> 까지 넣어 보세요」 였다. 시키는 대로 주소를
|
|
138
|
+
* 고쳐 넣어도 아무것도 안 바뀐다. 서버는 세 방식 모두에 401 을 냈고, 그건
|
|
139
|
+
* 주소 이야기가 아니라 **열쇠 이야기**였다.
|
|
140
|
+
*
|
|
141
|
+
* 왜 그랬나: 배포를 못 집으면(푼것.base 가 없으면) 위 갈래에 못 들어가고
|
|
142
|
+
* 여기서 그냥 `null` 이 나갔다. 세 번 두드려 받아 온 상태 코드를 **통째로
|
|
143
|
+
* 버린 것**이다. 부르는 쪽(detect)은 받은 것이 없으니 제가 아는 짐작 한
|
|
144
|
+
* 줄을 적었고, 그 짐작이 틀리는 자리가 바로 여기다.
|
|
145
|
+
*
|
|
146
|
+
* 이제 지키는 규칙: **본 것을 그대로 들고 나간다.** 상태 코드로 말하고,
|
|
147
|
+
* 주소를 고치라는 말은 그 말이 들을 만한 자리(404)에서만 한다. 상태 코드도
|
|
148
|
+
* 함께 넘겨서 부르는 쪽이 제 표로 옮길 수 있게 한다 (providers 의 막힌까닭).
|
|
149
|
+
*/
|
|
150
|
+
const 본상태 = 막힌것.map((x) => x.status);
|
|
151
|
+
/*
|
|
152
|
+
* ── 200 으로 답한 자리에 「주소를 고쳐 보세요」 라고 하지 않는다 (사냥6 막판-뒷단) ──
|
|
153
|
+
*
|
|
154
|
+
* 사내 SSO·프록시가 배포 목록 요청을 가로채 **200 으로 로그인 페이지**를 준다. 그건
|
|
155
|
+
* 거절이 아니라 위 막힌것 에 안 담기고(그건 맞다 — 인증 방식 표에 섞이면 안 된다),
|
|
156
|
+
* 그래서 여기서 `null` 이 나갔다. 부르는 쪽(detect)은 받은 것이 없으니 제 짐작 —
|
|
157
|
+
* 「주소에 /openai/deployments/<배포이름> 까지 넣어 보세요」— 를 적었는데, 주소에는
|
|
158
|
+
* 이미 그 길이 적혀 있고 서버는 200 으로 또박또박 답했다. 시키는 대로 고쳐도 아무것도
|
|
159
|
+
* 안 바뀐다. 바로 위 머리말이 「주소를 고치라는 말은 404 에서만」 이라고 못 박은 그 자리다.
|
|
160
|
+
*
|
|
161
|
+
* 본 것을 그대로 말한다. 여기서 사람이 할 일은 주소를 고치는 것이 아니라 **브라우저로
|
|
162
|
+
* 같은 주소를 열어 무엇이 뜨는지 보는 것**이다.
|
|
163
|
+
*/
|
|
164
|
+
if (!본상태.length) {
|
|
165
|
+
if (!글로답함) return null;
|
|
166
|
+
return {
|
|
167
|
+
kind: null,
|
|
168
|
+
status: 200,
|
|
169
|
+
why: '배포 목록 자리가 HTTP 200 으로 답했는데 JSON 이 아닙니다 — Azure 가 아니라'
|
|
170
|
+
+ ' 사내 로그인 페이지나 프록시 안내면이 가로챈 것으로 보입니다.'
|
|
171
|
+
+ ' 브라우저로 같은 주소를 열어 무엇이 뜨는지 보세요 — 주소를 고쳐도 이 답은 안 바뀝니다.',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const 대표 = 본상태.find((s) => s >= 500)
|
|
175
|
+
?? 본상태.find((s) => s === 401 || s === 403)
|
|
176
|
+
?? 본상태[0];
|
|
177
|
+
return { kind: null, status: 대표, why: 애저막힌말(대표, 본상태) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 배포를 못 집은 채 끝났을 때 무슨 말을 하나. 본 것만 말한다 — 짐작을 안 보탠다. */
|
|
181
|
+
function 애저막힌말(대표, 본상태) {
|
|
182
|
+
const 본것 = 본상태.length > 1
|
|
183
|
+
? `해 본 방식마다 HTTP ${본상태.join('·')}`
|
|
184
|
+
: `HTTP ${대표}`;
|
|
185
|
+
if (대표 >= 500) {
|
|
186
|
+
return `자원에는 닿았는데 배포 목록에서 서버가 오류를 냈습니다 (${본것})`
|
|
187
|
+
+ ' — 주소가 아니라 서버 쪽입니다. 잠시 뒤 다시 해 보세요.';
|
|
188
|
+
}
|
|
189
|
+
if (대표 === 401 || 대표 === 403) {
|
|
190
|
+
return `자원에는 닿았는데 열쇠를 안 받아 줍니다 (${본것})`
|
|
191
|
+
+ ' — 주소가 아니라 열쇠를 보세요. 배포 이름을 붙여도 이 답은 안 바뀝니다.';
|
|
192
|
+
}
|
|
193
|
+
if (대표 === 404) {
|
|
194
|
+
return `배포 목록을 못 봤습니다 (${본것})`
|
|
195
|
+
+ ' — 주소에 /openai/deployments/<배포이름> 까지 넣어 보세요.';
|
|
196
|
+
}
|
|
197
|
+
/*
|
|
198
|
+
* 404 가 아니면 **주소 이야기가 아니다.**
|
|
199
|
+
*
|
|
200
|
+
* 429 는 앞단이 지금 못 받는다는 말이고 400 은 우리가 보낸 것을 못 읽었다는
|
|
201
|
+
* 말이다. 둘 다 배포 이름을 붙여도 답이 안 바뀐다. 그런데도 이 자리는 코드를
|
|
202
|
+
* 가리지 않고 「주소에 배포 이름을 넣어 보세요」 로 끝났다 — 바로 위 머리말이
|
|
203
|
+
* 「그 말은 들을 만한 자리(404)에서만 한다」 고 적어 둔 그 약속을 깬 것이다.
|
|
204
|
+
* 시키는 대로 주소를 고쳐 넣어도 아무것도 안 바뀐다.
|
|
205
|
+
*/
|
|
206
|
+
return `배포 목록을 못 봤습니다 (${본것})`
|
|
207
|
+
+ ' — 404 가 아니라 주소 이야기가 아닙니다. 서버가 낸 코드를 그대로 보세요.';
|
|
108
208
|
}
|
|
109
209
|
|
|
110
210
|
/*
|
|
@@ -121,11 +221,53 @@ async function tryAzure(input, key) {
|
|
|
121
221
|
* 하나는 0(닿지도 못함)일 수 있는데, 401 쪽이 훨씬 쓸모 있는 단서다 — 적어도
|
|
122
222
|
* 서버가 거기 있다는 뜻이니까.
|
|
123
223
|
*/
|
|
124
|
-
function 막은것적기(막힌것, r) {
|
|
224
|
+
export function 막은것적기(막힌것, r) {
|
|
125
225
|
if (!막힌것) return;
|
|
126
226
|
const 말 = (r.status ? serverMessage(r) : r.error) ?? '';
|
|
127
|
-
|
|
227
|
+
/*
|
|
228
|
+
* ── 404 는 약한 단서다 ────────────────────────────────────────────────
|
|
229
|
+
*
|
|
230
|
+
* 사람이 본 것: `/v1/models` 는 404(길 없음), `/models` 는 401(열쇠 틀림)인 게이트웨이에서
|
|
231
|
+
* 화면이 「no route」 · HTTP 404 — 주소 탓 — 로 끝났다. 서버는 열쇠를 안 받는다고 분명히
|
|
232
|
+
* 말했는데, 후보 차례상 404 를 먼저 받았고 여기가 **처음 받은 것만** 적었다
|
|
233
|
+
* (6회차 Gemini 알아내기6 D4). 사람은 주소를 고치러 가고 열쇠는 안 본다.
|
|
234
|
+
*
|
|
235
|
+
* 이제 지키는 규칙: 404·405 와 2xx·3xx(다른 무엇이 답함)는 「이 길이 아니다」 일 뿐이라,
|
|
236
|
+
* 뒤에 401·403·5xx 같은 **그 서버가 제 말로 거절한 것**이 오면 그걸로 바꿔 적는다.
|
|
237
|
+
* 그 밖에는 여전히 처음 것이 이긴다.
|
|
238
|
+
*/
|
|
239
|
+
const 약한것 = (s) => s === 404 || s === 405 || (s > 0 && s < 400);
|
|
240
|
+
if (r.status && (!막힌것.status || (약한것(막힌것.status) && !약한것(r.status)))) { 막힌것.status = r.status; 막힌것.why = 말; }
|
|
128
241
|
else if (!막힌것.status && !막힌것.why) 막힌것.why = 말;
|
|
242
|
+
// 아무도 답한 적 없는데 시간이 다 됐다 — 이 주소는 먹통이다(시간다됨 머리말). 한 번 답한 곳은 먹통이 아니다.
|
|
243
|
+
if (시간다됨(r) && !막힌것.status) 막힌것.먹통 = true;
|
|
244
|
+
/*
|
|
245
|
+
* ...그리고 뒤늦게라도 서버가 제 말로 답하면 그 표시는 **내린다.**
|
|
246
|
+
*
|
|
247
|
+
* 먹통의 뜻은 「아무도 답한 적 없는데 시간이 다 됐다」 이다. 그 뒤에 401 이
|
|
248
|
+
* 와서 status 가 채워져도 표시는 그대로 남아 있었다. 남은 표시 하나로 부르는
|
|
249
|
+
* 쪽(detect)은 나머지 후보를 통째로 건너뛰고 먹통끝() 으로 나간다 — 서버가
|
|
250
|
+
* 거기 있다고 방금 말했는데 화면은 「닿지도 못했다」 로 끝난다.
|
|
251
|
+
*/
|
|
252
|
+
else if (r.status && 막힌것.먹통) 막힌것.먹통 = false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/*
|
|
256
|
+
* ── 안 답하는 곳을 인증 방식마다 또 기다리지 않는다 ─────────────────────
|
|
257
|
+
*
|
|
258
|
+
* 사람이 본 것: 방화벽이 삼키는 주소(VPN 이 반쯤 올라온 자리)를 넣었더니 설치 화면이 3분 넘게
|
|
259
|
+
* 멈췄다가 「시간 초과」 한 줄로 끝났다.
|
|
260
|
+
*
|
|
261
|
+
* 실제로 있었던 일: 후보 주소 셋 × 인증 방식 넷을 하나씩 12초씩 기다렸다 — 일반 주소 188초(16번),
|
|
262
|
+
* Azure 36초(3번) (6회차 Gemini 알아내기6 D3). 후보들은 **같은 호스트·포트**에 길만 다르고, 머리
|
|
263
|
+
* 하나 · 길 하나 바꾼다고 안 답하던 곳이 답하지 않는다.
|
|
264
|
+
*
|
|
265
|
+
* 이제 지키는 규칙: 아무도 답한 적 없는 채 **시간이 다 되면** 그 주소는 거기서 끝. 곧장 오는 실패
|
|
266
|
+
* (연결 거절 · DNS)는 시간이 안 드니 그대로 둔다 — 고칠 것은 기다림이지 차례가 아니다.
|
|
267
|
+
*/
|
|
268
|
+
const 시간코드 = new Set(['TimeoutError', 'CONNECT_TIMEOUT', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT']);
|
|
269
|
+
function 시간다됨(r) {
|
|
270
|
+
return !r?.status && 시간코드.has(r?.code);
|
|
129
271
|
}
|
|
130
272
|
|
|
131
273
|
/*
|
|
@@ -172,9 +314,15 @@ async function tryAnthropic(base, key, 막힌것 = null) {
|
|
|
172
314
|
timeout: 12000,
|
|
173
315
|
});
|
|
174
316
|
막은것적기(막힌것, r);
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
317
|
+
if (막힌것?.먹통) return null; // 안 답하는 곳을 인증 방식마다 또 기다리지 않는다 (시간다됨 머리말)
|
|
318
|
+
/*
|
|
319
|
+
* 목록 **모양**을 본다 — tryOpenAI 와 같은 잣대(모델목록모양 머리말). `data` 가 배열이기만
|
|
320
|
+
* 보면 모든 길에 `{"data":[{"status":"ok"}]}` 를 주는 개발 서버가, OpenAI 쪽에서는 걸러지고
|
|
321
|
+
* 여기로 넘어와 「anthropic · 모델 0개」 로 붙었다 (6회차 Gemini 알아내기6 E3). 사냥5 B5-08 이
|
|
322
|
+
* 막은 고장이 뒷문으로 다시 들어온 것이다.
|
|
323
|
+
*/
|
|
324
|
+
const 목록 = r.ok ? 모델목록모양(r.json) : null;
|
|
325
|
+
if (목록) return { kind: 'anthropic', base, auth, models: normalizeModels(목록), ms: r.ms };
|
|
178
326
|
}
|
|
179
327
|
return null;
|
|
180
328
|
}
|
|
@@ -183,7 +331,18 @@ async function tryAnthropic(base, key, 막힌것 = null) {
|
|
|
183
331
|
export function 앤트로픽같나(input, key) {
|
|
184
332
|
if (/^sk-ant-/.test(String(key ?? '').trim())) return true;
|
|
185
333
|
try {
|
|
186
|
-
|
|
334
|
+
/*
|
|
335
|
+
* 주소는 **candidates() 와 똑같이** 다듬고 본다.
|
|
336
|
+
*
|
|
337
|
+
* detect 는 origin 만 trim 하고 원본 input 은 그대로 여기로 넘긴다. 그래서
|
|
338
|
+
* 앞뒤에 빈칸이 붙은 ` api.anthropic.com ` 은 여기서 URL 이 안 만들어져
|
|
339
|
+
* false 가 됐다 — 정작 실제로 두드릴 주소는 candidates() 가 빈칸을 털어
|
|
340
|
+
* 제대로 만든다. 단서를 잃으면 Anthropic 규격을 먼저 볼 기회를 놓치고,
|
|
341
|
+
* OpenAI 쪽이 `{data:[...]}` 를 내주면 규격이 거기서 굳는다.
|
|
342
|
+
* setup --url 은 사람이 붙여 넣은 글을 trim 도 안 하고 준다.
|
|
343
|
+
*/
|
|
344
|
+
const 주소 = String(input ?? '').trim().replace(/\s+/g, '');
|
|
345
|
+
const u = new URL(/^https?:\/\//i.test(주소) ? 주소 : 'https://' + 주소);
|
|
187
346
|
if (/(^|\.)anthropic\.com$/i.test(u.hostname)) return true;
|
|
188
347
|
/*
|
|
189
348
|
* AWS mantle 의 Anthropic 창구. 길에 규격 이름이 적혀 있다.
|
|
@@ -204,6 +363,27 @@ export function 앤트로픽같나(input, key) {
|
|
|
204
363
|
} catch { return false; }
|
|
205
364
|
}
|
|
206
365
|
|
|
366
|
+
/**
|
|
367
|
+
* 받은 JSON 이 **모델 목록 모양**인가. 맞으면 그 목록, 아니면 null (사냥5 B5-08).
|
|
368
|
+
*
|
|
369
|
+
* `r.json.data ?? r.json.models ?? []` 로 읽었더니 없는 칸이 곧 빈 목록이 됐다. 그래서
|
|
370
|
+
* 모든 길에 `{"status":"ok"}` 나 `[]` 를 주는 개발 서버가 「openai · 모델 0개」 로 붙었고,
|
|
371
|
+
* 사람은 모델이 안 올라온 줄 알고 엉뚱한 서버를 뒤졌다.
|
|
372
|
+
*
|
|
373
|
+
* 빈 목록은 받는다 — 모델을 아직 안 올린 LM Studio 가 실제로 `{"data":[]}` 를 준다.
|
|
374
|
+
* 그러나 **칸은 있어야** 하고, 원소가 있으면 적어도 하나는 이름(id·name·model)을 가져야
|
|
375
|
+
* 한다. scan.js 도 같은 잣대를 쓴다 — 설치와 훑기가 서로 다른 서버를 LLM 이라 부르면 안 된다.
|
|
376
|
+
*/
|
|
377
|
+
export function 모델목록모양(json) {
|
|
378
|
+
if (!json || typeof json !== 'object' || Array.isArray(json)) return null;
|
|
379
|
+
const list = Array.isArray(json.data) ? json.data : (Array.isArray(json.models) ? json.models : null);
|
|
380
|
+
if (!list) return null;
|
|
381
|
+
const 이름있나 = (m) => (typeof m === 'string' && m.length > 0)
|
|
382
|
+
|| (!!m && typeof m === 'object' && !!(m.id ?? m.name ?? m.model));
|
|
383
|
+
if (list.length && !list.some(이름있나)) return null;
|
|
384
|
+
return list;
|
|
385
|
+
}
|
|
386
|
+
|
|
207
387
|
// OpenAI 호환인지 확인 (GET {base}/models). 막힌것은 막은것적기 머리말 참고.
|
|
208
388
|
async function tryOpenAI(base, key, 막힌것 = null) {
|
|
209
389
|
const 적기 = (r) => 막은것적기(막힌것, r);
|
|
@@ -212,12 +392,9 @@ async function tryOpenAI(base, key, 막힌것 = null) {
|
|
|
212
392
|
if (style.id === 'none' && key) { /* 키를 줬어도 인증 없는 서버일 수 있으니 마지막에 본다 */ }
|
|
213
393
|
const r = await req(`${base}/models`, { headers: headersFor(style.id, key), timeout: 12000 });
|
|
214
394
|
적기(r);
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return { kind: 'openai', base, auth: style.id, models: normalizeModels(list), ms: r.ms };
|
|
219
|
-
}
|
|
220
|
-
}
|
|
395
|
+
if (막힌것?.먹통) return null; // 안 답하는 곳을 인증 방식마다 또 기다리지 않는다 (시간다됨 머리말)
|
|
396
|
+
const 목록 = r.ok ? 모델목록모양(r.json) : null;
|
|
397
|
+
if (목록) return { kind: 'openai', base, auth: style.id, models: normalizeModels(목록), ms: r.ms };
|
|
221
398
|
// 401/403 이면 규격은 맞고 인증만 틀린 것 — 다음 방식으로 계속.
|
|
222
399
|
//
|
|
223
400
|
// 그 밖의 오류(500 등)는 '규격은 맞는데 서버가 지금 화가 난 것' 으로 본다.
|
|
@@ -237,9 +414,53 @@ async function tryOpenAI(base, key, 막힌것 = null) {
|
|
|
237
414
|
* 실제로 이 길로 온다.
|
|
238
415
|
*/
|
|
239
416
|
if (r.status === 400 && /anthropic-version/i.test(serverMessage(r) ?? '')) return null;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
417
|
+
/*
|
|
418
|
+
* 2xx 인데 위에서 목록 모양이 아니었으면(JSON 이든 아니든) 이 규격이 아니다. 3xx 도
|
|
419
|
+
* 같다 — 되돌림을 안 따라가므로, 모든 길을 `/login` 으로 보내는 관리 화면이 「HTTP 302」
|
|
420
|
+
* 경고를 단 openai 로 굳었다. 둘 다 서버가 **다른 무엇**이라는 말이지 화가 난 것이
|
|
421
|
+
* 아니다 (사냥5 B5-08).
|
|
422
|
+
*/
|
|
423
|
+
/*
|
|
424
|
+
* 405 도 같다. `/models` 가 GET 을 안 받는다는 말이라 모델 목록 문이 아니다 — 404 처럼 「이 길이
|
|
425
|
+
* 아니다」 로 보고 다음으로 간다. 여태는 LLM 이 아닌 서버가 「openai · 인증 bearer」 초록색으로
|
|
426
|
+
* 붙었고 짐작 표시도 없었다 (6회차 Gemini 알아내기6 E1). 400·402 는 그대로 둔다 — 서버가 요청을
|
|
427
|
+
* 읽고 제 말로 답한 것이다(아래 머리말과 detect.test 의 402 검사).
|
|
428
|
+
*/
|
|
429
|
+
const 이규격아님 = (r.status >= 200 && r.status < 400) || r.status === 405;
|
|
430
|
+
if (r.status && !이규격아님 && ![401, 403, 0, 404].includes(r.status)) {
|
|
431
|
+
/*
|
|
432
|
+
* ── 답을 했다는 것과 **이 규격이라는 것**은 다른 말이다 ──────────────
|
|
433
|
+
*
|
|
434
|
+
* 사람이 본 것: 설치 화면이 초록색 「연결됨 · 인증 bearer」 로 끝나고,
|
|
435
|
+
* 그 값이 프로필에 그대로 저장됐다. 그런데 첫 한마디부터 401 이다.
|
|
436
|
+
* 열쇠를 다시 발급받고, 다시 넣고, 또 401 이다 — 화면 어디에도 인증
|
|
437
|
+
* 방식을 우리가 **찍었다**는 말이 없으니 열쇠만 의심하게 된다.
|
|
438
|
+
*
|
|
439
|
+
* 실제로 있었던 일: `/models` 에 503 이 왔다. 5xx·429 는 앞단 프록시나
|
|
440
|
+
* 로드밸런서도 낸다. 그 한 마디로 알 수 있는 것은 「거기 무언가가 있다」
|
|
441
|
+
* 뿐인데, 우리는 규격을 openai 로 못 박고 **그때 쓰던 인증 방식**을
|
|
442
|
+
* 같이 적어 뒀다. 차례상 맨 앞이라 대개 bearer 였다. 서버가 그 방식을
|
|
443
|
+
* 받는다고 말한 적은 한 번도 없다.
|
|
444
|
+
*
|
|
445
|
+
* 이제 지키는 규칙: 서버가 한 말은 그대로 싣되, **짐작은 짐작이라고
|
|
446
|
+
* 적는다.** 5xx·429 는 규격도 인증도 확인된 것이 아니다 — 그 한 줄이
|
|
447
|
+
* 있으면 사람은 첫 401 에서 열쇠 대신 인증 방식을 먼저 본다.
|
|
448
|
+
*/
|
|
449
|
+
const 서버말 = serverMessage(r);
|
|
450
|
+
const 짐작 = r.status >= 500 || r.status === 429;
|
|
451
|
+
return {
|
|
452
|
+
kind: 'openai',
|
|
453
|
+
base,
|
|
454
|
+
auth: style.id,
|
|
455
|
+
models: [],
|
|
456
|
+
ms: r.ms,
|
|
457
|
+
짐작,
|
|
458
|
+
warn: 짐작
|
|
459
|
+
? `${서버말}\n서버가 HTTP ${r.status} 만 돌려줘서 규격도 인증도 확인 못 했습니다`
|
|
460
|
+
+ ` — 인증은 '${style.id}' 로 짐작해 적어 둡니다. 첫 한마디에서 401 이 나면`
|
|
461
|
+
+ ' 열쇠보다 /model 의 인증 방식을 먼저 보세요.'
|
|
462
|
+
: 서버말,
|
|
463
|
+
};
|
|
243
464
|
}
|
|
244
465
|
}
|
|
245
466
|
return null;
|
|
@@ -281,9 +502,12 @@ export async function detect(input, key) {
|
|
|
281
502
|
const hit = await tryAzure(input, key);
|
|
282
503
|
if (hit?.kind) return { ...hit, tried };
|
|
283
504
|
// 못 붙은 이유를 tryAzure 가 알고 있으면 그 말을 쓴다. 우리가 아는 것보다 정확하다.
|
|
505
|
+
// 상태 코드도 같이 넘긴다 — 401 을 받은 자리에 「주소를 고쳐 보세요」 라고
|
|
506
|
+
// 적던 것이 이 자리의 고장이었다 (tryAzure 끝의 머리말).
|
|
284
507
|
return {
|
|
285
508
|
kind: null,
|
|
286
509
|
tried,
|
|
510
|
+
status: hit?.status ?? 0,
|
|
287
511
|
why: hit?.why ?? 'Azure 주소로 보이는데 배포를 못 찾았습니다 — 주소에 /openai/deployments/<배포이름> 까지 넣어 보세요.',
|
|
288
512
|
};
|
|
289
513
|
}
|
|
@@ -293,22 +517,31 @@ export async function detect(input, key) {
|
|
|
293
517
|
const 막힌것 = { status: 0, why: '' };
|
|
294
518
|
|
|
295
519
|
// 단서가 있으면 이 규격을 먼저 본다 (tryAnthropic 머리말).
|
|
520
|
+
// 후보들은 같은 호스트·포트에 길만 다르다. 한 번 먹통이면 나머지 후보도 같은 답이다 (시간다됨 머리말).
|
|
521
|
+
const 먹통끝 = () => ({ kind: null, tried, status: 막힌것.status, why: 막힌것.why });
|
|
522
|
+
|
|
296
523
|
if (앤트로픽같나(input, key)) {
|
|
297
524
|
for (const base of candidates(input)) {
|
|
298
525
|
tried.push(base);
|
|
299
526
|
const hit = await tryAnthropic(base, key, 막힌것);
|
|
300
527
|
if (hit) return { ...hit, tried };
|
|
528
|
+
if (막힌것.먹통) return 먹통끝();
|
|
301
529
|
}
|
|
302
530
|
}
|
|
303
531
|
|
|
304
532
|
// Ollama 를 먼저 본다 — 로컬이면 대개 이쪽이고 확인이 빠르다.
|
|
305
|
-
|
|
533
|
+
// 두드린 자리는 여기서도 적는다. 이 갈래만 안 적어서, Ollama 로 붙으면
|
|
534
|
+
// 「어디를 두드렸나」 화면(setup.js)이 이 갈래에서만 빈칸이었다.
|
|
535
|
+
const 올라마주소 = /^https?:\/\//i.test(origin) ? origin : 'http://' + origin;
|
|
536
|
+
if (!tried.includes(올라마주소)) tried.push(올라마주소);
|
|
537
|
+
const oll = await tryOllama(올라마주소);
|
|
306
538
|
if (oll) return { ...oll, tried };
|
|
307
539
|
|
|
308
540
|
for (const base of candidates(input)) {
|
|
309
541
|
if (!tried.includes(base)) tried.push(base);
|
|
310
542
|
const hit = await tryOpenAI(base, key, 막힌것);
|
|
311
543
|
if (hit) return { ...hit, tried };
|
|
544
|
+
if (막힌것.먹통) return 먹통끝();
|
|
312
545
|
}
|
|
313
546
|
|
|
314
547
|
/*
|