@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.
- package/CHANGELOG.md +27 -0
- package/dist/types/auth-gateway/server.d.ts +0 -19
- package/dist/types/auth-retry.d.ts +2 -10
- package/dist/types/auth-storage.d.ts +1 -2
- package/dist/types/dialect/demotion.d.ts +22 -0
- package/dist/types/dialect/index.d.ts +2 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +70 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +1 -3
- package/dist/types/providers/amazon-bedrock.d.ts +0 -5
- package/dist/types/providers/anthropic-client.d.ts +2 -16
- package/dist/types/providers/anthropic.d.ts +6 -1
- package/dist/types/providers/aws-eventstream.d.ts +2 -1
- package/dist/types/providers/cursor.d.ts +8 -7
- package/dist/types/providers/google-gemini-cli.d.ts +0 -5
- package/dist/types/providers/google-shared.d.ts +0 -5
- package/dist/types/providers/ollama.d.ts +0 -5
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -2
- package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
- package/dist/types/providers/openai-responses-wire.d.ts +1 -1
- package/dist/types/providers/openai-responses.d.ts +1 -0
- package/dist/types/providers/openai-shared.d.ts +8 -6
- package/dist/types/providers/pi-native-client.d.ts +0 -9
- package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
- package/dist/types/types.d.ts +6 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/openai-http.d.ts +4 -8
- package/dist/types/utils/retry.d.ts +2 -14
- package/dist/types/utils/thinking-loop.d.ts +3 -1
- package/package.json +8 -4
- package/src/api-registry.ts +3 -1
- package/src/auth-broker/discover.ts +7 -2
- package/src/auth-broker/remote-store.ts +8 -7
- package/src/auth-gateway/server.ts +27 -115
- package/src/auth-retry.ts +9 -21
- package/src/auth-storage.ts +34 -49
- package/src/dialect/demotion.ts +31 -0
- package/src/dialect/factory.ts +0 -2
- package/src/dialect/index.ts +2 -0
- package/src/dialect/owned-stream.ts +0 -1
- package/src/dialect/thinking.ts +22 -10
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +486 -0
- package/src/error/format.ts +36 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +56 -0
- package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +1 -3
- package/src/providers/amazon-bedrock.ts +61 -57
- package/src/providers/anthropic-client.ts +13 -41
- package/src/providers/anthropic-messages-server.ts +9 -2
- package/src/providers/anthropic.ts +84 -147
- package/src/providers/aws-credentials.ts +41 -11
- package/src/providers/aws-eventstream.ts +12 -21
- package/src/providers/azure-openai-responses.ts +27 -17
- package/src/providers/cursor.ts +59 -53
- package/src/providers/devin.ts +28 -20
- package/src/providers/gitlab-duo-workflow.ts +30 -10
- package/src/providers/gitlab-duo.ts +22 -6
- package/src/providers/google-auth.ts +15 -5
- package/src/providers/google-gemini-cli.ts +46 -60
- package/src/providers/google-shared.ts +40 -38
- package/src/providers/google-vertex.ts +3 -2
- package/src/providers/google.ts +5 -1
- package/src/providers/mock.ts +10 -7
- package/src/providers/ollama.ts +34 -29
- package/src/providers/openai-chat-server.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +13 -12
- package/src/providers/openai-codex/response-handler.ts +1 -1
- package/src/providers/openai-codex-responses.ts +931 -1012
- package/src/providers/openai-completions.ts +46 -36
- package/src/providers/openai-responses-server-schema.ts +3 -0
- package/src/providers/openai-responses-server.ts +82 -30
- package/src/providers/openai-responses-wire.ts +1 -1
- package/src/providers/openai-responses.ts +46 -22
- package/src/providers/openai-shared.ts +118 -51
- package/src/providers/pi-native-client.ts +12 -18
- package/src/providers/pi-native-server.ts +9 -6
- package/src/providers/register-builtins.ts +5 -2
- package/src/providers/transform-messages.ts +31 -63
- package/src/registry/alibaba-coding-plan.ts +6 -5
- package/src/registry/api-key-login.ts +4 -3
- package/src/registry/api-key-validation.ts +4 -3
- package/src/registry/cloudflare-ai-gateway.ts +4 -3
- package/src/registry/coreweave.ts +2 -1
- package/src/registry/deepseek.ts +2 -1
- package/src/registry/google-antigravity.ts +2 -1
- package/src/registry/google-gemini-cli.ts +2 -1
- package/src/registry/kagi.ts +4 -3
- package/src/registry/kilo.ts +32 -10
- package/src/registry/litellm.ts +4 -3
- package/src/registry/llama-cpp.ts +3 -2
- package/src/registry/lm-studio.ts +3 -2
- package/src/registry/nvidia.ts +7 -7
- package/src/registry/oauth/anthropic.ts +23 -6
- package/src/registry/oauth/callback-server.ts +5 -3
- package/src/registry/oauth/cursor.ts +18 -4
- package/src/registry/oauth/devin.ts +14 -3
- package/src/registry/oauth/github-copilot.ts +21 -10
- package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
- package/src/registry/oauth/gitlab-duo.ts +30 -6
- package/src/registry/oauth/google-antigravity.ts +14 -5
- package/src/registry/oauth/google-gemini-cli.ts +24 -7
- package/src/registry/oauth/google-oauth-shared.ts +6 -3
- package/src/registry/oauth/index.ts +19 -8
- package/src/registry/oauth/kimi.ts +40 -10
- package/src/registry/oauth/openai-codex.ts +26 -11
- package/src/registry/oauth/opencode.ts +4 -3
- package/src/registry/oauth/perplexity.ts +33 -11
- package/src/registry/oauth/xai-oauth.ts +62 -18
- package/src/registry/oauth/xiaomi.ts +21 -9
- package/src/registry/ollama-cloud.ts +5 -4
- package/src/registry/ollama.ts +3 -2
- package/src/registry/parallel.ts +4 -3
- package/src/registry/qwen-portal.ts +4 -3
- package/src/registry/tavily.ts +4 -3
- package/src/registry/vercel-ai-gateway.ts +4 -3
- package/src/registry/vllm.ts +3 -2
- package/src/stream.ts +79 -24
- package/src/types.ts +6 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/github-copilot.ts +4 -3
- package/src/usage/google-antigravity.ts +2 -1
- package/src/utils/abort.ts +4 -2
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/event-stream.ts +15 -3
- package/src/utils/http-inspector.ts +4 -4
- package/src/utils/idle-iterator.ts +6 -5
- package/src/utils/openai-http.ts +11 -46
- package/src/utils/parse-bind.ts +7 -5
- package/src/utils/proxy.ts +2 -1
- package/src/utils/retry.ts +7 -23
- package/src/utils/schema/normalize.ts +3 -2
- package/src/utils/thinking-loop.ts +15 -11
- package/src/utils/validation.ts +4 -3
- package/dist/types/dialect/pi.d.ts +0 -9
- package/dist/types/errors.d.ts +0 -24
- package/dist/types/utils/overflow.d.ts +0 -55
- package/src/dialect/pi.md +0 -55
- package/src/dialect/pi.ts +0 -600
- package/src/errors.ts +0 -32
- package/src/utils/overflow.ts +0 -140
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
parseStreamingJsonThrottled,
|
|
28
28
|
structuredCloneJSON,
|
|
29
29
|
} from "@oh-my-pi/pi-utils";
|
|
30
|
+
import * as AIError from "../error";
|
|
30
31
|
import {
|
|
31
32
|
type Api,
|
|
32
33
|
type AssistantMessage,
|
|
@@ -59,6 +60,12 @@ import {
|
|
|
59
60
|
resolveCacheRetention,
|
|
60
61
|
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
61
62
|
} from "../utils";
|
|
63
|
+
import {
|
|
64
|
+
clearStreamingPartialJson,
|
|
65
|
+
kStreamingArgumentsDone,
|
|
66
|
+
kStreamingLastParseLen,
|
|
67
|
+
kStreamingPartialJson,
|
|
68
|
+
} from "../utils/block-symbols";
|
|
62
69
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
63
70
|
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
64
71
|
import { getOpenRouterHeaders } from "../utils/openrouter-headers";
|
|
@@ -69,6 +76,7 @@ import {
|
|
|
69
76
|
resolveGitHubCopilotBaseUrl,
|
|
70
77
|
} from "./github-copilot-headers";
|
|
71
78
|
import type { ChatCompletionCreateParamsStreaming } from "./openai-chat-wire";
|
|
79
|
+
import type { InputItem } from "./openai-codex/request-transformer";
|
|
72
80
|
import type {
|
|
73
81
|
ResponseContentPartAddedEvent,
|
|
74
82
|
ResponseCreateParamsStreaming,
|
|
@@ -77,6 +85,7 @@ import type {
|
|
|
77
85
|
ResponseInput,
|
|
78
86
|
ResponseInputContent,
|
|
79
87
|
ResponseInputImage,
|
|
88
|
+
ResponseInputItem,
|
|
80
89
|
ResponseInputText,
|
|
81
90
|
ResponseOutputItem,
|
|
82
91
|
ResponseOutputMessage,
|
|
@@ -169,7 +178,8 @@ export function resolveOpenAIRequestSetup(
|
|
|
169
178
|
let apiKey = options.apiKey;
|
|
170
179
|
if (!apiKey) {
|
|
171
180
|
if (!$env.OPENAI_API_KEY) {
|
|
172
|
-
throw new
|
|
181
|
+
throw new AIError.MissingApiKeyError(
|
|
182
|
+
undefined,
|
|
173
183
|
"OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.",
|
|
174
184
|
);
|
|
175
185
|
}
|
|
@@ -762,7 +772,7 @@ export function resolveOpenAICompatPolicy<TApi extends Api>(
|
|
|
762
772
|
) {
|
|
763
773
|
const minEffort = getSupportedEfforts(model)[0];
|
|
764
774
|
if (minEffort === undefined) {
|
|
765
|
-
throw new
|
|
775
|
+
throw new AIError.ConfigurationError(`Model ${model.provider}/${model.id} has no supported reasoning efforts`);
|
|
766
776
|
}
|
|
767
777
|
wireEffort = mapOpenAIReasoningEffort(model, compat, minEffort);
|
|
768
778
|
}
|
|
@@ -1615,7 +1625,7 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
|
1615
1625
|
* Codex uses the open item's recorded index) so the emitted stream events match
|
|
1616
1626
|
* each decoder's existing behavior byte-for-byte.
|
|
1617
1627
|
*/
|
|
1618
|
-
type ResponsesToolCallBlock = ToolCall & {
|
|
1628
|
+
type ResponsesToolCallBlock = ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number };
|
|
1619
1629
|
|
|
1620
1630
|
export function appendReasoningSummaryPart(
|
|
1621
1631
|
item: ResponseReasoningItem,
|
|
@@ -1702,11 +1712,11 @@ export function accumulateToolCallArgumentsDelta(
|
|
|
1702
1712
|
output: AssistantMessage,
|
|
1703
1713
|
contentIndex: number,
|
|
1704
1714
|
): void {
|
|
1705
|
-
block
|
|
1706
|
-
const throttled = parseStreamingJsonThrottled(block
|
|
1715
|
+
block[kStreamingPartialJson] += delta;
|
|
1716
|
+
const throttled = parseStreamingJsonThrottled(block[kStreamingPartialJson], block[kStreamingLastParseLen] ?? 0);
|
|
1707
1717
|
if (throttled) {
|
|
1708
1718
|
block.arguments = throttled.value;
|
|
1709
|
-
block
|
|
1719
|
+
block[kStreamingLastParseLen] = throttled.parsedLen;
|
|
1710
1720
|
}
|
|
1711
1721
|
stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
|
|
1712
1722
|
}
|
|
@@ -1718,10 +1728,9 @@ export function accumulateToolCallArgumentsDelta(
|
|
|
1718
1728
|
* drops the transient accumulation fields.
|
|
1719
1729
|
*/
|
|
1720
1730
|
export function finalizeToolCallArgumentsDone(block: ResponsesToolCallBlock, args: string): void {
|
|
1721
|
-
block
|
|
1722
|
-
block.arguments = parseStreamingJson(block
|
|
1723
|
-
|
|
1724
|
-
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
1731
|
+
block[kStreamingPartialJson] = args;
|
|
1732
|
+
block.arguments = parseStreamingJson(block[kStreamingPartialJson]);
|
|
1733
|
+
clearStreamingPartialJson(block);
|
|
1725
1734
|
}
|
|
1726
1735
|
|
|
1727
1736
|
export function accumulateCustomToolCallInputDelta(
|
|
@@ -1731,13 +1740,13 @@ export function accumulateCustomToolCallInputDelta(
|
|
|
1731
1740
|
output: AssistantMessage,
|
|
1732
1741
|
contentIndex: number,
|
|
1733
1742
|
): void {
|
|
1734
|
-
block
|
|
1735
|
-
block.arguments = { input: block
|
|
1743
|
+
block[kStreamingPartialJson] += delta;
|
|
1744
|
+
block.arguments = { input: block[kStreamingPartialJson] };
|
|
1736
1745
|
stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
|
|
1737
1746
|
}
|
|
1738
1747
|
|
|
1739
1748
|
export function finalizeCustomToolCallInputDone(block: ResponsesToolCallBlock, input: string): void {
|
|
1740
|
-
block
|
|
1749
|
+
block[kStreamingPartialJson] = input;
|
|
1741
1750
|
block.arguments = { input };
|
|
1742
1751
|
}
|
|
1743
1752
|
|
|
@@ -1766,7 +1775,11 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1766
1775
|
model: Model<TApi>,
|
|
1767
1776
|
options?: ProcessResponsesStreamOptions,
|
|
1768
1777
|
): Promise<void> {
|
|
1769
|
-
type StreamingToolCallBlock = ToolCall & {
|
|
1778
|
+
type StreamingToolCallBlock = ToolCall & {
|
|
1779
|
+
[kStreamingPartialJson]: string;
|
|
1780
|
+
[kStreamingLastParseLen]?: number;
|
|
1781
|
+
[kStreamingArgumentsDone]?: boolean;
|
|
1782
|
+
};
|
|
1770
1783
|
interface StreamingItem {
|
|
1771
1784
|
item: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
|
|
1772
1785
|
block: ThinkingContent | TextContent | StreamingToolCallBlock;
|
|
@@ -1863,7 +1876,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1863
1876
|
if (
|
|
1864
1877
|
candidate.item.type === "function_call" &&
|
|
1865
1878
|
candidate.block.type === "toolCall" &&
|
|
1866
|
-
!candidate.block
|
|
1879
|
+
!candidate.block[kStreamingArgumentsDone]
|
|
1867
1880
|
) {
|
|
1868
1881
|
return candidate;
|
|
1869
1882
|
}
|
|
@@ -1914,7 +1927,11 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1914
1927
|
registerOpenItem(event.output_index, item.id, { item, block });
|
|
1915
1928
|
stream.push({ type: "thinking_start", contentIndex: contentIndexOf(block), partial: output });
|
|
1916
1929
|
} else if (item.type === "message") {
|
|
1917
|
-
const block: TextContent = {
|
|
1930
|
+
const block: TextContent = {
|
|
1931
|
+
type: "text",
|
|
1932
|
+
text: "",
|
|
1933
|
+
textSignature: encodeTextSignatureV1(item.id, item.phase ?? undefined),
|
|
1934
|
+
};
|
|
1918
1935
|
output.content.push(block);
|
|
1919
1936
|
registerOpenItem(event.output_index, item.id, { item, block });
|
|
1920
1937
|
stream.push({ type: "text_start", contentIndex: contentIndexOf(block), partial: output });
|
|
@@ -1924,7 +1941,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1924
1941
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1925
1942
|
name: item.name,
|
|
1926
1943
|
arguments: {},
|
|
1927
|
-
|
|
1944
|
+
[kStreamingPartialJson]: item.arguments || "",
|
|
1928
1945
|
};
|
|
1929
1946
|
output.content.push(block);
|
|
1930
1947
|
registerOpenItem(
|
|
@@ -1948,7 +1965,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1948
1965
|
customWireName: item.name,
|
|
1949
1966
|
// Custom tools stream a raw string, but we reuse `partialJson` as the
|
|
1950
1967
|
// accumulation buffer so later code that inspects the field still works.
|
|
1951
|
-
|
|
1968
|
+
[kStreamingPartialJson]: item.input ?? "",
|
|
1952
1969
|
};
|
|
1953
1970
|
output.content.push(block);
|
|
1954
1971
|
registerOpenItem(
|
|
@@ -2031,7 +2048,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2031
2048
|
const entry = lookupOpenFunctionCallItem(event);
|
|
2032
2049
|
if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
|
|
2033
2050
|
finalizeToolCallArgumentsDone(entry.block, event.arguments);
|
|
2034
|
-
entry.block
|
|
2051
|
+
entry.block[kStreamingArgumentsDone] = true;
|
|
2035
2052
|
}
|
|
2036
2053
|
} else if (event.type === "response.custom_tool_call_input.delta") {
|
|
2037
2054
|
const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
|
|
@@ -2099,10 +2116,10 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2099
2116
|
closeOpenItem(event.output_index, item.id, entry);
|
|
2100
2117
|
} else if (item.type === "function_call") {
|
|
2101
2118
|
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2102
|
-
const args = block?.
|
|
2119
|
+
const args = block?.[kStreamingArgumentsDone]
|
|
2103
2120
|
? block.arguments
|
|
2104
|
-
: block?.
|
|
2105
|
-
? parseStreamingJson(block
|
|
2121
|
+
: block?.[kStreamingPartialJson]
|
|
2122
|
+
? parseStreamingJson(block[kStreamingPartialJson])
|
|
2106
2123
|
: parseStreamingJson(item.arguments || "{}");
|
|
2107
2124
|
const toolCall: ToolCall = {
|
|
2108
2125
|
type: "toolCall",
|
|
@@ -2117,9 +2134,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2117
2134
|
// leaving block.arguments stale (often `{}`); the emitted toolCall
|
|
2118
2135
|
// and the persisted block must agree.
|
|
2119
2136
|
block.arguments = args;
|
|
2120
|
-
|
|
2121
|
-
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
2122
|
-
delete (block as { argumentsDone?: boolean }).argumentsDone;
|
|
2137
|
+
clearStreamingPartialJson(block);
|
|
2123
2138
|
contentIndex = contentIndexOf(block);
|
|
2124
2139
|
} else {
|
|
2125
2140
|
// `output_item.added` never arrived (lossy proxy) — synthesize the
|
|
@@ -2132,7 +2147,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2132
2147
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2133
2148
|
} else if (item.type === "custom_tool_call") {
|
|
2134
2149
|
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2135
|
-
const rawInput = block?.
|
|
2150
|
+
const rawInput = block?.[kStreamingPartialJson] ? block[kStreamingPartialJson] : (item.input ?? "");
|
|
2136
2151
|
const toolCall: ToolCall = {
|
|
2137
2152
|
type: "toolCall",
|
|
2138
2153
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
@@ -2145,8 +2160,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2145
2160
|
// Persist the final input on the stored block and drop the transient
|
|
2146
2161
|
// accumulation buffer, mirroring the function_call branch above.
|
|
2147
2162
|
block.arguments = { input: rawInput };
|
|
2148
|
-
|
|
2149
|
-
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
2163
|
+
clearStreamingPartialJson(block);
|
|
2150
2164
|
contentIndex = contentIndexOf(block);
|
|
2151
2165
|
} else {
|
|
2152
2166
|
output.content.push(toolCall);
|
|
@@ -2181,13 +2195,16 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2181
2195
|
: typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
|
|
2182
2196
|
? `status_details: ${statusDetailsReason}`
|
|
2183
2197
|
: "Unknown error (no error details in response)";
|
|
2184
|
-
throw new
|
|
2198
|
+
throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
2185
2199
|
}
|
|
2186
2200
|
if (response?.status === "incomplete" && response.incomplete_details?.reason === "content_filter") {
|
|
2187
2201
|
// A content-filtered turn is a failure, not a token-cap truncation —
|
|
2188
2202
|
// mapping it to "length" would route the agent loop into "shorten your
|
|
2189
2203
|
// output" recovery against a filtered prompt.
|
|
2190
|
-
throw new
|
|
2204
|
+
throw new AIError.ProviderResponseError("incomplete: content_filter", {
|
|
2205
|
+
provider: model.provider,
|
|
2206
|
+
kind: "content-blocked",
|
|
2207
|
+
});
|
|
2191
2208
|
}
|
|
2192
2209
|
promoteResponsesToolUseStopReason(output, (response as { end_turn?: boolean } | undefined)?.end_turn);
|
|
2193
2210
|
options?.onCompleted?.();
|
|
@@ -2203,7 +2220,10 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2203
2220
|
const err = (event as any).error ?? event;
|
|
2204
2221
|
const code = err.code ?? "unknown";
|
|
2205
2222
|
const message = err.message ?? "no message";
|
|
2206
|
-
throw new
|
|
2223
|
+
throw new AIError.ProviderResponseError(`Error Code ${code}: ${message}`, {
|
|
2224
|
+
provider: model.provider,
|
|
2225
|
+
kind: "output",
|
|
2226
|
+
});
|
|
2207
2227
|
} else if (event.type === "response.failed") {
|
|
2208
2228
|
populateResponsesUsageFromResponse(output, event.response?.usage);
|
|
2209
2229
|
const error = event.response?.error ?? (event.response as any)?.status_details?.error;
|
|
@@ -2213,7 +2233,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2213
2233
|
: details?.reason
|
|
2214
2234
|
? `incomplete: ${details.reason}`
|
|
2215
2235
|
: "Unknown error (no error details in response)";
|
|
2216
|
-
throw new
|
|
2236
|
+
throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
2217
2237
|
}
|
|
2218
2238
|
}
|
|
2219
2239
|
}
|
|
@@ -2253,16 +2273,18 @@ export function mapOpenAIResponsesStopReason(status: ResponseStatus | undefined)
|
|
|
2253
2273
|
export function finalizePendingResponsesToolCalls(output: AssistantMessage): void {
|
|
2254
2274
|
for (const block of output.content) {
|
|
2255
2275
|
if (block.type !== "toolCall") continue;
|
|
2256
|
-
const pending = block as ToolCall & {
|
|
2257
|
-
|
|
2276
|
+
const pending = block as ToolCall & {
|
|
2277
|
+
[kStreamingPartialJson]?: string;
|
|
2278
|
+
[kStreamingLastParseLen]?: number;
|
|
2279
|
+
[kStreamingArgumentsDone]?: boolean;
|
|
2280
|
+
};
|
|
2281
|
+
if (pending[kStreamingPartialJson] && !pending[kStreamingArgumentsDone]) {
|
|
2258
2282
|
pending.arguments =
|
|
2259
2283
|
pending.customWireName !== undefined
|
|
2260
|
-
? { input: pending
|
|
2261
|
-
: parseStreamingJson(pending
|
|
2284
|
+
? { input: pending[kStreamingPartialJson] }
|
|
2285
|
+
: parseStreamingJson(pending[kStreamingPartialJson]);
|
|
2262
2286
|
}
|
|
2263
|
-
|
|
2264
|
-
delete pending.lastParseLen;
|
|
2265
|
-
delete pending.argumentsDone;
|
|
2287
|
+
clearStreamingPartialJson(pending);
|
|
2266
2288
|
}
|
|
2267
2289
|
}
|
|
2268
2290
|
|
|
@@ -2514,6 +2536,45 @@ export function populateResponsesUsageFromResponse(
|
|
|
2514
2536
|
}
|
|
2515
2537
|
}
|
|
2516
2538
|
|
|
2539
|
+
/**
|
|
2540
|
+
* Structural equality for the chain prefix/option check, equivalent to the
|
|
2541
|
+
* default {@link Bun.deepEquals} (own enumerable keys, `absent ≡ own-undefined`)
|
|
2542
|
+
* except for two deliberate exclusions:
|
|
2543
|
+
* - **symbol-keyed properties are ignored** — `for…in` walks enumerable
|
|
2544
|
+
* *string* keys only (never symbols); these are plain wire items whose
|
|
2545
|
+
* prototype contributes no enumerable keys, so iteration is effectively
|
|
2546
|
+
* own-string-keyed. That is how the transient streaming symbols
|
|
2547
|
+
* (`block-symbols.ts`) stamped onto live request items are excluded (the
|
|
2548
|
+
* deep-cloned baseline never carries them). Do NOT add an
|
|
2549
|
+
* `Object.getOwnPropertySymbols` pass, or those symbols resurface and break
|
|
2550
|
+
* chaining.
|
|
2551
|
+
* - keys listed in `omitKeys` are skipped (the option compare omits `input`
|
|
2552
|
+
* and the per-turn `client_metadata`).
|
|
2553
|
+
* A defined value differing across sides IS a difference; a key undefined or
|
|
2554
|
+
* absent on both stays equal. Nested values use full {@link Bun.deepEquals}.
|
|
2555
|
+
*/
|
|
2556
|
+
function deepEqualsWithout(a: unknown, b: unknown, omitKeys?: Record<string, boolean>): boolean {
|
|
2557
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return Bun.deepEquals(a, b);
|
|
2558
|
+
const ao = a as Record<string, unknown>;
|
|
2559
|
+
const bo = b as Record<string, unknown>;
|
|
2560
|
+
for (const key in ao) {
|
|
2561
|
+
if (omitKeys?.[key]) continue;
|
|
2562
|
+
const av = ao[key];
|
|
2563
|
+
const bv = bo[key];
|
|
2564
|
+
if (av !== bv && !Bun.deepEquals(av, bv)) return false;
|
|
2565
|
+
}
|
|
2566
|
+
for (const key in bo) {
|
|
2567
|
+
if (omitKeys?.[key]) continue;
|
|
2568
|
+
if (bo[key] !== undefined && !(key in ao)) return false;
|
|
2569
|
+
}
|
|
2570
|
+
return true;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
const TOP_LEVEL_EXCLUDE_MAP = {
|
|
2574
|
+
input: true,
|
|
2575
|
+
client_metadata: true,
|
|
2576
|
+
};
|
|
2577
|
+
|
|
2517
2578
|
/**
|
|
2518
2579
|
* Strict-prefix delta for stateful `previous_response_id` chaining (used by the
|
|
2519
2580
|
* platform Responses provider and the Codex provider on both transports):
|
|
@@ -2523,24 +2584,30 @@ export function populateResponsesUsageFromResponse(
|
|
|
2523
2584
|
* `client_metadata` (e.g. rotating turn ids) is excluded from the option
|
|
2524
2585
|
* comparison; codex-rs excludes it from the same check.
|
|
2525
2586
|
*/
|
|
2526
|
-
export function buildResponsesDeltaInput<TItem>(
|
|
2527
|
-
previous: { input?:
|
|
2587
|
+
export function buildResponsesDeltaInput<TItem extends ResponseInputItem | InputItem>(
|
|
2588
|
+
previous: { input?: TItem[] } | undefined,
|
|
2528
2589
|
previousResponseItems: readonly TItem[] | undefined,
|
|
2529
|
-
current: { input?:
|
|
2590
|
+
current: { input?: TItem[] },
|
|
2530
2591
|
): TItem[] | null {
|
|
2531
2592
|
if (!previous) return null;
|
|
2532
2593
|
if (!Array.isArray(previous.input) || !Array.isArray(current.input)) return null;
|
|
2533
|
-
|
|
2534
|
-
const currentWithoutInput = { ...current, input: undefined, client_metadata: undefined };
|
|
2535
|
-
if (!Bun.deepEquals(previousWithoutInput, currentWithoutInput)) {
|
|
2594
|
+
if (!deepEqualsWithout(previous, current, TOP_LEVEL_EXCLUDE_MAP)) {
|
|
2536
2595
|
return null;
|
|
2537
2596
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2597
|
+
|
|
2598
|
+
const baselineLen = (previous.input?.length ?? 0) + (previousResponseItems?.length ?? 0);
|
|
2599
|
+
if (current.input.length <= baselineLen) return null;
|
|
2600
|
+
|
|
2601
|
+
let index = 0;
|
|
2602
|
+
for (const series of [previous.input, previousResponseItems]) {
|
|
2603
|
+
if (!series) continue;
|
|
2604
|
+
for (const item of series) {
|
|
2605
|
+
if (deepEqualsWithout(item, current.input[index])) {
|
|
2606
|
+
index++;
|
|
2607
|
+
} else {
|
|
2608
|
+
return null;
|
|
2609
|
+
}
|
|
2543
2610
|
}
|
|
2544
2611
|
}
|
|
2545
|
-
return current.input.slice(
|
|
2612
|
+
return current.input.slice(index) as TItem[];
|
|
2546
2613
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* itself stays credential-free.
|
|
17
17
|
*/
|
|
18
18
|
import { readSseJson } from "@oh-my-pi/pi-utils";
|
|
19
|
-
import
|
|
19
|
+
import * as AIError from "../error";
|
|
20
20
|
import type {
|
|
21
21
|
Api,
|
|
22
22
|
AssistantMessage,
|
|
@@ -59,19 +59,7 @@ function buildWireOptions(options: SimpleStreamOptions | undefined): Record<stri
|
|
|
59
59
|
return wire;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
* Non-2xx response from the auth-gateway `/v1/pi/stream` endpoint. `code`
|
|
64
|
-
* carries the gateway's error-type token (`authentication_error`,
|
|
65
|
-
* `rate_limit_error`, `upstream_error`, ...).
|
|
66
|
-
*/
|
|
67
|
-
export class AuthGatewayError extends ProviderHttpError {
|
|
68
|
-
constructor(message: string, status: number, headers?: Headers, code?: string) {
|
|
69
|
-
super(message, status, { headers, code });
|
|
70
|
-
this.name = "AuthGatewayError";
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function decodeGatewayError(response: Response): Promise<AuthGatewayError> {
|
|
62
|
+
async function decodeGatewayError(response: Response): Promise<AIError.AuthGatewayError> {
|
|
75
63
|
const status = response.status;
|
|
76
64
|
let body: unknown;
|
|
77
65
|
try {
|
|
@@ -84,7 +72,7 @@ async function decodeGatewayError(response: Response): Promise<AuthGatewayError>
|
|
|
84
72
|
if (typeof err === "object" && err !== null) {
|
|
85
73
|
const message = (err as { message?: unknown }).message;
|
|
86
74
|
const type = (err as { type?: unknown }).type;
|
|
87
|
-
return new AuthGatewayError(
|
|
75
|
+
return new AIError.AuthGatewayError(
|
|
88
76
|
typeof message === "string" ? message : `auth-gateway ${status}`,
|
|
89
77
|
status,
|
|
90
78
|
response.headers,
|
|
@@ -93,7 +81,11 @@ async function decodeGatewayError(response: Response): Promise<AuthGatewayError>
|
|
|
93
81
|
}
|
|
94
82
|
}
|
|
95
83
|
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
96
|
-
return new AuthGatewayError(
|
|
84
|
+
return new AIError.AuthGatewayError(
|
|
85
|
+
`auth-gateway ${status}: ${text || response.statusText}`,
|
|
86
|
+
status,
|
|
87
|
+
response.headers,
|
|
88
|
+
);
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
/**
|
|
@@ -104,7 +96,7 @@ async function decodeGatewayError(response: Response): Promise<AuthGatewayError>
|
|
|
104
96
|
*/
|
|
105
97
|
function resolveStreamUrl(model: Model<Api>): string {
|
|
106
98
|
if (!model.baseUrl) {
|
|
107
|
-
throw new
|
|
99
|
+
throw new AIError.ConfigurationError(
|
|
108
100
|
`pi-native transport requires \`baseUrl\` on model ${model.id} (set it on the provider config in models.yml)`,
|
|
109
101
|
);
|
|
110
102
|
}
|
|
@@ -179,7 +171,9 @@ export function streamPiNative<TApi extends Api>(
|
|
|
179
171
|
return;
|
|
180
172
|
}
|
|
181
173
|
if (!response.body) {
|
|
182
|
-
stream.fail(
|
|
174
|
+
stream.fail(
|
|
175
|
+
new AIError.AuthGatewayError("auth-gateway returned empty body", response.status, response.headers),
|
|
176
|
+
);
|
|
183
177
|
return;
|
|
184
178
|
}
|
|
185
179
|
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
* 200 JSON (stream=false): { message: AssistantMessage }
|
|
26
26
|
* 4xx/5xx: { error: { type, message } }
|
|
27
27
|
*/
|
|
28
|
+
|
|
28
29
|
import type { AuthGatewayStreamControl } from "../auth-gateway/types";
|
|
30
|
+
import * as AIError from "../error";
|
|
29
31
|
import type { AssistantMessageEventStream, Context, SimpleStreamOptions } from "../types";
|
|
30
32
|
|
|
31
33
|
export interface PiNativeParsedRequest {
|
|
@@ -72,6 +74,7 @@ const ALLOWED_OPTION_KEYS: ReadonlySet<keyof SimpleStreamOptions> = new Set([
|
|
|
72
74
|
"syntheticApiFormat",
|
|
73
75
|
"preferWebsockets",
|
|
74
76
|
"openrouterVariant",
|
|
77
|
+
"loopGuard",
|
|
75
78
|
] as const satisfies readonly (keyof SimpleStreamOptions)[]);
|
|
76
79
|
|
|
77
80
|
// ---------------------------------------------------------------------------
|
|
@@ -91,7 +94,7 @@ const ALLOWED_OPTION_KEYS: ReadonlySet<keyof SimpleStreamOptions> = new Set([
|
|
|
91
94
|
*/
|
|
92
95
|
export function parseRequest(body: unknown, _headers?: Headers): PiNativeParsedRequest {
|
|
93
96
|
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
94
|
-
throw new
|
|
97
|
+
throw new AIError.ValidationError("Request body must be a JSON object");
|
|
95
98
|
}
|
|
96
99
|
const obj = body as Record<string, unknown>;
|
|
97
100
|
|
|
@@ -104,21 +107,21 @@ export function parseRequest(body: unknown, _headers?: Headers): PiNativeParsedR
|
|
|
104
107
|
const m = obj.model as Record<string, unknown>;
|
|
105
108
|
if (typeof m.id === "string" && m.id.length > 0) modelId = m.id;
|
|
106
109
|
}
|
|
107
|
-
if (!modelId) throw new
|
|
110
|
+
if (!modelId) throw new AIError.ValidationError("Missing `modelId` (or `model.id`) field");
|
|
108
111
|
|
|
109
112
|
const context = obj.context;
|
|
110
113
|
if (typeof context !== "object" || context === null || Array.isArray(context)) {
|
|
111
|
-
throw new
|
|
114
|
+
throw new AIError.ValidationError("Missing `context` object");
|
|
112
115
|
}
|
|
113
116
|
const ctxObj = context as Record<string, unknown>;
|
|
114
117
|
if (!Array.isArray(ctxObj.messages)) {
|
|
115
|
-
throw new
|
|
118
|
+
throw new AIError.ValidationError("`context.messages` must be an array");
|
|
116
119
|
}
|
|
117
120
|
if (ctxObj.systemPrompt !== undefined && !Array.isArray(ctxObj.systemPrompt)) {
|
|
118
|
-
throw new
|
|
121
|
+
throw new AIError.ValidationError("`context.systemPrompt` must be an array of strings when present");
|
|
119
122
|
}
|
|
120
123
|
if (ctxObj.tools !== undefined && !Array.isArray(ctxObj.tools)) {
|
|
121
|
-
throw new
|
|
124
|
+
throw new AIError.ValidationError("`context.tools` must be an array when present");
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
const options: SimpleStreamOptions = {};
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* wired into the main streaming path. It provides the infrastructure for lazy
|
|
11
11
|
* loading that can be integrated when stream.ts is refactored.
|
|
12
12
|
*/
|
|
13
|
+
|
|
14
|
+
import * as AIError from "../error";
|
|
13
15
|
import type {
|
|
14
16
|
Api,
|
|
15
17
|
AssistantMessage,
|
|
@@ -248,8 +250,9 @@ function forwardStream<TApi extends Api>(
|
|
|
248
250
|
firstItemTimeoutMs,
|
|
249
251
|
errorMessage: LAZY_STREAM_IDLE_TIMEOUT_ERROR,
|
|
250
252
|
firstItemErrorMessage: LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR,
|
|
251
|
-
onIdle: () => abortTracker.abortLocally(new
|
|
252
|
-
onFirstItemTimeout: () =>
|
|
253
|
+
onIdle: () => abortTracker.abortLocally(new AIError.StreamTimeoutError(LAZY_STREAM_IDLE_TIMEOUT_ERROR)),
|
|
254
|
+
onFirstItemTimeout: () =>
|
|
255
|
+
abortTracker.abortLocally(new AIError.StreamTimeoutError(LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR)),
|
|
253
256
|
abortSignal: options.signal,
|
|
254
257
|
// The synthetic `start` event is yielded immediately by every provider before
|
|
255
258
|
// the upstream model has emitted any tokens. Treating it as the first "real"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderDemotedThinking } from "../dialect/demotion";
|
|
1
2
|
import type { Api, AssistantMessage, Message, Model, ToolCall, ToolResultMessage, UserMessage } from "../types";
|
|
2
3
|
|
|
3
4
|
const enum ToolCallStatus {
|
|
@@ -226,52 +227,20 @@ function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messa
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* `openai-completions` branch of the cross-API path (#3433/#3434). 3p ↔ 3p
|
|
234
|
-
* replays between an Anthropic-compatible source (Z.AI Anthropic, Kimi
|
|
235
|
-
* Anthropic, …) and an OpenAI-compat reasoning target on the same vendor must
|
|
236
|
-
* keep reasoning as structured `reasoning_content` instead of degrading it to
|
|
237
|
-
* conversation text.
|
|
238
|
-
*
|
|
239
|
-
* `compat` MUST be the request-time RESOLVED compat that `convertMessages`
|
|
240
|
-
* threads into `transformMessages`, not `model.compat`. OpenCode-hosted
|
|
241
|
-
* reasoning models (`opencode-go`/`opencode-zen`) keep
|
|
242
|
-
* `requiresReasoningContentForToolCalls` off on the base compat to dodge the
|
|
243
|
-
* thinking-off `Extra inputs are not permitted` 400 (#1071) and reactivate it
|
|
244
|
-
* on `compat.whenThinking` for thinking-engaged requests to dodge the
|
|
245
|
-
* `thinking is enabled but reasoning_content is missing` 400 (#1484).
|
|
246
|
-
* `resolveOpenAICompatPolicy` already swaps in `whenThinking` for thinking-on
|
|
247
|
-
* requests, so basing this decision on the resolved compat keeps the predicate
|
|
248
|
-
* and the encoder in lockstep; reading `model.compat` would re-open #1484 for
|
|
249
|
-
* every cross-API switch into an OpenCode reasoning model.
|
|
250
|
-
*
|
|
251
|
-
* The downstream encoder MUST then surface the preserved block on the wire via
|
|
252
|
-
* `reasoningContentField` — see `openai-completions.ts` for the matching
|
|
253
|
-
* branch.
|
|
230
|
+
* Targets that have proven they read unsigned foreign thinking when replayed
|
|
231
|
+
* natively. This is a semantic-carry allowlist only: OpenAI-compatible
|
|
232
|
+
* `reasoning_content` schema requirements and llama.cpp cache-prefix replay are
|
|
233
|
+
* handled by their encoders and MUST NOT make foreign thinking look meaningful.
|
|
254
234
|
*/
|
|
255
|
-
function
|
|
235
|
+
function targetReadsForeignThinking(model: Model, compat: Model["compat"]): boolean {
|
|
236
|
+
if (compat === undefined) return false;
|
|
237
|
+
if (model.api === "anthropic-messages") {
|
|
238
|
+
return "replayUnsignedThinking" in compat && compat.replayUnsignedThinking === true;
|
|
239
|
+
}
|
|
256
240
|
if (model.api !== "openai-completions") return false;
|
|
257
|
-
if (
|
|
241
|
+
if (!("thinkingFormat" in compat)) return false;
|
|
258
242
|
if (compat.requiresThinkingAsText) return false;
|
|
259
|
-
|
|
260
|
-
// for KV-cache prefix reuse — Qwen3 / DeepSeek-R1 / GLM chat templates
|
|
261
|
-
// reconstruct the prior turn's `<think>` block from `reasoning_content`
|
|
262
|
-
// (#3528). Checked BEFORE the `model.reasoning` gate: the runtime discovery
|
|
263
|
-
// paths for `llama.cpp` / `lm-studio` / `openai-models-list` hardcode
|
|
264
|
-
// `reasoning: false` even when the upstream actually emits reasoning, so
|
|
265
|
-
// gating on the spec flag here would let a cross-API switch into such a
|
|
266
|
-
// target demote the prior `thinking` block to text and lose the
|
|
267
|
-
// cache-stable prefix `replayReasoningContent` is meant to preserve.
|
|
268
|
-
if (compat.replayReasoningContent) return true;
|
|
269
|
-
if (!model.reasoning) return false;
|
|
270
|
-
// Hosts that REQUIRE `reasoning_content` on tool-call turns (DeepSeek
|
|
271
|
-
// reasoning, Kimi, OpenRouter reasoning, OpenCode thinking-on) already
|
|
272
|
-
// accept the replay; Z.AI-format hosts (Z.AI, Zhipu, Moonshot Kimi native,
|
|
273
|
-
// Xiaomi MiMo) advertise `reasoning_content` as a continuation hint.
|
|
274
|
-
return compat.requiresReasoningContentForToolCalls || compat.thinkingFormat === "zai";
|
|
243
|
+
return model.reasoning && compat.thinkingFormat === "zai";
|
|
275
244
|
}
|
|
276
245
|
|
|
277
246
|
const ANTHROPIC_TOOL_CALL_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
@@ -461,29 +430,29 @@ export function transformMessages<TApi extends Api>(
|
|
|
461
430
|
// thinking blocks before the cross-model paths.
|
|
462
431
|
if (!sanitized.thinking || sanitized.thinking.trim() === "") return [];
|
|
463
432
|
if (isSameModel) return sanitized;
|
|
464
|
-
// Cross-model + cross-API: preserve
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
// `
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
// every cross-API 3p ↔ 3p switch (Z.AI Anthropic → Z.AI OpenAI,
|
|
473
|
-
// Kimi Anthropic → Kimi OpenAI, etc.) demoted prior reasoning to
|
|
474
|
-
// conversation text and lost it as structured reasoning context
|
|
475
|
-
// (#3433/#3434).
|
|
476
|
-
if (openAICompletionsReplaysUnsignedThinking(model, targetCompat)) {
|
|
433
|
+
// Cross-model + cross-API: preserve native thinking only for
|
|
434
|
+
// targets proven to read unsigned foreign reasoning (Z.AI-format
|
|
435
|
+
// OpenAI-compatible targets, plus Anthropic-compatible
|
|
436
|
+
// `replayUnsignedThinking`). Tool-call schema requirements and
|
|
437
|
+
// llama.cpp cache-prefix replay are orthogonal encoder concerns;
|
|
438
|
+
// keeping inert foreign CoT native for those flags loses the
|
|
439
|
+
// canonical visible-text fallback without adding model context.
|
|
440
|
+
if (targetReadsForeignThinking(model, targetCompat)) {
|
|
477
441
|
return sanitized.thinkingSignature ? { ...sanitized, thinkingSignature: undefined } : sanitized;
|
|
478
442
|
}
|
|
479
443
|
// Other cross-API targets (openai-responses encrypted blobs, google
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
444
|
+
// thought parts, anthropic-target from a non-Anthropic source, or any
|
|
445
|
+
// reasoning-disabled target) can't replay an unsigned thinking block:
|
|
446
|
+
// the native reasoning slot either rejects a foreign signature or — as
|
|
447
|
+
// verified end-to-end against Gemini 3 — silently discards unsigned
|
|
448
|
+
// thought content (it is neither recalled nor influences generation).
|
|
449
|
+
// Demote to text so the reasoning survives as context, wrapped in the
|
|
450
|
+
// TARGET model's own canonical thinking-block dialect (e.g. a ```thinking
|
|
451
|
+
// fence for Gemini) so it reads as reasoning rather than bare prose the
|
|
452
|
+
// model might mimic.
|
|
484
453
|
return {
|
|
485
454
|
type: "text" as const,
|
|
486
|
-
text: sanitized.thinking,
|
|
455
|
+
text: renderDemotedThinking(model.id, sanitized.thinking),
|
|
487
456
|
};
|
|
488
457
|
}
|
|
489
458
|
|
|
@@ -514,8 +483,7 @@ export function transformMessages<TApi extends Api>(
|
|
|
514
483
|
let normalizedToolCall: ToolCall = toolCall;
|
|
515
484
|
|
|
516
485
|
if (!isSameModel && toolCall.thoughtSignature) {
|
|
517
|
-
normalizedToolCall = { ...toolCall };
|
|
518
|
-
delete (normalizedToolCall as { thoughtSignature?: string }).thoughtSignature;
|
|
486
|
+
normalizedToolCall = { ...toolCall, thoughtSignature: undefined };
|
|
519
487
|
}
|
|
520
488
|
|
|
521
489
|
if (isAnthropicTarget) {
|