deel-local-cli 1.13.0 → 1.14.0
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 +32 -26
- package/README.md +32 -26
- package/bin/deel.js +6 -1
- package/package.json +3 -2
- package/src/acp/map.js +367 -367
- package/src/acp/serve.js +830 -810
- package/src/agent/budget.js +156 -156
- package/src/agent/commit.js +534 -534
- package/src/agent/compact.js +441 -441
- package/src/agent/effort.js +308 -308
- package/src/agent/evolve.js +263 -263
- package/src/agent/loop.js +1755 -1728
- package/src/agent/memory.js +156 -156
- package/src/agent/mention.js +210 -210
- package/src/agent/recall.js +222 -222
- package/src/agent/session.js +1056 -1056
- package/src/backend/adapter.js +1355 -1355
- package/src/backend/cachemark.js +223 -223
- package/src/backend/detect.js +327 -327
- package/src/backend/http.js +460 -460
- package/src/backend/mcp.js +407 -407
- package/src/backend/price.js +260 -260
- package/src/backend/probe.js +487 -487
- package/src/backend/quota.js +274 -274
- package/src/backend/tokens.js +59 -59
- package/src/backend/toolfit.js +352 -352
- package/src/backend/wire.js +715 -715
- package/src/cmdnames.js +68 -0
- package/src/commands.js +3116 -3184
- package/src/config.js +283 -283
- package/src/i18n/en.js +545 -545
- package/src/i18n/ja.js +498 -498
- package/src/i18n/ko.js +592 -592
- package/src/i18n/zh.js +498 -498
- package/src/oneshot.js +685 -625
- package/src/pack/selfpack.js +15 -2
- package/src/pack/sheet.en.js +16 -2
- package/src/pack/tar.js +154 -154
- package/src/pack/zip.js +235 -235
- package/src/plugins/manage.js +416 -416
- package/src/repl.js +2616 -2616
- package/src/safety/audit.js +148 -148
- package/src/safety/guard.js +714 -641
- package/src/safety/network.js +200 -200
- package/src/tools/fastgrep.js +229 -229
- package/src/tools/fsutil.js +312 -291
- package/src/tools/index.js +2590 -2560
- package/src/tools/jobs.js +876 -876
- package/src/tools/spawn.js +213 -213
- package/src/tools/verify.js +358 -358
- package/src/tools/webfetch.js +418 -418
- package/src/ui/export.js +233 -233
- package/src/ui/pick.js +115 -115
- package/src/ui/status.js +617 -617
package/src/backend/detect.js
CHANGED
|
@@ -1,327 +1,327 @@
|
|
|
1
|
-
// 주소만 받아서 "이 서버가 무슨 규격이고 인증을 어떻게 받는지" 알아낸다.
|
|
2
|
-
import { req, headersFor, AUTH_STYLES, serverMessage } from './http.js';
|
|
3
|
-
import { 맨틀호스트인가 } from './toolfit.js';
|
|
4
|
-
import { 애저인가, 애저풀기, 애저base, 배포목록 } from './azure.js';
|
|
5
|
-
import { ANTHROPIC_VERSION } from './adapter.js';
|
|
6
|
-
|
|
7
|
-
// 사람이 대충 적은 주소를 시도해볼 후보들로 넓힌다.
|
|
8
|
-
export function candidates(input) {
|
|
9
|
-
let u = String(input).trim().replace(/\s+/g, '');
|
|
10
|
-
if (!/^https?:\/\//i.test(u)) u = 'http://' + u;
|
|
11
|
-
u = u.replace(/\/+$/, '');
|
|
12
|
-
const out = [];
|
|
13
|
-
const push = (x) => { if (x && !out.includes(x)) out.push(x); };
|
|
14
|
-
|
|
15
|
-
if (/\/v\d+$/.test(u)) push(u); // .../v1 을 직접 준 경우
|
|
16
|
-
else { push(u + '/v1'); push(u); push(u + '/openai/v1'); }
|
|
17
|
-
return out;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/*
|
|
21
|
-
* Azure 확인.
|
|
22
|
-
*
|
|
23
|
-
* 배포 목록(`/openai/deployments?api-version=`)을 먼저 묻는다. 여기서 오는
|
|
24
|
-
* 이름이 곧 주소에 들어갈 이름이라, 목록을 받으면 사람은 고르기만 하면 된다.
|
|
25
|
-
*
|
|
26
|
-
* 목록을 막아 둔 테넌트가 많다(권한이 따로다). 그때도 **연결 실패로 치지
|
|
27
|
-
* 않는다** — 주소에 배포 이름이 이미 있으면 그것으로 그냥 간다. 목록을 못 본
|
|
28
|
-
* 것과 못 쓰는 것은 다르고, 여기서 실패로 처리하면 정작 잘 되는 설정이
|
|
29
|
-
* 설치 화면을 못 넘어간다.
|
|
30
|
-
*
|
|
31
|
-
* ── 다만 두 가지는 성공이 아니다 ──────────────────────────────────────
|
|
32
|
-
*
|
|
33
|
-
* 1) **아무 바이트도 안 왔을 때.** 포트가 닫혀 있거나, VPN 이 안 올라왔거나,
|
|
34
|
-
* 자원 이름을 잘못 적었을 때가 그렇다. 이걸 "목록만 못 봤다" 로 넘기면
|
|
35
|
-
* 설치 화면이 초록색 `연결됨` 을 띄우고, 사람은 권한 문제인 줄 알고
|
|
36
|
-
* 엉뚱한 데를 뒤진다. 서버에 닿지도 못했으면 닿지 못했다고 해야 한다.
|
|
37
|
-
*
|
|
38
|
-
* 2) **첫 번째 401 로 인증 방식을 정해 버리는 것.** Azure 앞단을 Entra ID 로
|
|
39
|
-
* 감싼 곳은 `api-key` 에 401 을 주고 `Bearer` 를 받는다. 첫 401 에서
|
|
40
|
-
* 멈추면 그 곳은 영영 못 붙는다 — 화면은 초록색인데 첫 한마디가 401 이다.
|
|
41
|
-
* 그래서 방식은 **다 해 보고** 고른다.
|
|
42
|
-
*/
|
|
43
|
-
async function tryAzure(input, key) {
|
|
44
|
-
const 푼것 = 애저풀기(input);
|
|
45
|
-
if (!푼것) return null;
|
|
46
|
-
// Azure 는 api-key 헤더가 제 방식이다. 그것부터 본다.
|
|
47
|
-
const 차례 = ['api-key', 'bearer', 'none'].map((id) => AUTH_STYLES.find((s) => s.id === id));
|
|
48
|
-
|
|
49
|
-
let 닿음 = false; // 서버가 HTTP 로 대답을 하기는 했나
|
|
50
|
-
const 막힌것 = []; // 거절당한 방식들 — 다 해 보고 고른다
|
|
51
|
-
let 마지막오류 = null;
|
|
52
|
-
|
|
53
|
-
for (const style of 차례) {
|
|
54
|
-
if (style.id !== 'none' && !key) continue;
|
|
55
|
-
const r = await req(푼것.목록주소, { headers: headersFor(style.id, key), timeout: 12000 });
|
|
56
|
-
if (r.status) 닿음 = true;
|
|
57
|
-
else 마지막오류 = r.error ?? 마지막오류;
|
|
58
|
-
|
|
59
|
-
if (r.ok && r.json) {
|
|
60
|
-
const models = 배포목록(r.json);
|
|
61
|
-
const base = 푼것.base ?? (models[0] ? 애저base(푼것.origin, models[0].id, 푼것.판, 푼것.앞길) : null);
|
|
62
|
-
if (base) return { kind: 'openai', base, auth: style.id, models, ms: r.ms, azure: true };
|
|
63
|
-
// 200 인데 배포가 하나도 없다. 인증은 통했으니 더 두드려 봐야 같은 답이다.
|
|
64
|
-
return {
|
|
65
|
-
kind: null,
|
|
66
|
-
ms: r.ms,
|
|
67
|
-
why: '배포 목록은 받았는데 비어 있습니다 — 이 자원에 배포된 모델이 없습니다.'
|
|
68
|
-
+ ' Azure 포털에서 모델을 배포한 뒤 다시 해 보세요.',
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
if ([401, 403, 404].includes(r.status)) 막힌것.push({ auth: style.id, status: r.status, ms: r.ms });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 아무도 못 닿았다. 이건 목록 권한 문제가 아니라 연결 문제다.
|
|
75
|
-
if (!닿음) return { kind: null, why: 마지막오류 ?? '주소에 닿지 못했습니다' };
|
|
76
|
-
|
|
77
|
-
if (푼것.base && 막힌것.length) {
|
|
78
|
-
/*
|
|
79
|
-
* 어느 방식으로 적어 둘까.
|
|
80
|
-
*
|
|
81
|
-
* 401·403 은 "인증이 틀렸다" 는 말이고, 404 는 "인증은 됐는데 그 자리가
|
|
82
|
-
* 없다"에 가깝다. 그러니 404 를 받은 방식이 있으면 그쪽이 맞을 확률이
|
|
83
|
-
* 높다. 아무것도 안 통했으면 Azure 의 제 방식(api-key)으로 적어 두되,
|
|
84
|
-
* **열쇠를 줬는데 '인증 없음' 으로 적지는 않는다** — 그렇게 적으면 그
|
|
85
|
-
* 뒤의 모든 요청이 열쇠 없이 나간다.
|
|
86
|
-
*/
|
|
87
|
-
// 열쇠를 줬으면 '인증 없음' 은 아예 후보에서 뺀다. 그것으로 적어 두면
|
|
88
|
-
// 그 뒤의 모든 요청이 맨몸으로 나간다 — 붙은 것처럼 보이는데 다 401 이다.
|
|
89
|
-
const 볼것 = key ? 막힌것.filter((x) => x.auth !== 'none') : 막힌것;
|
|
90
|
-
const 쓸것 = 볼것.length ? 볼것 : [{ auth: key ? 'api-key' : 'none', status: 막힌것[0].status, ms: 막힌것[0].ms }];
|
|
91
|
-
const 나은것 = 쓸것.find((x) => x.status === 404) ?? null;
|
|
92
|
-
const 고른것 = 나은것 ?? 쓸것[0];
|
|
93
|
-
const 다막힘 = !나은것;
|
|
94
|
-
return {
|
|
95
|
-
kind: 'openai',
|
|
96
|
-
base: 푼것.base,
|
|
97
|
-
auth: 고른것.auth,
|
|
98
|
-
models: [],
|
|
99
|
-
ms: 고른것.ms,
|
|
100
|
-
azure: true,
|
|
101
|
-
warn: 다막힘 && key
|
|
102
|
-
? `배포 목록도 인증도 확인 못 했습니다 (해 본 방식마다 HTTP ${막힌것.map((x) => x.status).join('·')})`
|
|
103
|
-
+ ' — 주소에 적힌 배포로 그냥 씁니다. 첫 한마디에서 걸리면 /model 로 인증 방식을 바꾸세요.'
|
|
104
|
-
: `배포 목록을 못 봤습니다 (HTTP ${고른것.status}) — 주소에 적힌 배포로 그냥 씁니다.`,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/*
|
|
111
|
-
* 못 붙었을 때 **무엇이 막았는지**를 적어 둔다.
|
|
112
|
-
*
|
|
113
|
-
* 여태는 실패한 응답을 그냥 버렸다. 그래서 401(닿았는데 열쇠) · 404(주소가
|
|
114
|
-
* 아님) · Bedrock 의 AccessDenied(모델 신청을 안 함)가 화면에서 전부
|
|
115
|
-
* 「연결 실패」 한 마디가 됐다. 셋은 고칠 자리가 완전히 다른데도.
|
|
116
|
-
*
|
|
117
|
-
* 값을 채워 주는 자리를 인자로 받는 것은, 두드리는 함수들이 돌려주는 모양을
|
|
118
|
-
* 안 바꾸려는 것이다 — 그 반환값(붙었나 아닌가)은 부르는 데가 여럿이다.
|
|
119
|
-
*
|
|
120
|
-
* **진짜 상태코드를 먼저 적는다.** 후보를 여럿 두드리다 보면 하나는 401 이고
|
|
121
|
-
* 하나는 0(닿지도 못함)일 수 있는데, 401 쪽이 훨씬 쓸모 있는 단서다 — 적어도
|
|
122
|
-
* 서버가 거기 있다는 뜻이니까.
|
|
123
|
-
*/
|
|
124
|
-
function 막은것적기(막힌것, r) {
|
|
125
|
-
if (!막힌것) return;
|
|
126
|
-
const 말 = (r.status ? serverMessage(r) : r.error) ?? '';
|
|
127
|
-
if (!막힌것.status && r.status) { 막힌것.status = r.status; 막힌것.why = 말; }
|
|
128
|
-
else if (!막힌것.status && !막힌것.why) 막힌것.why = 말;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/*
|
|
132
|
-
* Anthropic 규격인지 확인 (GET {base}/models · 판 머리를 얹어서).
|
|
133
|
-
*
|
|
134
|
-
* ── 왜 OpenAI 쪽과 안 헷갈리나 ─────────────────────────────────────────
|
|
135
|
-
*
|
|
136
|
-
* 문 이름이 `/models` 로 같다. 그런데 이쪽 서버는 `anthropic-version` 머리가
|
|
137
|
-
* 없으면 400 을 준다. tryOpenAI 는 그 머리를 안 보내므로 여기 붙을 수가 없다.
|
|
138
|
-
*
|
|
139
|
-
* 반대는 성립하지 않는다 — OpenAI 호환 서버는 모르는 머리를 그냥 무시하므로
|
|
140
|
-
* 이 함수로도 붙어 버린다. 그래서 **뒤에 두드린다.** 다만 열쇠가 sk-ant- 로
|
|
141
|
-
* 시작하거나 주소가 그 회사면 먼저 본다. 그때는 헷갈릴 일이 없고, 앞에서
|
|
142
|
-
* 네 가지 인증 방식으로 헛되이 두드리는 시간도 없앤다.
|
|
143
|
-
*
|
|
144
|
-
* ── 인증은 주소를 보고 고른다 ──────────────────────────────────────────
|
|
145
|
-
*
|
|
146
|
-
* 이 규격은 오랫동안 x-api-key 하나뿐이었다. 지금은 한 자리가 더 있다 —
|
|
147
|
-
* AWS 의 mantle 창구가 같은 Anthropic Messages 규격을 **Bedrock API 키를
|
|
148
|
-
* Bearer 로 받아서** 내어 준다.
|
|
149
|
-
*
|
|
150
|
-
* bedrock-mantle.<리전>.api.aws/anthropic/v1
|
|
151
|
-
*
|
|
152
|
-
* 그렇다고 아무 데나 두 방식을 다 두드리지는 않는다. 안 되는 방식으로
|
|
153
|
-
* 두드릴 때마다 401 이 하나씩 늘고 사람이 기다리는 시간도 그만큼 는다.
|
|
154
|
-
* 그래서 **그 주소에서만** Bearer 를 먼저 본다.
|
|
155
|
-
*
|
|
156
|
-
* 여기서 고른 값이 그대로 프로필에 남아 매 요청에 실린다. 틀리면 열쇠가
|
|
157
|
-
* 맞는데도 매번 401 이고, 화면에서는 열쇠가 틀린 것과 구별이 안 된다.
|
|
158
|
-
*/
|
|
159
|
-
function 맨틀인가(base) {
|
|
160
|
-
// 호스트를 알아보는 규칙은 backend/toolfit.js 한 곳에 둔다. 여기에 사본을
|
|
161
|
-
// 두면 AWS 가 이름을 하나 더 낼 때 한쪽만 고쳐진다.
|
|
162
|
-
try { return 맨틀호스트인가(new URL(String(base)).hostname); }
|
|
163
|
-
catch { return false; }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function tryAnthropic(base, key, 막힌것 = null) {
|
|
167
|
-
if (!key) return null;
|
|
168
|
-
const 볼것 = 맨틀인가(base) ? ['bearer', 'x-api-key'] : ['x-api-key'];
|
|
169
|
-
for (const auth of 볼것) {
|
|
170
|
-
const r = await req(`${base}/models`, {
|
|
171
|
-
headers: headersFor(auth, key, { 'anthropic-version': ANTHROPIC_VERSION }),
|
|
172
|
-
timeout: 12000,
|
|
173
|
-
});
|
|
174
|
-
막은것적기(막힌것, r);
|
|
175
|
-
if (r.ok && Array.isArray(r.json?.data)) {
|
|
176
|
-
return { kind: 'anthropic', base, auth, models: normalizeModels(r.json.data), ms: r.ms };
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** 이 규격을 먼저 볼 만한 단서가 있나. 열쇠 앞머리나 주소로만 본다. */
|
|
183
|
-
export function 앤트로픽같나(input, key) {
|
|
184
|
-
if (/^sk-ant-/.test(String(key ?? '').trim())) return true;
|
|
185
|
-
try {
|
|
186
|
-
const u = new URL(/^https?:\/\//i.test(input) ? String(input) : 'https://' + String(input));
|
|
187
|
-
if (/(^|\.)anthropic\.com$/i.test(u.hostname)) return true;
|
|
188
|
-
/*
|
|
189
|
-
* AWS mantle 의 Anthropic 창구. 길에 규격 이름이 적혀 있다.
|
|
190
|
-
*
|
|
191
|
-
* bedrock-mantle.<리전>.api.aws/anthropic/v1
|
|
192
|
-
*
|
|
193
|
-
* 같은 호스트가 `/openai/v1` 도 함께 연다. 그래서 호스트만 보면 안 되고
|
|
194
|
-
* **길까지** 봐야 한다 — 안 그러면 OpenAI 창구로 붙이려는 사람이
|
|
195
|
-
* Anthropic 쪽으로 끌려간다.
|
|
196
|
-
*
|
|
197
|
-
* 이 단서가 없으면 OpenAI 규격으로 먼저 두드리게 되는데, 그쪽 서버가
|
|
198
|
-
* 모르는 머리를 무시하고 `{data:[...]}` 를 내주면 규격이 openai 로
|
|
199
|
-
* 굳어 버린다. 그러면 캐시 표식을 붙일 자리가 사라진다 — 이 창구를
|
|
200
|
-
* 후보에 넣은 이유가 바로 그 표식이라, 순서 하나로 뜻이 없어진다.
|
|
201
|
-
*/
|
|
202
|
-
if (맨틀인가(u.origin) && /(^|\/)anthropic(\/|$)/i.test(u.pathname)) return true;
|
|
203
|
-
return false;
|
|
204
|
-
} catch { return false; }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// OpenAI 호환인지 확인 (GET {base}/models). 막힌것은 막은것적기 머리말 참고.
|
|
208
|
-
async function tryOpenAI(base, key, 막힌것 = null) {
|
|
209
|
-
const 적기 = (r) => 막은것적기(막힌것, r);
|
|
210
|
-
for (const style of AUTH_STYLES) {
|
|
211
|
-
if (style.id !== 'none' && !key) continue;
|
|
212
|
-
if (style.id === 'none' && key) { /* 키를 줬어도 인증 없는 서버일 수 있으니 마지막에 본다 */ }
|
|
213
|
-
const r = await req(`${base}/models`, { headers: headersFor(style.id, key), timeout: 12000 });
|
|
214
|
-
적기(r);
|
|
215
|
-
if (r.ok && r.json) {
|
|
216
|
-
const list = r.json.data ?? r.json.models ?? [];
|
|
217
|
-
if (Array.isArray(list)) {
|
|
218
|
-
return { kind: 'openai', base, auth: style.id, models: normalizeModels(list), ms: r.ms };
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// 401/403 이면 규격은 맞고 인증만 틀린 것 — 다음 방식으로 계속.
|
|
222
|
-
//
|
|
223
|
-
// 그 밖의 오류(500 등)는 '규격은 맞는데 서버가 지금 화가 난 것' 으로 본다.
|
|
224
|
-
// 그래야 서버가 한 말을 사람에게 그대로 보여 줄 수 있다.
|
|
225
|
-
//
|
|
226
|
-
// 단, **200 인데 JSON 이 아니면 여기 해당하지 않는다.** 사내 프록시가
|
|
227
|
-
// 로그인 페이지를 200 으로 내주는 일이 흔한데, 예전에는 그걸 "OpenAI 호환
|
|
228
|
-
// 서버 · 모델 0개" 로 잡았다. 그러면 사람은 모델이 안 올라온 줄 알고
|
|
229
|
-
// 엉뚱한 데를 파게 된다 — 실제로는 인증 페이지에 막힌 것이다.
|
|
230
|
-
/*
|
|
231
|
-
* 400 인데 「anthropic-version 이 필요하다」 고 하면, 그건 서버가 화가 난
|
|
232
|
-
* 것이 아니라 **규격이 다르다**는 말이다. 서버가 제 규격을 말해 줬다.
|
|
233
|
-
*
|
|
234
|
-
* 여기서 openai 로 잡아 버리면 설치는 초록색으로 끝나고 첫 한마디에서
|
|
235
|
-
* 400 이 난다 — 그 화면으로는 원인을 알 길이 없다. 열쇠 앞머리가 sk-ant-
|
|
236
|
-
* 가 아닌 열쇠(사내에서 다시 발급한 것 등)로 그 규격 서버에 붙을 때
|
|
237
|
-
* 실제로 이 길로 온다.
|
|
238
|
-
*/
|
|
239
|
-
if (r.status === 400 && /anthropic-version/i.test(serverMessage(r) ?? '')) return null;
|
|
240
|
-
const 성공인데JSON아님 = r.status >= 200 && r.status < 300 && !r.json;
|
|
241
|
-
if (r.status && !성공인데JSON아님 && ![401, 403, 0, 404].includes(r.status)) {
|
|
242
|
-
return { kind: 'openai', base, auth: style.id, models: [], ms: r.ms, warn: serverMessage(r) };
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Ollama 자체 규격인지 확인
|
|
249
|
-
async function tryOllama(origin) {
|
|
250
|
-
const v = await req(`${origin}/api/version`, { timeout: 8000 });
|
|
251
|
-
if (!v.ok || !v.json?.version) return null;
|
|
252
|
-
const tags = await req(`${origin}/api/tags`, { timeout: 12000 });
|
|
253
|
-
const models = (tags.json?.models ?? []).map((m) => ({
|
|
254
|
-
id: m.name ?? m.model,
|
|
255
|
-
note: m.details?.parameter_size ? `${m.details.parameter_size} · ${fmtSize(m.size)}` : fmtSize(m.size),
|
|
256
|
-
}));
|
|
257
|
-
return { kind: 'ollama', base: origin, auth: 'none', models, version: v.json.version, ms: v.ms };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function fmtSize(bytes) {
|
|
261
|
-
if (!bytes) return '';
|
|
262
|
-
const gb = bytes / 1024 ** 3;
|
|
263
|
-
return gb >= 1 ? `${gb.toFixed(1)}GB` : `${Math.round(bytes / 1024 ** 2)}MB`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function normalizeModels(list) {
|
|
267
|
-
return list
|
|
268
|
-
.map((m) => (typeof m === 'string' ? { id: m } : { id: m.id ?? m.name ?? m.model, note: m.owned_by ?? m.display_name ?? '' }))
|
|
269
|
-
.filter((m) => m.id);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export async function detect(input, key) {
|
|
273
|
-
const tried = [];
|
|
274
|
-
const origin = String(input).trim().replace(/\/+$/, '').replace(/\/v\d+$/, '');
|
|
275
|
-
|
|
276
|
-
// Azure 는 모양이 아예 다르다. 여기서 갈라 놓지 않으면 아래 후보들이
|
|
277
|
-
// 엉뚱한 자리를 두드리다 끝난다.
|
|
278
|
-
if (애저인가(input)) {
|
|
279
|
-
const 푼것 = 애저풀기(input);
|
|
280
|
-
tried.push(푼것?.목록주소 ?? String(input));
|
|
281
|
-
const hit = await tryAzure(input, key);
|
|
282
|
-
if (hit?.kind) return { ...hit, tried };
|
|
283
|
-
// 못 붙은 이유를 tryAzure 가 알고 있으면 그 말을 쓴다. 우리가 아는 것보다 정확하다.
|
|
284
|
-
return {
|
|
285
|
-
kind: null,
|
|
286
|
-
tried,
|
|
287
|
-
why: hit?.why ?? 'Azure 주소로 보이는데 배포를 못 찾았습니다 — 주소에 /openai/deployments/<배포이름> 까지 넣어 보세요.',
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// 못 붙으면 무엇이 막았는지를 같이 돌려준다 — 부르는 쪽이 사람 말로 옮긴다
|
|
292
|
-
// (providers/index.js 의 막힌까닭). tryOpenAI 머리말 참고.
|
|
293
|
-
const 막힌것 = { status: 0, why: '' };
|
|
294
|
-
|
|
295
|
-
// 단서가 있으면 이 규격을 먼저 본다 (tryAnthropic 머리말).
|
|
296
|
-
if (앤트로픽같나(input, key)) {
|
|
297
|
-
for (const base of candidates(input)) {
|
|
298
|
-
tried.push(base);
|
|
299
|
-
const hit = await tryAnthropic(base, key, 막힌것);
|
|
300
|
-
if (hit) return { ...hit, tried };
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Ollama 를 먼저 본다 — 로컬이면 대개 이쪽이고 확인이 빠르다.
|
|
305
|
-
const oll = await tryOllama(/^https?:\/\//i.test(origin) ? origin : 'http://' + origin);
|
|
306
|
-
if (oll) return { ...oll, tried };
|
|
307
|
-
|
|
308
|
-
for (const base of candidates(input)) {
|
|
309
|
-
if (!tried.includes(base)) tried.push(base);
|
|
310
|
-
const hit = await tryOpenAI(base, key, 막힌것);
|
|
311
|
-
if (hit) return { ...hit, tried };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/*
|
|
315
|
-
* 마지막으로 이 규격도 본다.
|
|
316
|
-
*
|
|
317
|
-
* 사내 게이트웨이가 이 규격만 열어 둔 경우가 있다. 여기까지 왔다는 것은
|
|
318
|
-
* OpenAI 쪽으로는 안 붙었다는 뜻이라, 이제 헷갈릴 일이 없다.
|
|
319
|
-
*/
|
|
320
|
-
if (key && !앤트로픽같나(input, key)) {
|
|
321
|
-
for (const base of candidates(input)) {
|
|
322
|
-
const hit = await tryAnthropic(base, key, 막힌것);
|
|
323
|
-
if (hit) return { ...hit, tried };
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return { kind: null, tried, status: 막힌것.status, why: 막힌것.why };
|
|
327
|
-
}
|
|
1
|
+
// 주소만 받아서 "이 서버가 무슨 규격이고 인증을 어떻게 받는지" 알아낸다.
|
|
2
|
+
import { req, headersFor, AUTH_STYLES, serverMessage } from './http.js';
|
|
3
|
+
import { 맨틀호스트인가 } from './toolfit.js';
|
|
4
|
+
import { 애저인가, 애저풀기, 애저base, 배포목록 } from './azure.js';
|
|
5
|
+
import { ANTHROPIC_VERSION } from './adapter.js';
|
|
6
|
+
|
|
7
|
+
// 사람이 대충 적은 주소를 시도해볼 후보들로 넓힌다.
|
|
8
|
+
export function candidates(input) {
|
|
9
|
+
let u = String(input).trim().replace(/\s+/g, '');
|
|
10
|
+
if (!/^https?:\/\//i.test(u)) u = 'http://' + u;
|
|
11
|
+
u = u.replace(/\/+$/, '');
|
|
12
|
+
const out = [];
|
|
13
|
+
const push = (x) => { if (x && !out.includes(x)) out.push(x); };
|
|
14
|
+
|
|
15
|
+
if (/\/v\d+$/.test(u)) push(u); // .../v1 을 직접 준 경우
|
|
16
|
+
else { push(u + '/v1'); push(u); push(u + '/openai/v1'); }
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Azure 확인.
|
|
22
|
+
*
|
|
23
|
+
* 배포 목록(`/openai/deployments?api-version=`)을 먼저 묻는다. 여기서 오는
|
|
24
|
+
* 이름이 곧 주소에 들어갈 이름이라, 목록을 받으면 사람은 고르기만 하면 된다.
|
|
25
|
+
*
|
|
26
|
+
* 목록을 막아 둔 테넌트가 많다(권한이 따로다). 그때도 **연결 실패로 치지
|
|
27
|
+
* 않는다** — 주소에 배포 이름이 이미 있으면 그것으로 그냥 간다. 목록을 못 본
|
|
28
|
+
* 것과 못 쓰는 것은 다르고, 여기서 실패로 처리하면 정작 잘 되는 설정이
|
|
29
|
+
* 설치 화면을 못 넘어간다.
|
|
30
|
+
*
|
|
31
|
+
* ── 다만 두 가지는 성공이 아니다 ──────────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* 1) **아무 바이트도 안 왔을 때.** 포트가 닫혀 있거나, VPN 이 안 올라왔거나,
|
|
34
|
+
* 자원 이름을 잘못 적었을 때가 그렇다. 이걸 "목록만 못 봤다" 로 넘기면
|
|
35
|
+
* 설치 화면이 초록색 `연결됨` 을 띄우고, 사람은 권한 문제인 줄 알고
|
|
36
|
+
* 엉뚱한 데를 뒤진다. 서버에 닿지도 못했으면 닿지 못했다고 해야 한다.
|
|
37
|
+
*
|
|
38
|
+
* 2) **첫 번째 401 로 인증 방식을 정해 버리는 것.** Azure 앞단을 Entra ID 로
|
|
39
|
+
* 감싼 곳은 `api-key` 에 401 을 주고 `Bearer` 를 받는다. 첫 401 에서
|
|
40
|
+
* 멈추면 그 곳은 영영 못 붙는다 — 화면은 초록색인데 첫 한마디가 401 이다.
|
|
41
|
+
* 그래서 방식은 **다 해 보고** 고른다.
|
|
42
|
+
*/
|
|
43
|
+
async function tryAzure(input, key) {
|
|
44
|
+
const 푼것 = 애저풀기(input);
|
|
45
|
+
if (!푼것) return null;
|
|
46
|
+
// Azure 는 api-key 헤더가 제 방식이다. 그것부터 본다.
|
|
47
|
+
const 차례 = ['api-key', 'bearer', 'none'].map((id) => AUTH_STYLES.find((s) => s.id === id));
|
|
48
|
+
|
|
49
|
+
let 닿음 = false; // 서버가 HTTP 로 대답을 하기는 했나
|
|
50
|
+
const 막힌것 = []; // 거절당한 방식들 — 다 해 보고 고른다
|
|
51
|
+
let 마지막오류 = null;
|
|
52
|
+
|
|
53
|
+
for (const style of 차례) {
|
|
54
|
+
if (style.id !== 'none' && !key) continue;
|
|
55
|
+
const r = await req(푼것.목록주소, { headers: headersFor(style.id, key), timeout: 12000 });
|
|
56
|
+
if (r.status) 닿음 = true;
|
|
57
|
+
else 마지막오류 = r.error ?? 마지막오류;
|
|
58
|
+
|
|
59
|
+
if (r.ok && r.json) {
|
|
60
|
+
const models = 배포목록(r.json);
|
|
61
|
+
const base = 푼것.base ?? (models[0] ? 애저base(푼것.origin, models[0].id, 푼것.판, 푼것.앞길) : null);
|
|
62
|
+
if (base) return { kind: 'openai', base, auth: style.id, models, ms: r.ms, azure: true };
|
|
63
|
+
// 200 인데 배포가 하나도 없다. 인증은 통했으니 더 두드려 봐야 같은 답이다.
|
|
64
|
+
return {
|
|
65
|
+
kind: null,
|
|
66
|
+
ms: r.ms,
|
|
67
|
+
why: '배포 목록은 받았는데 비어 있습니다 — 이 자원에 배포된 모델이 없습니다.'
|
|
68
|
+
+ ' Azure 포털에서 모델을 배포한 뒤 다시 해 보세요.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if ([401, 403, 404].includes(r.status)) 막힌것.push({ auth: style.id, status: r.status, ms: r.ms });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 아무도 못 닿았다. 이건 목록 권한 문제가 아니라 연결 문제다.
|
|
75
|
+
if (!닿음) return { kind: null, why: 마지막오류 ?? '주소에 닿지 못했습니다' };
|
|
76
|
+
|
|
77
|
+
if (푼것.base && 막힌것.length) {
|
|
78
|
+
/*
|
|
79
|
+
* 어느 방식으로 적어 둘까.
|
|
80
|
+
*
|
|
81
|
+
* 401·403 은 "인증이 틀렸다" 는 말이고, 404 는 "인증은 됐는데 그 자리가
|
|
82
|
+
* 없다"에 가깝다. 그러니 404 를 받은 방식이 있으면 그쪽이 맞을 확률이
|
|
83
|
+
* 높다. 아무것도 안 통했으면 Azure 의 제 방식(api-key)으로 적어 두되,
|
|
84
|
+
* **열쇠를 줬는데 '인증 없음' 으로 적지는 않는다** — 그렇게 적으면 그
|
|
85
|
+
* 뒤의 모든 요청이 열쇠 없이 나간다.
|
|
86
|
+
*/
|
|
87
|
+
// 열쇠를 줬으면 '인증 없음' 은 아예 후보에서 뺀다. 그것으로 적어 두면
|
|
88
|
+
// 그 뒤의 모든 요청이 맨몸으로 나간다 — 붙은 것처럼 보이는데 다 401 이다.
|
|
89
|
+
const 볼것 = key ? 막힌것.filter((x) => x.auth !== 'none') : 막힌것;
|
|
90
|
+
const 쓸것 = 볼것.length ? 볼것 : [{ auth: key ? 'api-key' : 'none', status: 막힌것[0].status, ms: 막힌것[0].ms }];
|
|
91
|
+
const 나은것 = 쓸것.find((x) => x.status === 404) ?? null;
|
|
92
|
+
const 고른것 = 나은것 ?? 쓸것[0];
|
|
93
|
+
const 다막힘 = !나은것;
|
|
94
|
+
return {
|
|
95
|
+
kind: 'openai',
|
|
96
|
+
base: 푼것.base,
|
|
97
|
+
auth: 고른것.auth,
|
|
98
|
+
models: [],
|
|
99
|
+
ms: 고른것.ms,
|
|
100
|
+
azure: true,
|
|
101
|
+
warn: 다막힘 && key
|
|
102
|
+
? `배포 목록도 인증도 확인 못 했습니다 (해 본 방식마다 HTTP ${막힌것.map((x) => x.status).join('·')})`
|
|
103
|
+
+ ' — 주소에 적힌 배포로 그냥 씁니다. 첫 한마디에서 걸리면 /model 로 인증 방식을 바꾸세요.'
|
|
104
|
+
: `배포 목록을 못 봤습니다 (HTTP ${고른것.status}) — 주소에 적힌 배포로 그냥 씁니다.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* 못 붙었을 때 **무엇이 막았는지**를 적어 둔다.
|
|
112
|
+
*
|
|
113
|
+
* 여태는 실패한 응답을 그냥 버렸다. 그래서 401(닿았는데 열쇠) · 404(주소가
|
|
114
|
+
* 아님) · Bedrock 의 AccessDenied(모델 신청을 안 함)가 화면에서 전부
|
|
115
|
+
* 「연결 실패」 한 마디가 됐다. 셋은 고칠 자리가 완전히 다른데도.
|
|
116
|
+
*
|
|
117
|
+
* 값을 채워 주는 자리를 인자로 받는 것은, 두드리는 함수들이 돌려주는 모양을
|
|
118
|
+
* 안 바꾸려는 것이다 — 그 반환값(붙었나 아닌가)은 부르는 데가 여럿이다.
|
|
119
|
+
*
|
|
120
|
+
* **진짜 상태코드를 먼저 적는다.** 후보를 여럿 두드리다 보면 하나는 401 이고
|
|
121
|
+
* 하나는 0(닿지도 못함)일 수 있는데, 401 쪽이 훨씬 쓸모 있는 단서다 — 적어도
|
|
122
|
+
* 서버가 거기 있다는 뜻이니까.
|
|
123
|
+
*/
|
|
124
|
+
function 막은것적기(막힌것, r) {
|
|
125
|
+
if (!막힌것) return;
|
|
126
|
+
const 말 = (r.status ? serverMessage(r) : r.error) ?? '';
|
|
127
|
+
if (!막힌것.status && r.status) { 막힌것.status = r.status; 막힌것.why = 말; }
|
|
128
|
+
else if (!막힌것.status && !막힌것.why) 막힌것.why = 말;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* Anthropic 규격인지 확인 (GET {base}/models · 판 머리를 얹어서).
|
|
133
|
+
*
|
|
134
|
+
* ── 왜 OpenAI 쪽과 안 헷갈리나 ─────────────────────────────────────────
|
|
135
|
+
*
|
|
136
|
+
* 문 이름이 `/models` 로 같다. 그런데 이쪽 서버는 `anthropic-version` 머리가
|
|
137
|
+
* 없으면 400 을 준다. tryOpenAI 는 그 머리를 안 보내므로 여기 붙을 수가 없다.
|
|
138
|
+
*
|
|
139
|
+
* 반대는 성립하지 않는다 — OpenAI 호환 서버는 모르는 머리를 그냥 무시하므로
|
|
140
|
+
* 이 함수로도 붙어 버린다. 그래서 **뒤에 두드린다.** 다만 열쇠가 sk-ant- 로
|
|
141
|
+
* 시작하거나 주소가 그 회사면 먼저 본다. 그때는 헷갈릴 일이 없고, 앞에서
|
|
142
|
+
* 네 가지 인증 방식으로 헛되이 두드리는 시간도 없앤다.
|
|
143
|
+
*
|
|
144
|
+
* ── 인증은 주소를 보고 고른다 ──────────────────────────────────────────
|
|
145
|
+
*
|
|
146
|
+
* 이 규격은 오랫동안 x-api-key 하나뿐이었다. 지금은 한 자리가 더 있다 —
|
|
147
|
+
* AWS 의 mantle 창구가 같은 Anthropic Messages 규격을 **Bedrock API 키를
|
|
148
|
+
* Bearer 로 받아서** 내어 준다.
|
|
149
|
+
*
|
|
150
|
+
* bedrock-mantle.<리전>.api.aws/anthropic/v1
|
|
151
|
+
*
|
|
152
|
+
* 그렇다고 아무 데나 두 방식을 다 두드리지는 않는다. 안 되는 방식으로
|
|
153
|
+
* 두드릴 때마다 401 이 하나씩 늘고 사람이 기다리는 시간도 그만큼 는다.
|
|
154
|
+
* 그래서 **그 주소에서만** Bearer 를 먼저 본다.
|
|
155
|
+
*
|
|
156
|
+
* 여기서 고른 값이 그대로 프로필에 남아 매 요청에 실린다. 틀리면 열쇠가
|
|
157
|
+
* 맞는데도 매번 401 이고, 화면에서는 열쇠가 틀린 것과 구별이 안 된다.
|
|
158
|
+
*/
|
|
159
|
+
function 맨틀인가(base) {
|
|
160
|
+
// 호스트를 알아보는 규칙은 backend/toolfit.js 한 곳에 둔다. 여기에 사본을
|
|
161
|
+
// 두면 AWS 가 이름을 하나 더 낼 때 한쪽만 고쳐진다.
|
|
162
|
+
try { return 맨틀호스트인가(new URL(String(base)).hostname); }
|
|
163
|
+
catch { return false; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function tryAnthropic(base, key, 막힌것 = null) {
|
|
167
|
+
if (!key) return null;
|
|
168
|
+
const 볼것 = 맨틀인가(base) ? ['bearer', 'x-api-key'] : ['x-api-key'];
|
|
169
|
+
for (const auth of 볼것) {
|
|
170
|
+
const r = await req(`${base}/models`, {
|
|
171
|
+
headers: headersFor(auth, key, { 'anthropic-version': ANTHROPIC_VERSION }),
|
|
172
|
+
timeout: 12000,
|
|
173
|
+
});
|
|
174
|
+
막은것적기(막힌것, r);
|
|
175
|
+
if (r.ok && Array.isArray(r.json?.data)) {
|
|
176
|
+
return { kind: 'anthropic', base, auth, models: normalizeModels(r.json.data), ms: r.ms };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 이 규격을 먼저 볼 만한 단서가 있나. 열쇠 앞머리나 주소로만 본다. */
|
|
183
|
+
export function 앤트로픽같나(input, key) {
|
|
184
|
+
if (/^sk-ant-/.test(String(key ?? '').trim())) return true;
|
|
185
|
+
try {
|
|
186
|
+
const u = new URL(/^https?:\/\//i.test(input) ? String(input) : 'https://' + String(input));
|
|
187
|
+
if (/(^|\.)anthropic\.com$/i.test(u.hostname)) return true;
|
|
188
|
+
/*
|
|
189
|
+
* AWS mantle 의 Anthropic 창구. 길에 규격 이름이 적혀 있다.
|
|
190
|
+
*
|
|
191
|
+
* bedrock-mantle.<리전>.api.aws/anthropic/v1
|
|
192
|
+
*
|
|
193
|
+
* 같은 호스트가 `/openai/v1` 도 함께 연다. 그래서 호스트만 보면 안 되고
|
|
194
|
+
* **길까지** 봐야 한다 — 안 그러면 OpenAI 창구로 붙이려는 사람이
|
|
195
|
+
* Anthropic 쪽으로 끌려간다.
|
|
196
|
+
*
|
|
197
|
+
* 이 단서가 없으면 OpenAI 규격으로 먼저 두드리게 되는데, 그쪽 서버가
|
|
198
|
+
* 모르는 머리를 무시하고 `{data:[...]}` 를 내주면 규격이 openai 로
|
|
199
|
+
* 굳어 버린다. 그러면 캐시 표식을 붙일 자리가 사라진다 — 이 창구를
|
|
200
|
+
* 후보에 넣은 이유가 바로 그 표식이라, 순서 하나로 뜻이 없어진다.
|
|
201
|
+
*/
|
|
202
|
+
if (맨틀인가(u.origin) && /(^|\/)anthropic(\/|$)/i.test(u.pathname)) return true;
|
|
203
|
+
return false;
|
|
204
|
+
} catch { return false; }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// OpenAI 호환인지 확인 (GET {base}/models). 막힌것은 막은것적기 머리말 참고.
|
|
208
|
+
async function tryOpenAI(base, key, 막힌것 = null) {
|
|
209
|
+
const 적기 = (r) => 막은것적기(막힌것, r);
|
|
210
|
+
for (const style of AUTH_STYLES) {
|
|
211
|
+
if (style.id !== 'none' && !key) continue;
|
|
212
|
+
if (style.id === 'none' && key) { /* 키를 줬어도 인증 없는 서버일 수 있으니 마지막에 본다 */ }
|
|
213
|
+
const r = await req(`${base}/models`, { headers: headersFor(style.id, key), timeout: 12000 });
|
|
214
|
+
적기(r);
|
|
215
|
+
if (r.ok && r.json) {
|
|
216
|
+
const list = r.json.data ?? r.json.models ?? [];
|
|
217
|
+
if (Array.isArray(list)) {
|
|
218
|
+
return { kind: 'openai', base, auth: style.id, models: normalizeModels(list), ms: r.ms };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// 401/403 이면 규격은 맞고 인증만 틀린 것 — 다음 방식으로 계속.
|
|
222
|
+
//
|
|
223
|
+
// 그 밖의 오류(500 등)는 '규격은 맞는데 서버가 지금 화가 난 것' 으로 본다.
|
|
224
|
+
// 그래야 서버가 한 말을 사람에게 그대로 보여 줄 수 있다.
|
|
225
|
+
//
|
|
226
|
+
// 단, **200 인데 JSON 이 아니면 여기 해당하지 않는다.** 사내 프록시가
|
|
227
|
+
// 로그인 페이지를 200 으로 내주는 일이 흔한데, 예전에는 그걸 "OpenAI 호환
|
|
228
|
+
// 서버 · 모델 0개" 로 잡았다. 그러면 사람은 모델이 안 올라온 줄 알고
|
|
229
|
+
// 엉뚱한 데를 파게 된다 — 실제로는 인증 페이지에 막힌 것이다.
|
|
230
|
+
/*
|
|
231
|
+
* 400 인데 「anthropic-version 이 필요하다」 고 하면, 그건 서버가 화가 난
|
|
232
|
+
* 것이 아니라 **규격이 다르다**는 말이다. 서버가 제 규격을 말해 줬다.
|
|
233
|
+
*
|
|
234
|
+
* 여기서 openai 로 잡아 버리면 설치는 초록색으로 끝나고 첫 한마디에서
|
|
235
|
+
* 400 이 난다 — 그 화면으로는 원인을 알 길이 없다. 열쇠 앞머리가 sk-ant-
|
|
236
|
+
* 가 아닌 열쇠(사내에서 다시 발급한 것 등)로 그 규격 서버에 붙을 때
|
|
237
|
+
* 실제로 이 길로 온다.
|
|
238
|
+
*/
|
|
239
|
+
if (r.status === 400 && /anthropic-version/i.test(serverMessage(r) ?? '')) return null;
|
|
240
|
+
const 성공인데JSON아님 = r.status >= 200 && r.status < 300 && !r.json;
|
|
241
|
+
if (r.status && !성공인데JSON아님 && ![401, 403, 0, 404].includes(r.status)) {
|
|
242
|
+
return { kind: 'openai', base, auth: style.id, models: [], ms: r.ms, warn: serverMessage(r) };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Ollama 자체 규격인지 확인
|
|
249
|
+
async function tryOllama(origin) {
|
|
250
|
+
const v = await req(`${origin}/api/version`, { timeout: 8000 });
|
|
251
|
+
if (!v.ok || !v.json?.version) return null;
|
|
252
|
+
const tags = await req(`${origin}/api/tags`, { timeout: 12000 });
|
|
253
|
+
const models = (tags.json?.models ?? []).map((m) => ({
|
|
254
|
+
id: m.name ?? m.model,
|
|
255
|
+
note: m.details?.parameter_size ? `${m.details.parameter_size} · ${fmtSize(m.size)}` : fmtSize(m.size),
|
|
256
|
+
}));
|
|
257
|
+
return { kind: 'ollama', base: origin, auth: 'none', models, version: v.json.version, ms: v.ms };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function fmtSize(bytes) {
|
|
261
|
+
if (!bytes) return '';
|
|
262
|
+
const gb = bytes / 1024 ** 3;
|
|
263
|
+
return gb >= 1 ? `${gb.toFixed(1)}GB` : `${Math.round(bytes / 1024 ** 2)}MB`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeModels(list) {
|
|
267
|
+
return list
|
|
268
|
+
.map((m) => (typeof m === 'string' ? { id: m } : { id: m.id ?? m.name ?? m.model, note: m.owned_by ?? m.display_name ?? '' }))
|
|
269
|
+
.filter((m) => m.id);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function detect(input, key) {
|
|
273
|
+
const tried = [];
|
|
274
|
+
const origin = String(input).trim().replace(/\/+$/, '').replace(/\/v\d+$/, '');
|
|
275
|
+
|
|
276
|
+
// Azure 는 모양이 아예 다르다. 여기서 갈라 놓지 않으면 아래 후보들이
|
|
277
|
+
// 엉뚱한 자리를 두드리다 끝난다.
|
|
278
|
+
if (애저인가(input)) {
|
|
279
|
+
const 푼것 = 애저풀기(input);
|
|
280
|
+
tried.push(푼것?.목록주소 ?? String(input));
|
|
281
|
+
const hit = await tryAzure(input, key);
|
|
282
|
+
if (hit?.kind) return { ...hit, tried };
|
|
283
|
+
// 못 붙은 이유를 tryAzure 가 알고 있으면 그 말을 쓴다. 우리가 아는 것보다 정확하다.
|
|
284
|
+
return {
|
|
285
|
+
kind: null,
|
|
286
|
+
tried,
|
|
287
|
+
why: hit?.why ?? 'Azure 주소로 보이는데 배포를 못 찾았습니다 — 주소에 /openai/deployments/<배포이름> 까지 넣어 보세요.',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 못 붙으면 무엇이 막았는지를 같이 돌려준다 — 부르는 쪽이 사람 말로 옮긴다
|
|
292
|
+
// (providers/index.js 의 막힌까닭). tryOpenAI 머리말 참고.
|
|
293
|
+
const 막힌것 = { status: 0, why: '' };
|
|
294
|
+
|
|
295
|
+
// 단서가 있으면 이 규격을 먼저 본다 (tryAnthropic 머리말).
|
|
296
|
+
if (앤트로픽같나(input, key)) {
|
|
297
|
+
for (const base of candidates(input)) {
|
|
298
|
+
tried.push(base);
|
|
299
|
+
const hit = await tryAnthropic(base, key, 막힌것);
|
|
300
|
+
if (hit) return { ...hit, tried };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Ollama 를 먼저 본다 — 로컬이면 대개 이쪽이고 확인이 빠르다.
|
|
305
|
+
const oll = await tryOllama(/^https?:\/\//i.test(origin) ? origin : 'http://' + origin);
|
|
306
|
+
if (oll) return { ...oll, tried };
|
|
307
|
+
|
|
308
|
+
for (const base of candidates(input)) {
|
|
309
|
+
if (!tried.includes(base)) tried.push(base);
|
|
310
|
+
const hit = await tryOpenAI(base, key, 막힌것);
|
|
311
|
+
if (hit) return { ...hit, tried };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/*
|
|
315
|
+
* 마지막으로 이 규격도 본다.
|
|
316
|
+
*
|
|
317
|
+
* 사내 게이트웨이가 이 규격만 열어 둔 경우가 있다. 여기까지 왔다는 것은
|
|
318
|
+
* OpenAI 쪽으로는 안 붙었다는 뜻이라, 이제 헷갈릴 일이 없다.
|
|
319
|
+
*/
|
|
320
|
+
if (key && !앤트로픽같나(input, key)) {
|
|
321
|
+
for (const base of candidates(input)) {
|
|
322
|
+
const hit = await tryAnthropic(base, key, 막힌것);
|
|
323
|
+
if (hit) return { ...hit, tried };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { kind: null, tried, status: 막힌것.status, why: 막힌것.why };
|
|
327
|
+
}
|