connectbase-client 5.6.2 → 5.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -234,6 +234,30 @@ 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)
253
+ details.retry_after_seconds = err.retryAfter;
254
+ return new ApiError(
255
+ err.status ?? statusByCode[err.code] ?? 500,
256
+ err.message,
257
+ err.detailCode || err.code,
258
+ Object.keys(details).length > 0 ? details : void 0
259
+ );
260
+ }
237
261
  var AI_CHAT_DEFAULT_TIMEOUT_MS = 9e4;
238
262
  var AI_CHAT_MAX_TIMEOUT_MS = 3e5;
239
263
  var AI_CHAT_CLIENT_GRACE_MS = 1e4;
@@ -247,19 +271,101 @@ var AIAPI = class {
247
271
  *
248
272
  * @param options `options.timeout` 으로 이 1회 생성의 시간 예산(ms)을 정한다 — 자세한
249
273
  * 의미와 상한은 {@link AIChatOptions.timeout} 참고. 미지정이면 90초.
274
+ *
275
+ * ### 전송 방식 (5.6.3+)
276
+ *
277
+ * 겉보기 API 는 그대로지만 **내부적으로는 스트리밍 전송을 쓰고 결과를 합쳐서 돌려준다.**
278
+ *
279
+ * 왜: 응답이 다 만들어질 때까지 한 바이트도 흐르지 않는 요청은, 중간의 어떤 계층이든
280
+ * "첫 바이트까지 N초" 제한을 두면 그 자리에서 끊긴다 — CDN(100초), 리버스 프록시,
281
+ * 로드밸런서, 모바일 캐리어 NAT 가 전부 그런 제한을 갖는다. 서버 타임아웃을 아무리
282
+ * 늘려도 이 성질은 남으므로, 상한을 올리는 게 아니라 **바이트가 계속 흐르게** 하는 것이
283
+ * 유일한 근본 해결이다(긴 생성에 스트리밍을 쓰는 것은 LLM API 의 표준 관행이다).
284
+ *
285
+ * 호출자가 볼 수 있는 차이는 없다 — `content`/`reasoning`/`toolCalls`/`sources`/
286
+ * `finishReason`/`usage` 모두 동일하게 채워지고, 에러도 같은 `code` 로 던져진다.
287
+ * 토큰을 실시간으로 보여줘야 하면 {@link AIAPI.chatStream} 을 쓴다.
250
288
  */
251
289
  async chat(request, options) {
252
290
  const budgetMs = Math.min(
253
291
  options?.timeout ?? this.http.requestTimeoutMs ?? AI_CHAT_DEFAULT_TIMEOUT_MS,
254
292
  AI_CHAT_MAX_TIMEOUT_MS
255
293
  );
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
- );
294
+ const controller = new AbortController();
295
+ let budgetExpired = false;
296
+ const timer = setTimeout(() => {
297
+ budgetExpired = true;
298
+ controller.abort();
299
+ }, budgetMs + AI_CHAT_CLIENT_GRACE_MS);
300
+ const external = options?.signal;
301
+ const forwardAbort = () => controller.abort();
302
+ if (external) {
303
+ if (external.aborted) controller.abort();
304
+ else external.addEventListener("abort", forwardAbort, { once: true });
305
+ }
306
+ const aggregated = { content: "", provider: "", model: "" };
307
+ let failure;
308
+ let aborted = false;
309
+ let reasoning = "";
310
+ try {
311
+ await this.runStream(
312
+ // timeoutSeconds 는 서버가 쓰는 생성 예산. 마지막에 펼쳐 호출자가 임의로 넣은
313
+ // 값이 이 계산을 덮어쓰지 못하게 한다.
314
+ { ...request, timeoutSeconds: Math.ceil(budgetMs / 1e3) },
315
+ {
316
+ onToken: (token) => {
317
+ aggregated.content += token;
318
+ },
319
+ onReasoning: (delta) => {
320
+ reasoning += delta;
321
+ },
322
+ onSources: (sources) => {
323
+ aggregated.sources = sources;
324
+ },
325
+ // 에러는 콜백으로 오므로 붙잡아 두었다가 아래에서 throw 한다 — 콜백 안에서
326
+ // 던지면 스트림 정리(reader cancel)를 건너뛴다.
327
+ onError: (error) => {
328
+ failure = error;
329
+ },
330
+ onAbort: () => {
331
+ aborted = true;
332
+ }
333
+ },
334
+ { signal: controller.signal },
335
+ {
336
+ // HTTP 레벨 실패는 비스트리밍 시절과 **완전히 같은 ApiError** 로 던진다.
337
+ onHttpError: async (response) => {
338
+ throw await this.http.buildApiError(response);
339
+ },
340
+ // 스트림 종료 청크에만 실리는 값들 — 토큰 콜백으로는 표현되지 않는다.
341
+ onResultMeta: (chunk) => {
342
+ if (chunk.finishReason)
343
+ aggregated.finishReason = chunk.finishReason;
344
+ if (chunk.toolCalls?.length) aggregated.toolCalls = chunk.toolCalls;
345
+ if (chunk.usage) aggregated.usage = chunk.usage;
346
+ if (chunk.provider) aggregated.provider = chunk.provider;
347
+ if (chunk.model) aggregated.model = chunk.model;
348
+ }
349
+ }
350
+ );
351
+ } finally {
352
+ clearTimeout(timer);
353
+ external?.removeEventListener("abort", forwardAbort);
354
+ }
355
+ if (failure) throw aiErrorToApiError(failure);
356
+ if (aborted) {
357
+ if (budgetExpired) {
358
+ throw new ApiError(
359
+ 504,
360
+ `AI \uC751\uB2F5\uC774 ${Math.round(budgetMs / 1e3)}\uCD08 \uC608\uC0B0 \uC548\uC5D0 \uB05D\uB098\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4`,
361
+ "provider_timeout"
362
+ );
363
+ }
364
+ throw new DOMException("The operation was aborted.", "AbortError");
365
+ }
366
+ if (failure) throw failure;
367
+ if (reasoning) aggregated.reasoning = reasoning;
368
+ return aggregated;
263
369
  }
