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.d.mts
CHANGED
|
@@ -387,6 +387,15 @@ declare class HttpClient {
|
|
|
387
387
|
private emitError;
|
|
388
388
|
private isTokenExpired;
|
|
389
389
|
private prepareHeaders;
|
|
390
|
+
/**
|
|
391
|
+
* 실패 응답(`!response.ok`)을 ApiError 로 변환한다. 던지지도, emitError 하지도 않는다.
|
|
392
|
+
*
|
|
393
|
+
* handleResponse 와 **같은 파싱을 두 곳이 필요로 해서** 뽑아냈다 — `ai.chat` 이 스트리밍
|
|
394
|
+
* 전송으로 옮겨가면서 SSE 응답의 HTTP 레벨 실패도 기존 비스트리밍과 **완전히 같은
|
|
395
|
+
* ApiError** 로 보여야 하기 때문이다. 파싱을 복제하면 그 순간부터 두 경로의 에러 모양이
|
|
396
|
+
* 갈린다 (실제로 코드 유실 사고가 있었다 — platform-issue 019fa21c).
|
|
397
|
+
*/
|
|
398
|
+
buildApiError(response: Response): Promise<ApiError>;
|
|
390
399
|
private handleResponse;
|
|
391
400
|
/**
|
|
392
401
|
* AbortController 를 관리하며 fetch 호출을 실행. 타임아웃/외부 signal 병합.
|
|
@@ -586,6 +595,11 @@ interface AIChatRequest {
|
|
|
586
595
|
*/
|
|
587
596
|
hybrid?: boolean;
|
|
588
597
|
toolGroupId?: string;
|
|
598
|
+
/**
|
|
599
|
+
* 서버가 쓸 생성 예산(초). 보통 직접 넣지 않고 `chat(request, { timeout })` 로 준다 —
|
|
600
|
+
* SDK 가 ms 예산에서 계산해 채운다. 직접 넣은 값은 그 계산에 덮인다.
|
|
601
|
+
*/
|
|
602
|
+
timeoutSeconds?: number;
|
|
589
603
|
/**
|
|
590
604
|
* 0 보다 크면, 서버측 도구 그룹 실행(`toolGroupId` 사용 시)에서 각 도구 결과를 모델에
|
|
591
605
|
* **재투입하기 전** N 자(rune 기준)로 잘라낸다(말미에 잘림 표시). SSE `onToolEvent` 의
|
|
@@ -745,6 +759,15 @@ interface AIStreamChunk {
|
|
|
745
759
|
done: boolean;
|
|
746
760
|
toolCalls?: AIToolCall[];
|
|
747
761
|
sources?: AISource[];
|
|
762
|
+
/**
|
|
763
|
+
* 토큰 사용량. **스트림 종료 청크에만** 실린다 (서버 5.6.3+ / core-server 2026-08-01+).
|
|
764
|
+
* 그 전 서버는 이 필드를 보내지 않으므로 `undefined` 일 수 있다.
|
|
765
|
+
*/
|
|
766
|
+
usage?: {
|
|
767
|
+
promptTokens: number;
|
|
768
|
+
completionTokens: number;
|
|
769
|
+
totalTokens: number;
|
|
770
|
+
};
|
|
748
771
|
name?: string;
|
|
749
772
|
toolCallId?: string;
|
|
750
773
|
arguments?: Record<string, unknown>;
|
|
@@ -757,9 +780,12 @@ interface AIStreamChunk {
|
|
|
757
780
|
message?: string;
|
|
758
781
|
/** 프로바이더가 준 하위 코드 (`max_tokens_exceeds_context` 등). */
|
|
759
782
|
code?: string;
|
|
760
|
-
/**
|
|
783
|
+
/**
|
|
784
|
+
* 프로바이더 식별자. 에러 이벤트에서는 **실패를 일으킨** 프로바이더, 종료 청크에서는
|
|
785
|
+
* **실제로 응답한** 프로바이더다 (둘 다 요청에서 생략 가능해 서버가 해결한다).
|
|
786
|
+
*/
|
|
761
787
|
provider?: string;
|
|
762
|
-
/**
|
|
788
|
+
/** 모델 식별자. `provider` 와 같은 규칙. */
|
|
763
789
|
model?: string;
|
|
764
790
|
}
|
|
765
791
|
|
|
@@ -821,6 +847,20 @@ declare class AIAPI {
|
|
|
821
847
|
*
|
|
822
848
|
* @param options `options.timeout` 으로 이 1회 생성의 시간 예산(ms)을 정한다 — 자세한
|
|
823
849
|
* 의미와 상한은 {@link AIChatOptions.timeout} 참고. 미지정이면 90초.
|
|
850
|
+
*
|
|
851
|
+
* ### 전송 방식 (5.6.3+)
|
|
852
|
+
*
|
|
853
|
+
* 겉보기 API 는 그대로지만 **내부적으로는 스트리밍 전송을 쓰고 결과를 합쳐서 돌려준다.**
|
|
854
|
+
*
|
|
855
|
+
* 왜: 응답이 다 만들어질 때까지 한 바이트도 흐르지 않는 요청은, 중간의 어떤 계층이든
|
|
856
|
+
* "첫 바이트까지 N초" 제한을 두면 그 자리에서 끊긴다 — CDN(100초), 리버스 프록시,
|
|
857
|
+
* 로드밸런서, 모바일 캐리어 NAT 가 전부 그런 제한을 갖는다. 서버 타임아웃을 아무리
|
|
858
|
+
* 늘려도 이 성질은 남으므로, 상한을 올리는 게 아니라 **바이트가 계속 흐르게** 하는 것이
|
|
859
|
+
* 유일한 근본 해결이다(긴 생성에 스트리밍을 쓰는 것은 LLM API 의 표준 관행이다).
|
|
860
|
+
*
|
|
861
|
+
* 호출자가 볼 수 있는 차이는 없다 — `content`/`reasoning`/`toolCalls`/`sources`/
|
|
862
|
+
* `finishReason`/`usage` 모두 동일하게 채워지고, 에러도 같은 `code` 로 던져진다.
|
|
863
|
+
* 토큰을 실시간으로 보여줘야 하면 {@link AIAPI.chatStream} 을 쓴다.
|
|
824
864
|
*/
|
|
825
865
|
chat(request: AIChatRequest, options?: AIChatOptions): Promise<AIChatResponse>;
|
|
826
866
|
/**
|
|
@@ -861,6 +901,15 @@ declare class AIAPI {
|
|
|
861
901
|
* ```
|
|
862
902
|
*/
|
|
863
903
|
chatStream(request: AIChatRequest, callbacks: AIChatStreamCallbacks, options?: AIChatStreamOptions): Promise<void>;
|
|
904
|
+
/**
|
|
905
|
+
* SSE 소비 본체. `chatStream`(공개)과 `chat`(집계)이 공유한다.
|
|
906
|
+
*
|
|
907
|
+
* `hooks.onFinalChunk` 은 종료 청크처럼 **토큰 콜백으로는 표현되지 않는 값**
|
|
908
|
+
* (usage / finishReason / toolCalls)을 집는 내부 훅이다. 공개 옵션으로 열지 않는 이유:
|
|
909
|
+
* 스트리밍 사용자는 그 값들을 콜백으로 이미 받거나 필요로 하지 않는데, 공개하면
|
|
910
|
+
* "언제 몇 번 불리는가" 가 계약이 되어 와이어 포맷을 못 바꾸게 된다.
|
|
911
|
+
*/
|
|
912
|
+
private runStream;
|
|
864
913
|
}
|
|
865
914
|
|
|
866
915
|
interface AnalyticsConfig {
|
package/dist/index.d.ts
CHANGED
|
@@ -387,6 +387,15 @@ declare class HttpClient {
|
|
|
387
387
|
private emitError;
|
|
388
388
|
private isTokenExpired;
|
|
389
389
|
private prepareHeaders;
|
|
390
|
+
/**
|
|
391
|
+
* 실패 응답(`!response.ok`)을 ApiError 로 변환한다. 던지지도, emitError 하지도 않는다.
|
|
392
|
+
*
|
|
393
|
+
* handleResponse 와 **같은 파싱을 두 곳이 필요로 해서** 뽑아냈다 — `ai.chat` 이 스트리밍
|
|
394
|
+
* 전송으로 옮겨가면서 SSE 응답의 HTTP 레벨 실패도 기존 비스트리밍과 **완전히 같은
|
|
395
|
+
* ApiError** 로 보여야 하기 때문이다. 파싱을 복제하면 그 순간부터 두 경로의 에러 모양이
|
|
396
|
+
* 갈린다 (실제로 코드 유실 사고가 있었다 — platform-issue 019fa21c).
|
|
397
|
+
*/
|
|
398
|
+
buildApiError(response: Response): Promise<ApiError>;
|
|
390
399
|
private handleResponse;
|
|
391
400
|
/**
|
|
392
401
|
* AbortController 를 관리하며 fetch 호출을 실행. 타임아웃/외부 signal 병합.
|
|
@@ -586,6 +595,11 @@ interface AIChatRequest {
|
|
|
586
595
|
*/
|
|
587
596
|
hybrid?: boolean;
|
|
588
597
|
toolGroupId?: string;
|
|
598
|
+
/**
|
|
599
|
+
* 서버가 쓸 생성 예산(초). 보통 직접 넣지 않고 `chat(request, { timeout })` 로 준다 —
|
|
600
|
+
* SDK 가 ms 예산에서 계산해 채운다. 직접 넣은 값은 그 계산에 덮인다.
|
|
601
|
+
*/
|
|
602
|
+
timeoutSeconds?: number;
|
|
589
603
|
/**
|
|
590
604
|
* 0 보다 크면, 서버측 도구 그룹 실행(`toolGroupId` 사용 시)에서 각 도구 결과를 모델에
|
|
591
605
|
* **재투입하기 전** N 자(rune 기준)로 잘라낸다(말미에 잘림 표시). SSE `onToolEvent` 의
|
|
@@ -745,6 +759,15 @@ interface AIStreamChunk {
|
|
|
745
759
|
done: boolean;
|
|
746
760
|
toolCalls?: AIToolCall[];
|
|
747
761
|
sources?: AISource[];
|
|
762
|
+
/**
|
|
763
|
+
* 토큰 사용량. **스트림 종료 청크에만** 실린다 (서버 5.6.3+ / core-server 2026-08-01+).
|
|
764
|
+
* 그 전 서버는 이 필드를 보내지 않으므로 `undefined` 일 수 있다.
|
|
765
|
+
*/
|
|
766
|
+
usage?: {
|
|
767
|
+
promptTokens: number;
|
|
768
|
+
completionTokens: number;
|
|
769
|
+
totalTokens: number;
|
|
770
|
+
};
|
|
748
771
|
name?: string;
|
|
749
772
|
toolCallId?: string;
|
|
750
773
|
arguments?: Record<string, unknown>;
|
|
@@ -757,9 +780,12 @@ interface AIStreamChunk {
|
|
|
757
780
|
message?: string;
|
|
758
781
|
/** 프로바이더가 준 하위 코드 (`max_tokens_exceeds_context` 등). */
|
|
759
782
|
code?: string;
|
|
760
|
-
/**
|
|
783
|
+
/**
|
|
784
|
+
* 프로바이더 식별자. 에러 이벤트에서는 **실패를 일으킨** 프로바이더, 종료 청크에서는
|
|
785
|
+
* **실제로 응답한** 프로바이더다 (둘 다 요청에서 생략 가능해 서버가 해결한다).
|
|
786
|
+
*/
|
|
761
787
|
provider?: string;
|
|
762
|
-
/**
|
|
788
|
+
/** 모델 식별자. `provider` 와 같은 규칙. */
|
|
763
789
|
model?: string;
|
|
764
790
|
}
|
|
765
791
|
|
|
@@ -821,6 +847,20 @@ declare class AIAPI {
|
|
|
821
847
|
*
|
|
822
848
|
* @param options `options.timeout` 으로 이 1회 생성의 시간 예산(ms)을 정한다 — 자세한
|
|
823
849
|
* 의미와 상한은 {@link AIChatOptions.timeout} 참고. 미지정이면 90초.
|
|
850
|
+
*
|
|
851
|
+
* ### 전송 방식 (5.6.3+)
|
|
852
|
+
*
|
|
853
|
+
* 겉보기 API 는 그대로지만 **내부적으로는 스트리밍 전송을 쓰고 결과를 합쳐서 돌려준다.**
|
|
854
|
+
*
|
|
855
|
+
* 왜: 응답이 다 만들어질 때까지 한 바이트도 흐르지 않는 요청은, 중간의 어떤 계층이든
|
|
856
|
+
* "첫 바이트까지 N초" 제한을 두면 그 자리에서 끊긴다 — CDN(100초), 리버스 프록시,
|
|
857
|
+
* 로드밸런서, 모바일 캐리어 NAT 가 전부 그런 제한을 갖는다. 서버 타임아웃을 아무리
|
|
858
|
+
* 늘려도 이 성질은 남으므로, 상한을 올리는 게 아니라 **바이트가 계속 흐르게** 하는 것이
|
|
859
|
+
* 유일한 근본 해결이다(긴 생성에 스트리밍을 쓰는 것은 LLM API 의 표준 관행이다).
|
|
860
|
+
*
|
|
861
|
+
* 호출자가 볼 수 있는 차이는 없다 — `content`/`reasoning`/`toolCalls`/`sources`/
|
|
862
|
+
* `finishReason`/`usage` 모두 동일하게 채워지고, 에러도 같은 `code` 로 던져진다.
|
|
863
|
+
* 토큰을 실시간으로 보여줘야 하면 {@link AIAPI.chatStream} 을 쓴다.
|
|
824
864
|
*/
|
|
825
865
|
chat(request: AIChatRequest, options?: AIChatOptions): Promise<AIChatResponse>;
|
|
826
866
|
/**
|
|
@@ -861,6 +901,15 @@ declare class AIAPI {
|
|
|
861
901
|
* ```
|
|
862
902
|
*/
|
|
863
903
|
chatStream(request: AIChatRequest, callbacks: AIChatStreamCallbacks, options?: AIChatStreamOptions): Promise<void>;
|
|
904
|
+
/**
|
|
905
|
+
* SSE 소비 본체. `chatStream`(공개)과 `chat`(집계)이 공유한다.
|
|
906
|
+
*
|
|
907
|
+
* `hooks.onFinalChunk` 은 종료 청크처럼 **토큰 콜백으로는 표현되지 않는 값**
|
|
908
|
+
* (usage / finishReason / toolCalls)을 집는 내부 훅이다. 공개 옵션으로 열지 않는 이유:
|
|
909
|
+
* 스트리밍 사용자는 그 값들을 콜백으로 이미 받거나 필요로 하지 않는데, 공개하면
|
|
910
|
+
* "언제 몇 번 불리는가" 가 계약이 되어 와이어 포맷을 못 바꾸게 된다.
|
|
911
|
+
*/
|
|
912
|
+
private runStream;
|
|
864
913
|
}
|
|
865
914
|
|
|
866
915
|
interface AnalyticsConfig {
|
package/dist/index.js
CHANGED
|
@@ -283,6 +283,29 @@ var GameError = class extends Error {
|
|
|
283
283
|
};
|
|
284
284
|
|
|
285
285
|
// src/api/ai.ts
|
|
286
|
+
function aiErrorToApiError(err) {
|
|
287
|
+
const statusByCode = {
|
|
288
|
+
provider_timeout: 504,
|
|
289
|
+
gateway_timeout: 504,
|
|
290
|
+
bad_gateway: 502,
|
|
291
|
+
service_unavailable: 503,
|
|
292
|
+
rate_limit_exceeded: 429,
|
|
293
|
+
quota_exceeded: 429,
|
|
294
|
+
invalid_request: 400,
|
|
295
|
+
config_error: 400,
|
|
296
|
+
unauthorized: 401
|
|
297
|
+
};
|
|
298
|
+
const details = {};
|
|
299
|
+
if (err.provider) details.provider = err.provider;
|
|
300
|
+
if (err.model) details.model = err.model;
|
|
301
|
+
if (err.retryAfter !== void 0) details.retry_after_seconds = err.retryAfter;
|
|
302
|
+
return new ApiError(
|
|
303
|
+
err.status ?? statusByCode[err.code] ?? 500,
|
|
304
|
+
err.message,
|
|
305
|
+
err.detailCode || err.code,
|
|
306
|
+
Object.keys(details).length > 0 ? details : void 0
|
|
307
|
+
);
|
|
308
|
+
}
|
|
286
309
|
var AI_CHAT_DEFAULT_TIMEOUT_MS = 9e4;
|
|
287
310
|
var AI_CHAT_MAX_TIMEOUT_MS = 3e5;
|
|
288
311
|
var AI_CHAT_CLIENT_GRACE_MS = 1e4;
|
|
@@ -296,19 +319,101 @@ var AIAPI = class {
|
|
|
296
319
|
*
|
|
297
320
|
* @param options `options.timeout` 으로 이 1회 생성의 시간 예산(ms)을 정한다 — 자세한
|
|
298
321
|
* 의미와 상한은 {@link AIChatOptions.timeout} 참고. 미지정이면 90초.
|
|
322
|
+
*
|
|
323
|
+
* ### 전송 방식 (5.6.3+)
|
|
324
|
+
*
|
|
325
|
+
* 겉보기 API 는 그대로지만 **내부적으로는 스트리밍 전송을 쓰고 결과를 합쳐서 돌려준다.**
|
|
326
|
+
*
|
|
327
|
+
* 왜: 응답이 다 만들어질 때까지 한 바이트도 흐르지 않는 요청은, 중간의 어떤 계층이든
|
|
328
|
+
* "첫 바이트까지 N초" 제한을 두면 그 자리에서 끊긴다 — CDN(100초), 리버스 프록시,
|
|
329
|
+
* 로드밸런서, 모바일 캐리어 NAT 가 전부 그런 제한을 갖는다. 서버 타임아웃을 아무리
|
|
330
|
+
* 늘려도 이 성질은 남으므로, 상한을 올리는 게 아니라 **바이트가 계속 흐르게** 하는 것이
|
|
331
|
+
* 유일한 근본 해결이다(긴 생성에 스트리밍을 쓰는 것은 LLM API 의 표준 관행이다).
|
|
332
|
+
*
|
|
333
|
+
* 호출자가 볼 수 있는 차이는 없다 — `content`/`reasoning`/`toolCalls`/`sources`/
|
|
334
|
+
* `finishReason`/`usage` 모두 동일하게 채워지고, 에러도 같은 `code` 로 던져진다.
|
|
335
|
+
* 토큰을 실시간으로 보여줘야 하면 {@link AIAPI.chatStream} 을 쓴다.
|
|
299
336
|
*/
|
|
300
337
|
async chat(request, options) {
|
|
301
338
|
const budgetMs = Math.min(
|
|
302
339
|
options?.timeout ?? this.http.requestTimeoutMs ?? AI_CHAT_DEFAULT_TIMEOUT_MS,
|
|
303
340
|
AI_CHAT_MAX_TIMEOUT_MS
|
|
304
341
|
);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
342
|
+
const controller = new AbortController();
|
|
343
|
+
let budgetExpired = false;
|
|
344
|
+
const timer = setTimeout(() => {
|
|
345
|
+
budgetExpired = true;
|
|
346
|
+
controller.abort();
|
|
347
|
+
}, budgetMs + AI_CHAT_CLIENT_GRACE_MS);
|
|
348
|
+
const external = options?.signal;
|
|
349
|
+
const forwardAbort = () => controller.abort();
|
|
350
|
+
if (external) {
|
|
351
|
+
if (external.aborted) controller.abort();
|
|
352
|
+
else external.addEventListener("abort", forwardAbort, { once: true });
|
|
353
|
+
}
|
|
354
|
+
const aggregated = { content: "", provider: "", model: "" };
|
|
355
|
+
let failure;
|
|
356
|
+
let aborted = false;
|
|
357
|
+
let reasoning = "";
|
|
358
|
+
try {
|
|
359
|
+
await this.runStream(
|
|
360
|
+
// timeoutSeconds 는 서버가 쓰는 생성 예산. 마지막에 펼쳐 호출자가 임의로 넣은
|
|
361
|
+
// 값이 이 계산을 덮어쓰지 못하게 한다.
|
|
362
|
+
{ ...request, timeoutSeconds: Math.ceil(budgetMs / 1e3) },
|
|
363
|
+
{
|
|
364
|
+
onToken: (token) => {
|
|
365
|
+
aggregated.content += token;
|
|
366
|
+
},
|
|
367
|
+
onReasoning: (delta) => {
|
|
368
|
+
reasoning += delta;
|
|
369
|
+
},
|
|
370
|
+
onSources: (sources) => {
|
|
371
|
+
aggregated.sources = sources;
|
|
372
|
+
},
|
|
373
|
+
// 에러는 콜백으로 오므로 붙잡아 두었다가 아래에서 throw 한다 — 콜백 안에서
|
|
374
|
+
// 던지면 스트림 정리(reader cancel)를 건너뛴다.
|
|
375
|
+
onError: (error) => {
|
|
376
|
+
failure = error;
|
|
377
|
+
},
|
|
378
|
+
onAbort: () => {
|
|
379
|
+
aborted = true;
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{ signal: controller.signal },
|
|
383
|
+
{
|
|
384
|
+
// HTTP 레벨 실패는 비스트리밍 시절과 **완전히 같은 ApiError** 로 던진다.
|
|
385
|
+
onHttpError: async (response) => {
|
|
386
|
+
throw await this.http.buildApiError(response);
|
|
387
|
+
},
|
|
388
|
+
// 스트림 종료 청크에만 실리는 값들 — 토큰 콜백으로는 표현되지 않는다.
|
|
389
|
+
onFinalChunk: (chunk) => {
|
|
390
|
+
if (chunk.finishReason)
|
|
391
|
+
aggregated.finishReason = chunk.finishReason;
|
|
392
|
+
if (chunk.toolCalls?.length) aggregated.toolCalls = chunk.toolCalls;
|
|
393
|
+
if (chunk.usage) aggregated.usage = chunk.usage;
|
|
394
|
+
if (chunk.provider) aggregated.provider = chunk.provider;
|
|
395
|
+
if (chunk.model) aggregated.model = chunk.model;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
} finally {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
external?.removeEventListener("abort", forwardAbort);
|
|
402
|
+
}
|
|
403
|
+
if (failure) throw aiErrorToApiError(failure);
|
|
404
|
+
if (aborted) {
|
|
405
|
+
if (budgetExpired) {
|
|
406
|
+
throw new ApiError(
|
|
407
|
+
504,
|
|
408
|
+
`AI \uC751\uB2F5\uC774 ${Math.round(budgetMs / 1e3)}\uCD08 \uC608\uC0B0 \uC548\uC5D0 \uB05D\uB098\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4`,
|
|
409
|
+
"provider_timeout"
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
throw new DOMException("The operation was aborted.", "AbortError");
|
|
413
|
+
}
|
|
414
|
+
if (failure) throw failure;
|
|
415
|
+
if (reasoning) aggregated.reasoning = reasoning;
|
|
416
|
+
return aggregated;
|
|
312
417
|
}
|
|
313
418
|
/**
|
|
314
419
|
* AI 채팅 스트리밍 (SSE)
|
|
@@ -348,6 +453,17 @@ var AIAPI = class {
|
|
|
348
453
|
* ```
|
|
349
454
|
*/
|
|
350
455
|
async chatStream(request, callbacks, options) {
|
|
456
|
+
return this.runStream(request, callbacks, options);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* SSE 소비 본체. `chatStream`(공개)과 `chat`(집계)이 공유한다.
|
|
460
|
+
*
|
|
461
|
+
* `hooks.onFinalChunk` 은 종료 청크처럼 **토큰 콜백으로는 표현되지 않는 값**
|
|
462
|
+
* (usage / finishReason / toolCalls)을 집는 내부 훅이다. 공개 옵션으로 열지 않는 이유:
|
|
463
|
+
* 스트리밍 사용자는 그 값들을 콜백으로 이미 받거나 필요로 하지 않는데, 공개하면
|
|
464
|
+
* "언제 몇 번 불리는가" 가 계약이 되어 와이어 포맷을 못 바꾸게 된다.
|
|
465
|
+
*/
|
|
466
|
+
async runStream(request, callbacks, options, hooks) {
|
|
351
467
|
const signal = options?.signal;
|
|
352
468
|
const isAbort = (err) => signal?.aborted === true || err instanceof DOMException && err.name === "AbortError" || typeof err === "object" && err !== null && err.name === "AbortError";
|
|
353
469
|
let reader;
|
|
@@ -362,6 +478,7 @@ var AIAPI = class {
|
|
|
362
478
|
signal
|
|
363
479
|
});
|
|
364
480
|
if (!response.ok) {
|
|
481
|
+
if (hooks?.onHttpError) await hooks.onHttpError(response);
|
|
365
482
|
const errorData = await response.json().catch(() => ({ error: "stream_failed" }));
|
|
366
483
|
callbacks.onError?.(
|
|
367
484
|
toAIError(errorData, {
|
|
@@ -426,9 +543,11 @@ var AIAPI = class {
|
|
|
426
543
|
if (event.type === "heartbeat") {
|
|
427
544
|
continue;
|
|
428
545
|
}
|
|
546
|
+
if (event.usage) hooks?.onFinalChunk?.(event);
|
|
429
547
|
if (event.reasoning) callbacks.onReasoning?.(event.reasoning);
|
|
430
548
|
if (event.content) callbacks.onToken?.(event.content);
|
|
431
549
|
if (event.done) {
|
|
550
|
+
if (!event.usage) hooks?.onFinalChunk?.(event);
|
|
432
551
|
callbacks.onDone?.();
|
|
433
552
|
return;
|
|
434
553
|
}
|
|
@@ -11652,70 +11771,81 @@ var HttpClient = class {
|
|
|
11652
11771
|
}
|
|
11653
11772
|
return headers;
|
|
11654
11773
|
}
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11774
|
+
/**
|
|
11775
|
+
* 실패 응답(`!response.ok`)을 ApiError 로 변환한다. 던지지도, emitError 하지도 않는다.
|
|
11776
|
+
*
|
|
11777
|
+
* handleResponse 와 **같은 파싱을 두 곳이 필요로 해서** 뽑아냈다 — `ai.chat` 이 스트리밍
|
|
11778
|
+
* 전송으로 옮겨가면서 SSE 응답의 HTTP 레벨 실패도 기존 비스트리밍과 **완전히 같은
|
|
11779
|
+
* ApiError** 로 보여야 하기 때문이다. 파싱을 복제하면 그 순간부터 두 경로의 에러 모양이
|
|
11780
|
+
* 갈린다 (실제로 코드 유실 사고가 있었다 — platform-issue 019fa21c).
|
|
11781
|
+
*/
|
|
11782
|
+
async buildApiError(response) {
|
|
11783
|
+
let errorData = {};
|
|
11784
|
+
let bodyIsJson = true;
|
|
11785
|
+
try {
|
|
11786
|
+
errorData = await response.json();
|
|
11787
|
+
} catch {
|
|
11788
|
+
bodyIsJson = false;
|
|
11789
|
+
}
|
|
11790
|
+
const retryAfterHeader = response.status === 429 ? response.headers.get("Retry-After") : null;
|
|
11791
|
+
let retryAfterSeconds;
|
|
11792
|
+
if (retryAfterHeader) {
|
|
11793
|
+
const asInt = Number.parseInt(retryAfterHeader, 10);
|
|
11794
|
+
if (Number.isFinite(asInt) && asInt >= 0) {
|
|
11795
|
+
retryAfterSeconds = asInt;
|
|
11796
|
+
} else {
|
|
11797
|
+
const dateMs = Date.parse(retryAfterHeader);
|
|
11798
|
+
if (Number.isFinite(dateMs)) {
|
|
11799
|
+
retryAfterSeconds = Math.max(
|
|
11800
|
+
0,
|
|
11801
|
+
Math.round((dateMs - Date.now()) / 1e3)
|
|
11802
|
+
);
|
|
11678
11803
|
}
|
|
11679
11804
|
}
|
|
11680
|
-
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
}
|
|
11686
|
-
|
|
11687
|
-
details.retry_after_seconds = retryAfterSeconds;
|
|
11688
|
-
}
|
|
11689
|
-
const err2 = new ApiError(
|
|
11690
|
-
response.status,
|
|
11691
|
-
typeof structured.message === "string" && structured.message !== "" ? structured.message : "Unknown error",
|
|
11692
|
-
typeof structured.code === "string" ? structured.code : void 0,
|
|
11693
|
-
Object.keys(details).length > 0 ? details : void 0
|
|
11694
|
-
);
|
|
11695
|
-
this.emitError(err2);
|
|
11696
|
-
throw err2;
|
|
11697
|
-
}
|
|
11698
|
-
const flatMessage = typeof errorData.message === "string" && errorData.message !== "" ? errorData.message : void 0;
|
|
11699
|
-
const explicitCode = typeof errorData.code === "string" && errorData.code !== "" ? errorData.code : void 0;
|
|
11700
|
-
const errorIsCode = typeof rawError === "string" && /^[a-z][a-z0-9_]*$/.test(rawError);
|
|
11701
|
-
const message = flatMessage ?? (typeof rawError === "string" && rawError !== "" ? rawError : void 0) ?? (bodyIsJson ? void 0 : gatewayMessageFromStatus(response.status)) ?? (response.statusText !== "" ? response.statusText : "Unknown error");
|
|
11702
|
-
const code = explicitCode ?? (errorIsCode ? rawError : void 0) ?? gatewayCodeFromStatus(response.status);
|
|
11703
|
-
const legacyDetails = {};
|
|
11805
|
+
}
|
|
11806
|
+
const rawError = errorData.error;
|
|
11807
|
+
if (rawError && typeof rawError === "object" && "message" in rawError) {
|
|
11808
|
+
const structured = rawError;
|
|
11809
|
+
const details = {
|
|
11810
|
+
...structured.details && typeof structured.details === "object" ? structured.details : {}
|
|
11811
|
+
};
|
|
11704
11812
|
if (retryAfterSeconds !== void 0) {
|
|
11705
|
-
|
|
11813
|
+
details.retry_after_seconds = retryAfterSeconds;
|
|
11706
11814
|
}
|
|
11707
|
-
|
|
11708
|
-
legacyDetails.provider = errorData.provider;
|
|
11709
|
-
}
|
|
11710
|
-
if (typeof errorData.model === "string") {
|
|
11711
|
-
legacyDetails.model = errorData.model;
|
|
11712
|
-
}
|
|
11713
|
-
const err = new ApiError(
|
|
11815
|
+
const err2 = new ApiError(
|
|
11714
11816
|
response.status,
|
|
11715
|
-
message,
|
|
11716
|
-
code,
|
|
11717
|
-
Object.keys(
|
|
11817
|
+
typeof structured.message === "string" && structured.message !== "" ? structured.message : "Unknown error",
|
|
11818
|
+
typeof structured.code === "string" ? structured.code : void 0,
|
|
11819
|
+
Object.keys(details).length > 0 ? details : void 0
|
|
11718
11820
|
);
|
|
11821
|
+
return err2;
|
|
11822
|
+
}
|
|
11823
|
+
const flatMessage = typeof errorData.message === "string" && errorData.message !== "" ? errorData.message : void 0;
|
|
11824
|
+
const explicitCode = typeof errorData.code === "string" && errorData.code !== "" ? errorData.code : void 0;
|
|
11825
|
+
const errorIsCode = typeof rawError === "string" && /^[a-z][a-z0-9_]*$/.test(rawError);
|
|
11826
|
+
const message = flatMessage ?? (typeof rawError === "string" && rawError !== "" ? rawError : void 0) ?? (bodyIsJson ? void 0 : gatewayMessageFromStatus(response.status)) ?? (response.statusText !== "" ? response.statusText : "Unknown error");
|
|
11827
|
+
const code = explicitCode ?? (errorIsCode ? rawError : void 0) ?? gatewayCodeFromStatus(response.status);
|
|
11828
|
+
const legacyDetails = {};
|
|
11829
|
+
if (retryAfterSeconds !== void 0) {
|
|
11830
|
+
legacyDetails.retry_after_seconds = retryAfterSeconds;
|
|
11831
|
+
}
|
|
11832
|
+
if (typeof errorData.provider === "string") {
|
|
11833
|
+
legacyDetails.provider = errorData.provider;
|
|
11834
|
+
}
|
|
11835
|
+
if (typeof errorData.model === "string") {
|
|
11836
|
+
legacyDetails.model = errorData.model;
|
|
11837
|
+
}
|
|
11838
|
+
const err = new ApiError(
|
|
11839
|
+
response.status,
|
|
11840
|
+
message,
|
|
11841
|
+
code,
|
|
11842
|
+
Object.keys(legacyDetails).length > 0 ? legacyDetails : void 0
|
|
11843
|
+
);
|
|
11844
|
+
return err;
|
|
11845
|
+
}
|
|
11846
|
+
async handleResponse(response) {
|
|
11847
|
+
if (!response.ok) {
|
|
11848
|
+
const err = await this.buildApiError(response);
|
|
11719
11849
|
this.emitError(err);
|
|
11720
11850
|
throw err;
|
|
11721
11851
|
}
|