@oh-my-pi/pi-ai 18.4.1 → 18.4.3
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 +19 -25
- package/dist/types/error/flags.d.ts +9 -2
- package/dist/types/images/openai-hosted.d.ts +1 -1
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -18
- package/dist/types/usage/commandcode.d.ts +4 -0
- package/dist/types/utils/event-stream.d.ts +7 -0
- package/dist/types/utils/openai-http.d.ts +7 -1
- package/package.json +6 -6
- package/src/dialect/thinking.ts +24 -6
- package/src/error/flags.ts +10 -3
- package/src/images/openai-hosted.ts +5 -2
- package/src/providers/amazon-bedrock.ts +58 -16
- package/src/providers/aws-eventstream.ts +4 -3
- package/src/providers/azure-openai-responses.ts +3 -4
- package/src/providers/cursor/exec-modern.ts +1 -1
- package/src/providers/cursor-pi-args.ts +13 -18
- package/src/providers/cursor.ts +179 -57
- package/src/providers/google-gemini-cli.ts +9 -2
- package/src/providers/google-shared.ts +8 -4
- package/src/providers/openai-codex-responses.ts +8 -2
- package/src/providers/openai-completions.ts +51 -36
- package/src/providers/openai-responses.ts +3 -4
- package/src/providers/openai-shared.ts +126 -18
- package/src/registry/engine/api-key.ts +8 -3
- package/src/stream.ts +25 -15
- package/src/usage/commandcode.ts +209 -0
- package/src/usage/registry.ts +3 -0
- package/src/utils/event-stream.ts +38 -2
- package/src/utils/openai-http.ts +10 -3
package/src/providers/cursor.ts
CHANGED
|
@@ -59,7 +59,6 @@ import {
|
|
|
59
59
|
GrepContentResultSchema,
|
|
60
60
|
GrepCountResultSchema,
|
|
61
61
|
GrepErrorSchema,
|
|
62
|
-
type GrepFileCount,
|
|
63
62
|
GrepFileCountSchema,
|
|
64
63
|
GrepFileMatchSchema,
|
|
65
64
|
GrepFilesResultSchema,
|
|
@@ -99,6 +98,7 @@ import {
|
|
|
99
98
|
McpToolResultSchema,
|
|
100
99
|
ModelDetailsSchema,
|
|
101
100
|
ReadErrorSchema,
|
|
101
|
+
ReadFileNotFoundSchema,
|
|
102
102
|
ReadMcpResourceErrorSchema,
|
|
103
103
|
type ReadMcpResourceExecResult,
|
|
104
104
|
ReadMcpResourceExecResultSchema,
|
|
@@ -233,7 +233,8 @@ import {
|
|
|
233
233
|
buildPiWriteError,
|
|
234
234
|
buildPiWriteRejected,
|
|
235
235
|
buildPiWriteResult,
|
|
236
|
-
|
|
236
|
+
cursorExecReadPath,
|
|
237
|
+
cursorRawReadPath,
|
|
237
238
|
omitUndefinedArgs,
|
|
238
239
|
piEscapeRegexLiteral,
|
|
239
240
|
piGrepSkip,
|
|
@@ -1654,39 +1655,93 @@ async function handleExecServerMessage(
|
|
|
1654
1655
|
const args = execMsg.message.value;
|
|
1655
1656
|
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1656
1657
|
const editOwned = isEditOwnedToolCallId(state, output, args.toolCallId);
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
//
|
|
1660
|
-
//
|
|
1661
|
-
//
|
|
1662
|
-
const
|
|
1663
|
-
const
|
|
1664
|
-
?
|
|
1658
|
+
// Cursor numbers raw content itself; StrReplace also writes it back.
|
|
1659
|
+
// Neither frame may receive hashline gutters or summarized bodies.
|
|
1660
|
+
// Edit-owned reads omit the extra transcript block owned by the edit card.
|
|
1661
|
+
// The handler needs the original negative offset to resolve it against
|
|
1662
|
+
// the file length; positive windows can be composed before dispatch.
|
|
1663
|
+
const negativeOffset = args.offset !== undefined && args.offset < 0;
|
|
1664
|
+
const composed = negativeOffset
|
|
1665
|
+
? cursorRawReadPath(args.path)
|
|
1666
|
+
: cursorExecReadPath(args.path, args.offset, args.limit);
|
|
1667
|
+
const handlerArgs =
|
|
1668
|
+
composed === null
|
|
1665
1669
|
? args
|
|
1666
|
-
: {
|
|
1667
|
-
|
|
1670
|
+
: {
|
|
1671
|
+
...args,
|
|
1672
|
+
path: composed,
|
|
1673
|
+
offset: negativeOffset ? args.offset : undefined,
|
|
1674
|
+
limit: negativeOffset ? args.limit : undefined,
|
|
1675
|
+
};
|
|
1668
1676
|
if (!editOwned) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1677
|
+
synthesizeCursorExecToolCall(
|
|
1678
|
+
output,
|
|
1679
|
+
stream,
|
|
1680
|
+
state,
|
|
1681
|
+
args.toolCallId,
|
|
1682
|
+
"read",
|
|
1683
|
+
negativeOffset
|
|
1684
|
+
? { path: composed, offset: args.offset, limit: args.limit }
|
|
1685
|
+
: { path: piReadDisplayPath(args.path, args.offset, args.limit) },
|
|
1686
|
+
);
|
|
1675
1687
|
}
|
|
1676
|
-
const { execResult } = await resolveExecHandler(
|
|
1688
|
+
const { execResult: readResult, toolResult } = await resolveExecHandler(
|
|
1677
1689
|
handlerArgs,
|
|
1678
1690
|
execHandlers?.read?.bind(execHandlers),
|
|
1679
1691
|
editOwned ? undefined : onToolResult,
|
|
1680
|
-
|
|
1692
|
+
result =>
|
|
1681
1693
|
buildReadResultFromToolResult(
|
|
1682
1694
|
args.path,
|
|
1683
|
-
|
|
1695
|
+
result,
|
|
1684
1696
|
args.offset !== undefined || args.limit !== undefined || piReadPathHasRange(args.path),
|
|
1685
1697
|
),
|
|
1686
1698
|
reason => buildReadRejectedResult(args.path, reason),
|
|
1687
1699
|
error => buildReadErrorResult(args.path, error),
|
|
1688
1700
|
editOwned ? null : { toolCallId: args.toolCallId, toolName: "read" },
|
|
1689
1701
|
);
|
|
1702
|
+
let execResult = readResult;
|
|
1703
|
+
// StrReplace writes the returned bytes back. A truncated raw read would
|
|
1704
|
+
// make every replacement below the read limit invisible to the server.
|
|
1705
|
+
if (
|
|
1706
|
+
editOwned &&
|
|
1707
|
+
args.offset === undefined &&
|
|
1708
|
+
args.limit === undefined &&
|
|
1709
|
+
!piReadPathHasRange(args.path) &&
|
|
1710
|
+
toolResult &&
|
|
1711
|
+
!toolResult.isError &&
|
|
1712
|
+
toolResultWasTruncated(toolResult)
|
|
1713
|
+
) {
|
|
1714
|
+
const details = toolResult.details;
|
|
1715
|
+
const source = details && typeof details === "object" && "meta" in details && details.meta;
|
|
1716
|
+
const path = source && typeof source === "object" && "source" in source && source.source;
|
|
1717
|
+
if (
|
|
1718
|
+
path &&
|
|
1719
|
+
typeof path === "object" &&
|
|
1720
|
+
"type" in path &&
|
|
1721
|
+
path.type === "path" &&
|
|
1722
|
+
"value" in path &&
|
|
1723
|
+
typeof path.value === "string"
|
|
1724
|
+
) {
|
|
1725
|
+
try {
|
|
1726
|
+
const content = await readEditMaterialization(path.value);
|
|
1727
|
+
execResult =
|
|
1728
|
+
content === null
|
|
1729
|
+
? buildReadErrorResult(
|
|
1730
|
+
args.path,
|
|
1731
|
+
`File exceeds ${EDIT_MATERIALIZATION_MAX_BYTES} bytes; StrReplace cannot load it whole. Use a line-range read and a targeted edit instead.`,
|
|
1732
|
+
)
|
|
1733
|
+
: buildReadResultFromToolResult(args.path, {
|
|
1734
|
+
...toolResult,
|
|
1735
|
+
content: [{ type: "text", text: content }],
|
|
1736
|
+
details: { fileSize: Buffer.byteLength(content, "utf8") },
|
|
1737
|
+
});
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
execResult = buildReadErrorResult(args.path, error instanceof Error ? error.message : String(error));
|
|
1740
|
+
}
|
|
1741
|
+
} else {
|
|
1742
|
+
execResult = buildReadErrorResult(args.path, "Unable to read the complete file for StrReplace");
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1690
1745
|
sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
|
|
1691
1746
|
return;
|
|
1692
1747
|
}
|
|
@@ -2842,9 +2897,50 @@ function readFileSizeFromDetails(toolResult: ToolResultMessage): number | undefi
|
|
|
2842
2897
|
return typeof fileSize === "number" && Number.isSafeInteger(fileSize) && fileSize >= 0 ? fileSize : undefined;
|
|
2843
2898
|
}
|
|
2844
2899
|
|
|
2900
|
+
/**
|
|
2901
|
+
* Largest file a native StrReplace materializes whole; matches the local
|
|
2902
|
+
* `read` tool's whole-file snapshot cap (`SNAPSHOT_MAX_BYTES`).
|
|
2903
|
+
*/
|
|
2904
|
+
const EDIT_MATERIALIZATION_MAX_BYTES = 4 * 1024 * 1024;
|
|
2905
|
+
|
|
2906
|
+
/**
|
|
2907
|
+
* Read a file for native StrReplace materialization, or `null` when it exceeds
|
|
2908
|
+
* {@link EDIT_MATERIALIZATION_MAX_BYTES}. The buffer is sized from the handle's
|
|
2909
|
+
* stat and reading stops one byte past it, so a file growing during the read
|
|
2910
|
+
* cannot exhaust memory.
|
|
2911
|
+
*
|
|
2912
|
+
* Throws when the file grew during the read while still under the cap.
|
|
2913
|
+
*/
|
|
2914
|
+
async function readEditMaterialization(filePath: string): Promise<string | null> {
|
|
2915
|
+
const handle = await fs.open(filePath, "r");
|
|
2916
|
+
try {
|
|
2917
|
+
const { size } = await handle.stat();
|
|
2918
|
+
if (size > EDIT_MATERIALIZATION_MAX_BYTES) return null;
|
|
2919
|
+
const buffer = Buffer.allocUnsafe(size + 1);
|
|
2920
|
+
let length = 0;
|
|
2921
|
+
while (length < buffer.length) {
|
|
2922
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
2923
|
+
if (bytesRead === 0) break;
|
|
2924
|
+
length += bytesRead;
|
|
2925
|
+
}
|
|
2926
|
+
if (length > size) {
|
|
2927
|
+
if (size === EDIT_MATERIALIZATION_MAX_BYTES) return null;
|
|
2928
|
+
throw new Error(`File changed while reading: ${filePath}`);
|
|
2929
|
+
}
|
|
2930
|
+
return buffer.toString("utf8", 0, length);
|
|
2931
|
+
} finally {
|
|
2932
|
+
await handle.close();
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2845
2936
|
function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage, rangeApplied = false) {
|
|
2846
2937
|
const text = toolResultToText(toolResult);
|
|
2847
2938
|
if (toolResult.isError) {
|
|
2939
|
+
if (/^Path '.*' not found$/.test(text)) {
|
|
2940
|
+
return create(ReadResultSchema, {
|
|
2941
|
+
result: { case: "fileNotFound", value: create(ReadFileNotFoundSchema, { path }) },
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2848
2944
|
return buildReadErrorResult(path, text || "Read failed");
|
|
2849
2945
|
}
|
|
2850
2946
|
// Counting the payload is only the file's length when the payload is the
|
|
@@ -2942,7 +3038,7 @@ function buildDeleteResultFromToolResult(path: string, toolResult: ToolResultMes
|
|
|
2942
3038
|
value: create(DeleteSuccessSchema, {
|
|
2943
3039
|
path,
|
|
2944
3040
|
deletedFile: path,
|
|
2945
|
-
fileSize: BigInt(0),
|
|
3041
|
+
fileSize: BigInt(readFileSizeFromDetails(toolResult) ?? 0),
|
|
2946
3042
|
prevContent: "",
|
|
2947
3043
|
}),
|
|
2948
3044
|
},
|
|
@@ -2973,7 +3069,14 @@ function buildShellResultFromToolResult(
|
|
|
2973
3069
|
) {
|
|
2974
3070
|
const output = toolResultToText(toolResult);
|
|
2975
3071
|
if (toolResult.isError) {
|
|
2976
|
-
|
|
3072
|
+
const details = toolResult.details;
|
|
3073
|
+
const code = details && typeof details === "object" && "exitCode" in details ? details.exitCode : undefined;
|
|
3074
|
+
return buildShellFailureResult(
|
|
3075
|
+
args.command,
|
|
3076
|
+
args.workingDirectory,
|
|
3077
|
+
output || "Shell failed",
|
|
3078
|
+
typeof code === "number" && Number.isInteger(code) ? code : 1,
|
|
3079
|
+
);
|
|
2977
3080
|
}
|
|
2978
3081
|
return create(ShellResultSchema, {
|
|
2979
3082
|
result: {
|
|
@@ -2991,14 +3094,14 @@ function buildShellResultFromToolResult(
|
|
|
2991
3094
|
});
|
|
2992
3095
|
}
|
|
2993
3096
|
|
|
2994
|
-
function buildShellFailureResult(command: string, workingDirectory: string, error: string) {
|
|
3097
|
+
function buildShellFailureResult(command: string, workingDirectory: string, error: string, exitCode = 1) {
|
|
2995
3098
|
return create(ShellResultSchema, {
|
|
2996
3099
|
result: {
|
|
2997
3100
|
case: "failure",
|
|
2998
3101
|
value: create(ShellFailureSchema, {
|
|
2999
3102
|
command,
|
|
3000
3103
|
workingDirectory,
|
|
3001
|
-
exitCode
|
|
3104
|
+
exitCode,
|
|
3002
3105
|
signal: "",
|
|
3003
3106
|
stdout: "",
|
|
3004
3107
|
stderr: error,
|
|
@@ -3101,16 +3204,16 @@ function buildGrepResultFromToolResult(
|
|
|
3101
3204
|
|
|
3102
3205
|
const outputMode = args.outputMode || "content";
|
|
3103
3206
|
const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
|
|
3104
|
-
const
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3207
|
+
const details = toolResult.details;
|
|
3208
|
+
const files =
|
|
3209
|
+
details && typeof details === "object" && "files" in details && Array.isArray(details.files)
|
|
3210
|
+
? details.files.filter((file): file is string => typeof file === "string")
|
|
3211
|
+
: [];
|
|
3108
3212
|
|
|
3109
3213
|
const workspaceKey = args.path || ".";
|
|
3110
3214
|
let unionResult: GrepUnionResult;
|
|
3111
3215
|
|
|
3112
3216
|
if (outputMode === "files_with_matches") {
|
|
3113
|
-
const files = lines;
|
|
3114
3217
|
unionResult = create(GrepUnionResultSchema, {
|
|
3115
3218
|
result: {
|
|
3116
3219
|
case: "files",
|
|
@@ -3128,20 +3231,21 @@ function buildGrepResultFromToolResult(
|
|
|
3128
3231
|
},
|
|
3129
3232
|
});
|
|
3130
3233
|
} else if (outputMode === "count") {
|
|
3131
|
-
const
|
|
3132
|
-
.
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3234
|
+
const fileMatches =
|
|
3235
|
+
details && typeof details === "object" && "fileMatches" in details && Array.isArray(details.fileMatches)
|
|
3236
|
+
? details.fileMatches
|
|
3237
|
+
: [];
|
|
3238
|
+
const counts = fileMatches
|
|
3239
|
+
.filter(
|
|
3240
|
+
(entry): entry is { path: string; count: number } =>
|
|
3241
|
+
entry !== null &&
|
|
3242
|
+
typeof entry === "object" &&
|
|
3243
|
+
"path" in entry &&
|
|
3244
|
+
typeof entry.path === "string" &&
|
|
3245
|
+
"count" in entry &&
|
|
3246
|
+
typeof entry.count === "number",
|
|
3247
|
+
)
|
|
3248
|
+
.map(({ path, count }) => create(GrepFileCountSchema, { file: path, count }));
|
|
3145
3249
|
const totalMatches = counts.reduce((sum, entry) => sum + entry.count, 0);
|
|
3146
3250
|
unionResult = create(GrepUnionResultSchema, {
|
|
3147
3251
|
result: {
|
|
@@ -3159,22 +3263,40 @@ function buildGrepResultFromToolResult(
|
|
|
3159
3263
|
} else {
|
|
3160
3264
|
const matchMap = new Map<string, Array<{ line: number; content: string; isContextLine: boolean }>>();
|
|
3161
3265
|
let totalMatchedLines = 0;
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
const
|
|
3167
|
-
if (
|
|
3266
|
+
const directories = new Map<number, string>();
|
|
3267
|
+
let currentFile = files.length === 1 ? files[0] : undefined;
|
|
3268
|
+
|
|
3269
|
+
for (const line of text.split("\n")) {
|
|
3270
|
+
const header = /^(#+) (.+)$/.exec(line);
|
|
3271
|
+
if (header) {
|
|
3272
|
+
const depth = header[1]!.length;
|
|
3273
|
+
const name = header[2]!;
|
|
3274
|
+
for (const level of directories.keys()) {
|
|
3275
|
+
if (level >= depth) directories.delete(level);
|
|
3276
|
+
}
|
|
3277
|
+
const parent = directories.get(depth - 1);
|
|
3278
|
+
const candidate = parent ? `${parent}/${name}` : name;
|
|
3279
|
+
if (name.endsWith("/")) {
|
|
3280
|
+
directories.set(depth, candidate.slice(0, -1));
|
|
3281
|
+
currentFile = undefined;
|
|
3282
|
+
} else {
|
|
3283
|
+
currentFile = files.includes(candidate) ? candidate : candidate.replace(/#[0-9a-f]{4,}$/i, "");
|
|
3284
|
+
}
|
|
3168
3285
|
continue;
|
|
3169
3286
|
}
|
|
3170
|
-
const
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
matchMap.set(file, list);
|
|
3175
|
-
if (!isContextLine) {
|
|
3176
|
-
totalMatchedLines += 1;
|
|
3287
|
+
const singleHeader = /^\[(.+)#[0-9a-f]{4,}\]$/i.exec(line);
|
|
3288
|
+
if (singleHeader) {
|
|
3289
|
+
currentFile = files[0] ?? singleHeader[1]!;
|
|
3290
|
+
continue;
|
|
3177
3291
|
}
|
|
3292
|
+
const match = /^([* ])(\d+)[:|](.*)$/.exec(line);
|
|
3293
|
+
if (!match || !currentFile) continue;
|
|
3294
|
+
const [, marker, lineNumber, content] = match;
|
|
3295
|
+
const isContextLine = marker === " ";
|
|
3296
|
+
const list = matchMap.get(currentFile) ?? [];
|
|
3297
|
+
list.push({ line: Number(lineNumber), content, isContextLine });
|
|
3298
|
+
matchMap.set(currentFile, list);
|
|
3299
|
+
if (!isContextLine) totalMatchedLines++;
|
|
3178
3300
|
}
|
|
3179
3301
|
|
|
3180
3302
|
const matches = Array.from(matchMap.entries()).map(([file, matches]) =>
|
|
@@ -751,9 +751,16 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
751
751
|
const responseSignal = options?.signal
|
|
752
752
|
? AbortSignal.any([options.signal, responseAbortController.signal])
|
|
753
753
|
: responseAbortController.signal;
|
|
754
|
+
const onSseEvent = options?.onSseEvent;
|
|
754
755
|
const chunks = iterateWithIdleTimeout(
|
|
755
|
-
|
|
756
|
-
|
|
756
|
+
// Attach the observer only when a diagnostic listener exists: any
|
|
757
|
+
// observer turns on per-line raw capture in `readSseJson`.
|
|
758
|
+
readSseJson<CloudCodeAssistResponseChunk>(
|
|
759
|
+
activeResponse.body,
|
|
760
|
+
responseSignal,
|
|
761
|
+
onSseEvent
|
|
762
|
+
? event => onSseEvent({ event: event.event, data: event.data, raw: [...event.raw] }, model)
|
|
763
|
+
: undefined,
|
|
757
764
|
),
|
|
758
765
|
{
|
|
759
766
|
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { scheduler } from "node:timers/promises";
|
|
6
6
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
7
|
-
import { readSseJson } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { readSseJson, type SseEventObserver } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { renderDemotedThinking } from "../dialect/demotion";
|
|
9
9
|
import { ThinkingFenceStripper } from "../dialect/thinking-fence-strip";
|
|
10
10
|
import * as AIError from "../error";
|
|
@@ -1014,13 +1014,17 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
1014
1014
|
let body = await openStream();
|
|
1015
1015
|
stream.push({ type: "start", partial: output });
|
|
1016
1016
|
|
|
1017
|
+
// Attach the observer only when a diagnostic listener exists: any
|
|
1018
|
+
// observer turns on per-line raw capture in `readSseJson`.
|
|
1019
|
+
const onSseEvent = options?.onSseEvent;
|
|
1020
|
+
const sseObserver: SseEventObserver | undefined = onSseEvent
|
|
1021
|
+
? event => onSseEvent({ event: event.event, data: event.data, raw: [...event.raw] }, model)
|
|
1022
|
+
: undefined;
|
|
1017
1023
|
// Gemini occasionally finishes with `finishReason: STOP` while emitting only an empty
|
|
1018
1024
|
// text part and no tool call. Delivered as-is the agent receives a blank message and
|
|
1019
1025
|
// silently halts mid-task, so retry a bounded number of times before giving up.
|
|
1020
1026
|
for (let emptyAttempt = 0; ; emptyAttempt++) {
|
|
1021
|
-
const googleStream = readSseJson<GenerateContentResponse>(body, options?.signal,
|
|
1022
|
-
options?.onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, model),
|
|
1023
|
-
);
|
|
1027
|
+
const googleStream = readSseJson<GenerateContentResponse>(body, options?.signal, sseObserver);
|
|
1024
1028
|
await consumeGoogleStream({
|
|
1025
1029
|
googleStream,
|
|
1026
1030
|
output,
|
|
@@ -4730,8 +4730,14 @@ async function openCodexSseEventStream(
|
|
|
4730
4730
|
if (!response.body) {
|
|
4731
4731
|
throw new CodexProviderStreamError("No response body", false);
|
|
4732
4732
|
}
|
|
4733
|
-
|
|
4734
|
-
|
|
4733
|
+
// Attach the observer only when a diagnostic listener exists: any observer
|
|
4734
|
+
// turns on per-line raw capture in `readSseJson`.
|
|
4735
|
+
return readSseJson<Record<string, unknown>>(
|
|
4736
|
+
response.body,
|
|
4737
|
+
signal,
|
|
4738
|
+
onSseEvent
|
|
4739
|
+
? event => onSseEvent({ event: event.event, data: event.data, raw: [...event.raw] }, undefined)
|
|
4740
|
+
: undefined,
|
|
4735
4741
|
);
|
|
4736
4742
|
}
|
|
4737
4743
|
|
|
@@ -4,7 +4,13 @@ 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
6
|
import { clinePassClientHeaders } from "@oh-my-pi/pi-catalog/wire/cline-pass";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
$env,
|
|
9
|
+
logger,
|
|
10
|
+
parseStreamingJson,
|
|
11
|
+
parseStreamingJsonThrottled,
|
|
12
|
+
type ServerSentEvent,
|
|
13
|
+
} from "@oh-my-pi/pi-utils";
|
|
8
14
|
import { renderDemotedThinking } from "../dialect/demotion";
|
|
9
15
|
import * as AIError from "../error";
|
|
10
16
|
import { getKimiCommonHeaders } from "../registry/oauth/kimi";
|
|
@@ -16,7 +22,6 @@ import type {
|
|
|
16
22
|
MessageAttribution,
|
|
17
23
|
Model,
|
|
18
24
|
ProviderSessionState,
|
|
19
|
-
RawSseEvent,
|
|
20
25
|
ServiceTier,
|
|
21
26
|
StopReason,
|
|
22
27
|
StreamFunction,
|
|
@@ -800,28 +805,29 @@ const streamOpenAICompletionsOnce = (
|
|
|
800
805
|
// Track the OpenAI `[DONE]` sentinel independently of `onSseEvent`: it is
|
|
801
806
|
// the streaming protocol's terminal signal, so a stream that ends with it
|
|
802
807
|
// completed by server agreement even when no `finish_reason` chunk arrived.
|
|
808
|
+
// It arrives through `onDoneSentinel`, so the diagnostic observer below
|
|
809
|
+
// stays unset (and raw wire-line capture off) when nobody listens.
|
|
803
810
|
let sawDoneSentinel = false;
|
|
804
|
-
const rawSseObserver =
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
}
|
|
811
|
+
const rawSseObserver = onSseEvent
|
|
812
|
+
? (event: ServerSentEvent) => {
|
|
813
|
+
if (!event.event && event.data && event.data !== "[DONE]") {
|
|
814
|
+
try {
|
|
815
|
+
const parsed = JSON.parse(event.data);
|
|
816
|
+
const resolvedEvent =
|
|
817
|
+
typeof parsed.type === "string"
|
|
818
|
+
? parsed.type
|
|
819
|
+
: typeof parsed.object === "string"
|
|
820
|
+
? parsed.object
|
|
821
|
+
: null;
|
|
822
|
+
if (resolvedEvent) {
|
|
823
|
+
event.event = resolvedEvent;
|
|
824
|
+
event.raw = [`event: ${resolvedEvent}`, ...event.raw];
|
|
825
|
+
}
|
|
826
|
+
} catch {}
|
|
827
|
+
}
|
|
828
|
+
onSseEvent({ event: event.event, data: event.data, raw: [...event.raw] }, model);
|
|
821
829
|
}
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
};
|
|
830
|
+
: undefined;
|
|
825
831
|
// Assigned once the block helpers exist (they are scoped to the `try`);
|
|
826
832
|
// the catch handler uses it to close open blocks before emitting the
|
|
827
833
|
// terminal error so both exit paths obey the same block lifecycle.
|
|
@@ -931,6 +937,9 @@ const streamOpenAICompletionsOnce = (
|
|
|
931
937
|
// bounds every attempt and backoff sleep — retries cannot
|
|
932
938
|
// extend the deadline.
|
|
933
939
|
onSseEvent: rawSseObserver,
|
|
940
|
+
onDoneSentinel: () => {
|
|
941
|
+
sawDoneSentinel = true;
|
|
942
|
+
},
|
|
934
943
|
});
|
|
935
944
|
// Disarm the first-event watchdog as soon as headers arrive — a slow
|
|
936
945
|
// onResponse callback must not abort an already-connected stream.
|
|
@@ -1030,9 +1039,19 @@ const streamOpenAICompletionsOnce = (
|
|
|
1030
1039
|
};
|
|
1031
1040
|
let currentBlock: OpenAIStreamBlock | undefined;
|
|
1032
1041
|
let messageThoughtSignature: GeminiMessageThoughtSignature | undefined;
|
|
1042
|
+
// Content blocks are append-only for the lifetime of the stream, so each
|
|
1043
|
+
// block's index is stable once pushed. Map block → index to keep the
|
|
1044
|
+
// per-delta contentIndex lookup O(1): a linear `indexOf` per delta turns a
|
|
1045
|
+
// long turn (many blocks × many deltas) quadratic, as openai-shared's
|
|
1046
|
+
// Responses decoder documents (issue #10605).
|
|
1047
|
+
const contentIndexByBlock = new Map<OpenAIStreamBlock, number>();
|
|
1048
|
+
const pushContentBlock = (block: OpenAIStreamBlock): void => {
|
|
1049
|
+
contentIndexByBlock.set(block, output.content.length);
|
|
1050
|
+
output.content.push(block);
|
|
1051
|
+
};
|
|
1033
1052
|
const blockIndex = (block: OpenAIStreamBlock | undefined): number => {
|
|
1034
1053
|
if (!block) return Math.max(0, output.content.length - 1);
|
|
1035
|
-
return output.content.indexOf(block);
|
|
1054
|
+
return contentIndexByBlock.get(block) ?? output.content.indexOf(block);
|
|
1036
1055
|
};
|
|
1037
1056
|
const finishToolCallBlock = (block: ToolCallStreamBlock): void => {
|
|
1038
1057
|
if (block.partialArgs === undefined) return;
|
|
@@ -1087,11 +1106,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
1087
1106
|
if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
|
|
1088
1107
|
finishPendingToolCallBlocks();
|
|
1089
1108
|
};
|
|
1090
|
-
const appendText = (
|
|
1091
|
-
message: AssistantMessage,
|
|
1092
|
-
eventStream: AssistantMessageEventStream,
|
|
1093
|
-
text: string,
|
|
1094
|
-
): void => {
|
|
1109
|
+
const appendText = (text: string): void => {
|
|
1095
1110
|
if (currentBlock?.type !== "text") {
|
|
1096
1111
|
// Leave toolCall blocks pending across text transitions: chunks after
|
|
1097
1112
|
// the first typically carry only `index`, so a finished (de-registered)
|
|
@@ -1099,15 +1114,15 @@ const streamOpenAICompletionsOnce = (
|
|
|
1099
1114
|
// resume. The stream-end sweep finalizes pending calls.
|
|
1100
1115
|
if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
|
|
1101
1116
|
currentBlock = { type: "text", text: "" };
|
|
1102
|
-
|
|
1103
|
-
|
|
1117
|
+
pushContentBlock(currentBlock);
|
|
1118
|
+
stream.push({ type: "text_start", contentIndex: blockIndex(currentBlock), partial: output });
|
|
1104
1119
|
}
|
|
1105
1120
|
currentBlock.text += text;
|
|
1106
|
-
|
|
1121
|
+
stream.push({
|
|
1107
1122
|
type: "text_delta",
|
|
1108
1123
|
contentIndex: blockIndex(currentBlock),
|
|
1109
1124
|
delta: text,
|
|
1110
|
-
partial:
|
|
1125
|
+
partial: output,
|
|
1111
1126
|
});
|
|
1112
1127
|
};
|
|
1113
1128
|
const openThinkingBlock = (signature?: string): ThinkingContent => {
|
|
@@ -1116,7 +1131,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
1116
1131
|
if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
|
|
1117
1132
|
const block: ThinkingContent = { type: "thinking", thinking: "", thinkingSignature: signature };
|
|
1118
1133
|
currentBlock = block;
|
|
1119
|
-
|
|
1134
|
+
pushContentBlock(block);
|
|
1120
1135
|
stream.push({ type: "thinking_start", contentIndex: blockIndex(block), partial: output });
|
|
1121
1136
|
return block;
|
|
1122
1137
|
};
|
|
@@ -1177,7 +1192,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
1177
1192
|
const appendTextDelta = (text: string): void => {
|
|
1178
1193
|
if (!text) return;
|
|
1179
1194
|
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
1180
|
-
appendText(
|
|
1195
|
+
appendText(text);
|
|
1181
1196
|
};
|
|
1182
1197
|
// Tracks the last full cumulative reasoning snapshot per signature (the
|
|
1183
1198
|
// reasoning field name) so dedup survives block transitions. Required
|
|
@@ -1249,7 +1264,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
1249
1264
|
};
|
|
1250
1265
|
block.arguments = parseStreamingJson(call.arguments);
|
|
1251
1266
|
currentBlock = block;
|
|
1252
|
-
|
|
1267
|
+
pushContentBlock(block);
|
|
1253
1268
|
stream.push({ type: "toolcall_start", contentIndex: blockIndex(block), partial: output });
|
|
1254
1269
|
stream.push({
|
|
1255
1270
|
type: "toolcall_delta",
|
|
@@ -1473,7 +1488,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
1473
1488
|
if (streamIndex !== undefined) toolCallBlockByIndex.set(streamIndex, block);
|
|
1474
1489
|
pendingToolCallBlocks.push(block);
|
|
1475
1490
|
currentBlock = block;
|
|
1476
|
-
|
|
1491
|
+
pushContentBlock(block);
|
|
1477
1492
|
stream.push({
|
|
1478
1493
|
type: "toolcall_start",
|
|
1479
1494
|
contentIndex: blockIndex(block),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
|
-
import { $flag, logger, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $flag, logger, type ServerSentEvent, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import * as AIError from "../error";
|
|
4
4
|
import { getEnvApiKey } from "../stream";
|
|
5
5
|
import type {
|
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
Model,
|
|
10
10
|
OpenAICompat,
|
|
11
11
|
ProviderSessionState,
|
|
12
|
-
RawSseEvent,
|
|
13
12
|
ServiceTier,
|
|
14
13
|
StreamFunction,
|
|
15
14
|
StreamOptions,
|
|
@@ -455,7 +454,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
455
454
|
const { requestAbortController, requestSignal } = abortTracker;
|
|
456
455
|
const onSseEvent = options?.onSseEvent;
|
|
457
456
|
const rawSseObserver = onSseEvent
|
|
458
|
-
? (event:
|
|
457
|
+
? (event: ServerSentEvent) => {
|
|
459
458
|
if (!event.event && event.data && event.data !== "[DONE]") {
|
|
460
459
|
try {
|
|
461
460
|
const parsed = JSON.parse(event.data);
|
|
@@ -471,7 +470,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
471
470
|
}
|
|
472
471
|
} catch {}
|
|
473
472
|
}
|
|
474
|
-
onSseEvent(event, model);
|
|
473
|
+
onSseEvent({ event: event.event, data: event.data, raw: [...event.raw] }, model);
|
|
475
474
|
}
|
|
476
475
|
: undefined;
|
|
477
476
|
|