264
370
  /**
265
371
  * AI 채팅 스트리밍 (SSE)
@@ -299,6 +405,20 @@ var AIAPI = class {
299
405
  * ```
300
406
  */
301
407
  async chatStream(request, callbacks, options) {
408
+ return this.runStream(request, callbacks, options);
409
+ }
410
+ /**
411
+ * SSE 소비 본체. `chatStream`(공개)과 `chat`(집계)이 공유한다.
412
+ *
413
+ * `hooks.onResultMeta` 는 **토큰 콜백으로는 표현되지 않는 결과 메타**
414
+ * (finishReason / toolCalls / usage / provider / model)를 집는 내부 훅이다.
415
+ * 이 값들은 한 청크에 모여 오지 않는다 — 실측상 finishReason 은 마지막 토큰 다음
416
+ * 청크에, usage/provider/model 은 그 다음 `done` 청크에 실린다. 그래서 "종료 청크에서만"
417
+ * 집으면 finishReason 을 통째로 놓친다. 공개 옵션으로 열지 않는 이유:
418
+ * 스트리밍 사용자는 그 값들을 콜백으로 이미 받거나 필요로 하지 않는데, 공개하면
419
+ * "언제 몇 번 불리는가" 가 계약이 되어 와이어 포맷을 못 바꾸게 된다.
420
+ */
421
+ async runStream(request, callbacks, options, hooks) {
302
422
  const signal = options?.signal;
303
423
  const isAbort = (err) => signal?.aborted === true || err instanceof DOMException && err.name === "AbortError" || typeof err === "object" && err !== null && err.name === "AbortError";
304
424
  let reader;
@@ -313,6 +433,7 @@ var AIAPI = class {
313
433
  signal
314
434
  });
