@oh-my-pi/pi-ai 16.2.1 → 16.2.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.
Files changed (166) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/types/auth-gateway/server.d.ts +0 -19
  3. package/dist/types/auth-retry.d.ts +2 -10
  4. package/dist/types/auth-storage.d.ts +1 -2
  5. package/dist/types/dialect/demotion.d.ts +22 -0
  6. package/dist/types/dialect/index.d.ts +2 -0
  7. package/dist/types/error/abort.d.ts +14 -0
  8. package/dist/types/error/auth-classify.d.ts +16 -0
  9. package/dist/types/error/auth.d.ts +27 -0
  10. package/dist/types/error/aws.d.ts +23 -0
  11. package/dist/types/error/classes.d.ts +102 -0
  12. package/dist/types/error/finalize.d.ts +39 -0
  13. package/dist/types/error/flags.d.ts +70 -0
  14. package/dist/types/error/format.d.ts +20 -0
  15. package/dist/types/error/gateway.d.ts +20 -0
  16. package/dist/types/error/index.d.ts +13 -0
  17. package/dist/types/error/oauth.d.ts +43 -0
  18. package/dist/types/error/provider.d.ts +42 -0
  19. package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
  20. package/dist/types/error/retryable.d.ts +27 -0
  21. package/dist/types/error/validation.d.ts +32 -0
  22. package/dist/types/index.d.ts +1 -3
  23. package/dist/types/providers/amazon-bedrock.d.ts +0 -5
  24. package/dist/types/providers/anthropic-client.d.ts +2 -16
  25. package/dist/types/providers/anthropic.d.ts +6 -1
  26. package/dist/types/providers/aws-eventstream.d.ts +2 -1
  27. package/dist/types/providers/cursor.d.ts +8 -7
  28. package/dist/types/providers/google-gemini-cli.d.ts +0 -5
  29. package/dist/types/providers/google-shared.d.ts +0 -5
  30. package/dist/types/providers/ollama.d.ts +0 -5
  31. package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
  32. package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
  33. package/dist/types/providers/openai-codex-responses.d.ts +2 -2
  34. package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
  35. package/dist/types/providers/openai-responses-wire.d.ts +1 -1
  36. package/dist/types/providers/openai-responses.d.ts +1 -0
  37. package/dist/types/providers/openai-shared.d.ts +8 -6
  38. package/dist/types/providers/pi-native-client.d.ts +0 -9
  39. package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
  40. package/dist/types/types.d.ts +6 -0
  41. package/dist/types/utils/block-symbols.d.ts +20 -0
  42. package/dist/types/utils/openai-http.d.ts +4 -8
  43. package/dist/types/utils/retry.d.ts +2 -14
  44. package/dist/types/utils/thinking-loop.d.ts +3 -1
  45. package/package.json +8 -4
  46. package/src/api-registry.ts +3 -1
  47. package/src/auth-broker/discover.ts +7 -2
  48. package/src/auth-broker/remote-store.ts +8 -7
  49. package/src/auth-gateway/server.ts +27 -115
  50. package/src/auth-retry.ts +9 -21
  51. package/src/auth-storage.ts +34 -49
  52. package/src/dialect/demotion.ts +31 -0
  53. package/src/dialect/factory.ts +0 -2
  54. package/src/dialect/index.ts +2 -0
  55. package/src/dialect/owned-stream.ts +0 -1
  56. package/src/dialect/thinking.ts +22 -10
  57. package/src/error/abort.ts +18 -0
  58. package/src/error/auth-classify.ts +30 -0
  59. package/src/error/auth.ts +48 -0
  60. package/src/error/aws.ts +31 -0
  61. package/src/error/classes.ts +186 -0
  62. package/src/error/finalize.ts +69 -0
  63. package/src/error/flags.ts +486 -0
  64. package/src/error/format.ts +36 -0
  65. package/src/error/gateway.ts +96 -0
  66. package/src/error/index.ts +13 -0
  67. package/src/error/oauth.ts +58 -0
  68. package/src/error/provider.ts +56 -0
  69. package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
  70. package/src/error/retryable.ts +70 -0
  71. package/src/error/validation.ts +44 -0
  72. package/src/index.ts +1 -3
  73. package/src/providers/amazon-bedrock.ts +61 -57
  74. package/src/providers/anthropic-client.ts +13 -41
  75. package/src/providers/anthropic-messages-server.ts +9 -2
  76. package/src/providers/anthropic.ts +84 -147
  77. package/src/providers/aws-credentials.ts +41 -11
  78. package/src/providers/aws-eventstream.ts +12 -21
  79. package/src/providers/azure-openai-responses.ts +27 -17
  80. package/src/providers/cursor.ts +59 -53
  81. package/src/providers/devin.ts +28 -20
  82. package/src/providers/gitlab-duo-workflow.ts +30 -10
  83. package/src/providers/gitlab-duo.ts +22 -6
  84. package/src/providers/google-auth.ts +15 -5
  85. package/src/providers/google-gemini-cli.ts +46 -60
  86. package/src/providers/google-shared.ts +40 -38
  87. package/src/providers/google-vertex.ts +3 -2
  88. package/src/providers/google.ts +5 -1
  89. package/src/providers/mock.ts +10 -7
  90. package/src/providers/ollama.ts +34 -29
  91. package/src/providers/openai-chat-server.ts +2 -1
  92. package/src/providers/openai-codex/request-transformer.ts +13 -12
  93. package/src/providers/openai-codex/response-handler.ts +1 -1
  94. package/src/providers/openai-codex-responses.ts +931 -1012
  95. package/src/providers/openai-completions.ts +46 -36
  96. package/src/providers/openai-responses-server-schema.ts +3 -0
  97. package/src/providers/openai-responses-server.ts +82 -30
  98. package/src/providers/openai-responses-wire.ts +1 -1
  99. package/src/providers/openai-responses.ts +46 -22
  100. package/src/providers/openai-shared.ts +118 -51
  101. package/src/providers/pi-native-client.ts +12 -18
  102. package/src/providers/pi-native-server.ts +9 -6
  103. package/src/providers/register-builtins.ts +5 -2
  104. package/src/providers/transform-messages.ts +31 -63
  105. package/src/registry/alibaba-coding-plan.ts +6 -5
  106. package/src/registry/api-key-login.ts +4 -3
  107. package/src/registry/api-key-validation.ts +4 -3
  108. package/src/registry/cloudflare-ai-gateway.ts +4 -3
  109. package/src/registry/coreweave.ts +2 -1
  110. package/src/registry/deepseek.ts +2 -1
  111. package/src/registry/google-antigravity.ts +2 -1
  112. package/src/registry/google-gemini-cli.ts +2 -1
  113. package/src/registry/kagi.ts +4 -3
  114. package/src/registry/kilo.ts +32 -10
  115. package/src/registry/litellm.ts +4 -3
  116. package/src/registry/llama-cpp.ts +3 -2
  117. package/src/registry/lm-studio.ts +3 -2
  118. package/src/registry/nvidia.ts +7 -7
  119. package/src/registry/oauth/anthropic.ts +23 -6
  120. package/src/registry/oauth/callback-server.ts +5 -3
  121. package/src/registry/oauth/cursor.ts +18 -4
  122. package/src/registry/oauth/devin.ts +14 -3
  123. package/src/registry/oauth/github-copilot.ts +21 -10
  124. package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
  125. package/src/registry/oauth/gitlab-duo.ts +30 -6
  126. package/src/registry/oauth/google-antigravity.ts +14 -5
  127. package/src/registry/oauth/google-gemini-cli.ts +24 -7
  128. package/src/registry/oauth/google-oauth-shared.ts +6 -3
  129. package/src/registry/oauth/index.ts +19 -8
  130. package/src/registry/oauth/kimi.ts +40 -10
  131. package/src/registry/oauth/openai-codex.ts +26 -11
  132. package/src/registry/oauth/opencode.ts +4 -3
  133. package/src/registry/oauth/perplexity.ts +33 -11
  134. package/src/registry/oauth/xai-oauth.ts +62 -18
  135. package/src/registry/oauth/xiaomi.ts +21 -9
  136. package/src/registry/ollama-cloud.ts +5 -4
  137. package/src/registry/ollama.ts +3 -2
  138. package/src/registry/parallel.ts +4 -3
  139. package/src/registry/qwen-portal.ts +4 -3
  140. package/src/registry/tavily.ts +4 -3
  141. package/src/registry/vercel-ai-gateway.ts +4 -3
  142. package/src/registry/vllm.ts +3 -2
  143. package/src/stream.ts +79 -24
  144. package/src/types.ts +6 -0
  145. package/src/usage/claude.ts +2 -1
  146. package/src/usage/github-copilot.ts +4 -3
  147. package/src/usage/google-antigravity.ts +2 -1
  148. package/src/utils/abort.ts +4 -2
  149. package/src/utils/block-symbols.ts +32 -0
  150. package/src/utils/event-stream.ts +15 -3
  151. package/src/utils/http-inspector.ts +4 -4
  152. package/src/utils/idle-iterator.ts +6 -5
  153. package/src/utils/openai-http.ts +11 -46
  154. package/src/utils/parse-bind.ts +7 -5
  155. package/src/utils/proxy.ts +2 -1
  156. package/src/utils/retry.ts +7 -23
  157. package/src/utils/schema/normalize.ts +3 -2
  158. package/src/utils/thinking-loop.ts +15 -11
  159. package/src/utils/validation.ts +4 -3
  160. package/dist/types/dialect/pi.d.ts +0 -9
  161. package/dist/types/errors.d.ts +0 -24
  162. package/dist/types/utils/overflow.d.ts +0 -55
  163. package/src/dialect/pi.md +0 -55
  164. package/src/dialect/pi.ts +0 -600
  165. package/src/errors.ts +0 -32
  166. package/src/utils/overflow.ts +0 -140
