@oh-my-pi/pi-ai 17.2.12 → 17.2.13
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 -0
- package/dist/types/error/aws.d.ts +5 -1
- package/dist/types/providers/aws-credentials.d.ts +4 -3
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +14 -0
- package/dist/types/providers/openai-shared.d.ts +9 -1
- package/dist/types/types.d.ts +7 -0
- package/dist/types/usage/cursor.d.ts +11 -0
- package/dist/types/utils/block-symbols.d.ts +12 -0
- package/package.json +5 -5
- package/src/dialect/owned-stream.ts +3 -0
- package/src/error/aws.ts +5 -1
- package/src/providers/amazon-bedrock.ts +38 -0
- package/src/providers/aws-credentials.ts +222 -29
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +22 -0
- package/src/providers/cursor.ts +81 -1
- package/src/providers/google-gemini-cli.ts +49 -15
- package/src/providers/google-shared.ts +7 -1
- package/src/providers/openai-codex/request-transformer.ts +38 -17
- package/src/providers/openai-codex-responses.ts +2 -3
- package/src/providers/openai-responses.ts +4 -0
- package/src/providers/openai-shared.ts +55 -1
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/register-builtins.ts +18 -14
- package/src/registry/aws.ts +13 -6
- package/src/registry/oauth/callback-server.ts +93 -5
- package/src/stream.ts +1 -0
- package/src/types.ts +7 -0
- package/src/usage/cursor.ts +174 -42
- package/src/usage/kimi.ts +29 -5
- package/src/usage/openai-codex-reset.ts +2 -1
- package/src/usage/openai-codex.ts +2 -1
- package/src/usage/zai.ts +2 -1
- package/src/utils/aws-profile.ts +39 -1
- package/src/utils/block-symbols.ts +18 -0
- package/src/utils/leaked-thinking-stream.ts +3 -0
- package/src/utils/openrouter-headers.ts +3 -3
package/src/providers/cursor.ts
CHANGED
|
@@ -210,6 +210,7 @@ import {
|
|
|
210
210
|
buildPiWriteError,
|
|
211
211
|
buildPiWriteRejected,
|
|
212
212
|
buildPiWriteResult,
|
|
213
|
+
omitUndefinedArgs,
|
|
213
214
|
piEscapeRegexLiteral,
|
|
214
215
|
piGrepSkip,
|
|
215
216
|
piJoinPath,
|
|
@@ -223,6 +224,67 @@ import {
|
|
|
223
224
|
export const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
224
225
|
export const CURSOR_CLIENT_VERSION = "cli-2026.07.23-e383d2b";
|
|
225
226
|
|
|
227
|
+
/**
|
|
228
|
+
* HTTP/1 connection-specific headers that HTTP/2 forbids. Node's `http2.request()`
|
|
229
|
+
* throws `ERR_HTTP2_INVALID_CONNECTION_HEADERS` on these rather than dropping
|
|
230
|
+
* them, so a caller sending one would kill the request outright.
|
|
231
|
+
*/
|
|
232
|
+
const HTTP2_FORBIDDEN_HEADERS = new Set([
|
|
233
|
+
"connection",
|
|
234
|
+
"keep-alive",
|
|
235
|
+
"proxy-connection",
|
|
236
|
+
"transfer-encoding",
|
|
237
|
+
"upgrade",
|
|
238
|
+
"http2-settings",
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Header names the Cursor request sets for itself. A caller copy in ANY casing
|
|
243
|
+
* has to go: the spread below adds the fixed lower-case name regardless, and two
|
|
244
|
+
* spellings of one field are a duplicate rather than an override.
|
|
245
|
+
*/
|
|
246
|
+
const CURSOR_RESERVED_HEADERS = new Set([
|
|
247
|
+
"content-type",
|
|
248
|
+
"connect-protocol-version",
|
|
249
|
+
"te",
|
|
250
|
+
"authorization",
|
|
251
|
+
"x-ghost-mode",
|
|
252
|
+
"x-cursor-client-version",
|
|
253
|
+
"x-cursor-client-type",
|
|
254
|
+
"x-request-id",
|
|
255
|
+
// Transport-owned even though this request never sets it: node's http2 client
|
|
256
|
+
// suppresses the `:authority` it derives from the URL when a plain `host`
|
|
257
|
+
// header is present, so a caller value here silently retargets the request at
|
|
258
|
+
// a different virtual host.
|
|
259
|
+
"host",
|
|
260
|
+
// The Connect body is streamed after the headers (initial frame, heartbeats,
|
|
261
|
+
// tool responses), so no caller-supplied length can describe it and an HTTP/2
|
|
262
|
+
// peer resets the stream once the body diverges.
|
|
263
|
+
"content-length",
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Reduce caller-supplied headers to what this HTTP/2 request can legally carry.
|
|
268
|
+
*
|
|
269
|
+
* Everything is lower-cased, because HTTP/2 field names are lower-case and node
|
|
270
|
+
* compares them that way. A caller `Authorization` next to the fixed
|
|
271
|
+
* `authorization` does not lose to it, it DUPLICATES it, and node throws
|
|
272
|
+
* `ERR_HTTP2_HEADER_SINGLE_VALUE` before the request goes out. Same for a `TE`
|
|
273
|
+
* that is not `trailers`. Node throws on all three classes here rather than
|
|
274
|
+
* ignoring them, so a miss turns a harmless header into a dead request.
|
|
275
|
+
*/
|
|
276
|
+
function sanitizeCursorCallerHeaders(headers: Record<string, string> | undefined): Record<string, string> {
|
|
277
|
+
const sanitized: Record<string, string> = {};
|
|
278
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
279
|
+
const field = name.toLowerCase();
|
|
280
|
+
if (field.startsWith(":")) continue;
|
|
281
|
+
if (HTTP2_FORBIDDEN_HEADERS.has(field)) continue;
|
|
282
|
+
if (CURSOR_RESERVED_HEADERS.has(field)) continue;
|
|
283
|
+
sanitized[field] = value;
|
|
284
|
+
}
|
|
285
|
+
return sanitized;
|
|
286
|
+
}
|
|
287
|
+
|
|
226
288
|
const CURSOR_PROXY_TUNNEL_TIMEOUT_MS = 30_000;
|
|
227
289
|
|
|
228
290
|
/**
|
|
@@ -545,7 +607,22 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
545
607
|
|
|
546
608
|
const baseUrl = model.baseUrl || CURSOR_API_URL;
|
|
547
609
|
const requestPath = "/agent.v1.AgentService/Run";
|
|
610
|
+
// Caller headers are additive, and are spread FIRST so the protocol
|
|
611
|
+
// framing, auth, and request id below always win. Cursor built this map
|
|
612
|
+
// from scratch and never read `options.headers`, so tracing/attribution
|
|
613
|
+
// headers set by a caller (or a `before_provider_headers` extension) were
|
|
614
|
+
// silently dropped here while working on other providers.
|
|
615
|
+
//
|
|
616
|
+
// Two classes are stripped because node's http2 client THROWS on them
|
|
617
|
+
// rather than ignoring them, which would turn a harmless header into a
|
|
618
|
+
// dead request: pseudo-headers, which belong to the transport, and the
|
|
619
|
+
// HTTP/1 connection-specific headers HTTP/2 forbids outright
|
|
620
|
+
// (ERR_HTTP2_INVALID_CONNECTION_HEADERS). `te` needs no filtering here —
|
|
621
|
+
// HTTP/2 allows it only as `trailers`, which is exactly what the fixed
|
|
622
|
+
// set below re-applies over anything a caller sent.
|
|
623
|
+
const callerHeaders = sanitizeCursorCallerHeaders(options?.headers);
|
|
548
624
|
const requestHeaders = {
|
|
625
|
+
...callerHeaders,
|
|
549
626
|
":method": "POST",
|
|
550
627
|
":path": requestPath,
|
|
551
628
|
"content-type": "application/connect+proto",
|
|
@@ -3592,11 +3669,14 @@ export function synthesizeCursorExecToolCall(
|
|
|
3592
3669
|
): void {
|
|
3593
3670
|
endCurrentTextBlock(output, stream, state);
|
|
3594
3671
|
endCurrentThinkingBlock(output, stream, state);
|
|
3672
|
+
// Exec-frame translators often write `optional: value || undefined`. A
|
|
3673
|
+
// present `undefined` fails ArkType optional-field validation; drop those
|
|
3674
|
+
// keys so the transcript block matches what a model-native call would omit.
|
|
3595
3675
|
const block: ToolCallState = {
|
|
3596
3676
|
type: "toolCall",
|
|
3597
3677
|
id: toolCallId,
|
|
3598
3678
|
name: toolName,
|
|
3599
|
-
arguments: args,
|
|
3679
|
+
arguments: omitUndefinedArgs(args),
|
|
3600
3680
|
[kStreamingBlockIndex]: output.content.length,
|
|
3601
3681
|
[kStreamingBlockKind]: "cursor-exec",
|
|
3602
3682
|
[kCursorExecResolved]: true,
|
|
@@ -31,7 +31,7 @@ import { normalizeSystemPrompts } from "../utils";
|
|
|
31
31
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
32
32
|
import { extractGoogleValidationUrl, formatGoogleValidationRequiredMessage } from "../utils/google-validation";
|
|
33
33
|
import type { RawHttpRequestDump } from "../utils/http-inspector";
|
|
34
|
-
import { armPreResponseTimeout, getStreamFirstEventTimeoutMs } from "../utils/idle-iterator";
|
|
34
|
+
import { armPreResponseTimeout, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
35
35
|
// Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
|
|
36
36
|
// the stream provider trusts the access token threaded through `options.apiKey`.
|
|
37
37
|
import { normalizeSchemaForCCA } from "../utils/schema";
|
|
@@ -325,6 +325,9 @@ export {
|
|
|
325
325
|
// Retry configuration
|
|
326
326
|
const MAX_RETRIES = 3;
|
|
327
327
|
const BASE_DELAY_MS = 1000;
|
|
328
|
+
const FLASH_FIRST_EVENT_TIMEOUT_MS = 60_000;
|
|
329
|
+
const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 300_000;
|
|
330
|
+
const FIRST_EVENT_TIMEOUT_ERROR = "Cloud Code Assist stream timed out while waiting for the first event";
|
|
328
331
|
const RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
|
|
329
332
|
const CLAUDE_THINKING_BETA_HEADER = "interleaved-thinking-2025-05-14";
|
|
330
333
|
const GOOGLE_GEMINI_REFRESH_SKEW_MS = 60_000;
|
|
@@ -616,12 +619,16 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
616
619
|
headers: requestHeaders,
|
|
617
620
|
};
|
|
618
621
|
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
622
|
+
// The provider owns the first-event watchdog so a silent successful
|
|
623
|
+
// response can fail over to the alternate Antigravity endpoint before
|
|
624
|
+
// anything user-visible has streamed. Flash should not inherit the
|
|
625
|
+
// five-minute allowance reserved for cold Pro reasoning starts.
|
|
623
626
|
const firstEventTimeoutMs =
|
|
624
|
-
options?.streamFirstEventTimeoutMs ??
|
|
627
|
+
options?.streamFirstEventTimeoutMs ??
|
|
628
|
+
getStreamFirstEventTimeoutMs(
|
|
629
|
+
undefined,
|
|
630
|
+
model.id.includes("flash") ? FLASH_FIRST_EVENT_TIMEOUT_MS : DEFAULT_FIRST_EVENT_TIMEOUT_MS,
|
|
631
|
+
);
|
|
625
632
|
const callerSignal = options?.signal;
|
|
626
633
|
const toolNames = new Set(context.tools?.map(t => t.name) ?? []);
|
|
627
634
|
const isFlashLeakModel = model.id.includes("flash");
|
|
@@ -653,7 +660,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
653
660
|
sawFinishReason = false;
|
|
654
661
|
};
|
|
655
662
|
|
|
656
|
-
const streamResponse = async (
|
|
663
|
+
const streamResponse = async (
|
|
664
|
+
activeResponse: Response,
|
|
665
|
+
): Promise<{ meaningful: boolean; strippedPlanningLeak: boolean }> => {
|
|
657
666
|
if (!activeResponse.body) {
|
|
658
667
|
throw new AIError.ProviderResponseError("No response body", {
|
|
659
668
|
provider: model.provider,
|
|
@@ -673,6 +682,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
673
682
|
let isBuffering = false;
|
|
674
683
|
let textBuffer = "";
|
|
675
684
|
let bufferedTextSignature: string | undefined;
|
|
685
|
+
let strippedPlanningLeak = false;
|
|
676
686
|
|
|
677
687
|
const endCurrentBlock = (): void => {
|
|
678
688
|
if (!currentBlock) return;
|
|
@@ -755,11 +765,24 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
755
765
|
}
|
|
756
766
|
};
|
|
757
767
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
options
|
|
761
|
-
|
|
762
|
-
|
|
768
|
+
const responseAbortController = new AbortController();
|
|
769
|
+
const responseSignal = options?.signal
|
|
770
|
+
? AbortSignal.any([options.signal, responseAbortController.signal])
|
|
771
|
+
: responseAbortController.signal;
|
|
772
|
+
const chunks = iterateWithIdleTimeout(
|
|
773
|
+
readSseJson<CloudCodeAssistResponseChunk>(activeResponse.body, responseSignal, event =>
|
|
774
|
+
options?.onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, model),
|
|
775
|
+
),
|
|
776
|
+
{
|
|
777
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
778
|
+
errorMessage: FIRST_EVENT_TIMEOUT_ERROR,
|
|
779
|
+
firstItemErrorMessage: FIRST_EVENT_TIMEOUT_ERROR,
|
|
780
|
+
onFirstItemTimeout: () =>
|
|
781
|
+
responseAbortController.abort(new AIError.StreamTimeoutError(FIRST_EVENT_TIMEOUT_ERROR)),
|
|
782
|
+
abortSignal: options?.signal,
|
|
783
|
+
},
|
|
784
|
+
);
|
|
785
|
+
for await (const chunk of chunks) {
|
|
763
786
|
if (chunk.error) {
|
|
764
787
|
const detail = chunk.error.message || chunk.error.status || "unknown error";
|
|
765
788
|
const message = `Cloud Code Assist stream error: ${detail}`;
|
|
@@ -815,6 +838,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
815
838
|
if (isBuffering) {
|
|
816
839
|
const buffered = consumePlanningBuffer(textBuffer, toolNames);
|
|
817
840
|
if (buffered.kind !== "incomplete") {
|
|
841
|
+
if (buffered.kind === "leak") strippedPlanningLeak = true;
|
|
818
842
|
const visibleSignature = bufferedTextSignature;
|
|
819
843
|
isBuffering = false;
|
|
820
844
|
textBuffer = "";
|
|
@@ -895,6 +919,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
895
919
|
const buffered = consumePlanningBuffer(textBuffer, toolNames, true);
|
|
896
920
|
|
|
897
921
|
if (buffered.kind !== "incomplete") {
|
|
922
|
+
if (buffered.kind === "leak") strippedPlanningLeak = true;
|
|
898
923
|
feedVisibleText(buffered.visibleText, bufferedTextSignature);
|
|
899
924
|
}
|
|
900
925
|
bufferedTextSignature = undefined;
|
|
@@ -905,7 +930,10 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
905
930
|
flushVisibleText(bufferedTextSignature);
|
|
906
931
|
endCurrentBlock();
|
|
907
932
|
|
|
908
|
-
return
|
|
933
|
+
return {
|
|
934
|
+
meaningful: hasMeaningfulGoogleContent(output),
|
|
935
|
+
strippedPlanningLeak,
|
|
936
|
+
};
|
|
909
937
|
};
|
|
910
938
|
|
|
911
939
|
let receivedContent = false;
|
|
@@ -998,8 +1026,14 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
998
1026
|
}
|
|
999
1027
|
|
|
1000
1028
|
const streamed = await streamResponse(currentResponse);
|
|
1001
|
-
|
|
1002
|
-
|
|
1029
|
+
// Only accept an empty STOP as valid silence once every fallback
|
|
1030
|
+
// endpoint is exhausted: an earlier endpoint returning empty
|
|
1031
|
+
// successful streams must still fail over (Antigravity auto mode)
|
|
1032
|
+
// rather than be recorded as a real silent review.
|
|
1033
|
+
const acceptedSilence =
|
|
1034
|
+
options?.acceptEmptyResponse === true && !streamed.strippedPlanningLeak && isLastEndpoint;
|
|
1035
|
+
if (output.stopReason !== "stop" || streamed.meaningful || acceptedSilence) {
|
|
1036
|
+
receivedContent = streamed.meaningful || acceptedSilence;
|
|
1003
1037
|
break;
|
|
1004
1038
|
}
|
|
1005
1039
|
|
|
@@ -1031,7 +1031,13 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
1031
1031
|
},
|
|
1032
1032
|
});
|
|
1033
1033
|
|
|
1034
|
-
if (
|
|
1034
|
+
if (
|
|
1035
|
+
output.stopReason !== "stop" ||
|
|
1036
|
+
hasMeaningfulGoogleContent(output) ||
|
|
1037
|
+
options?.acceptEmptyResponse === true
|
|
1038
|
+
) {
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1035
1041
|
if (emptyAttempt >= MAX_EMPTY_STREAM_RETRIES) {
|
|
1036
1042
|
throw new AIError.ProviderResponseError(
|
|
1037
1043
|
`Google API returned an empty response (finishReason STOP with no content) after ${MAX_EMPTY_STREAM_RETRIES + 1} attempts`,
|
|
@@ -109,6 +109,22 @@ export function resolveCodexResponsesLite(
|
|
|
109
109
|
return model.useResponsesLite === true;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Whether to request `stream_options.reasoning_summary_delivery =
|
|
114
|
+
* "sequential_cutoff"` (codex-rs `concurrent_reasoning_summaries`), enabled by
|
|
115
|
+
* `PI_CODEX_CONCURRENT_SUMMARIES=1`.
|
|
116
|
+
*
|
|
117
|
+
* Off by default because the mode cancels summary sections still in flight when
|
|
118
|
+
* the reasoning item closes: measured over 12 interleaved turns it halved
|
|
119
|
+
* visible thinking (0.83 vs 1.67 summary parts, 37 vs 69 chars per turn) and
|
|
120
|
+
* produced no summary at all on 3 of 12 turns. codex-rs ships it disabled too
|
|
121
|
+
* (`Stage::UnderDevelopment`, `default_enabled: false`).
|
|
122
|
+
*/
|
|
123
|
+
function concurrentSummariesEnabled(): boolean {
|
|
124
|
+
const env = $env.PI_CODEX_CONCURRENT_SUMMARIES?.trim().toLowerCase();
|
|
125
|
+
return env === "1" || env === "true";
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
/**
|
|
113
129
|
* Clamp a user-facing effort to the model's ladder, then remap to the wire
|
|
114
130
|
* tier. User efforts map 1:1 onto wire tiers; the effort map only covers
|
|
@@ -145,12 +161,14 @@ function getReasoningConfig(
|
|
|
145
161
|
const config: ReasoningConfig = {
|
|
146
162
|
effort: effort === "none" ? "none" : mapCodexWireEffort(model, effort),
|
|
147
163
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
// The backend only emits reasoning summaries when `reasoning.summary` is
|
|
165
|
+
// present: omitting it yields zero `response.reasoning_summary_text.*`
|
|
166
|
+
// events (measured against gpt-5.5, gpt-5.6-sol and gpt-5.6-terra). So
|
|
167
|
+
// `undefined` means "default on" — matching `applyResponsesCompatPolicy`
|
|
168
|
+
// on the plain Responses path — and only an explicit `null` (the caller
|
|
169
|
+
// hiding thinking) opts out.
|
|
170
|
+
if (options.reasoningSummary !== null && supportsCodexReasoningSummary(model.id)) {
|
|
171
|
+
config.summary = options.reasoningSummary ?? "auto";
|
|
154
172
|
}
|
|
155
173
|
return config;
|
|
156
174
|
}
|
|
@@ -443,13 +461,15 @@ export async function transformRequestBody(
|
|
|
443
461
|
...body.reasoning,
|
|
444
462
|
...reasoningConfig,
|
|
445
463
|
};
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
|
|
464
|
+
// Lite requires `all_turns` even for opaque/codenamed model ids. Only explicit
|
|
465
|
+
// full-transport overrides are gated by the known model wire generation.
|
|
466
|
+
if (responsesLite) {
|
|
467
|
+
body.reasoning.context = "all_turns";
|
|
468
|
+
} else if (options.reasoningContext !== undefined) {
|
|
469
|
+
if (options.reasoningContext === "all_turns" && !supportsAllTurnsReasoningContext(model.id)) {
|
|
450
470
|
delete body.reasoning.context;
|
|
451
471
|
} else {
|
|
452
|
-
body.reasoning.context =
|
|
472
|
+
body.reasoning.context = options.reasoningContext;
|
|
453
473
|
}
|
|
454
474
|
}
|
|
455
475
|
} else {
|
|
@@ -462,12 +482,13 @@ export async function transformRequestBody(
|
|
|
462
482
|
body.reasoning = { ...body.reasoning, mode: model.reasoningMode };
|
|
463
483
|
}
|
|
464
484
|
|
|
465
|
-
// Concurrent reasoning summaries (codex-rs `concurrent_reasoning_summaries`
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
485
|
+
// Concurrent reasoning summaries (codex-rs `concurrent_reasoning_summaries`):
|
|
486
|
+
// `sequential_cutoff` lets the server stream output without blocking on
|
|
487
|
+
// summary generation, delivering each completed section as an atomic
|
|
488
|
+
// `response.reasoning_summary_text.done`. Opt-in only — see
|
|
489
|
+
// {@link concurrentSummariesEnabled} for why. Requires a requested summary;
|
|
490
|
+
// codex-rs additionally gates on its OpenAI provider check, inherent here.
|
|
491
|
+
if (body.reasoning?.summary !== undefined && concurrentSummariesEnabled()) {
|
|
471
492
|
body.stream_options = { reasoning_summary_delivery: "sequential_cutoff" };
|
|
472
493
|
} else {
|
|
473
494
|
delete body.stream_options;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
1
|
import { scheduler } from "node:timers/promises";
|
|
3
2
|
import { type } from "@oh-my-pi/omptype";
|
|
4
3
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
@@ -19,8 +18,8 @@ import {
|
|
|
19
18
|
parseStreamingJson,
|
|
20
19
|
readSseJson,
|
|
21
20
|
structuredCloneJSON,
|
|
21
|
+
USER_AGENT,
|
|
22
22
|
} from "@oh-my-pi/pi-utils";
|
|
23
|
-
import packageJson from "../../package.json" with { type: "json" };
|
|
24
23
|
import * as AIError from "../error";
|
|
25
24
|
import { getEnvApiKey, isOfficialCodexApiUrl } from "../stream";
|
|
26
25
|
import type {
|
|
@@ -4250,7 +4249,7 @@ function createCodexHeaders(
|
|
|
4250
4249
|
headers.set(OPENAI_HEADERS.BETA, betaHeader);
|
|
4251
4250
|
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
4252
4251
|
headers.set(OPENAI_HEADERS.VERSION, codexClientVersion);
|
|
4253
|
-
headers.set("User-Agent",
|
|
4252
|
+
headers.set("User-Agent", USER_AGENT);
|
|
4254
4253
|
if (sessionId) {
|
|
4255
4254
|
headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
|
|
4256
4255
|
headers.set(OPENAI_HEADERS.SESSION_ID, sessionId);
|
|
@@ -1113,6 +1113,10 @@ export function buildParams(
|
|
|
1113
1113
|
filterReasoning: policy.reasoning.filterReasoningHistory,
|
|
1114
1114
|
},
|
|
1115
1115
|
includeThinkingSignatures: shouldReplayNativeHistory && !policy.reasoning.filterReasoningHistory,
|
|
1116
|
+
requiresReasoningReplayForAllTurns:
|
|
1117
|
+
policy.reasoning.enabled && policy.reasoning.requiresReasoningContentForAllAssistantTurns,
|
|
1118
|
+
requiresReasoningReplayForToolCalls:
|
|
1119
|
+
policy.reasoning.enabled && policy.reasoning.requiresReasoningContentForToolCalls,
|
|
1116
1120
|
repairOrphanOutputs: true,
|
|
1117
1121
|
});
|
|
1118
1122
|
|
|
@@ -1635,6 +1635,14 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
|
1635
1635
|
repairOrphanOutputs?: boolean;
|
|
1636
1636
|
/** Preserve assistant message item IDs from text signatures during fallback replay. */
|
|
1637
1637
|
preserveAssistantMessageIds?: boolean;
|
|
1638
|
+
/**
|
|
1639
|
+
* Synthesize a reasoning item for every replayed assistant turn that carries
|
|
1640
|
+
* content but no reasoning item. Set for DeepSeek-family Responses targets
|
|
1641
|
+
* that reject a thinking-mode continuation lacking `reasoning_text`.
|
|
1642
|
+
*/
|
|
1643
|
+
requiresReasoningReplayForAllTurns?: boolean;
|
|
1644
|
+
/** As {@link requiresReasoningReplayForAllTurns}, but only for turns that contain a tool call. */
|
|
1645
|
+
requiresReasoningReplayForToolCalls?: boolean;
|
|
1638
1646
|
}
|
|
1639
1647
|
|
|
1640
1648
|
/**
|
|
@@ -1861,6 +1869,8 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
|
|
|
1861
1869
|
supportsCustomToolCalls,
|
|
1862
1870
|
customToolWireNameMap,
|
|
1863
1871
|
computerCallIds,
|
|
1872
|
+
options.requiresReasoningReplayForAllTurns ?? false,
|
|
1873
|
+
options.requiresReasoningReplayForToolCalls ?? false,
|
|
1864
1874
|
);
|
|
1865
1875
|
const outputItems = suppressHiddenEmptyFallback
|
|
1866
1876
|
? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
|
|
@@ -1914,6 +1924,8 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
1914
1924
|
supportsCustomToolCalls = true,
|
|
1915
1925
|
customToolWireNameMap?: ReadonlyMap<string, string>,
|
|
1916
1926
|
computerCallIds?: Set<string>,
|
|
1927
|
+
requiresReasoningReplayForAllTurns = false,
|
|
1928
|
+
requiresReasoningReplayForToolCalls = false,
|
|
1917
1929
|
): ResponseInput {
|
|
1918
1930
|
const outputItems: ResponseInput = [];
|
|
1919
1931
|
let unsignedTextBlocks = 0;
|
|
@@ -1925,14 +1937,36 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
1925
1937
|
);
|
|
1926
1938
|
const isDifferentModel =
|
|
1927
1939
|
assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
|
|
1940
|
+
// DeepSeek-family Responses targets (e.g. opencode-go) reject a thinking-mode
|
|
1941
|
+
// continuation whose replayed assistant turns carry no reasoning item: "The
|
|
1942
|
+
// reasoning_text in the thinking mode must be passed back to the API." After a
|
|
1943
|
+
// cross-model prewalk hand-off or a compaction that drops the native replay
|
|
1944
|
+
// payload, the block re-encode below demotes reasoning to text and emits no
|
|
1945
|
+
// reasoning item. Track reasoning emission so a placeholder can be synthesized,
|
|
1946
|
+
// mirroring the chat-completions `requiresReasoningContentForAllAssistantTurns`
|
|
1947
|
+
// empty-`reasoning_content` safety net.
|
|
1948
|
+
const requiresReasoningItem =
|
|
1949
|
+
assistantMsg.stopReason !== "error" &&
|
|
1950
|
+
(requiresReasoningReplayForAllTurns ||
|
|
1951
|
+
(requiresReasoningReplayForToolCalls && assistantMsg.content.some(block => block.type === "toolCall")));
|
|
1952
|
+
let reasoningItemEmitted = false;
|
|
1953
|
+
const carriedReasoningTexts: string[] = [];
|
|
1954
|
+
let synthesizedReasoningItemId: string | undefined;
|
|
1928
1955
|
|
|
1929
1956
|
for (const block of assistantMsg.content) {
|
|
1930
1957
|
if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
|
|
1958
|
+
if (requiresReasoningItem) {
|
|
1959
|
+
if (block.itemId) synthesizedReasoningItemId ??= block.itemId;
|
|
1960
|
+
if (block.thinking.trim().length > 0) carriedReasoningTexts.push(block.thinking);
|
|
1961
|
+
}
|
|
1931
1962
|
if (!includeThinkingSignatures) {
|
|
1932
1963
|
continue;
|
|
1933
1964
|
}
|
|
1934
1965
|
const reasoningItem = parseResponseReasoningReplayItem(block.thinkingSignature);
|
|
1935
|
-
if (reasoningItem)
|
|
1966
|
+
if (reasoningItem) {
|
|
1967
|
+
outputItems.push(reasoningItem);
|
|
1968
|
+
reasoningItemEmitted = true;
|
|
1969
|
+
}
|
|
1936
1970
|
continue;
|
|
1937
1971
|
}
|
|
1938
1972
|
|
|
@@ -2033,6 +2067,26 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
2033
2067
|
});
|
|
2034
2068
|
}
|
|
2035
2069
|
|
|
2070
|
+
if (requiresReasoningItem && !reasoningItemEmitted && outputItems.length > 0) {
|
|
2071
|
+
// Replay the demoted reasoning (already present in `content` as visible
|
|
2072
|
+
// text) as a structured reasoning item so the thinking-mode continuation
|
|
2073
|
+
// carries the `reasoning_text` the provider requires. The text may be empty
|
|
2074
|
+
// when the source turn was minted by another model and its reasoning is
|
|
2075
|
+
// already folded into the message text; the item's presence is what
|
|
2076
|
+
// satisfies the provider contract, mirroring the empty `reasoning_content`
|
|
2077
|
+
// placeholder used on the chat-completions path.
|
|
2078
|
+
const reasoningText = carriedReasoningTexts.join("\n");
|
|
2079
|
+
const reasoningId =
|
|
2080
|
+
synthesizedReasoningItemId ?? `rs_${Bun.hash(`${model.id}:${msgIndex}:${reasoningText}`).toString(36)}`;
|
|
2081
|
+
const reasoningItem: ResponseReasoningItem = {
|
|
2082
|
+
type: "reasoning",
|
|
2083
|
+
id: reasoningId,
|
|
2084
|
+
summary: [],
|
|
2085
|
+
content: [{ type: "reasoning_text", text: reasoningText }],
|
|
2086
|
+
};
|
|
2087
|
+
outputItems.unshift(reasoningItem);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2036
2090
|
return outputItems;
|
|
2037
2091
|
}
|
|
2038
2092
|
|
|
@@ -78,6 +78,7 @@ const ALLOWED_OPTION_KEYS: ReadonlySet<keyof SimpleStreamOptions> = new Set([
|
|
|
78
78
|
"preferWebsockets",
|
|
79
79
|
"openrouterVariant",
|
|
80
80
|
"loopGuard",
|
|
81
|
+
"acceptEmptyResponse",
|
|
81
82
|
] as const satisfies readonly (keyof SimpleStreamOptions)[]);
|
|
82
83
|
|
|
83
84
|
// ---------------------------------------------------------------------------
|
|
@@ -202,6 +202,11 @@ interface LazyStreamLimits {
|
|
|
202
202
|
* stream timeouts. Keep the lazy loader from racing it with generic errors.
|
|
203
203
|
*/
|
|
204
204
|
providerHandlesStreamTimeouts?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* The provider retries or fails over when no first event arrives, while the
|
|
207
|
+
* lazy wrapper continues to own steady-state idle detection.
|
|
208
|
+
*/
|
|
209
|
+
providerHandlesFirstEventTimeouts?: boolean;
|
|
205
210
|
/**
|
|
206
211
|
* Apply OpenAI-family idle timeout precedence in the lazy wrapper. Used by
|
|
207
212
|
* local backends whose users historically tune slow prompt-processing gaps
|
|
@@ -210,16 +215,13 @@ interface LazyStreamLimits {
|
|
|
210
215
|
openAIIdleEnvFloorsFirstEvent?: boolean;
|
|
211
216
|
}
|
|
212
217
|
/**
|
|
213
|
-
* Cloud Code Assist
|
|
214
|
-
*
|
|
215
|
-
* the
|
|
216
|
-
*
|
|
217
|
-
* waiting for the first event" aborts on legitimate cold reasoning starts.
|
|
218
|
-
* The steady-state idle watchdog stays on the global default since the upstream
|
|
219
|
-
* emits thinking tokens frequently once it gets going.
|
|
218
|
+
* Cloud Code Assist owns first-event detection because Antigravity can return
|
|
219
|
+
* successful headers and then never emit an SSE event. Keeping the watchdog in
|
|
220
|
+
* the provider lets it fail over before surfacing an error; the lazy wrapper
|
|
221
|
+
* still catches post-first-event stalls.
|
|
220
222
|
*/
|
|
221
223
|
const GOOGLE_GEMINI_CLI_LAZY_STREAM_LIMITS: LazyStreamLimits = {
|
|
222
|
-
|
|
224
|
+
providerHandlesFirstEventTimeouts: true,
|
|
223
225
|
};
|
|
224
226
|
|
|
225
227
|
const PROVIDER_HANDLED_STREAM_TIMEOUTS: LazyStreamLimits = {
|
|
@@ -241,6 +243,7 @@ function forwardStream<TApi extends Api>(
|
|
|
241
243
|
(async () => {
|
|
242
244
|
try {
|
|
243
245
|
const providerHandlesStreamTimeouts = limits?.providerHandlesStreamTimeouts === true;
|
|
246
|
+
const providerHandlesFirstEventTimeouts = limits?.providerHandlesFirstEventTimeouts === true;
|
|
244
247
|
// Per-model catalog compat can widen the fallback watchdog for hosts
|
|
245
248
|
// with no keepalive events (e.g. Bedrock reasoning models that go
|
|
246
249
|
// quiet for minutes mid-thinking, issue #4758). Caller options and
|
|
@@ -258,12 +261,13 @@ function forwardStream<TApi extends Api>(
|
|
|
258
261
|
(limits?.openAIIdleEnvFloorsFirstEvent
|
|
259
262
|
? getOpenAIStreamIdleTimeoutMs(idleTimeoutFallbackMs)
|
|
260
263
|
: getStreamIdleTimeoutMs(idleTimeoutFallbackMs)));
|
|
261
|
-
const firstItemTimeoutMs =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
(
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
const firstItemTimeoutMs =
|
|
265
|
+
providerHandlesStreamTimeouts || providerHandlesFirstEventTimeouts
|
|
266
|
+
? 0
|
|
267
|
+
: (options.streamFirstEventTimeoutMs ??
|
|
268
|
+
(limits?.openAIIdleEnvFloorsFirstEvent
|
|
269
|
+
? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs, limits.defaultFirstEventTimeoutMs)
|
|
270
|
+
: getStreamFirstEventTimeoutMs(idleTimeoutMs, limits?.defaultFirstEventTimeoutMs)));
|
|
267
271
|
// Providers with a server-driven local tool bridge (e.g. the Cursor
|
|
268
272
|
// exec channel) mark their stream busy while a local tool runs; the
|
|
269
273
|
// watchdog must not read that silence as a provider stall (#4593).
|
package/src/registry/aws.ts
CHANGED
|
@@ -13,14 +13,21 @@ export interface AwsBedrockProviderOptions extends Readonly<Record<string, unkno
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function isEc2Host(): boolean {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Xen instances tag DMI/hypervisor UUIDs with an `ec2` prefix. Nitro instances
|
|
17
|
+
// (EKS, modern EC2) expose the instance id in board_asset_tag (`i-...`) and
|
|
18
|
+
// "Amazon EC2" in the DMI vendor fields; cover both so Nitro/EKS hosts aren't
|
|
19
|
+
// misread as non-EC2 (product_uuid is often mode 0400 and unreadable there).
|
|
20
|
+
const checks: Array<[path: string, matches: (value: string) => boolean]> = [
|
|
21
|
+
["/sys/hypervisor/uuid", v => v.startsWith("ec2")],
|
|
22
|
+
["/sys/devices/virtual/dmi/id/product_uuid", v => v.startsWith("ec2")],
|
|
23
|
+
["/sys/devices/virtual/dmi/id/board_asset_tag", v => v.startsWith("ec2") || v.startsWith("i-")],
|
|
24
|
+
["/sys/devices/virtual/dmi/id/sys_vendor", v => v.includes("amazon ec2")],
|
|
25
|
+
["/sys/devices/virtual/dmi/id/bios_vendor", v => v.includes("amazon ec2")],
|
|
26
|
+
];
|
|
27
|
+
for (const [candidate, matches] of checks) {
|
|
21
28
|
try {
|
|
22
29
|
const value = fs.readFileSync(candidate, "utf8").trim().toLowerCase();
|
|
23
|
-
if (value
|
|
30
|
+
if (matches(value)) return true;
|
|
24
31
|
} catch {
|
|
25
32
|
// Missing/unreadable DMI metadata means this probe is inconclusive.
|
|
26
33
|
}
|