@linxiraos/pi-ai 1.1.13 → 1.1.15

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +2 -70
  2. package/dist/types/error/flags.d.ts +6 -0
  3. package/dist/types/providers/anthropic-wire.d.ts +44 -7
  4. package/dist/types/providers/anthropic.d.ts +42 -0
  5. package/dist/types/providers/github-copilot-headers.d.ts +71 -1
  6. package/dist/types/providers/openai-completions.d.ts +1 -1
  7. package/dist/types/providers/openai-shared.d.ts +8 -0
  8. package/dist/types/providers/vision-guard.d.ts +16 -2
  9. package/dist/types/registry/oauth/github-copilot.d.ts +1 -0
  10. package/dist/types/registry/oauth/muse-code.d.ts +4 -4
  11. package/dist/types/types.d.ts +47 -1
  12. package/dist/types/usage/charm-hyper.d.ts +2 -0
  13. package/dist/types/utils/block-symbols.d.ts +41 -0
  14. package/package.json +6 -6
  15. package/src/auth-storage.ts +2 -0
  16. package/src/error/finalize.ts +9 -2
  17. package/src/error/flags.ts +46 -1
  18. package/src/error/rate-limit.ts +14 -1
  19. package/src/error/retryable.ts +2 -0
  20. package/src/providers/amazon-bedrock.ts +35 -25
  21. package/src/providers/anthropic-wire.ts +42 -9
  22. package/src/providers/anthropic.ts +648 -82
  23. package/src/providers/cursor.ts +1 -1
  24. package/src/providers/devin.ts +1 -1
  25. package/src/providers/github-copilot-headers.ts +242 -1
  26. package/src/providers/google-gemini-cli.ts +1 -1
  27. package/src/providers/google-shared.ts +1 -1
  28. package/src/providers/ollama.ts +11 -2
  29. package/src/providers/openai-codex-responses.ts +7 -2
  30. package/src/providers/openai-completions.ts +23 -4
  31. package/src/providers/openai-responses.ts +17 -9
  32. package/src/providers/openai-shared.ts +25 -4
  33. package/src/providers/pi-native-server.ts +3 -0
  34. package/src/providers/transform-messages.ts +7 -3
  35. package/src/providers/vision-guard.ts +28 -2
  36. package/src/registry/oauth/github-copilot.ts +28 -4
  37. package/src/registry/oauth/muse-code.ts +2 -2
  38. package/src/registry/oauth/openai-codex.ts +7 -8
  39. package/src/stream.ts +1 -0
  40. package/src/types.ts +49 -1
  41. package/src/usage/charm-hyper.ts +95 -0
  42. package/src/usage/claude.ts +106 -12
  43. package/src/usage/kimi.ts +11 -1
  44. package/src/utils/block-symbols.ts +47 -0
  45. package/src/utils/empty-completion-retry.ts +7 -4
  46. package/src/utils/http-inspector.ts +1 -1
  47. package/src/utils/proxy.ts +4 -3
@@ -156,6 +156,13 @@ const TIMEOUT_PATTERN = /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream
156
156
  const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
157
157
  const TRANSIENT_ENVELOPE_TRUNCATION_PATTERN = /before message_(?:start|stop)/i;
158
158
  export const STREAM_READ_ERROR_PATTERN = /stream[_ -]?read[_ -]?error/i;
159
+ /** Python h2/httpx diagnostics forwarded through provider or proxy error events. */
160
+ export const PYTHON_HTTP2_STREAM_RESET_PATTERN = /<StreamReset stream_id:\d+, error_code:(?:2|7), remote_reset:True>/;
161
+ /** Python h11/httpx EOF while reading an HTTP/1.1 chunked response body. */
162
+ export const PYTHON_HTTP_INCOMPLETE_CHUNK_PATTERN =
163
+ /peer closed connection without sending complete message body \(incomplete chunked read\)/;
164
+ /** reqwest body-frame failures forwarded by the Codex HTTP proxy. */
165
+ export const CODEX_HTTP_BODY_READ_ERROR_PATTERN = /\btransport error reading codex response body\b/i;
159
166
  export const TRANSIENT_TRANSPORT_PATTERN =
160
167
  /\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|auth-gateway\s+5\d{2}(?=[:\s]|$)|\b(?:429|500|502|503|504)\b|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|nghttp2_(?:internal_error|refused_stream)|stream closed with error code nghttp2_(?:internal_error|refused_stream)|malformed.?function.?call/i;
161
168
  const AUTH_FAILURE_PATTERN =