@@ -3,7 +3,9 @@ import { isKimiModelId } from "@oh-my-pi/pi-catalog/identity";
3
3
  import { resolveWireModelId } from "@oh-my-pi/pi-catalog/model-thinking";
4
4
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
5
5
  import type { ResolvedOpenAICompat } from "@oh-my-pi/pi-catalog/types";
6
- import { $env, extractHttpStatusFromError, parseStreamingJson, parseStreamingJsonThrottled } from "@oh-my-pi/pi-utils";
6
+ import { $env, parseStreamingJson, parseStreamingJsonThrottled } from "@oh-my-pi/pi-utils";
7
+ import { renderDemotedThinking } from "../dialect/demotion";
8
+ import * as AIError from "../error";
7
9
  import { getKimiCommonHeaders } from "../registry/oauth/kimi";
8
10
  import { getEnvApiKey } from "../stream";
9
11
  import type {
@@ -27,9 +29,10 @@ import type {
27
29
  } from "../types";
28
30
  import { normalizeSystemPrompts } from "../utils";
29
31
  import { createAbortSourceTracker } from "../utils/abort";
32
+ import { kStreamingLastParseLen } from "../utils/block-symbols";
30
33
  import { hasVisibleAssistantContent, withEmptyCompletionRetry } from "../utils/empty-completion-retry";
31
34
  import { AssistantMessageEventStream } from "../utils/event-stream";
32
- import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
35
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
33
36
  import {
34
37
  getOpenAIStreamFirstEventTimeoutMs,
35
38
  getOpenAIStreamIdleTimeoutMs,
@@ -126,10 +129,6 @@ type OpenAICompletionsToolMessageParam = ChatCompletionToolMessageParam & {
126
129
  name?: string;
127
130
  };
128
131
 
129
- type OpenAICompletionsContentBlockWithIndex = AssistantMessage["content"][number] & {
130
- index?: unknown;
131
- };
132
-
133
132
  type OpenAICompletionsUsageLike = {
134
133
  completion_tokens?: unknown;
135
134
  prompt_tokens?: unknown;
@@ -559,14 +558,16 @@ const streamOpenAICompletionsOnce = (
559
558
  const stream = new AssistantMessageEventStream();
560
559
 
561
560
  (async () => {
562
- const startTime = Date.now();
561
+ const startTime = performance.now();
563
562
  let firstTokenTime: number | undefined;
564
563
  const policy = resolveOpenAICompatForRequest(model, options);
565
564
 
566
565
  const output: AssistantMessage = createInitialResponsesAssistantMessage(model.api, model.provider, model.id);
567
566
  let rawRequestDump: RawHttpRequestDump | undefined;
568
567
  const abortTracker = createAbortSourceTracker(options?.signal);
569
- const firstEventTimeoutAbortError = new Error(OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE);
568
+ const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(
569
+ OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE,
570
+ );
570
571
  const { requestAbortController, requestSignal } = abortTracker;
571
572
  const onSseEvent = options?.onSseEvent;
572
573
  const rawSseObserver = onSseEvent
@@ -746,7 +747,7 @@ const streamOpenAICompletionsOnce = (
746
747
  type ToolCallStreamBlock = ToolCall & {
747
748
  partialArgs?: string | Record<string, unknown>;
748
749
  streamIndex?: number;
749
- lastParseLen?: number;
750
+ [kStreamingLastParseLen]?: number;
750
751
  };
751
752
  type OpenAIStreamBlock = TextContent | ThinkingContent | ToolCallStreamBlock;
752
753
  const pendingToolCallBlocks: ToolCallStreamBlock[] = [];
@@ -777,7 +778,6 @@ const streamOpenAICompletionsOnce = (
777
778
  block.arguments =
778
779
  typeof block.partialArgs === "string" ? parseStreamingJson(block.partialArgs) : block.partialArgs;
779
780
  delete block.partialArgs;
780
- delete block.lastParseLen;
781
781
  if (block.streamIndex !== undefined) {
782
782
  toolCallBlockByIndex.delete(block.streamIndex);
783
783
  delete block.streamIndex;
@@ -867,7 +867,7 @@ const streamOpenAICompletionsOnce = (
867
867
 
868
868
  const appendTextDelta = (text: string): void => {
869
869
  if (!text) return;
870
- if (!firstTokenTime) firstTokenTime = Date.now();
870
+ if (!firstTokenTime) firstTokenTime = performance.now();
871
871
  appendText(output, stream, text);
872
872
  };
873
873
  // Tracks the last full cumulative reasoning snapshot per signature (the
@@ -894,7 +894,7 @@ const streamOpenAICompletionsOnce = (
894
894
  lastCumulativeReasoningBySignature.set(key, thinking);
895
895
  if (!emittedThinking) return;
896
896
  }
897
- if (!firstTokenTime) firstTokenTime = Date.now();
897
+ if (!firstTokenTime) firstTokenTime = performance.now();
898
898
  appendThinking(output, stream, emittedThinking, signature);
899
899
  };
900
900
 
@@ -1068,7 +1068,7 @@ const streamOpenAICompletionsOnce = (
1068
1068
 
1069
1069
  const normalizedDeltaText = normalizeStreamingContentText(choice.delta.content);
1070
1070
  if (normalizedDeltaText.length > 0) {
1071
- if (!firstTokenTime) firstTokenTime = Date.now();
1071
+ if (!firstTokenTime) firstTokenTime = performance.now();
1072
1072
  const hasStructuredToolCalls =
1073
1073
  Array.isArray(choice.delta.tool_calls) && choice.delta.tool_calls.length > 0;
1074
1074
 
@@ -1145,10 +1145,13 @@ const streamOpenAICompletionsOnce = (
1145
1145
  delta = rawArgs;
1146
1146
  const prev = typeof block.partialArgs === "string" ? block.partialArgs : "";
1147
1147
  block.partialArgs = prev + rawArgs;
1148
- const throttled = parseStreamingJsonThrottled(block.partialArgs, block.lastParseLen ?? 0);
1148
+ const throttled = parseStreamingJsonThrottled(
1149
+ block.partialArgs,
1150
+ block[kStreamingLastParseLen] ?? 0,
1151
+ );
1149
1152
  if (throttled) {
1150
1153
  block.arguments = throttled.value;
1151
- block.lastParseLen = throttled.parsedLen;
1154
+ block[kStreamingLastParseLen] = throttled.parsedLen;
1152
1155
  }
1153
1156
  }
1154
1157
  } else if (rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs)) {
@@ -1253,23 +1256,26 @@ const streamOpenAICompletionsOnce = (
1253
1256
  output.stopReason = "error";
1254
1257
  output.errorMessage = EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE;
1255
1258
  }
1256
- const firstEventTimeoutError = abortTracker.getLocalAbortReason();
1257
- if (firstEventTimeoutError) {
1258
- throw firstEventTimeoutError;
1259
+ const localAbortReason = abortTracker.getLocalAbortReason();
1260
+ if (localAbortReason) {
1261
+ throw localAbortReason;
1259
1262
  }
1260
1263
  if (abortTracker.wasCallerAbort()) {
1261
- throw new Error("Request was aborted");
1264
+ throw new AIError.AbortError();
1262
1265
  }
1263
1266
 
1264
1267
  if (output.stopReason === "aborted") {
1265
- throw new Error("Request was aborted");
1268
+ throw new AIError.AbortError();
1266
1269
  }
1267
1270
  if (output.stopReason === "error") {
1268
- throw new Error(output.errorMessage || "Provider returned an error stop reason");
1271
+ throw new AIError.ProviderResponseError(output.errorMessage || "Provider returned an error stop reason", {
1272
+ provider: model.provider,
1273
+ kind: "runtime",
1274
+ });
1269
1275
  }
1270
1276
 
1271
1277
  output.errorMessage = undefined;
1272
- output.duration = Date.now() - startTime;
1278
+ output.duration = performance.now() - startTime;
1273
1279
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1274
1280
  stream.push({ type: "done", reason: output.stopReason, message: output });
1275
1281
  stream.end();
@@ -1280,19 +1286,22 @@ const streamOpenAICompletionsOnce = (
1280
1286
  try {
1281
1287
  finishOpenBlocksOnError();
1282
1288
  } catch {}
1283
- for (const block of output.content) delete (block as OpenAICompletionsContentBlockWithIndex).index;
1284
- const firstEventTimeoutError = abortTracker.getLocalAbortReason();
1285
- output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
1286
1289
  const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
1287
- output.errorStatus = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
1288
- output.errorMessage =
1289
- firstEventTimeoutError?.message ??
1290
- (await finalizeErrorMessage(error, rawRequestDump, capturedErrorResponse));
1290
+ const result = await AIError.finalize(error, {
1291
+ api: model.api,
1292
+ provider: model.provider,
1293
+ abortTracker,
1294
+ rawRequestDump,
1295
+ capturedErrorResponse,
1296
+ });
1297
+ output.stopReason = result.stopReason;
1298
+ output.errorStatus = result.status;
1299
+ output.errorId = result.id;
1300
+ output.errorMessage = result.message;
1291
1301
  // Some providers via OpenRouter include extra details here.
1292
1302
  const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
1293
1303
  if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
1294
- output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
1295
- output.duration = Date.now() - startTime;
1304
+ output.duration = performance.now() - startTime;
1296
1305
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1297
1306
  stream.push({ type: "error", reason: output.stopReason, error: output });
1298
1307
  stream.end();
@@ -1334,7 +1343,7 @@ function createRequestSetup(
1334
1343
  azureChatCompletions: { apiVersion, deploymentName },
1335
1344
  });
1336
1345
  if (!setup.baseUrl) {
1337
- throw new Error("OpenAI request setup did not resolve a base URL");
1346
+ throw new AIError.ConfigurationError("OpenAI request setup did not resolve a base URL");
1338
1347
  }
1339
1348
  return setup as OpenAIRequestSetup & { baseUrl: string };
1340
1349
  }
@@ -1775,13 +1784,14 @@ export function convertMessages(
1775
1784
  const nonEmptyThinkingBlocks = thinkingBlocks.filter(b => b.thinking && b.thinking.trim().length > 0);
1776
1785
  if (nonEmptyThinkingBlocks.length > 0) {
1777
1786
  if (compat.requiresThinkingAsText) {
1778
- // Convert thinking blocks to plain text (no tags to avoid model mimicking them)
1779
- const thinkingText = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n\n");
1787
+ const thinkingText = nonEmptyThinkingBlocks
1788
+ .map(b => renderDemotedThinking(model.id, b.thinking))
1789
+ .join("");
1780
1790
  // `content` is a plain string at this point (set above) or null —
1781
- // never an array. Prepend the thinking text to the string form.
1791
+ // never an array. Prepend the demoted thinking to the string form.
1782
1792
  assistantMsg.content =
1783
1793
  typeof assistantMsg.content === "string" && assistantMsg.content.length > 0
1784
- ? `${thinkingText}\n\n${assistantMsg.content}`
1794
+ ? `${thinkingText}${assistantMsg.content}`
1785
1795
  : thinkingText;
1786
1796
  } else if (compat.requiresReasoningContentForToolCalls) {
1787
1797
  // Use the streamed signature when the backend accepts whichever
@@ -92,8 +92,11 @@ const systemMessageItemSchema = type({
92
92
 
93
93
  const assistantMessageItemSchema = type({
94
94
  "type?": "'message'",
95
+ "id?": "string",
95
96
  role: "'assistant'",
96
97
  "content?": type("string").or(outputContentBlockSchema.array()),
98
+ "status?": "'in_progress' | 'completed' | 'incomplete'",
99
+ "phase?": "'commentary' | 'final_answer' | null",
97
100
  });
98
101
 
99
102
  const reasoningItemSchema = type({
@@ -13,6 +13,7 @@ import { logger } from "@oh-my-pi/pi-utils";
13
13
  import { type } from "arktype";
14
14
  import { resolvePromptCacheKey } from "../auth-gateway/http";
15
15
  import type { AuthGatewayStreamControl, AuthGatewayParsedRequest as ParsedRequest } from "../auth-gateway/types";
16
+ import * as AIError from "../error";
16
17
  import type {
17
18
  AssistantMessage,
18
19
  AssistantMessageEventStream,
@@ -32,6 +33,7 @@ import {
32
33
  type OpenAIResponsesTool,
33
34
  openaiResponsesRequestSchema,
34
35
  } from "./openai-responses-server-schema";
36
+ import { encodeTextSignatureV1, parseTextSignature } from "./openai-shared";
35
37
 
36
38
  export type { ParsedRequest };
37
39
 
@@ -53,6 +55,20 @@ function asString(v: unknown): string | undefined {
53
55
  return typeof v === "string" ? v : undefined;
54
56
  }
55
57
 
58
+ type AssistantItemPhase = "commentary" | "final_answer";
59
+ type MessageSignature = { id: string; phase?: AssistantItemPhase };
60
+
61
+ function parseAssistantItemPhase(value: unknown): AssistantItemPhase | undefined {
62
+ return value === "commentary" || value === "final_answer" ? value : undefined;
63
+ }
64
+
65
+ function messageTextSignature(id: unknown, phase: unknown): string | undefined {
66
+ const parsedPhase = parseAssistantItemPhase(phase);
67
+ if (typeof id === "string" && id.length > 0) return encodeTextSignatureV1(id, parsedPhase);
68
+ if (!parsedPhase) return undefined;
69
+ return encodeTextSignatureV1(makeMsgId(), parsedPhase);
70
+ }
71
+
56
72
  // ─── id helpers ─────────────────────────────────────────────────────────────
57
73
 
58
74
  function uuidNoDashes(): string {
@@ -146,20 +162,27 @@ type OutputBlockUnion =
146
162
  | { type: "text"; text: string }
147
163
  | { type: "refusal"; refusal: string };
148
164
 
149
- function outputTextOf(blocks: OpenAIResponsesOutputContent[] | string | undefined): TextContent[] {
150
- if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : [];
165
+ function outputTextOf(
166
+ blocks: OpenAIResponsesOutputContent[] | string | undefined,
167
+ message?: { id?: unknown; phase?: unknown },
168
+ ): TextContent[] {
169
+ const textSignature = messageTextSignature(message?.id, message?.phase);
170
+ const textContent = (text: string): TextContent =>
171
+ textSignature ? { type: "text", text, textSignature } : { type: "text", text };
172
+ if (typeof blocks === "string") return blocks.length > 0 ? [textContent(blocks)] : [];
151
173
  if (!blocks) return [];
152
- const out: TextContent[] = [];
174
+ const parts: string[] = [];
153
175
  for (const raw of blocks) {
154
176
  const block = raw as OutputBlockUnion;
155
177
  if (block.type === "output_text" || block.type === "text") {
156
- out.push({ type: "text", text: block.text });
178
+ parts.push(block.text);
157
179
  } else if (block.type === "refusal") {
158
180
  // Preserve the refusal reason so history replay still carries it.
159
- out.push({ type: "text", text: `[refusal: ${block.refusal}]` });
181
+ parts.push(`[refusal: ${block.refusal}]`);
160
182
  }
161
183
  }
162
- return out;
184
+ const text = parts.join("");
185
+ return text.length > 0 ? [textContent(text)] : [];
163
186
  }
164
187
 
165
188
  // The schema accepts a much wider tool_choice union than the SDK type so the
@@ -266,7 +289,7 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
266
289
 
267
290
  const data = openaiResponsesRequestSchema(body);
268
291
  if (data instanceof type.errors) {
269
- throw new Error(`openai-responses: ${data.summary}`);
292
+ throw new AIError.ValidationError(`openai-responses: ${data.summary}`);
270
293
  }
271
294
 
272
295
  const now = Date.now();
@@ -287,6 +310,8 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
287
310
  const msg = item as {
288
311
  role?: string;
289
312
  content?: OpenAIResponsesInputContent[] | OpenAIResponsesOutputContent[] | string;
313
+ id?: unknown;
314
+ phase?: unknown;
290
315
  };
291
316
  switch (msg.role) {
292
317
  case "system": {
@@ -302,7 +327,10 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
302
327
  break;
303
328
  }
304
329
  case "assistant": {
305
- const parts = outputTextOf(msg.content as OpenAIResponsesOutputContent[] | string | undefined);
330
+ const parts = outputTextOf(msg.content as OpenAIResponsesOutputContent[] | string | undefined, {
331
+ id: msg.id,
332
+ phase: msg.phase,
333
+ });
306
334
  messages.push({
307
335
  role: "assistant",
308
336
  content: parts,
@@ -345,7 +373,9 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
345
373
  const parsedArgs: unknown = JSON.parse(argsRaw);
346
374
  args = isObj(parsedArgs) ? parsedArgs : {};
347
375
  } catch {
348
- throw new Error(`openai-responses: function_call ${call.call_id} has invalid JSON arguments`);
376
+ throw new AIError.ValidationError(
377
+ `openai-responses: function_call ${call.call_id} has invalid JSON arguments`,
378
+ );
349
379
  }
350
380
  const toolCall: ToolCall = {
351
381
  type: "toolCall",
@@ -500,6 +530,7 @@ type MessageOutputItem = {
500
530
  role: "assistant";
501
531
  status: "completed";
502
532
  content: Array<{ type: "output_text"; text: string; annotations: never[] }>;
533
+ phase?: AssistantItemPhase;
503
534
  };
504
535
 
505
536
  type FunctionCallOutputItem = {
@@ -591,23 +622,32 @@ function wireCallId(id: string): string {
591
622
  function buildOutputItems(message: AssistantMessage): OutputItem[] {
592
623
  const out: OutputItem[] = [];
593
624
  let pendingMessage: MessageOutputItem | null = null;
625
+ let pendingMessageSignature: { id: string; phase?: AssistantItemPhase } | undefined;
594
626
  const flushMessage = () => {
595
627
  if (pendingMessage) {
596
628
  out.push(pendingMessage);
597
629
  pendingMessage = null;
630
+ pendingMessageSignature = undefined;
598
631
  }
599
632
  };
600
633
 
601
634
  for (const part of message.content) {
602
635
  if (part.type === "text") {
636
+ const signature = parseTextSignature(part.textSignature);
637
+ const sameSignature =
638
+ !pendingMessage ||
639
+ (pendingMessageSignature?.id === signature?.id && pendingMessageSignature?.phase === signature?.phase);
640
+ if (!sameSignature) flushMessage();
603
641
  if (!pendingMessage) {
604
642
  pendingMessage = {
605
643
  type: "message",
606
- id: makeMsgId(),
644
+ id: signature?.id ?? makeMsgId(),
607
645
  role: "assistant",
608
646
  status: "completed",
609
647
  content: [],
648
+ ...(signature?.phase ? { phase: signature.phase } : {}),
610
649
  };
650
+ pendingMessageSignature = signature;
611
651
  }
612
652
  pendingMessage.content.push({ type: "output_text", text: part.text, annotations: [] });
613
653
  } else if (part.type === "thinking") {
@@ -616,7 +656,8 @@ function buildOutputItems(message: AssistantMessage): OutputItem[] {
616
656
  } else if (part.type === "toolCall") {
617
657
  flushMessage();
618
658
  if (part.customWireName) {
619
- const rawInput = typeof part.arguments?.input === "string" ? (part.arguments.input as string) : "";
659
+ const input = part.arguments?.input;
660
+ const rawInput = typeof input === "string" ? input : "";
620
661
  out.push({
621
662
  type: "custom_tool_call",
622
663
  id: part.thoughtSignature ?? makeCustomCallId(),
@@ -698,6 +739,7 @@ interface OpenMessage {
698
739
  contentIndex: number;
699
740
  currentPartText: string;
700
741
  content: Array<{ type: "output_text"; text: string; annotations: never[] }>;
742
+ signature?: MessageSignature;
701
743
  }
702
744
  interface OpenReasoning {
703
745
  kind: "reasoning";
@@ -765,15 +807,16 @@ export function encodeStream(
765
807
  usage: null,
766
808
  });
767
809
 
768
- const openMessage = (): OpenMessage => {
810
+ const openMessage = (signature?: MessageSignature): OpenMessage => {
769
811
  const itemOutputIndex = allocateOutputIndex();
770
- const itemId = makeMsgId();
812
+ const itemId = signature?.id ?? makeMsgId();
771
813
  const item = {
772
814
  type: "message" as const,
773
815
  id: itemId,
774
- status: "in_progress",
816
+ status: "in_progress" as const,
775
817
  role: "assistant" as const,
776
818
  content: [] as Array<{ type: "output_text"; text: string; annotations: never[] }>,
819
+ ...(signature?.phase ? { phase: signature.phase } : {}),
777
820
  };
778
821
  emit("response.output_item.added", { output_index: itemOutputIndex, item });
779
822
  const next: OpenMessage = {
@@ -783,6 +826,7 @@ export function encodeStream(
783
826
  contentIndex: 0,
784
827
  currentPartText: "",
785
828
  content: [],
829
+ ...(signature ? { signature } : {}),
786
830
  };
787
831
  state.open = next;
788
832
  return next;
@@ -904,20 +948,15 @@ export function encodeStream(
904
948
  if (!state.open) return;
905
949
  if (state.open.kind === "message") {
906
950
  const item = {
907
- type: "message",
951
+ type: "message" as const,
908
952
  id: state.open.itemId,
909
- status: "completed",
910
- role: "assistant",
953
+ status: "completed" as const,
954
+ role: "assistant" as const,
911
955
  content: state.open.content,
956
+ ...(state.open.signature?.phase ? { phase: state.open.signature.phase } : {}),
912
957
  };
913
958
  emit("response.output_item.done", { output_index: state.open.outputIndex, item });
914
- finishedItems.push({
915
- type: "message",
916
- id: state.open.itemId,
917
- role: "assistant",
918
- status: "completed",
919
- content: state.open.content,
920
- });
959
+ finishedItems.push(item);
921
960
  state.open = null;
922
961
  } else if (state.open.kind === "reasoning") {
923
962
  const summary = [{ type: "summary_text" as const, text: state.open.reasoningText ?? "" }];
@@ -970,20 +1009,33 @@ export function encodeStream(
970
1009
  }
971
1010
  case "text_start": {
972
1011
  let cur: OpenMessage;
1012
+ const textBlock = ev.partial.content[ev.contentIndex];
1013
+ const signature =
1014
+ textBlock?.type === "text" ? parseTextSignature(textBlock.textSignature) : undefined;
973
1015
  if (state.open && state.open.kind === "message") {
974
- // continue same message item, new content part
975
- cur = state.open;
976
- cur.currentPartText = "";
1016
+ const sameSignature =
1017
+ (!signature && !state.open.signature) ||
1018
+ (signature !== undefined &&
1019
+ state.open.signature?.id === signature.id &&
1020
+ state.open.signature.phase === signature.phase);
1021
+ if (sameSignature) {
1022
+ // Continue same message item, new content part.
1023
+ cur = state.open;
1024
+ cur.currentPartText = "";
1025
+ } else {
1026
+ closeOpen();
1027
+ cur = openMessage(signature);
1028
+ }
977
1029
  } else {
978
1030
  if (state.open && state.open.kind !== "function_call") closeOpen();
979
- cur = openMessage();
1031
+ cur = openMessage(signature);
980
1032
  }
981
- const part = { type: "output_text", text: "", annotations: [] as never[] };
1033
+ const contentPart = { type: "output_text", text: "", annotations: [] as never[] };
982
1034
  emit("response.content_part.added", {
983
1035
  item_id: cur.itemId,
984
1036
  output_index: cur.outputIndex,
985
1037
  content_index: cur.contentIndex,
986
- part,
1038
+ part: contentPart,
987
1039
  });
988
1040
  break;
989
1041
  }
@@ -5849,7 +5849,7 @@ export interface ResponseCreateParamsBase {
5849
5849
  * - [Conversation state](https://platform.openai.com/docs/guides/conversation-state)
5850
5850
  * - [Function calling](https://platform.openai.com/docs/guides/function-calling)
5851
5851
  */
5852
- input?: string | ResponseInput;
5852
+ input?: ResponseInput;
5853
5853
  /**
5854
5854
  * A system (or developer) message inserted into the model's context.
5855
5855
  *
@@ -1,5 +1,6 @@
1
1
  import { hostMatchesUrl } from "@oh-my-pi/pi-catalog/hosts";
2
- import { $flag, extractHttpStatusFromError, logger, structuredCloneJSON } from "@oh-my-pi/pi-utils";
2
+ import { $flag, logger, structuredCloneJSON } from "@oh-my-pi/pi-utils";
3
+ import * as AIError from "../error";
3
4
  import { getEnvApiKey } from "../stream";
4
5
  import type {
5
6
  AssistantMessage,
@@ -24,7 +25,7 @@ import {
24
25
  import { createAbortSourceTracker } from "../utils/abort";
25
26
  import { withEmptyCompletionRetry } from "../utils/empty-completion-retry";
26
27
  import { AssistantMessageEventStream } from "../utils/event-stream";
27
- import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
28
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
28
29
  import {
29
30
  getOpenAIStreamFirstEventTimeoutMs,
30
31
  getOpenAIStreamIdleTimeoutMs,
@@ -97,6 +98,7 @@ export interface OpenAIResponsesOptions extends StreamOptions {
97
98
  reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
98
99
  reasoningSummary?: "auto" | "detailed" | "concise" | null;
99
100
  serviceTier?: ServiceTier;
101
+ textVerbosity?: "low" | "medium" | "high";
100
102
  toolChoice?: ToolChoice;
101
103
  openrouterVariant?: string;
102
104
  maxTokensExplicit?: boolean;
@@ -269,7 +271,7 @@ function buildOpenAIResponsesChainedParams(
269
271
  ? { ...params, input: params.input.slice(0, params.input.length - trailingScaffoldingItems) }
270
272
  : params;
271
273
  const deltaInput = chain.canAppend
272
- ? buildResponsesDeltaInput<ResponseInput[number]>(chain.lastParams, chain.lastResponseItems, historyParams)
274
+ ? buildResponsesDeltaInput(chain.lastParams, chain.lastResponseItems, historyParams)
273
275
  : null;
274
276
  if (deltaInput && deltaInput.length > 0 && chain.lastResponseId) {
275
277
  const scaffolding =
@@ -362,7 +364,7 @@ const streamOpenAIResponsesOnce = (
362
364
 
363
365
  // Start async processing
364
366
  (async () => {
365
- const startTime = Date.now();
367
+ const startTime = performance.now();
366
368
  let firstTokenTime: number | undefined;
367
369
 
368
370
  const output: AssistantMessage = createInitialResponsesAssistantMessage(model.api, model.provider, model.id);
@@ -370,7 +372,7 @@ const streamOpenAIResponsesOnce = (
370
372
  let chainState: OpenAIResponsesChainState | undefined;
371
373
  let sentPreviousResponseId: string | undefined;
372
374
  const abortTracker = createAbortSourceTracker(options?.signal);
373
- const firstEventTimeoutAbortError = new Error(OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE);
375
+ const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE);
374
376
  const { requestAbortController, requestSignal } = abortTracker;
375
377
  const onSseEvent = options?.onSseEvent;
376
378
  const rawSseObserver = onSseEvent
@@ -665,7 +667,7 @@ const streamOpenAIResponsesOnce = (
665
667
  });
666
668
  await processResponsesStream(timedOpenaiStream, output, stream, model, {
667
669
  onFirstToken: () => {
668
- if (!firstTokenTime) firstTokenTime = Date.now();
670
+ if (!firstTokenTime) firstTokenTime = performance.now();
669
671
  },
670
672
  onOutputItemDone: item => {
671
673
  // `processResponsesStream` hands over a private clone already; no
@@ -678,12 +680,12 @@ const streamOpenAIResponsesOnce = (
678
680
  requestServiceTier: options?.serviceTier,
679
681
  });
680
682
 
681
- const firstEventTimeoutError = abortTracker.getLocalAbortReason();
682
- if (firstEventTimeoutError) {
683
- throw firstEventTimeoutError;
683
+ const localAbortReason = abortTracker.getLocalAbortReason();
684
+ if (localAbortReason) {
685
+ throw localAbortReason;
684
686
  }
685
687
  if (abortTracker.wasCallerAbort()) {
686
- throw new Error("Request was aborted");
688
+ throw new AIError.AbortError();
687
689
  }
688
690
 
689
691
  // Detect premature stream closure: the HTTP stream ended without the
@@ -692,11 +694,17 @@ const streamOpenAIResponsesOnce = (
692
694
  // this guard the incomplete output is silently surfaced as a successful
693
695
  // "stop".
694
696
  if (!sawTerminalResponseEvent) {
695
- throw new Error("OpenAI responses stream closed before a terminal response event was received");
697
+ throw new AIError.ProviderResponseError(
698
+ "OpenAI responses stream closed before a terminal response event was received",
699
+ { provider: model.provider, kind: "incomplete-stream" },
700
+ );
696
701
  }
697
702
 
698
703
  if (output.stopReason === "aborted" || output.stopReason === "error") {
699
- throw new Error(output.errorMessage ?? "An unknown error occurred");
704
+ throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
705
+ provider: model.provider,
706
+ kind: "runtime",
707
+ });
700
708
  }
701
709
 
702
710
  output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
@@ -725,25 +733,28 @@ const streamOpenAIResponsesOnce = (
725
733
  }
726
734
  }
727
735
 
728
- output.duration = Date.now() - startTime;
736
+ output.duration = performance.now() - startTime;
729
737
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
730
738
  stream.push({ type: "done", reason: output.stopReason, message: output });
731
739
  stream.end();
732
740
  } catch (error) {
733
- for (const block of output.content) delete (block as { index?: number }).index;
734
741
  if (chainState) resetOpenAIResponsesChainState(chainState);
735
- const firstEventTimeoutError = abortTracker.getLocalAbortReason();
736
- output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
737
742
  const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
738
- output.errorStatus = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
739
- output.errorMessage =
740
- firstEventTimeoutError?.message ??
741
- (await finalizeErrorMessage(error, rawRequestDump, capturedErrorResponse));
743
+ const result = await AIError.finalize(error, {
744
+ api: model.api,
745
+ provider: model.provider,
746
+ abortTracker,
747
+ rawRequestDump,
748
+ capturedErrorResponse,
749
+ });
750
+ output.stopReason = result.stopReason;
751
+ output.errorStatus = result.status;
752
+ output.errorId = result.id;
753
+ output.errorMessage = result.message;
742
754
  // Some providers via OpenRouter include extra details here.
743
755
  const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
744
756
  if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
745
- output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
746
- output.duration = Date.now() - startTime;
757
+ output.duration = performance.now() - startTime;
747
758
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
748
759
  stream.push({ type: "error", reason: output.stopReason, error: output });
749
760
  stream.end();
@@ -762,6 +773,16 @@ const streamOpenAIResponsesOnce = (
762
773
  export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (model, context, options) =>
763
774
  withEmptyCompletionRetry(model, context, options, streamOpenAIResponsesOnce);
764
775
 
776
+ function isOfficialOpenAIResponsesEndpoint(model: Model<"openai-responses">): boolean {
777
+ if (model.provider !== "openai") return false;
778
+ if (!model.baseUrl) return true;
779
+ try {
780
+ return new URL(model.baseUrl).hostname === "api.openai.com";
781
+ } catch {
782
+ return false;
783
+ }
784
+ }
785
+
765
786
  export function buildParams(
766
787
  model: Model<"openai-responses">,
767
788
  context: Context,
@@ -849,6 +870,9 @@ export function buildParams(
849
870
  });
850
871
 
851
872
  applyCommonResponsesSamplingParams(params, { ...options, maxTokens: outputToken?.value }, model);
873
+ if (options?.textVerbosity && isOfficialOpenAIResponsesEndpoint(model)) {
874
+ params.text = { ...params.text, verbosity: options.textVerbosity };
875
+ }
852
876
  // TODO: openai responses has no top-level `stop`/`stop_sequences`; surface via reasoning.stop?
853
877
  // `StreamOptions.stopSequences` is intentionally dropped for this provider.
854
878
  // TODO: openai responses has no top-level `frequency_penalty` field as of the current SDK;