connectbase-client 5.6.2 → 5.6.3
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/CHANGELOG.md +25 -0
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +51 -2
- package/dist/index.d.ts +51 -2
- package/dist/index.js +195 -65
- package/dist/index.mjs +195 -65
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -234,6 +234,29 @@ var GameError = class extends Error {
|
|
|
234
234
|
};
|
|
235
235
|
|
|
236
236
|
// src/api/ai.ts
|
|
237
|
+
function aiErrorToApiError(err) {
|
|
238
|
+
const statusByCode = {
|
|
239
|
+
provider_timeout: 504,
|
|
240
|
+
gateway_timeout: 504,
|
|
241
|
+
bad_gateway: 502,
|
|
242
|
+
service_unavailable: 503,
|
|
243
|
+
rate_limit_exceeded: 429,
|
|
244
|
+
quota_exceeded: 429,
|
|
245
|
+
invalid_request: 400,
|
|
246
|
+
config_error: 400,
|
|
247
|
+
unauthorized: 401
|
|
248
|
+
};
|
|
249
|
+
const details = {};
|
|
250
|
+
if (err.provider) details.provider = err.provider;
|
|
251
|
+
if (err.model) details.model = err.model;
|
|
252
|
+
if (err.retryAfter !== void 0) details.retry_after_seconds = err.retryAfter;
|
|
253
|
+
return new ApiError(
|
|
254
|
+
err.status ?? statusByCode[err.code] ?? 500,
|
|
255
|
+
err.message,
|
|
256
|
+
err.detailCode || err.code,
|
|
257
|
+
Object.keys(details).length > 0 ? details : void 0
|
|
258
|
+
);
|
|
259
|
+
}
|
|
237
260
|
var AI_CHAT_DEFAULT_TIMEOUT_MS = 9e4;
|
|
238
261
|
var AI_CHAT_MAX_TIMEOUT_MS = 3e5;
|
|
239
262
|
var AI_CHAT_CLIENT_GRACE_MS = 1e4;
|
|
@@ -247,19 +270,101 @@ var AIAPI = class {
|
|
|
247
270
|
*
|
|
248
271
|
* @param options `options.timeout` 으로 이 1회 생성의 시간 예산(ms)을 정한다 — 자세한
|
|
249
272
|
* 의미와 상한은 {@link AIChatOptions.timeout} 참고. 미지정이면 90초.
|
|
273
|
+
*
|
|
274
|
+
* ### 전송 방식 (5.6.3+)
|
|
275
|
+
*
|
|
276
|
+
* 겉보기 API 는 그대로지만 **내부적으로는 스트리밍 전송을 쓰고 결과를 합쳐서 돌려준다.**
|
|
277
|
+
*
|
|
278
|
+
* 왜: 응답이 다 만들어질 때까지 한 바이트도 흐르지 않는 요청은, 중간의 어떤 계층이든
|
|
279
|
+
* "첫 바이트까지 N초" 제한을 두면 그 자리에서 끊긴다 — CDN(100초), 리버스 프록시,
|
|
280
|
+
* 로드밸런서, 모바일 캐리어 NAT 가 전부 그런 제한을 갖는다. 서버 타임아웃을 아무리
|
|
281
|
+
* 늘려도 이 성질은 남으므로, 상한을 올리는 게 아니라 **바이트가 계속 흐르게** 하는 것이
|
|
282
|
+
* 유일한 근본 해결이다(긴 생성에 스트리밍을 쓰는 것은 LLM API 의 표준 관행이다).
|
|
283
|
+
*
|
|
284
|
+
* 호출자가 볼 수 있는 차이는 없다 — `content`/`reasoning`/`toolCalls`/`sources`/
|
|
285
|
+
* `finishReason`/`usage` 모두 동일하게 채워지고, 에러도 같은 `code` 로 던져진다.
|
|
286
|
+
* 토큰을 실시간으로 보여줘야 하면 {@link AIAPI.chatStream} 을 쓴다.
|
|
250
287
|
*/
|
|
251
288
|
async chat(request, options) {
|
|
252
289
|
const budgetMs = Math.min(
|
|
253
290
|
options?.timeout ?? this.http.requestTimeoutMs ?? AI_CHAT_DEFAULT_TIMEOUT_MS,
|
|
254
291
|
AI_CHAT_MAX_TIMEOUT_MS
|
|
255
292
|
);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
const controller = new AbortController();
|
|
294
|
+
let budgetExpired = false;
|
|
295
|
+
const timer = setTimeout(() => {
|
|
296
|
+
budgetExpired = true;
|
|
297
|
+
controller.abort();
|
|
298
|
+
}, budgetMs + AI_CHAT_CLIENT_GRACE_MS);
|
|
299
|
+
const external = options?.signal;
|
|
300
|
+
const forwardAbort = () => controller.abort();
|
|
301
|
+
if (external) {
|
|
302
|
+
if (external.aborted) controller.abort();
|
|
303
|
+
else external.addEventListener("abort", forwardAbort, { once: true });
|
|
304
|
+
}
|
|
305
|
+
const aggregated = { content: "", provider: "", model: "" };
|
|
306
|
+
let failure;
|
|
307
|
+
let aborted = false;
|
|
308
|
+
let reasoning = "";
|
|
309
|
+
try {
|
|
310
|
+
await this.runStream(
|
|
311
|
+
// timeoutSeconds 는 서버가 쓰는 생성 예산. 마지막에 펼쳐 호출자가 임의로 넣은
|
|
312
|
+
// 값이 이 계산을 덮어쓰지 못하게 한다.
|
|
313
|
+
{ ...request, timeoutSeconds: Math.ceil(budgetMs / 1e3) },
|
|
314
|
+
{
|
|
315
|
+
onToken: (token) => {
|
|
316
|
+
aggregated.content += token;
|
|
317
|
+
},
|
|
318
|
+
onReasoning: (delta) => {
|
|
319
|
+
reasoning += delta;
|
|
320
|
+
},
|
|
321
|
+
onSources: (sources) => {
|
|
322
|
+
aggregated.sources = sources;
|
|
323
|
+
},
|
|
324
|
+
// 에러는 콜백으로 오므로 붙잡아 두었다가 아래에서 throw 한다 — 콜백 안에서
|
|
325
|
+
// 던지면 스트림 정리(reader cancel)를 건너뛴다.
|
|
326
|
+
onError: (error) => {
|
|
327
|
+
failure = error;
|
|
328
|
+
},
|
|
329
|
+
onAbort: () => {
|
|
330
|
+
aborted = true;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{ signal: controller.signal },
|
|
334
|
+
{
|
|
335
|
+
// HTTP 레벨 실패는 비스트리밍 시절과 **완전히 같은 ApiError** 로 던진다.
|
|
336
|
+
onHttpError: async (response) => {
|
|
337
|
+
throw await this.http.buildApiError(response);
|
|
338
|
+
},
|
|
339
|
+
// 스트림 종료 청크에만 실리는 값들 — 토큰 콜백으로는 표현되지 않는다.
|
|
340
|
+
onFinalChunk: (chunk) => {
|
|
341
|
+
if (chunk.finishReason)
|
|
342
|
+
aggregated.finishReason = chunk.finishReason;
|
|
343
|
+
if (chunk.toolCalls?.length) aggregated.toolCalls = chunk.toolCalls;
|
|
344
|
+
if (chunk.usage) aggregated.usage = chunk.usage;
|
|
345
|
+
if (chunk.provider) aggregated.provider = chunk.provider;
|
|
346
|
+
if (chunk.model) aggregated.model = chunk.model;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
} finally {
|
|
351
|
+
clearTimeout(timer);
|
|
352
|
+
external?.removeEventListener("abort", forwardAbort);
|
|
353
|
+
}
|
|
354
|
+
if (failure) throw aiErrorToApiError(failure);
|
|
355
|
+
if (aborted) {
|
|
356
|
+
if (budgetExpired) {
|
|
357
|
+
throw new ApiError(
|
|
358
|
+
504,
|
|
359
|
+
`AI \uC751\uB2F5\uC774 ${Math.round(budgetMs / 1e3)}\uCD08 \uC608\uC0B0 \uC548\uC5D0 \uB05D\uB098\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4`,
|
|
360
|
+
"provider_timeout"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
throw new DOMException("The operation was aborted.", "AbortError");
|
|
364
|
+
}
|
|
365
|
+
if (failure) throw failure;
|
|
366
|
+
if (reasoning) aggregated.reasoning = reasoning;
|
|
367
|
+
return aggregated;
|
|
263
368
|
}
|
|
264
369
|
/**
|
|
265
370
|
* AI 채팅 스트리밍 (SSE)
|
|
@@ -299,6 +404,17 @@ var AIAPI = class {
|
|
|
299
404
|
* ```
|
|
300
405
|
*/
|
|
301
406
|
async chatStream(request, callbacks, options) {
|
|
407
|
+
return this.runStream(request, callbacks, options);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* SSE 소비 본체. `chatStream`(공개)과 `chat`(집계)이 공유한다.
|
|
411
|
+
*
|
|
412
|
+
* `hooks.onFinalChunk` 은 종료 청크처럼 **토큰 콜백으로는 표현되지 않는 값**
|
|
413
|
+
* (usage / finishReason / toolCalls)을 집는 내부 훅이다. 공개 옵션으로 열지 않는 이유:
|
|
414
|
+
* 스트리밍 사용자는 그 값들을 콜백으로 이미 받거나 필요로 하지 않는데, 공개하면
|
|
415
|
+
* "언제 몇 번 불리는가" 가 계약이 되어 와이어 포맷을 못 바꾸게 된다.
|
|
416
|
+
*/
|
|
417
|
+
async runStream(request, callbacks, options, hooks) {
|
|
302
418
|
const signal = options?.signal;
|
|
303
419
|
const isAbort = (err) => signal?.aborted === true || err instanceof DOMException && err.name === "AbortError" || typeof err === "object" && err !== null && err.name === "AbortError";
|
|
304
420
|
let reader;
|
|
@@ -313,6 +429,7 @@ var AIAPI = class {
|
|
|
313
429
|
signal
|
|
314
430
|
});
|
|
315
431
|
if (!response.ok) {
|
|
432
|
+
if (hooks?.onHttpError) await hooks.onHttpError(response);
|
|
316
433
|
const errorData = await response.json().catch(() => ({ error: "stream_failed" }));
|
|
317
434
|
callbacks.onError?.(
|
|
318
435
|
toAIError(errorData, {
|
|
@@ -377,9 +494,11 @@ var AIAPI = class {
|
|
|
377
494
|
if (event.type === "heartbeat") {
|
|
378
495
|
continue;
|
|
379
496
|
}
|
|
497
|
+
if (event.usage) hooks?.onFinalChunk?.(event);
|
|
380
498
|
if (event.reasoning) callbacks.onReasoning?.(event.reasoning);
|
|
381
499
|
if (event.content) callbacks.onToken?.(event.content);
|
|
382
500
|
if (event.done) {
|
|
501
|
+
if (!event.usage) hooks?.onFinalChunk?.(event);
|
|
383
502
|
callbacks.onDone?.();
|
|
384
503
|
return;
|
|
385
504
|
}
|
|
@@ -11603,70 +11722,81 @@ var HttpClient = class {
|
|
|
11603
11722
|
}
|
|
11604
11723
|
return headers;
|
|
11605
11724
|
}
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11725
|
+
/**
|
|
11726
|
+
* 실패 응답(`!response.ok`)을 ApiError 로 변환한다. 던지지도, emitError 하지도 않는다.
|
|
11727
|
+
*
|
|
11728
|
+
* handleResponse 와 **같은 파싱을 두 곳이 필요로 해서** 뽑아냈다 — `ai.chat` 이 스트리밍
|
|
11729
|
+
* 전송으로 옮겨가면서 SSE 응답의 HTTP 레벨 실패도 기존 비스트리밍과 **완전히 같은
|
|
11730
|
+
* ApiError** 로 보여야 하기 때문이다. 파싱을 복제하면 그 순간부터 두 경로의 에러 모양이
|
|
11731
|
+
* 갈린다 (실제로 코드 유실 사고가 있었다 — platform-issue 019fa21c).
|
|
11732
|
+
*/
|
|
11733
|
+
async buildApiError(response) {
|
|
11734
|
+
let errorData = {};
|
|
11735
|
+
let bodyIsJson = true;
|
|
11736
|
+
try {
|
|
11737
|
+
errorData = await response.json();
|
|
11738
|
+
} catch {
|
|
11739
|
+
bodyIsJson = false;
|
|
11740
|
+
}
|
|
11741
|
+
const retryAfterHeader = response.status === 429 ? response.headers.get("Retry-After") : null;
|
|
11742
|
+
let retryAfterSeconds;
|
|
11743
|
+
if (retryAfterHeader) {
|
|
11744
|
+
const asInt = Number.parseInt(retryAfterHeader, 10);
|
|
11745
|
+
if (Number.isFinite(asInt) && asInt >= 0) {
|
|
11746
|
+
retryAfterSeconds = asInt;
|
|
11747
|
+
} else {
|
|
11748
|
+
const dateMs = Date.parse(retryAfterHeader);
|
|
11749
|
+
if (Number.isFinite(dateMs)) {
|
|
11750
|
+
retryAfterSeconds = Math.max(
|
|
11751
|
+
0,
|
|
11752
|
+
Math.round((dateMs - Date.now()) / 1e3)
|
|
11753
|
+
);
|
|
11629
11754
|
}
|
|
11630
11755
|
}
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
}
|
|
11637
|
-
|
|
11638
|
-
details.retry_after_seconds = retryAfterSeconds;
|
|
11639
|
-
}
|
|
11640
|
-
const err2 = new ApiError(
|
|
11641
|
-
response.status,
|
|
11642
|
-
typeof structured.message === "string" && structured.message !== "" ? structured.message : "Unknown error",
|
|
11643
|
-
typeof structured.code === "string" ? structured.code : void 0,
|
|
11644
|
-
Object.keys(details).length > 0 ? details : void 0
|
|
11645
|
-
);
|
|
11646
|
-
this.emitError(err2);
|
|
11647
|
-
throw err2;
|
|
11648
|
-
}
|
|
11649
|
-
const flatMessage = typeof errorData.message === "string" && errorData.message !== "" ? errorData.message : void 0;
|
|
11650
|
-
const explicitCode = typeof errorData.code === "string" && errorData.code !== "" ? errorData.code : void 0;
|
|
11651
|
-
const errorIsCode = typeof rawError === "string" && /^[a-z][a-z0-9_]*$/.test(rawError);
|
|
11652
|
-
const message = flatMessage ?? (typeof rawError === "string" && rawError !== "" ? rawError : void 0) ?? (bodyIsJson ? void 0 : gatewayMessageFromStatus(response.status)) ?? (response.statusText !== "" ? response.statusText : "Unknown error");
|
|
11653
|
-
const code = explicitCode ?? (errorIsCode ? rawError : void 0) ?? gatewayCodeFromStatus(response.status);
|
|
11654
|
-
const legacyDetails = {};
|
|
11756
|
+
}
|
|
11757
|
+
const rawError = errorData.error;
|
|
11758
|
+
if (rawError && typeof rawError === "object" && "message" in rawError) {
|
|
11759
|
+
const structured = rawError;
|
|
11760
|
+
const details = {
|
|
11761
|
+
...structured.details && typeof structured.details === "object" ? structured.details : {}
|
|
11762
|
+
};
|
|
11655
11763
|
if (retryAfterSeconds !== void 0) {
|
|
11656
|
-
|
|
11764
|
+
details.retry_after_seconds = retryAfterSeconds;
|
|
11657
11765
|
}
|
|
11658
|
-
|
|
11659
|
-
legacyDetails.provider = errorData.provider;
|
|
11660
|
-
}
|
|
11661
|
-
if (typeof errorData.model === "string") {
|
|
11662
|
-
legacyDetails.model = errorData.model;
|
|
11663
|
-
}
|
|
11664
|
-
const err = new ApiError(
|
|
11766
|
+
const err2 = new ApiError(
|
|
11665
11767
|
response.status,
|
|
11666
|
-
message,
|
|
11667
|
-
code,
|
|
11668
|
-
Object.keys(
|
|
11768
|
+
typeof structured.message === "string" && structured.message !== "" ? structured.message : "Unknown error",
|
|
11769
|
+
typeof structured.code === "string" ? structured.code : void 0,
|
|
11770
|
+
Object.keys(details).length > 0 ? details : void 0
|
|
11669
11771
|
);
|
|
11772
|
+
return err2;
|
|
11773
|
+
}
|
|
11774
|
+
const flatMessage = typeof errorData.message === "string" && errorData.message !== "" ? errorData.message : void 0;
|
|
11775
|
+
const explicitCode = typeof errorData.code === "string" && errorData.code !== "" ? errorData.code : void 0;
|
|
11776
|
+
const errorIsCode = typeof rawError === "string" && /^[a-z][a-z0-9_]*$/.test(rawError);
|
|
11777
|
+
const message = flatMessage ?? (typeof rawError === "string" && rawError !== "" ? rawError : void 0) ?? (bodyIsJson ? void 0 : gatewayMessageFromStatus(response.status)) ?? (response.statusText !== "" ? response.statusText : "Unknown error");
|
|
11778
|
+
const code = explicitCode ?? (errorIsCode ? rawError : void 0) ?? gatewayCodeFromStatus(response.status);
|
|
11779
|
+
const legacyDetails = {};
|
|
11780
|
+
if (retryAfterSeconds !== void 0) {
|
|
11781
|
+
legacyDetails.retry_after_seconds = retryAfterSeconds;
|
|
11782
|
+
}
|
|
11783
|
+
if (typeof errorData.provider === "string") {
|
|
11784
|
+
legacyDetails.provider = errorData.provider;
|
|
11785
|
+
}
|
|
11786
|
+
if (typeof errorData.model === "string") {
|
|
11787
|
+
legacyDetails.model = errorData.model;
|
|
11788
|
+
}
|
|
11789
|
+
const err = new ApiError(
|
|
11790
|
+
response.status,
|
|
11791
|
+
message,
|
|
11792
|
+
code,
|
|
11793
|
+
Object.keys(legacyDetails).length > 0 ? legacyDetails : void 0
|
|
11794
|
+
);
|
|
11795
|
+
return err;
|
|
11796
|
+
}
|
|
11797
|
+
async handleResponse(response) {
|
|
11798
|
+
if (!response.ok) {
|
|
11799
|
+
const err = await this.buildApiError(response);
|
|
11670
11800
|
this.emitError(err);
|
|
11671
11801
|
throw err;
|
|
11672
11802
|
}
|