@@ -396,6 +403,8 @@ function isTransientErrorText(text: string): boolean {
396
403
  return (
397
404
  isUnexpectedSocketCloseMessage(text) ||
398
405
  isStreamReadErrorText(text) ||
406
+ PYTHON_HTTP2_STREAM_RESET_PATTERN.test(text) ||
407
+ PYTHON_HTTP_INCOMPLETE_CHUNK_PATTERN.test(text) ||
399
408
  (TRANSIENT_ENVELOPE_PATTERN.test(text) && TRANSIENT_ENVELOPE_TRUNCATION_PATTERN.test(text)) ||
400
409
  TRANSIENT_TRANSPORT_PATTERN.test(text)
401
410
  );
@@ -432,6 +441,17 @@ function matchesOverflowText(text: string): boolean {
432
441
  return OVERFLOW_PATTERNS.some(p => p.test(text)) || OVERFLOW_NO_BODY_PATTERN.test(text);
433
442
  }
434
443
 
444
+ /**
445
+ * A 4xx the provider rejected as a deterministic client error — every 4xx
446
+ * except 408 (Request Timeout) and 429 (Too Many Requests), the retryable
447
+ * pair. Mirrors the 4xx policy in {@link isProviderRetryableError}: such a
448
+ * request replays identically, so a transient signal riding on it (e.g. a
449
+ * truncation phrase in the body) must not flip it to retryable.
450
+ */
451
+ function isTerminalClientErrorStatus(status: number | undefined): boolean {
452
+ return status !== undefined && status >= 400 && status < 500 && status !== 408 && status !== 429;
453
+ }
454
+
435
455
  function classifyText(
436
456
  errorMessage: string | undefined,
437
457
  errorStatus: number | undefined,
@@ -483,6 +503,24 @@ function classifyText(
483
503
  }
484
504
  if (isTimeoutText(errorMessage)) kinds |= Flag.Transient | Flag.Timeout;
485
505
  else if (isTransientErrorText(errorMessage)) kinds |= Flag.Transient;
506
+ // A stream truncation or forwarded Codex HTTP body-read failure may not
507
+ // match TRANSIENT_TRANSPORT_PATTERN. Flag it explicitly so AIError.retriable and
508
+ // the turn-recovery layer treat it as retryable, matching the provider
509
+ // retry path (isProviderRetryableError). Separate `if` (not chained onto
510
+ // the else-if) so a timeout whose text also reads as a truncation keeps
511
+ // Flag.Timeout alongside Flag.Transient. The string arm applies the strict
512
+ // STREAM_PARSE_DIAGNOSTIC_PATTERN, per the rationale on isTransientStreamParseError.
513
+ // Skip a truncation phrase that rides on a terminal 4xx (e.g. a malformed
514
+ // request rejected as "400 unexpected EOF"): that is a deterministic client
515
+ // error that replays identically, so keep it terminal. classify() carries
516
+ // the outer terminal status down the cause chain so a wrapped truncation
517
+ // (ProviderHttpError 400 → cause "unexpected EOF") is caught here too.
518
+ if (
519
+ !isTerminalClientErrorStatus(statusClean) &&
520
+ (isTransientStreamParseError(errorMessage) || CODEX_HTTP_BODY_READ_ERROR_PATTERN.test(errorMessage))
521
+ ) {
522
+ kinds |= Flag.Transient;
523
+ }
486
524
  // A concurrency cap (e.g. Vertex "Online prediction concurrent requests
487
525
  // quota exceeded") is transient — shed-and-backoff. The bare wording need
488
526
  // not match TRANSIENT_TRANSPORT_PATTERN, so flag it explicitly to keep
@@ -521,6 +559,11 @@ export function classify(error: unknown, api?: Api): number {
521
559
  const seen = new Set<object>();
522
560
  const causeTokenEvidence = hasCauseTokenContextOverflowEvidence(error);
523
561
  let link: unknown = error;
562
+ // A terminal 4xx on an outer link governs its own cause diagnostics: a
563
+ // wrapped truncation is describing why the deterministic request failed,
564
+ // not an independently retryable transport fault. Carry it down so the
565
+ // stream-parse guard in classifyText sees it on the status-less cause.
566
+ let governingTerminalStatus: number | undefined;
524
567
  while (link !== undefined && link !== null) {
525
568
  if (typeof link === "object") {
526
569
  if (seen.has(link)) break;
@@ -595,8 +638,10 @@ export function classify(error: unknown, api?: Api): number {
595
638
  linkMessage = (link as { message: string }).message;
596
639
  }
597
640
 
598
- const textId = classifyText(linkMessage, status(link), causeTokenEvidence, api);
641
+ const linkStatus = status(link);
642
+ const textId = classifyText(linkMessage, linkStatus ?? governingTerminalStatus, causeTokenEvidence, api);
599
643
  kinds |= textId & KIND_MASK;
644
+ if (isTerminalClientErrorStatus(linkStatus)) governingTerminalStatus = linkStatus;
600
645
 
601
646
  link = typeof link === "object" && "cause" in link ? (link as { cause: unknown }).cause : undefined;
602
647
  }
@@ -30,6 +30,13 @@ const INSUFFICIENT_BALANCE_PATTERN = /insufficient.?balance/i;
30
30
  // "credits exhausted". Account-local, so rotate to a sibling credential.
31
31
  const CREDITS_EXHAUSTED_PATTERN =
32
32
  /\b(?:exceed\w*|insufficient|not enough)\b[^\n]{0,40}\bcredits?\b|\bcredits?\b[^\n]{0,40}\b(?:exhausted|depleted)\b/i;
33
+ // Anthropic subscription entitlement wall: "Usage credits are required for this
34
+ // model" with `error_code: credits_required`. The account cannot serve the model
35
+ // at all, so rotate to a sibling rather than backing off on this one. Bounded to
36
+ // the documented sentence and the exact code: bare "usage credits" also appears
37
+ // in unrelated diagnostics ("Failed to fetch usage credits from billing
38
+ // service"), which must not rotate a healthy credential.
39
+ const ANTHROPIC_CREDITS_REQUIRED_PATTERN = /\busage credits are required\b|\bcredits_required\b/i;
33
40
  const SPEND_LIMIT_PATTERN = /spend.?limit/i;
34
41
  const SUBSCRIPTION_CAP_PATTERN =
35
42
  /\b(?:subscription|plan|membership)\b[^\n]{0,80}\b(?:rate.?limits?|quota|cap)\b|\b(?:rate.?limits?|quota|cap)\b[^\n]{0,80}\b(?:subscription|plan|membership)\b/i;
@@ -234,6 +241,10 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
234
241
  return "QUOTA_EXHAUSTED";
235
242
  }
236
243
 
244
+ if (ANTHROPIC_CREDITS_REQUIRED_PATTERN.test(errorMessage)) {
245
+ return "QUOTA_EXHAUSTED";
246
+ }
247
+
237
248
  if (
238
249
  lower.includes("per minute") ||
239
250
  lower.includes("rate limit") ||
@@ -253,6 +264,7 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
253
264
  lower.includes("out of credits") ||
254
265
  lower.includes("spending-limit") ||
255
266
  lower.includes("spending limit") ||
267
+ lower.includes("access_terminated_error") ||
256
268
  INSUFFICIENT_BALANCE_PATTERN.test(errorMessage) ||
257
269
  CREDITS_EXHAUSTED_PATTERN.test(errorMessage)
258
270
  ) {
@@ -294,7 +306,7 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
294
306
 
295
307
  /** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
296
308
  const USAGE_LIMIT_PATTERN =
297
- /usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)|balance.?exhausted|run out of credits|out of credits|spending[- _]?limit|personal-team-blocked|clinepass limit|free limit reached on model/i;
309
+ /usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)|balance.?exhausted|run out of credits|out of credits|spending[- _]?limit|personal-team-blocked|clinepass limit|free limit reached on model|access_terminated_error/i;
298
310
 
299
311
  /**
300
312
  * HTTP status codes that, absent richer body classification, represent an
@@ -397,6 +409,7 @@ export function matchesUsageLimitText(errorMessage: string): boolean {
397
409
  if (isDashScopeTokenLimitText(errorMessage)) return false;
398
410
  return (
399
411
  USAGE_LIMIT_PATTERN.test(errorMessage) ||
412
+ ANTHROPIC_CREDITS_REQUIRED_PATTERN.test(errorMessage) ||
400
413
  CREDITS_EXHAUSTED_PATTERN.test(errorMessage) ||
401
414
  (CN_QUOTA_EXHAUSTED_PATTERN.test(errorMessage) && !CN_TRANSIENT_CAP_PATTERN.test(errorMessage)) ||
402
415
  SPEND_LIMIT_PATTERN.test(errorMessage) ||
@@ -1,5 +1,6 @@
1
1
  import { isRetryableError, isUnexpectedSocketCloseMessage } from "@linxiraos/pi-utils";
2
2
  import {
3
+ CODEX_HTTP_BODY_READ_ERROR_PATTERN,
3
4
  isRetryableStreamEnvelopeError,
4
5
  isTransientStreamParseError,
5
6
  isUsageLimit,
@@ -51,6 +52,7 @@ export function isProviderRetryableError(error: unknown): boolean {
51
52
  isUnexpectedSocketCloseMessage(msg) ||
52
53
  isTransientTransportMessage(msg) ||
53
54
  TRANSIENT_TRANSPORT_PATTERN.test(msg) ||
55
+ CODEX_HTTP_BODY_READ_ERROR_PATTERN.test(msg) ||
54
56
  PROVIDER_TRANSIENT_EXTRA_PATTERN.test(msg) ||
55
57
  isTransientStreamParseError(error) ||
56
58
  isRetryableStreamEnvelopeError(error)
@@ -836,7 +836,7 @@ function handleMetadata(event: MetadataEvent, model: Model<"bedrock-converse-str
836
836
  output.usage.cacheRead = event.usage.cacheReadInputTokens || 0;
837
837
  output.usage.cacheWrite = event.usage.cacheWriteInputTokens || 0;
838
838
  output.usage.totalTokens = event.usage.totalTokens || output.usage.input + output.usage.output;
839
- calculateCost(model, output.usage);
839
+ calculateCost(model, output.usage, output.timestamp);
840
840
  }
841
841
  }
842
842
 
@@ -924,6 +924,34 @@ function buildSystemPrompt(
924
924
  return blocks;
925
925
  }
926
926
 
927
+ function buildToolResultBlock(
928
+ message: ToolResultMessage,
929
+ model: Model<"bedrock-converse-stream">,
930
+ hoistedImages: ImageBlockWire[],
931
+ ): ToolResultBlockWire {
932
+ const content: Array<TextBlockWire | ImageBlockWire> = [];
933
+ for (const block of message.content) {
934
+ if (block.type === "image") {
935
+ const image: ImageBlockWire = { image: createImageBlock(block.mimeType, block.data) };
936
+ if (model.requiresToolResultImageHoisting) {
937
+ content.push({ text: "(see attached image)" });
938
+ hoistedImages.push(image);
939
+ } else {
940
+ content.push(image);
941
+ }
942
+ } else {
943
+ content.push({ text: block.text.toWellFormed() });
944
+ }
945
+ }
946
+ return {
947
+ toolResult: {
948
+ toolUseId: normalizeToolCallId(message.toolCallId),
949
+ content,
950
+ status: message.isError ? "error" : "success",
951
+ },
952
+ };
953
+ }
954
+
927
955
  function convertMessages(
928
956
  context: Context,
929
957
  model: Model<"bedrock-converse-stream">,
@@ -1022,38 +1050,20 @@ function convertMessages(
1022
1050
  case "toolResult": {
1023
1051
  // Collect all consecutive toolResult messages into a single user message —
1024
1052
  // Bedrock requires all tool results to be in one message.
1025
- const toolResults: ToolResultBlockWire[] = [];
1026
- toolResults.push({
1027
- toolResult: {
1028
- toolUseId: normalizeToolCallId(m.toolCallId),
1029
- content: m.content.map(c =>
1030
- c.type === "image"
1031
- ? { image: createImageBlock(c.mimeType, c.data) }
1032
- : { text: c.text.toWellFormed() },
1033
- ),
1034
- status: m.isError ? "error" : "success",
1035
- },
1036
- });
1053
+ const contentBlocks: UserContent[] = [];
1054
+ const hoistedImages: ImageBlockWire[] = [];
1055
+ contentBlocks.push(buildToolResultBlock(m, model, hoistedImages));
1037
1056
 
1038
1057
  let j = i + 1;
1039
1058
  while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
1040
1059
  const nextMsg = transformedMessages[j] as ToolResultMessage;
1041
- toolResults.push({
1042
- toolResult: {
1043
- toolUseId: normalizeToolCallId(nextMsg.toolCallId),
1044
- content: nextMsg.content.map(c =>
1045
- c.type === "image"
1046
- ? { image: createImageBlock(c.mimeType, c.data) }
1047
- : { text: c.text.toWellFormed() },
1048
- ),
1049
- status: nextMsg.isError ? "error" : "success",
1050
- },
1051
- });
1060
+ contentBlocks.push(buildToolResultBlock(nextMsg, model, hoistedImages));
1052
1061
  j++;
1053
1062
  }
1054
1063
  i = j - 1;
1055
1064
 
1056
- result.push({ role: "user", content: toolResults });
1065
+ contentBlocks.push(...hoistedImages);
1066
+ result.push({ role: "user", content: contentBlocks });
1057
1067
  break;
1058
1068
  }
1059
1069
  default:
@@ -168,6 +168,22 @@ export type FallbackBlockParam = {
168
168
  to: { model: string };
169
169
  };
170
170
 
171
+ /** Beta enabling server-side compaction (`compact_20260112` edit, `compaction` blocks). */
172
+ export const COMPACTION_BETA = "compact-2026-01-12";
173
+
174
+ /**
175
+ * Server-side compaction summary (compact-2026-01-12). Returned at the start
176
+ * of the assistant response that crossed the trigger; on replay the API drops
177
+ * every block that precedes it, so it may open the messages array. The
178
+ * `encrypted_content` is opaque provider state, round-tripped verbatim.
179
+ */
180
+ export type CompactionBlockParam = {
181
+ type: "compaction";
182
+ content: string;
183
+ encrypted_content?: string | null;
184
+ cache_control?: CacheControlEphemeral | null;
185
+ };
186
+
171
187
  export type ContentBlockParam =
172
188
  | TextBlockParam
173
189
  | ImageBlockParam
@@ -180,7 +196,8 @@ export type ContentBlockParam =
180
196
  | ToolRemovalBlockParam
181
197
  | ThinkingBlockParam
182
198
  | RedactedThinkingBlockParam
183
- | FallbackBlockParam;
199
+ | FallbackBlockParam
200
+ | CompactionBlockParam;
184
201
 
185
202
  /**
186
203
  * A single conversation turn.
@@ -276,9 +293,19 @@ export type FallbackParam = {
276
293
  speed?: "fast";
277
294
  };
278
295
 
296
+ /** Server-side compaction edit (compact-2026-01-12). */
297
+ export type CompactionEdit = {
298
+ type: "compact_20260112";
299
+ /** `input_tokens` is the only trigger; `value` must be at least 50,000. */
300
+ trigger?: { type: "input_tokens"; value: number };
301
+ pause_after_compaction?: boolean;
302
+ /** Replaces the API's default summarization prompt entirely. */
303
+ instructions?: string;
304
+ };
305
+
279
306
  /** Claude Code context-management beta payload. */
280
307
  export type ContextManagement = {
281
- edits: Array<{ type: "clear_thinking_20251015"; keep: "all" }>;
308
+ edits: Array<{ type: "clear_thinking_20251015"; keep: "all" } | CompactionEdit>;
282
309
  };
283
310
 
284
311
  export type MessageCreateParams = {
@@ -322,7 +349,8 @@ export type StopReason =
322
349
  | "pause_turn"
323
350
  | "refusal"
324
351
  | "sensitive"
325
- | "model_context_window_exceeded";
352
+ | "model_context_window_exceeded"
353
+ | "compaction";
326
354
 
327
355
  export type CacheCreation = {
328
356
  ephemeral_5m_input_tokens?: number | null;
@@ -336,12 +364,15 @@ export type ServerToolUsage = {
336
364
 
337
365
  /**
338
366
  * Per-attempt token accounting inside a multi-run turn
339
- * (server-side-fallback-2026-06-01). Populated whenever a fallback chain
340
- * ran, including sticky-served turns with no `fallback` content block.
341
- * A `fallback_message` entry is the definitive "served by fallback" signal.
367
+ * (server-side-fallback-2026-06-01, compact-2026-01-12). Populated whenever
368
+ * a fallback chain ran, including sticky-served turns with no `fallback`
369
+ * content block, and whenever the compaction beta is active. A
370
+ * `fallback_message` entry is the definitive "served by fallback" signal; a
371
+ * `compaction` entry is the summarization sampling the top-level usage
372
+ * excludes.
342
373
  */
343
374
  export type UsageIteration = {
344
- type?: "message" | "fallback_message" | string;
375
+ type?: "message" | "fallback_message" | "compaction" | string;
345
376
  model?: string | null;
346
377
  input_tokens?: number | null;
347
378
  output_tokens?: number | null;
@@ -401,13 +432,15 @@ export type ResponseContentBlock =
401
432
  | ServerToolUseBlockParam
402
433
  | WebSearchToolResultBlockParam
403
434
  | ToolSearchToolResultBlockParam
404
- | { type: "fallback"; from: { model: string }; to: { model: string } };
435
+ | { type: "fallback"; from: { model: string }; to: { model: string } }
436
+ | { type: "compaction"; content?: string | null; encrypted_content?: string | null };
405
437
 
406
438
  export type ContentBlockDelta =
407
439
  | { type: "text_delta"; text: string }
408
440
  | { type: "input_json_delta"; partial_json: string }
409
441
  | { type: "thinking_delta"; thinking: string }
410
- | { type: "signature_delta"; signature: string };
442
+ | { type: "signature_delta"; signature: string }
443
+ | { type: "compaction_delta"; content?: string | null; encrypted_content?: string | null };
411
444
 
412
445
  export type StopDetails = {
413
446
  type: string;