bunnyquery 1.3.2 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -34,6 +34,55 @@ export function isErrorResponseBody(response: any): boolean {
34
34
  return false;
35
35
  }
36
36
 
37
+ // A request that failed because the REQUEST ITSELF is malformed — an unknown or
38
+ // rejected parameter on a 400/422 — will deterministically re-fail if resent
39
+ // unchanged; no session refresh or retry can fix it. Detecting it lets the retry
40
+ // gates refuse to auto-resend (otherwise a 400 whose param name merely contains
41
+ // "token", e.g. `max_output_tokens`, trips the `invalid_request + token` branch
42
+ // of isAuthExpiredError below and loops refresh->resend forever, burning tokens).
43
+ // This is the `_skapi_extract` 400 class. Distinct from auth-expiry, which IS
44
+ // retryable after a token refresh — so 401 (auth) and 429/5xx (transient) are
45
+ // intentionally NOT treated as non-retryable here.
46
+ export function isNonRetryableRequestError(input: any): boolean {
47
+ if (!input || typeof input !== 'object') return false;
48
+
49
+ var status = typeof input.status_code === 'number' ? input.status_code
50
+ : typeof input.status === 'number' ? input.status : undefined;
51
+
52
+ // Collect the error envelope wherever it lives (response, response.body, or a
53
+ // thrown error), mirroring isErrorResponseBody / isAuthExpiredError.
54
+ var param: any = undefined;
55
+ var blobs: string[] = [];
56
+ var sources = [input.error, input.body && input.body.error, input.body, input];
57
+ for (var i = 0; i < sources.length; i++) {
58
+ var e = sources[i];
59
+ if (!e) continue;
60
+ if (typeof e === 'string') { blobs.push(e); continue; }
61
+ if (typeof e !== 'object') continue;
62
+ if (param === undefined && e.param != null) param = e.param;
63
+ if (typeof e.code === 'string') blobs.push(e.code);
64
+ if (typeof e.type === 'string') blobs.push(e.type);
65
+ if (typeof e.message === 'string') blobs.push(e.message);
66
+ }
67
+ var hay = blobs.join(' | ').toLowerCase();
68
+
69
+ // Explicit "unknown / unsupported parameter" is always a malformed request.
70
+ if (hay.indexOf('unknown_parameter') !== -1 || hay.indexOf('unknown parameter') !== -1 ||
71
+ hay.indexOf('unsupported_parameter') !== -1 || hay.indexOf('unsupported parameter') !== -1) {
72
+ return true;
73
+ }
74
+
75
+ // A 400/422 invalid_request that points at a specific rejected field: resending
76
+ // the identical body re-fails. (401 -> isAuthExpiredError; 429/5xx -> transient.)
77
+ var isClientReqStatus = status === 400 || status === 422;
78
+ if (isClientReqStatus && param != null && param !== '') return true;
79
+ if (isClientReqStatus && hay.indexOf('invalid_request') !== -1 &&
80
+ (hay.indexOf('parameter') !== -1 || hay.indexOf('param') !== -1)) {
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+
37
86
  export function isAuthExpiredError(input: any): boolean {
38
87
  if (!input) return false;
39
88
  var blobs: string[] = [];
@@ -30,7 +30,7 @@ export * from './prompts';
30
30
 
31
31
  // Pure helpers (Tier-1.5): error detection, token budgeting, link/path
32
32
  // normalization, and history mapping — shared so both consumers stay identical.
33
- export { getErrorMessage, isErrorResponseBody, isAuthExpiredError } from './errors';
33
+ export { getErrorMessage, isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError } from './errors';
34
34
  export * from './budget';
35
35
  export * from './links';
36
36
  export {
@@ -30,7 +30,7 @@ import {
30
30
  OPENAI_RESPONSES_API_URL,
31
31
  type BgTaskEntry,
32
32
  } from './requests';
33
- import { isErrorResponseBody, isAuthExpiredError, getErrorMessage } from './errors';
33
+ import { isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError, getErrorMessage } from './errors';
34
34
  import { buildBoundedChatMessages } from './budget';
35
35
  import { createInlineLinkRegex } from './links';
36
36
  import { mapHistoryListToMessages, extractLastUserTextFromRequest } from './history';
@@ -116,11 +116,15 @@ export class ChatSession {
116
116
  };
117
117
  var run = sendAndPoll()
118
118
  .catch(function (err: any) {
119
- if (isAuthExpiredError(err)) return self.host.refreshSession().then(sendAndPoll);
119
+ // Only auth-expiry is worth a refresh+resend; a malformed-request 400
120
+ // (e.g. the `_skapi_extract`/unknown-parameter class) re-fails identically,
121
+ // so never loop on it — guard against isAuthExpiredError's heuristic
122
+ // misfiring on a param name that merely contains "token".
123
+ if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(sendAndPoll);
120
124
  throw err;
121
125
  })
122
126
  .then(function (response: any) {
123
- if (isErrorResponseBody(response) && isAuthExpiredError(response)) {
127
+ if (isErrorResponseBody(response) && isAuthExpiredError(response) && !isNonRetryableRequestError(response)) {
124
128
  return self.host.refreshSession().then(sendAndPoll);
125
129
  }
126
130
  return response;
@@ -656,7 +660,7 @@ export class ChatSession {
656
660
  var fetchHistory = function () { return getChatHistory({ service: serviceId, owner: owner, platform: platform }, options); };
657
661
 
658
662
  return Promise.resolve().then(fetchHistory).catch(function (err: any) {
659
- if (isAuthExpiredError(err)) return self.host.refreshSession().then(fetchHistory);
663
+ if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(fetchHistory);
660
664
  throw err;
661
665
  }).then(function (history: any) {
662
666
  if (token !== self.state.gateRefreshToken) return;