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/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
- return this.http.post(
257
- "/v1/public/ai/chat",
258
- // timeoutSeconds 서버가 쓰는 생성 예산. 마지막에 펼쳐 호출자가 임의로 넣은
259
- // 값이 이 계산을 덮어쓰지 못하게 한다.
260
- { ...request, timeoutSeconds: Math.ceil(budgetMs / 1e3) },
261
- { timeout: budgetMs + AI_CHAT_CLIENT_GRACE_MS, signal: options?.signal }
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
- async handleResponse(response) {
11607
- if (!response.ok) {
11608
- let errorData = {};
11609
- let bodyIsJson = true;
11610
- try {
11611
- errorData = await response.json();
11612
- } catch {
11613
- bodyIsJson = false;
11614
- }
11615
- const retryAfterHeader = response.status === 429 ? response.headers.get("Retry-After") : null;
11616
- let retryAfterSeconds;
11617
- if (retryAfterHeader) {
11618
- const asInt = Number.parseInt(retryAfterHeader, 10);
11619
- if (Number.isFinite(asInt) && asInt >= 0) {
11620
- retryAfterSeconds = asInt;
11621
- } else {
11622
- const dateMs = Date.parse(retryAfterHeader);
11623
- if (Number.isFinite(dateMs)) {
11624
- retryAfterSeconds = Math.max(
11625
- 0,
11626
- Math.round((dateMs - Date.now()) / 1e3)
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
- const rawError = errorData.error;
11632
- if (rawError && typeof rawError === "object" && "message" in rawError) {
11633
- const structured = rawError;
11634
- const details = {
11635
- ...structured.details && typeof structured.details === "object" ? structured.details : {}
11636
- };
11637
- if (retryAfterSeconds !== void 0) {
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
- legacyDetails.retry_after_seconds = retryAfterSeconds;
11764
+ details.retry_after_seconds = retryAfterSeconds;
11657
11765
  }
11658
- if (typeof errorData.provider === "string") {
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(legacyDetails).length > 0 ? legacyDetails : void 0
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "5.6.2",
3
+ "version": "5.6.3",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",