315
435
  if (!response.ok) {
436
+ if (hooks?.onHttpError) await hooks.onHttpError(response);
316
437
  const errorData = await response.json().catch(() => ({ error: "stream_failed" }));
317
438
  callbacks.onError?.(
318
439
  toAIError(errorData, {
@@ -377,6 +498,9 @@ var AIAPI = class {
377
498
  if (event.type === "heartbeat") {
378
499
  continue;
379
500
  }
501
+ if (event.finishReason || event.toolCalls?.length || event.usage || event.provider || event.model) {
502
+ hooks?.onResultMeta?.(event);
503
+ }
380
504
  if (event.reasoning) callbacks.onReasoning?.(event.reasoning);
381
505
  if (event.content) callbacks.onToken?.(event.content);
382
506
  if (event.done) {
@@ -11603,70 +11727,81 @@ var HttpClient = class {
11603
11727
  }
11604
11728
  return headers;
11605
11729
  }
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
- }
11730
+ /**
11731
+ * 실패 응답(`!response.ok`) ApiError 로 변환한다. 던지지도, emitError 하지도 않는다.
11732
+ *
11733
+ * handleResponse **같은 파싱을 두 곳이 필요로 해서** 뽑아냈다 — `ai.chat` 이 스트리밍
11734
+ * 전송으로 옮겨가면서 SSE 응답의 HTTP 레벨 실패도 기존 비스트리밍과 **완전히 같은
11735
+ * ApiError** 보여야 하기 때문이다. 파싱을 복제하면 그 순간부터 두 경로의 에러 모양이
11736
+ * 갈린다 (실제로 코드 유실 사고가 있었다 — platform-issue 019fa21c).
11737
+ */
11738
+ async buildApiError(response) {
11739
+ let errorData = {};
11740
+ let bodyIsJson = true;
11741
+ try {
11742
+ errorData = await response.json();
11743
+ } catch {
11744
+ bodyIsJson = false;
11745
+ }
11746
+ const retryAfterHeader = response.status === 429 ? response.headers.get("Retry-After") : null;
11747
+ let retryAfterSeconds;
11748
+ if (retryAfterHeader) {
11749
+ const asInt = Number.parseInt(retryAfterHeader, 10);
11750
+ if (Number.isFinite(asInt) && asInt >= 0) {
11751
+ retryAfterSeconds = asInt;
11752
+ } else {
11753
+ const dateMs = Date.parse(retryAfterHeader);
11754
+ if (Number.isFinite(dateMs)) {
11755
+ retryAfterSeconds = Math.max(
11756
+ 0,
11757
+ Math.round((dateMs - Date.now()) / 1e3)
11758
+ );
11629
11759
  }
11630
11760
  }
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 = {};
11761
+ }
11762
+ const rawError = errorData.error;
11763
+ if (rawError && typeof rawError === "object" && "message" in rawError) {
11764
+ const structured = rawError;
11765
+ const details = {
11766
+ ...structured.details && typeof structured.details === "object" ? structured.details : {}
11767
+ };
11655
11768
  if (retryAfterSeconds !== void 0) {
11656
- legacyDetails.retry_after_seconds = retryAfterSeconds;
11769
+ details.retry_after_seconds = retryAfterSeconds;
11657
11770
  }
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(
11771
+ const err2 = new ApiError(
11665
11772
  response.status,
11666
- message,
11667
- code,
11668
- Object.keys(legacyDetails).length > 0 ? legacyDetails : void 0
11773
+ typeof structured.message === "string" && structured.message !== "" ? structured.message : "Unknown error",
11774
+ typeof structured.code === "string" ? structured.code : void 0,
11775
+ Object.keys(details).length > 0 ? details : void 0
11669
11776
  );
11777
+ return err2;
11778
+ }
11779
+ const flatMessage = typeof errorData.message === "string" && errorData.message !== "" ? errorData.message : void 0;
11780
+ const explicitCode = typeof errorData.code === "string" && errorData.code !== "" ? errorData.code : void 0;
11781
+ const errorIsCode = typeof rawError === "string" && /^[a-z][a-z0-9_]*$/.test(rawError);
11782
+ const message = flatMessage ?? (typeof rawError === "string" && rawError !== "" ? rawError : void 0) ?? (bodyIsJson ? void 0 : gatewayMessageFromStatus(response.status)) ?? (response.statusText !== "" ? response.statusText : "Unknown error");
11783
+ const code = explicitCode ?? (errorIsCode ? rawError : void 0) ?? gatewayCodeFromStatus(response.status);
11784
+ const legacyDetails = {};
11785
+ if (retryAfterSeconds !== void 0) {
11786
+ legacyDetails.retry_after_seconds = retryAfterSeconds;
11787
+ }
11788
+ if (typeof errorData.provider === "string") {
11789
+ legacyDetails.provider = errorData.provider;
11790
+ }
11791
+ if (typeof errorData.model === "string") {
11792
+ legacyDetails.model = errorData.model;
11793
+ }
11794
+ const err = new ApiError(
11795
+ response.status,
11796
+ message,
11797
+ code,
11798
+ Object.keys(legacyDetails).length > 0 ? legacyDetails : void 0
11799
+ );
11800
+ return err;
11801
+ }
11802
+ async handleResponse(response) {
11803
+ if (!response.ok) {
11804
+ const err = await this.buildApiError(response);
11670
11805
  this.emitError(err);
11671
11806
  throw err;
11672
11807
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "5.6.2",
3
+ "version": "5.6.4",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",