pi-openai-toolkit 0.5.0 → 0.7.0
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/README.md +22 -33
- package/README_zh.md +22 -33
- package/package.json +8 -12
- package/src/config.ts +70 -51
- package/src/deferred-tool-carryover.ts +280 -0
- package/src/extension-runtime.ts +9 -1
- package/src/inline-compaction.ts +1264 -0
- package/src/payload-rewrite.ts +22 -16
- package/src/types.ts +94 -20
- package/tsconfig.check.json +3 -8
- package/extensions/reasoning-translation.ts +0 -2
- package/src/reasoning-translation/client.ts +0 -187
- package/src/reasoning-translation/extension.ts +0 -105
- package/src/reasoning-translation/persistence.ts +0 -73
- package/src/reasoning-translation/segmenter.ts +0 -60
- package/src/reasoning-translation/state.ts +0 -399
- package/src/reasoning-translation/types.ts +0 -64
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { MAX_SEGMENT_LENGTH } from "./types";
|
|
2
|
-
|
|
3
|
-
export type SegmentationResult = {
|
|
4
|
-
segments: string[];
|
|
5
|
-
remainder: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
function isNaturalBoundaryCharacter(character: string): boolean {
|
|
9
|
-
return "\n.!?。!?;;".includes(character);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function findLatestNaturalBoundary(source: string, limit: number): number | undefined {
|
|
13
|
-
const end = Math.min(limit, source.length);
|
|
14
|
-
for (let index = end - 1; index >= 0; index--) {
|
|
15
|
-
if (!isNaturalBoundaryCharacter(source[index])) continue;
|
|
16
|
-
let boundary = index + 1;
|
|
17
|
-
while (boundary < end && /\s/u.test(source[boundary])) boundary++;
|
|
18
|
-
return boundary;
|
|
19
|
-
}
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function addSegment(segments: string[], candidate: string): void {
|
|
24
|
-
if (candidate.trim().length > 0) segments.push(candidate);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Extract complete thinking segments without changing their source text. Natural
|
|
29
|
-
* boundaries are preferred; long unbroken text is cut at the hard UTF-16 limit.
|
|
30
|
-
*/
|
|
31
|
-
export function segmentThinkingSource(source: string, flush = false): SegmentationResult {
|
|
32
|
-
let remainder = source;
|
|
33
|
-
const segments: string[] = [];
|
|
34
|
-
|
|
35
|
-
while (remainder.length > 0) {
|
|
36
|
-
const boundary = findLatestNaturalBoundary(remainder, MAX_SEGMENT_LENGTH);
|
|
37
|
-
if (boundary !== undefined) {
|
|
38
|
-
addSegment(segments, remainder.slice(0, boundary));
|
|
39
|
-
remainder = remainder.slice(boundary);
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (remainder.length > MAX_SEGMENT_LENGTH) {
|
|
43
|
-
addSegment(segments, remainder.slice(0, MAX_SEGMENT_LENGTH));
|
|
44
|
-
remainder = remainder.slice(MAX_SEGMENT_LENGTH);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (flush && remainder.trim().length > 0) {
|
|
51
|
-
addSegment(segments, remainder);
|
|
52
|
-
remainder = "";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return { segments, remainder };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function flushThinkingSource(source: string): SegmentationResult {
|
|
59
|
-
return segmentThinkingSource(source, true);
|
|
60
|
-
}
|
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
|
|
3
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { parseModelSpec } from "../runtime";
|
|
5
|
-
import type { ReasoningTranslationConfig } from "../types";
|
|
6
|
-
import {
|
|
7
|
-
resolveTranslator,
|
|
8
|
-
translateSegment,
|
|
9
|
-
type ResolvedTranslator,
|
|
10
|
-
type TranslateSegmentResult,
|
|
11
|
-
} from "./client";
|
|
12
|
-
import {
|
|
13
|
-
createReasoningTranslationEntry,
|
|
14
|
-
hashThinkingMarkdown,
|
|
15
|
-
projectThinkingRuns,
|
|
16
|
-
replayReasoningTranslations,
|
|
17
|
-
type ThinkingRunProjection,
|
|
18
|
-
} from "./persistence";
|
|
19
|
-
import { flushThinkingSource, segmentThinkingSource } from "./segmenter";
|
|
20
|
-
import {
|
|
21
|
-
IDLE_FLUSH_MS,
|
|
22
|
-
REASONING_TRANSLATION_ENTRY_TYPE,
|
|
23
|
-
TRANSLATING_LABEL,
|
|
24
|
-
type ReasoningTranslationEntryV1,
|
|
25
|
-
} from "./types";
|
|
26
|
-
|
|
27
|
-
export type ReasoningTranslationStateOptions = {
|
|
28
|
-
resolveTranslator?: typeof resolveTranslator;
|
|
29
|
-
translateSegment?: typeof translateSegment;
|
|
30
|
-
setTimeout?: typeof globalThis.setTimeout;
|
|
31
|
-
clearTimeout?: typeof globalThis.clearTimeout;
|
|
32
|
-
onDisplayChanged?: () => void;
|
|
33
|
-
appendEntry?: (customType: string, data: ReasoningTranslationEntryV1) => void;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type ThinkingRunState = {
|
|
37
|
-
runKey: number;
|
|
38
|
-
sourceMarkdown: string;
|
|
39
|
-
pendingSource: string;
|
|
40
|
-
displayedText: string;
|
|
41
|
-
usage?: Usage;
|
|
42
|
-
idleTimer?: ReturnType<typeof setTimeout>;
|
|
43
|
-
/** A natural/idle/final flush requested before the translator was ready. */
|
|
44
|
-
flushRequested: boolean;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
type ActiveTranslation = {
|
|
48
|
-
generation: number;
|
|
49
|
-
sourceModelKey: string;
|
|
50
|
-
targetLanguage: string;
|
|
51
|
-
translator?: ResolvedTranslator;
|
|
52
|
-
translatorReady: boolean;
|
|
53
|
-
translatorFailed: boolean;
|
|
54
|
-
translatorPromise?: Promise<void>;
|
|
55
|
-
abortController: AbortController;
|
|
56
|
-
queue: Promise<void>;
|
|
57
|
-
runs: Map<number, ThinkingRunState>;
|
|
58
|
-
ctx: ExtensionContext;
|
|
59
|
-
finalized: boolean;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function isAssistantMessage(message: AgentMessage): message is AssistantMessage {
|
|
63
|
-
return message.role === "assistant";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function getModelKey(ctx: ExtensionContext): string | undefined {
|
|
67
|
-
return ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isEligible(ctx: ExtensionContext, config: ReasoningTranslationConfig): boolean {
|
|
71
|
-
const modelKey = getModelKey(ctx);
|
|
72
|
-
return (
|
|
73
|
-
ctx.mode === "tui" &&
|
|
74
|
-
config.enabled &&
|
|
75
|
-
modelKey !== undefined &&
|
|
76
|
-
config.models.includes(modelKey) &&
|
|
77
|
-
config.model !== undefined &&
|
|
78
|
-
parseModelSpec(config.model) !== undefined
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function preserveSourceWhitespace(source: string, translated: string): string {
|
|
83
|
-
const leading = source.match(/^\s*/u)?.[0] ?? "";
|
|
84
|
-
const trailing = source.match(/\s*$/u)?.[0] ?? "";
|
|
85
|
-
const core = translated.trim();
|
|
86
|
-
return `${leading}${core}${trailing}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function addUsage(left: Usage | undefined, right: Usage | undefined): Usage | undefined {
|
|
90
|
-
if (!left) return right;
|
|
91
|
-
if (!right) return left;
|
|
92
|
-
return {
|
|
93
|
-
input: left.input + right.input,
|
|
94
|
-
output: left.output + right.output,
|
|
95
|
-
cacheRead: left.cacheRead + right.cacheRead,
|
|
96
|
-
cacheWrite: left.cacheWrite + right.cacheWrite,
|
|
97
|
-
...(left.cacheWrite1h !== undefined || right.cacheWrite1h !== undefined
|
|
98
|
-
? { cacheWrite1h: (left.cacheWrite1h ?? 0) + (right.cacheWrite1h ?? 0) }
|
|
99
|
-
: {}),
|
|
100
|
-
...(left.reasoning !== undefined || right.reasoning !== undefined
|
|
101
|
-
? { reasoning: (left.reasoning ?? 0) + (right.reasoning ?? 0) }
|
|
102
|
-
: {}),
|
|
103
|
-
totalTokens: left.totalTokens + right.totalTokens,
|
|
104
|
-
cost: {
|
|
105
|
-
input: left.cost.input + right.cost.input,
|
|
106
|
-
output: left.cost.output + right.cost.output,
|
|
107
|
-
cacheRead: left.cost.cacheRead + right.cost.cacheRead,
|
|
108
|
-
cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
|
|
109
|
-
total: left.cost.total + right.cost.total,
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function findRunForContentIndex(runs: readonly ThinkingRunProjection[], contentIndex: number): ThinkingRunProjection | undefined {
|
|
115
|
-
let selected: ThinkingRunProjection | undefined;
|
|
116
|
-
for (const run of runs) {
|
|
117
|
-
if (run.runKey <= contentIndex) selected = run;
|
|
118
|
-
else break;
|
|
119
|
-
}
|
|
120
|
-
return selected;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Lifecycle-local state for one translation extension instance. It owns no Pi
|
|
125
|
-
* session data; persisted projections are rebuilt from the active branch.
|
|
126
|
-
*/
|
|
127
|
-
export class ReasoningTranslationState {
|
|
128
|
-
private readonly options: Required<Pick<ReasoningTranslationStateOptions, "resolveTranslator" | "translateSegment" | "setTimeout" | "clearTimeout">> &
|
|
129
|
-
Pick<ReasoningTranslationStateOptions, "onDisplayChanged" | "appendEntry">;
|
|
130
|
-
private generation = 0;
|
|
131
|
-
private active: ActiveTranslation | undefined;
|
|
132
|
-
private persisted = new Map<string, ReasoningTranslationEntryV1>();
|
|
133
|
-
private displayEnabled = false;
|
|
134
|
-
|
|
135
|
-
constructor(options: ReasoningTranslationStateOptions = {}) {
|
|
136
|
-
this.options = {
|
|
137
|
-
resolveTranslator: options.resolveTranslator ?? resolveTranslator,
|
|
138
|
-
translateSegment: options.translateSegment ?? translateSegment,
|
|
139
|
-
setTimeout: options.setTimeout ?? globalThis.setTimeout,
|
|
140
|
-
clearTimeout: options.clearTimeout ?? globalThis.clearTimeout,
|
|
141
|
-
onDisplayChanged: options.onDisplayChanged,
|
|
142
|
-
appendEntry: options.appendEntry,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
hydrate(ctx: ExtensionContext, config?: ReasoningTranslationConfig): void {
|
|
147
|
-
this.persisted = replayReasoningTranslations(ctx.sessionManager.getBranch());
|
|
148
|
-
this.displayEnabled = config ? isEligible(ctx, config) : false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
setDisplayEligibility(ctx: ExtensionContext, config: ReasoningTranslationConfig): void {
|
|
152
|
-
this.displayEnabled = isEligible(ctx, config);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
getPersistedDisplay(markdown: string): string | undefined {
|
|
156
|
-
if (!this.displayEnabled) return undefined;
|
|
157
|
-
return this.persisted.get(hashThinkingMarkdown(markdown))?.displayText;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
getActiveDisplay(markdown: string): string | undefined {
|
|
161
|
-
const active = this.active;
|
|
162
|
-
if (!active || active.ctx.mode !== "tui") return undefined;
|
|
163
|
-
for (const run of active.runs.values()) {
|
|
164
|
-
if (run.sourceMarkdown !== markdown) continue;
|
|
165
|
-
if (run.displayedText.length > 0) return run.displayedText;
|
|
166
|
-
if (!active.translatorFailed) return TRANSLATING_LABEL;
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
lookupDisplay(markdown: string): string | undefined {
|
|
172
|
-
return this.getActiveDisplay(markdown) ?? this.getPersistedDisplay(markdown);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
beginMessage(message: AgentMessage, ctx: ExtensionContext, config: ReasoningTranslationConfig): void {
|
|
176
|
-
const eligible = isEligible(ctx, config);
|
|
177
|
-
this.displayEnabled = eligible;
|
|
178
|
-
// message_start also fires for toolResult and user messages. They must not
|
|
179
|
-
// cancel a finished assistant translation before its FIFO queue can settle.
|
|
180
|
-
if (!isAssistantMessage(message)) return;
|
|
181
|
-
this.abortActive();
|
|
182
|
-
if (!eligible) return;
|
|
183
|
-
|
|
184
|
-
const sourceModelKey = getModelKey(ctx);
|
|
185
|
-
if (!sourceModelKey) return;
|
|
186
|
-
const generation = ++this.generation;
|
|
187
|
-
const abortController = new AbortController();
|
|
188
|
-
const active: ActiveTranslation = {
|
|
189
|
-
generation,
|
|
190
|
-
sourceModelKey,
|
|
191
|
-
targetLanguage: config.targetLanguage,
|
|
192
|
-
translatorReady: false,
|
|
193
|
-
translatorFailed: false,
|
|
194
|
-
abortController,
|
|
195
|
-
queue: Promise.resolve(),
|
|
196
|
-
runs: new Map(),
|
|
197
|
-
ctx,
|
|
198
|
-
finalized: false,
|
|
199
|
-
};
|
|
200
|
-
this.active = active;
|
|
201
|
-
|
|
202
|
-
if (ctx.signal) {
|
|
203
|
-
if (ctx.signal.aborted) {
|
|
204
|
-
this.abortActive();
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
ctx.signal.addEventListener("abort", () => {
|
|
208
|
-
if (this.active?.generation === generation) this.abortActive();
|
|
209
|
-
}, { once: true });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const resolve = this.options.resolveTranslator(ctx, config.model).then((result) => {
|
|
213
|
-
if (this.active?.generation !== generation) return;
|
|
214
|
-
if (!("model" in result) || !("provider" in result)) {
|
|
215
|
-
active.translatorFailed = true;
|
|
216
|
-
this.displayEnabled = false;
|
|
217
|
-
this.abortActive();
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
active.translator = result;
|
|
221
|
-
active.translatorReady = true;
|
|
222
|
-
for (const run of active.runs.values()) this.enqueuePending(active, run, run.flushRequested);
|
|
223
|
-
});
|
|
224
|
-
active.translatorPromise = resolve.catch(() => {
|
|
225
|
-
if (this.active?.generation === generation) {
|
|
226
|
-
active.translatorFailed = true;
|
|
227
|
-
this.displayEnabled = false;
|
|
228
|
-
this.abortActive();
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
handleMessageUpdate(message: AgentMessage, updateType: string, contentIndex: number): void {
|
|
234
|
-
const active = this.active;
|
|
235
|
-
if (!active || !isAssistantMessage(message) || active.ctx.mode !== "tui") return;
|
|
236
|
-
if (active.ctx.signal?.aborted) {
|
|
237
|
-
this.abortActive();
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const projections = projectThinkingRuns(message);
|
|
242
|
-
for (const projection of projections) this.updateRunSource(active, projection);
|
|
243
|
-
if (updateType === "thinking_end") {
|
|
244
|
-
const projection = findRunForContentIndex(projections, contentIndex);
|
|
245
|
-
if (projection) {
|
|
246
|
-
const run = active.runs.get(projection.runKey);
|
|
247
|
-
if (run) this.enqueuePending(active, run, true);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
finishMessage(message: AgentMessage): void {
|
|
253
|
-
if (isAssistantMessage(message) && (message.stopReason === "aborted" || message.stopReason === "error")) {
|
|
254
|
-
this.abortActive();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
const active = this.active;
|
|
258
|
-
if (!active || !isAssistantMessage(message) || active.finalized) return;
|
|
259
|
-
const projections = projectThinkingRuns(message);
|
|
260
|
-
for (const projection of projections) this.updateRunSource(active, projection);
|
|
261
|
-
for (const run of active.runs.values()) this.enqueuePending(active, run, true);
|
|
262
|
-
active.finalized = true;
|
|
263
|
-
|
|
264
|
-
const generation = active.generation;
|
|
265
|
-
void (active.translatorPromise ?? Promise.resolve())
|
|
266
|
-
.then(() => active.queue)
|
|
267
|
-
.then(() => {
|
|
268
|
-
if (this.active?.generation !== generation || !this.active || !active.translator) return;
|
|
269
|
-
for (const run of active.runs.values()) {
|
|
270
|
-
if (!run.sourceMarkdown || !run.displayedText) continue;
|
|
271
|
-
const entry = createReasoningTranslationEntry({
|
|
272
|
-
sourceMarkdown: run.sourceMarkdown,
|
|
273
|
-
displayText: run.displayedText,
|
|
274
|
-
targetLanguage: active.targetLanguage,
|
|
275
|
-
translator: { provider: active.translator.model.provider, model: active.translator.model.id },
|
|
276
|
-
usage: run.usage,
|
|
277
|
-
});
|
|
278
|
-
this.persisted.set(entry.sourceHash, entry);
|
|
279
|
-
try {
|
|
280
|
-
this.options.appendEntry?.(REASONING_TRANSLATION_ENTRY_TYPE, entry);
|
|
281
|
-
} catch {
|
|
282
|
-
// Persistence is additive and must not affect the completed response.
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
abortActive(): void {
|
|
289
|
-
const active = this.active;
|
|
290
|
-
if (!active) return;
|
|
291
|
-
for (const run of active.runs.values()) {
|
|
292
|
-
if (run.idleTimer !== undefined) this.options.clearTimeout(run.idleTimer);
|
|
293
|
-
}
|
|
294
|
-
active.abortController.abort();
|
|
295
|
-
this.generation++;
|
|
296
|
-
this.active = undefined;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private updateRunSource(active: ActiveTranslation, projection: ThinkingRunProjection): void {
|
|
300
|
-
let run = active.runs.get(projection.runKey);
|
|
301
|
-
if (!run) {
|
|
302
|
-
run = {
|
|
303
|
-
runKey: projection.runKey,
|
|
304
|
-
sourceMarkdown: "",
|
|
305
|
-
pendingSource: "",
|
|
306
|
-
displayedText: "",
|
|
307
|
-
flushRequested: false,
|
|
308
|
-
};
|
|
309
|
-
active.runs.set(projection.runKey, run);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (projection.sourceMarkdown === run.sourceMarkdown) return;
|
|
313
|
-
const previous = run.sourceMarkdown;
|
|
314
|
-
const suffix = projection.sourceMarkdown.startsWith(previous)
|
|
315
|
-
? projection.sourceMarkdown.slice(previous.length)
|
|
316
|
-
: projection.sourceMarkdown.slice(commonPrefixLength(previous, projection.sourceMarkdown));
|
|
317
|
-
run.sourceMarkdown = projection.sourceMarkdown;
|
|
318
|
-
if (suffix.length > 0) run.pendingSource += suffix;
|
|
319
|
-
this.scheduleIdleFlush(active, run);
|
|
320
|
-
if (active.translatorReady) this.enqueuePending(active, run, false);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
private scheduleIdleFlush(active: ActiveTranslation, run: ThinkingRunState): void {
|
|
324
|
-
if (run.idleTimer !== undefined) this.options.clearTimeout(run.idleTimer);
|
|
325
|
-
const generation = active.generation;
|
|
326
|
-
run.idleTimer = this.options.setTimeout(() => {
|
|
327
|
-
if (this.active?.generation !== generation) return;
|
|
328
|
-
this.enqueuePending(active, run, true);
|
|
329
|
-
}, IDLE_FLUSH_MS);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private enqueuePending(active: ActiveTranslation, run: ThinkingRunState, flush: boolean): void {
|
|
333
|
-
if (flush) run.flushRequested = true;
|
|
334
|
-
if (!active.translatorReady || !active.translator) return;
|
|
335
|
-
|
|
336
|
-
const shouldFlush = run.flushRequested;
|
|
337
|
-
const segmented = shouldFlush
|
|
338
|
-
? flushThinkingSource(run.pendingSource)
|
|
339
|
-
: segmentThinkingSource(run.pendingSource);
|
|
340
|
-
run.pendingSource = segmented.remainder;
|
|
341
|
-
if (shouldFlush && run.pendingSource.length === 0) run.flushRequested = false;
|
|
342
|
-
for (const segment of segmented.segments) {
|
|
343
|
-
const generation = active.generation;
|
|
344
|
-
active.queue = active.queue
|
|
345
|
-
.then(async () => {
|
|
346
|
-
if (this.active?.generation !== generation || active.abortController.signal.aborted) return;
|
|
347
|
-
const result = await this.options.translateSegment!({
|
|
348
|
-
translator: active.translator!,
|
|
349
|
-
targetLanguage: active.targetLanguage,
|
|
350
|
-
sourceSegment: segment,
|
|
351
|
-
signal: active.abortController.signal,
|
|
352
|
-
});
|
|
353
|
-
this.publishSegment(active, run, segment, result);
|
|
354
|
-
})
|
|
355
|
-
.catch(() => {
|
|
356
|
-
if (this.active?.generation === generation && !active.abortController.signal.aborted) {
|
|
357
|
-
this.publishSegment(active, run, segment, { ok: false, reason: "request-failed" });
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
if (shouldFlush && run.idleTimer !== undefined) {
|
|
362
|
-
this.options.clearTimeout(run.idleTimer);
|
|
363
|
-
run.idleTimer = undefined;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private publishSegment(
|
|
368
|
-
active: ActiveTranslation,
|
|
369
|
-
run: ThinkingRunState,
|
|
370
|
-
sourceSegment: string,
|
|
371
|
-
result: TranslateSegmentResult,
|
|
372
|
-
): void {
|
|
373
|
-
if (this.active?.generation !== active.generation) return;
|
|
374
|
-
if (result.ok) {
|
|
375
|
-
run.displayedText += preserveSourceWhitespace(sourceSegment, result.text);
|
|
376
|
-
run.usage = addUsage(run.usage, result.usage);
|
|
377
|
-
} else {
|
|
378
|
-
// A stale result is filtered by the generation check above. Any failure
|
|
379
|
-
// that reaches this method is therefore a per-segment fallback, including
|
|
380
|
-
// a provider-reported abort that did not abort this active session.
|
|
381
|
-
run.displayedText += sourceSegment;
|
|
382
|
-
}
|
|
383
|
-
this.options.onDisplayChanged?.();
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function commonPrefixLength(left: string, right: string): number {
|
|
388
|
-
const limit = Math.min(left.length, right.length);
|
|
389
|
-
let index = 0;
|
|
390
|
-
while (index < limit && left[index] === right[index]) index++;
|
|
391
|
-
return index;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export function isReasoningTranslationEligible(
|
|
395
|
-
ctx: ExtensionContext,
|
|
396
|
-
config: ReasoningTranslationConfig,
|
|
397
|
-
): boolean {
|
|
398
|
-
return isEligible(ctx, config);
|
|
399
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
-
import { REASONING_TRANSLATION_EXTENSION_ID } from "../types";
|
|
3
|
-
|
|
4
|
-
export const REASONING_TRANSLATION_ENTRY_TYPE = REASONING_TRANSLATION_EXTENSION_ID;
|
|
5
|
-
export const TRANSLATING_LABEL = "Translating…";
|
|
6
|
-
export const IDLE_FLUSH_MS = 800;
|
|
7
|
-
export const MAX_SEGMENT_LENGTH = 400;
|
|
8
|
-
export const MAX_TRANSLATION_OUTPUT_TOKENS = 2048;
|
|
9
|
-
/** Hard upper bound so an uncooperative provider cannot hold the FIFO forever. */
|
|
10
|
-
export const TRANSLATION_TIMEOUT_MS = 30_000;
|
|
11
|
-
|
|
12
|
-
export type ReasoningTranslationEntryV1 = {
|
|
13
|
-
version: 1;
|
|
14
|
-
sourceHash: string;
|
|
15
|
-
displayText: string;
|
|
16
|
-
targetLanguage: string;
|
|
17
|
-
translator: {
|
|
18
|
-
provider: string;
|
|
19
|
-
model: string;
|
|
20
|
-
};
|
|
21
|
-
usage?: Usage;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
29
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isFiniteNonNegativeNumber(value: unknown): value is number {
|
|
33
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isUsage(value: unknown): value is Usage {
|
|
37
|
-
if (!isRecord(value) || !isRecord(value.cost)) return false;
|
|
38
|
-
const cost = value.cost;
|
|
39
|
-
const numericFields = ["input", "output", "cacheRead", "cacheWrite", "totalTokens"] as const;
|
|
40
|
-
if (numericFields.some((field) => !isFiniteNonNegativeNumber(value[field]))) return false;
|
|
41
|
-
if (value.reasoning !== undefined && !isFiniteNonNegativeNumber(value.reasoning)) return false;
|
|
42
|
-
if (value.cacheWrite1h !== undefined && !isFiniteNonNegativeNumber(value.cacheWrite1h)) return false;
|
|
43
|
-
return ["input", "output", "cacheRead", "cacheWrite", "total"].every((field) =>
|
|
44
|
-
isFiniteNonNegativeNumber(cost[field]),
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function isReasoningTranslationEntry(value: unknown): value is ReasoningTranslationEntryV1 {
|
|
49
|
-
if (!isRecord(value) || value.version !== 1) return false;
|
|
50
|
-
if (typeof value.sourceHash !== "string" || !/^[a-f0-9]{64}$/iu.test(value.sourceHash)) return false;
|
|
51
|
-
if (!isNonEmptyString(value.displayText)) return false;
|
|
52
|
-
if (!isNonEmptyString(value.targetLanguage) || !isRecord(value.translator)) return false;
|
|
53
|
-
if (!isNonEmptyString(value.translator.provider) || !isNonEmptyString(value.translator.model)) return false;
|
|
54
|
-
return value.usage === undefined || isUsage(value.usage);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function cloneUsage(usage: Usage | undefined): Usage | undefined {
|
|
58
|
-
return usage
|
|
59
|
-
? {
|
|
60
|
-
...usage,
|
|
61
|
-
cost: { ...usage.cost },
|
|
62
|
-
}
|
|
63
|
-
: undefined;
|
|
64
|
-
}
|