@f5-sales-demo/pi-ai 21.13.1 → 21.13.2

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 CHANGED
@@ -10,6 +10,15 @@
10
10
 
11
11
  ### Fixed
12
12
 
13
+ - Removed output-only lifecycle statuses before replaying persisted OpenAI Responses items,
14
+ including remote compaction and compaction summaries. Semantically ported from
15
+ [can1357/oh-my-pi#7743](https://github.com/can1357/oh-my-pi/pull/7743) and
16
+ [#10415](https://github.com/can1357/oh-my-pi/pull/10415)
17
+ ([#3721](https://github.com/f5-sales-demo/xcsh/issues/3721)).
18
+ - Extended rate-limit handling to keep API-key credentials blocked until their live usage reset
19
+ and to understand absolute English and Chinese reset timestamps. Semantically ported from
20
+ [can1357/oh-my-pi#10325](https://github.com/can1357/oh-my-pi/pull/10325)
21
+ ([#3721](https://github.com/f5-sales-demo/xcsh/issues/3721)).
13
22
  - Prevented case-insensitive Windows environment lookup from replacing literal stored credentials,
14
23
  and delayed Cursor success until the HTTP/2/CONNECT protocol terminates cleanly so late stream
15
24
  errors remain visible. Semantically ported from
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "21.13.1",
4
+ "version": "21.13.2",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "^0.115",
47
47
  "@aws-sdk/client-bedrock-runtime": "^3",
48
48
  "@bufbuild/protobuf": "^2.11",
49
- "@f5-sales-demo/pi-utils": "21.13.1",
49
+ "@f5-sales-demo/pi-utils": "21.13.2",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -1015,7 +1015,10 @@ export class AuthStorage {
1015
1015
  // Queries provider usage endpoints to detect rate limits before they occur.
1016
1016
  // ─────────────────────────────────────────────────────────────────────────────
1017
1017
 
1018
- #buildUsageCredential(credential: OAuthCredential): UsageCredential {
1018
+ #buildUsageCredential(credential: AuthCredential): UsageCredential {
1019
+ if (credential.type === "api_key") {
1020
+ return { type: "api_key", apiKey: credential.key };
1021
+ }
1019
1022
  return {
1020
1023
  type: "oauth",
1021
1024
  accessToken: credential.access,
@@ -1417,11 +1420,11 @@ export class AuthStorage {
1417
1420
 
1418
1421
  async #getUsageReport(
1419
1422
  provider: Provider,
1420
- credential: OAuthCredential,
1423
+ credential: AuthCredential,
1421
1424
  options?: { baseUrl?: string; timeoutMs?: number },
1422
1425
  ): Promise<UsageReport | null> {
1423
1426
  return this.#fetchUsageCached(
1424
- this.#buildUsageRequestForOauth(provider, credential, options?.baseUrl),
1427
+ this.#buildUsageRequest(provider, this.#buildUsageCredential(credential), options?.baseUrl),
1425
1428
  options?.timeoutMs ?? this.#usageRequestTimeoutMs,
1426
1429
  );
1427
1430
  }
@@ -1510,15 +1513,13 @@ export class AuthStorage {
1510
1513
  const now = Date.now();
1511
1514
  let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
1512
1515
 
1513
- if (sessionCredential.type === "oauth" && this.#rankingStrategyResolver?.(provider)) {
1516
+ if (this.#rankingStrategyResolver?.(provider)) {
1514
1517
  const credential = this.#getCredentialsForProvider(provider)[sessionCredential.index];
1515
- if (credential?.type === "oauth") {
1518
+ if (credential?.type === sessionCredential.type) {
1516
1519
  const report = await this.#getUsageReport(provider, credential, options);
1517
1520
  if (report && this.#isUsageLimitReached(report)) {
1518
- const resetAtMs = this.#getUsageResetAtMs(report, Date.now());
1519
- if (resetAtMs && resetAtMs > blockedUntil) {
1520
- blockedUntil = resetAtMs;
1521
- }
1521
+ const resetAtMs = this.#getUsageResetAtMs(report, now);
1522
+ if (resetAtMs && resetAtMs > blockedUntil) blockedUntil = resetAtMs;
1522
1523
  }
1523
1524
  }
1524
1525
  }
@@ -1,6 +1,31 @@
1
1
  export type HeadersLike = Headers | Record<string, string | undefined> | undefined | null;
2
2
 
3
3
  const RETRY_AFTER_HINT = "retry-after-ms=";
4
+ const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms\s*[:=]\s*([0-9]+)\b/i;
5
+ const WILL_RESET_AT_PATTERN =
6
+ /(?:will\s+)?reset at\s+([0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:?[0-9]{2})?)/i;
7
+ const CN_RESET_AT_PATTERN = /将在\s*([0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}:[0-9]{2})\s*重置/;
8
+
9
+ /** Parse body-level retry hints that are not represented by HTTP headers. */
10
+ export function getRetryAfterMsFromErrorText(message: string): number | undefined {
11
+ const retryAfterMsMatch = RETRY_AFTER_MS_BODY_PATTERN.exec(message);
12
+ if (retryAfterMsMatch?.[1]) {
13
+ const milliseconds = Number(retryAfterMsMatch[1]);
14
+ if (Number.isFinite(milliseconds) && milliseconds > 0) return milliseconds;
15
+ }
16
+
17
+ for (const pattern of [WILL_RESET_AT_PATTERN, CN_RESET_AT_PATTERN]) {
18
+ const match = pattern.exec(message);
19
+ if (!match?.[1]) continue;
20
+ const normalized = match[1].replace(" ", "T");
21
+ const timestamp = /(?:Z|[+-][0-9]{2}:?[0-9]{2})$/i.test(normalized) ? normalized : `${normalized}Z`;
22
+ const resetAt = Date.parse(timestamp);
23
+ const delay = resetAt - Date.now();
24
+ if (!Number.isNaN(resetAt) && delay > 0) return delay;
25
+ }
26
+
27
+ return undefined;
28
+ }
4
29
 
5
30
  export function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string {
6
31
  const message = error instanceof Error ? error.message : JSON.stringify(error);
package/src/utils.ts CHANGED
@@ -65,12 +65,42 @@ export function truncateResponseItemId(id: string, prefix: string): string {
65
65
  return `${prefix}_${Bun.hash(id).toString(36)}`;
66
66
  }
67
67
 
68
+ /**
69
+ * Remove response-only lifecycle state from item types that reject it when
70
+ * replayed as OpenAI Responses input. The original array and untouched item
71
+ * identities are preserved when no change is needed.
72
+ */
73
+ export function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem extends { type?: unknown; status?: unknown }>(
74
+ items: TItem[],
75
+ ): TItem[] {
76
+ let sanitized: TItem[] | undefined;
77
+ for (let index = 0; index < items.length; index++) {
78
+ const item = items[index]!;
79
+ const rejectsOutputStatus =
80
+ item.type === "message" ||
81
+ item.type === "function_call" ||
82
+ item.type === "custom_tool_call" ||
83
+ item.type === "compaction" ||
84
+ item.type === "compaction_summary";
85
+ if (!rejectsOutputStatus || !Object.hasOwn(item, "status")) {
86
+ sanitized?.push(item);
87
+ continue;
88
+ }
89
+ if (!sanitized) sanitized = items.slice(0, index);
90
+ const withoutStatus = { ...item };
91
+ delete withoutStatus.status;
92
+ sanitized.push(withoutStatus);
93
+ }
94
+ return sanitized ?? items;
95
+ }
96
+
68
97
  export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>): ResponseInput {
69
98
  const normalizedCallIds = new Map<string, string>();
70
- return items.flatMap(item => {
99
+ const sanitized = items.flatMap(item => {
71
100
  const sanitized = sanitizeOpenAIResponsesHistoryItemForReplay(item, normalizedCallIds);
72
101
  return sanitized ? [sanitized] : [];
73
102
  });
103
+ return stripOpenAIResponsesOutputOnlyStatusesForReplay(sanitized);
74
104
  }
75
105
 
76
106
  function sanitizeOpenAIResponsesHistoryItemForReplay(