@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 +9 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +10 -9
- package/src/utils/retry-after.ts +25 -0
- package/src/utils.ts +31 -1
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.
|
|
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.
|
|
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",
|
package/src/auth-storage.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
1423
|
+
credential: AuthCredential,
|
|
1421
1424
|
options?: { baseUrl?: string; timeoutMs?: number },
|
|
1422
1425
|
): Promise<UsageReport | null> {
|
|
1423
1426
|
return this.#fetchUsageCached(
|
|
1424
|
-
this.#
|
|
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 (
|
|
1516
|
+
if (this.#rankingStrategyResolver?.(provider)) {
|
|
1514
1517
|
const credential = this.#getCredentialsForProvider(provider)[sessionCredential.index];
|
|
1515
|
-
if (credential?.type ===
|
|
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,
|
|
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
|
}
|
package/src/utils/retry-after.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|