@oh-my-pi/pi-ai 16.2.5 → 16.2.7
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 +28 -0
- package/dist/types/auth-storage.d.ts +11 -10
- package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
- package/dist/types/providers/google-auth.d.ts +8 -0
- package/dist/types/providers/google-interactions.d.ts +65 -0
- package/dist/types/providers/google-shared.d.ts +20 -5
- package/dist/types/providers/google-types.d.ts +5 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/openai-shared.d.ts +3 -3
- package/dist/types/types.d.ts +80 -30
- package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
- package/package.json +4 -4
- package/src/auth-storage.ts +56 -52
- package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
- package/src/providers/anthropic.ts +11 -10
- package/src/providers/google-auth.ts +25 -0
- package/src/providers/google-gemini-cli.ts +90 -41
- package/src/providers/google-interactions.ts +753 -0
- package/src/providers/google-shared.ts +74 -13
- package/src/providers/google-types.ts +5 -1
- package/src/providers/google-vertex.ts +117 -26
- package/src/providers/google.ts +61 -19
- package/src/providers/openai-chat-server.ts +2 -2
- package/src/providers/openai-codex/request-transformer.ts +17 -4
- package/src/providers/openai-codex-responses.ts +23 -5
- package/src/providers/openai-shared.ts +5 -11
- package/src/stream.ts +34 -12
- package/src/types.ts +164 -51
- package/src/usage/google-antigravity.ts +59 -5
- package/src/utils/leaked-thinking-stream.ts +260 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central live healing for leaked reasoning markup in the visible text channel.
|
|
3
|
+
*
|
|
4
|
+
* Some providers emit their canonical reasoning idioms (` ```thinking `,
|
|
5
|
+
* `<think>`, Gemma/Harmony channels, …) into the *visible* text stream instead
|
|
6
|
+
* of a structured thinking part. {@link wrapLeakedThinkingStream} re-projects any
|
|
7
|
+
* provider stream into a fresh {@link AssistantMessageEventStream}, splitting the
|
|
8
|
+
* leaked fences out into proper `thinking` blocks *live* as deltas arrive — so
|
|
9
|
+
* every provider gets the same healing, not just the three with provider-local
|
|
10
|
+
* {@link StreamMarkupHealing} loops.
|
|
11
|
+
*
|
|
12
|
+
* The healing is idempotent: a second pass over already-clean text finds no
|
|
13
|
+
* fences, so wrapping a provider that already heals (or wrapping twice) is a
|
|
14
|
+
* harmless pass-through. Signatures are load-bearing for Google/Gemini/Vertex
|
|
15
|
+
* thought round-tripping, so text sub-blocks carry the source `textSignature`,
|
|
16
|
+
* forwarded thinking blocks their `thinkingSignature`, and forwarded tool calls
|
|
17
|
+
* their `thoughtSignature`.
|
|
18
|
+
*
|
|
19
|
+
* Modeled on {@link wrapInbandToolStream} / `InbandStreamProjector` in
|
|
20
|
+
* `../dialect/owned-stream.ts`, minus all in-band tool-call grammar: tool-call
|
|
21
|
+
* events are forwarded verbatim.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { AssistantMessage, TextContent, ThinkingContent, ToolCall } from "../types";
|
|
25
|
+
import { AssistantMessageEventStream } from "./event-stream";
|
|
26
|
+
import { StreamMarkupHealing, type StreamMarkupHealingEvent } from "./stream-markup-healing";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a provider stream so leaked reasoning fences are healed into thinking
|
|
30
|
+
* blocks live, for every provider. Returns a new stream that re-projects the
|
|
31
|
+
* inner one; the inner stream is fully consumed.
|
|
32
|
+
*/
|
|
33
|
+
export function wrapLeakedThinkingStream(inner: AssistantMessageEventStream): AssistantMessageEventStream {
|
|
34
|
+
const out = new AssistantMessageEventStream();
|
|
35
|
+
void (async () => {
|
|
36
|
+
try {
|
|
37
|
+
let projector: LeakedThinkingProjector | undefined;
|
|
38
|
+
for await (const event of inner) {
|
|
39
|
+
switch (event.type) {
|
|
40
|
+
case "start":
|
|
41
|
+
projector = new LeakedThinkingProjector(out, event.partial);
|
|
42
|
+
break;
|
|
43
|
+
case "text_delta": {
|
|
44
|
+
projector ??= new LeakedThinkingProjector(out, event.partial);
|
|
45
|
+
const block = event.partial.content[event.contentIndex];
|
|
46
|
+
projector.text(event.delta, block?.type === "text" ? block.textSignature : undefined);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case "thinking_delta": {
|
|
50
|
+
projector ??= new LeakedThinkingProjector(out, event.partial);
|
|
51
|
+
const block = event.partial.content[event.contentIndex];
|
|
52
|
+
projector.thinking(event.delta, block?.type === "thinking" ? block.thinkingSignature : undefined);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "toolcall_start": {
|
|
56
|
+
projector ??= new LeakedThinkingProjector(out, event.partial);
|
|
57
|
+
const block = event.partial.content[event.contentIndex];
|
|
58
|
+
projector.toolStart(event.contentIndex, block?.type === "toolCall" ? block.name : "");
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "toolcall_delta":
|
|
62
|
+
projector?.toolDelta(event.contentIndex, event.delta);
|
|
63
|
+
break;
|
|
64
|
+
case "toolcall_end":
|
|
65
|
+
projector?.toolEnd(event.contentIndex, event.toolCall);
|
|
66
|
+
break;
|
|
67
|
+
case "done": {
|
|
68
|
+
projector ??= new LeakedThinkingProjector(out, event.message);
|
|
69
|
+
const content = projector.finish(event.message);
|
|
70
|
+
out.push({ type: "done", reason: event.reason, message: { ...event.message, content } });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case "error": {
|
|
74
|
+
projector ??= new LeakedThinkingProjector(out, event.error);
|
|
75
|
+
const content = projector.finish(event.error);
|
|
76
|
+
out.push({ type: "error", reason: event.reason, error: { ...event.error, content } });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// text_start/text_end/thinking_start/thinking_end are ignored: the
|
|
80
|
+
// projector owns block boundaries (matches wrapInbandToolStream).
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Inner ended via end(result) without a terminal event.
|
|
84
|
+
if (!out.done) {
|
|
85
|
+
const result = await inner.result();
|
|
86
|
+
projector ??= new LeakedThinkingProjector(out, result);
|
|
87
|
+
const content = projector.finish(result);
|
|
88
|
+
out.end({ ...result, content });
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (!out.done) out.fail(err);
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type OpenBlock = { index: number } | undefined;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Re-projects an inner stream's events into `out`, healing leaked reasoning out
|
|
101
|
+
* of the visible text channel while forwarding native thinking and tool calls.
|
|
102
|
+
*/
|
|
103
|
+
class LeakedThinkingProjector {
|
|
104
|
+
readonly #out: AssistantMessageEventStream;
|
|
105
|
+
readonly #healer = new StreamMarkupHealing({ pattern: "thinking" });
|
|
106
|
+
#partial: AssistantMessage;
|
|
107
|
+
#text: OpenBlock;
|
|
108
|
+
#thinking: OpenBlock;
|
|
109
|
+
/** Total visible text length fed to the healer, to replay any un-streamed tail in {@link finish}. */
|
|
110
|
+
#fedLen = 0;
|
|
111
|
+
/** Latest non-undefined text signature seen, stamped onto held-back text flushed later. */
|
|
112
|
+
#lastTextSignature: string | undefined;
|
|
113
|
+
/** Forwarded native tool calls, keyed by the inner stream's `contentIndex`. */
|
|
114
|
+
#toolBlocks = new Map<number, { index: number }>();
|
|
115
|
+
|
|
116
|
+
constructor(out: AssistantMessageEventStream, seed: AssistantMessage) {
|
|
117
|
+
this.#out = out;
|
|
118
|
+
this.#partial = { ...seed, content: [] };
|
|
119
|
+
this.#out.push({ type: "start", partial: this.#partial });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Feed a visible-text delta through the healer, splitting leaked fences live. */
|
|
123
|
+
text(delta: string, signature: string | undefined): void {
|
|
124
|
+
this.#fedLen += delta.length;
|
|
125
|
+
if (signature !== undefined) this.#lastTextSignature = signature;
|
|
126
|
+
this.#apply(this.#healer.feedEvents(delta), this.#lastTextSignature);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Forward a native thinking delta, preserving its signature. */
|
|
130
|
+
thinking(delta: string, signature: string | undefined): void {
|
|
131
|
+
const index = this.#openThinking();
|
|
132
|
+
const block = this.#partial.content[index] as ThinkingContent;
|
|
133
|
+
block.thinking += delta;
|
|
134
|
+
if (signature !== undefined) block.thinkingSignature = signature;
|
|
135
|
+
this.#out.push({ type: "thinking_delta", contentIndex: index, delta, partial: this.#partial });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Forward a native tool call's start, releasing any held-back text first. */
|
|
139
|
+
toolStart(srcIndex: number, name: string): void {
|
|
140
|
+
this.#apply(this.#healer.flushEvents(), this.#lastTextSignature);
|
|
141
|
+
this.#closeText();
|
|
142
|
+
this.#closeThinking();
|
|
143
|
+
const block: ToolCall = { type: "toolCall", id: "", name, arguments: {} };
|
|
144
|
+
this.#partial.content.push(block);
|
|
145
|
+
const index = this.#partial.content.length - 1;
|
|
146
|
+
this.#toolBlocks.set(srcIndex, { index });
|
|
147
|
+
this.#out.push({ type: "toolcall_start", contentIndex: index, partial: this.#partial });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
toolDelta(srcIndex: number, delta: string): void {
|
|
151
|
+
const entry = this.#toolBlocks.get(srcIndex);
|
|
152
|
+
if (!entry) return;
|
|
153
|
+
this.#out.push({ type: "toolcall_delta", contentIndex: entry.index, delta, partial: this.#partial });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
toolEnd(srcIndex: number, toolCall: ToolCall): void {
|
|
157
|
+
const entry = this.#toolBlocks.get(srcIndex);
|
|
158
|
+
if (entry) {
|
|
159
|
+
const block = this.#partial.content[entry.index] as ToolCall;
|
|
160
|
+
Object.assign(block, toolCall);
|
|
161
|
+
this.#out.push({ type: "toolcall_end", contentIndex: entry.index, toolCall: block, partial: this.#partial });
|
|
162
|
+
this.#toolBlocks.delete(srcIndex);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// `end` without a matching `start` — release held text, then forward whole.
|
|
166
|
+
this.#apply(this.#healer.flushEvents(), this.#lastTextSignature);
|
|
167
|
+
this.#closeText();
|
|
168
|
+
this.#closeThinking();
|
|
169
|
+
const block: ToolCall = { ...toolCall };
|
|
170
|
+
this.#partial.content.push(block);
|
|
171
|
+
const index = this.#partial.content.length - 1;
|
|
172
|
+
this.#out.push({ type: "toolcall_start", contentIndex: index, partial: this.#partial });
|
|
173
|
+
this.#out.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: this.#partial });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Finalize: replay any un-streamed visible-text tail from `message.content`,
|
|
178
|
+
* flush held-back fragments, close open blocks, and return the healed content.
|
|
179
|
+
*/
|
|
180
|
+
finish(message: AssistantMessage): AssistantMessage["content"] {
|
|
181
|
+
let fullText = "";
|
|
182
|
+
let tailSignature: string | undefined;
|
|
183
|
+
for (const block of message.content) {
|
|
184
|
+
if (block.type === "text") {
|
|
185
|
+
fullText += block.text;
|
|
186
|
+
tailSignature = block.textSignature;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (tailSignature !== undefined) this.#lastTextSignature = tailSignature;
|
|
190
|
+
if (fullText.length > this.#fedLen) {
|
|
191
|
+
this.#apply(this.#healer.feedEvents(fullText.slice(this.#fedLen)), this.#lastTextSignature);
|
|
192
|
+
}
|
|
193
|
+
this.#apply(this.#healer.flushEvents(), this.#lastTextSignature);
|
|
194
|
+
this.#closeText();
|
|
195
|
+
this.#closeThinking();
|
|
196
|
+
return this.#partial.content;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#apply(events: readonly StreamMarkupHealingEvent[], signature?: string): void {
|
|
200
|
+
for (const event of events) {
|
|
201
|
+
if (event.type === "text") this.#emitText(event.text, signature);
|
|
202
|
+
else if (event.type === "thinking") this.#emitHealedThinking(event.thinking);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#emitText(text: string, signature: string | undefined): void {
|
|
207
|
+
if (text.length === 0) return;
|
|
208
|
+
this.#closeThinking();
|
|
209
|
+
if (!this.#text) {
|
|
210
|
+
const block: TextContent =
|
|
211
|
+
signature === undefined ? { type: "text", text: "" } : { type: "text", text: "", textSignature: signature };
|
|
212
|
+
this.#partial.content.push(block);
|
|
213
|
+
this.#text = { index: this.#partial.content.length - 1 };
|
|
214
|
+
this.#out.push({ type: "text_start", contentIndex: this.#text.index, partial: this.#partial });
|
|
215
|
+
} else if (signature !== undefined) {
|
|
216
|
+
(this.#partial.content[this.#text.index] as TextContent).textSignature = signature;
|
|
217
|
+
}
|
|
218
|
+
const block = this.#partial.content[this.#text.index] as TextContent;
|
|
219
|
+
block.text += text;
|
|
220
|
+
this.#out.push({ type: "text_delta", contentIndex: this.#text.index, delta: text, partial: this.#partial });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Healed (leaked) thinking carries no signature, matching the source fence. */
|
|
224
|
+
#emitHealedThinking(text: string): void {
|
|
225
|
+
if (text.length === 0) return;
|
|
226
|
+
const index = this.#openThinking();
|
|
227
|
+
const block = this.#partial.content[index] as ThinkingContent;
|
|
228
|
+
block.thinking += text;
|
|
229
|
+
this.#out.push({ type: "thinking_delta", contentIndex: index, delta: text, partial: this.#partial });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#openThinking(): number {
|
|
233
|
+
this.#closeText();
|
|
234
|
+
if (!this.#thinking) {
|
|
235
|
+
this.#partial.content.push({ type: "thinking", thinking: "" });
|
|
236
|
+
this.#thinking = { index: this.#partial.content.length - 1 };
|
|
237
|
+
this.#out.push({ type: "thinking_start", contentIndex: this.#thinking.index, partial: this.#partial });
|
|
238
|
+
}
|
|
239
|
+
return this.#thinking.index;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#closeText(): void {
|
|
243
|
+
if (!this.#text) return;
|
|
244
|
+
const block = this.#partial.content[this.#text.index] as TextContent;
|
|
245
|
+
this.#out.push({ type: "text_end", contentIndex: this.#text.index, content: block.text, partial: this.#partial });
|
|
246
|
+
this.#text = undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#closeThinking(): void {
|
|
250
|
+
if (!this.#thinking) return;
|
|
251
|
+
const block = this.#partial.content[this.#thinking.index] as ThinkingContent;
|
|
252
|
+
this.#out.push({
|
|
253
|
+
type: "thinking_end",
|
|
254
|
+
contentIndex: this.#thinking.index,
|
|
255
|
+
content: block.thinking,
|
|
256
|
+
partial: this.#partial,
|
|
257
|
+
});
|
|
258
|
+
this.#thinking = undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|