@oh-my-pi/pi-ai 17.2.11 → 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 +25 -0
- package/dist/types/auth-retry.d.ts +3 -3
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/error/auth-classify.d.ts +5 -5
- package/dist/types/error/aws.d.ts +5 -1
- package/dist/types/error/flags.d.ts +4 -0
- 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/registry/api-key-login.d.ts +6 -4
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +2 -2
- package/dist/types/registry/kagi.d.ts +2 -2
- package/dist/types/registry/litellm.d.ts +2 -2
- package/dist/types/registry/llama-cpp.d.ts +2 -2
- package/dist/types/registry/lm-studio.d.ts +2 -2
- package/dist/types/registry/parallel.d.ts +2 -2
- package/dist/types/registry/tavily.d.ts +1 -1
- package/dist/types/registry/vercel-ai-gateway.d.ts +2 -2
- package/dist/types/registry/vllm.d.ts +2 -2
- package/dist/types/types.d.ts +7 -0
- package/dist/types/usage/cursor.d.ts +11 -0
- package/dist/types/usage/openai-codex-reset.d.ts +0 -20
- package/dist/types/usage/shared.d.ts +13 -1
- package/dist/types/utils/block-symbols.d.ts +12 -0
- package/package.json +5 -5
- package/src/auth-retry.ts +5 -4
- package/src/auth-storage.ts +77 -33
- package/src/dialect/owned-stream.ts +3 -0
- package/src/error/auth-classify.ts +7 -6
- package/src/error/aws.ts +5 -1
- package/src/error/flags.ts +14 -0
- 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/api-key-login.ts +26 -12
- package/src/registry/aws.ts +13 -6
- package/src/registry/cloudflare-ai-gateway.ts +10 -29
- package/src/registry/kagi.ts +11 -29
- package/src/registry/litellm.ts +11 -29
- package/src/registry/llama-cpp.ts +11 -21
- package/src/registry/lm-studio.ts +9 -20
- package/src/registry/oauth/callback-server.ts +93 -5
- package/src/registry/parallel.ts +10 -28
- package/src/registry/tavily.ts +9 -27
- package/src/registry/vercel-ai-gateway.ts +10 -28
- package/src/registry/vllm.ts +11 -21
- package/src/stream.ts +10 -7
- package/src/types.ts +7 -0
- package/src/usage/alibaba-token-plan.ts +8 -15
- package/src/usage/claude.ts +12 -19
- package/src/usage/cursor.ts +179 -59
- package/src/usage/gemini.ts +3 -2
- package/src/usage/github-copilot.ts +3 -2
- package/src/usage/google-antigravity.ts +10 -17
- package/src/usage/kimi.ts +35 -17
- package/src/usage/minimax-code.ts +7 -27
- package/src/usage/openai-codex-reset.ts +3 -2
- package/src/usage/openai-codex.ts +5 -5
- package/src/usage/opencode-go.ts +1 -2
- package/src/usage/shared.ts +33 -10
- package/src/usage/synthetic.ts +4 -11
- package/src/usage/umans.ts +2 -2
- package/src/usage/xai-oauth.ts +13 -26
- package/src/usage/zai.ts +4 -6
- 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
|
@@ -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).
|
|
@@ -38,16 +38,18 @@ type ModelsEndpointValidation = {
|
|
|
38
38
|
export type ApiKeyLoginConfig = {
|
|
39
39
|
/** Display name used in error messages, e.g. "Cerebras", "NanoGPT". */
|
|
40
40
|
providerLabel: string;
|
|
41
|
-
/** URL opened in browser for the user to grab their key. */
|
|
42
|
-
authUrl
|
|
43
|
-
/** Instructions shown with the onAuth callback. */
|
|
44
|
-
instructions
|
|
41
|
+
/** URL opened in browser for the user to grab their key, or omitted to skip onAuth. */
|
|
42
|
+
authUrl?: string;
|
|
43
|
+
/** Instructions shown with the onAuth callback, or omitted to skip onAuth. */
|
|
44
|
+
instructions?: string;
|
|
45
45
|
/** Prompt message shown when asking for the key paste. */
|
|
46
46
|
promptMessage: string;
|
|
47
47
|
/** Placeholder string for the prompt (e.g. "sk-...", "csk-..."). */
|
|
48
48
|
placeholder: string;
|
|
49
49
|
/** Validation strategy, or `null` to skip validation. */
|
|
50
50
|
validation: ChatCompletionsValidation | AnthropicMessagesValidation | ModelsEndpointValidation | null;
|
|
51
|
+
/** Value returned for an empty key; also allows an empty prompt response. */
|
|
52
|
+
emptyKeyFallback?: string;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthController) => Promise<string> {
|
|
@@ -56,15 +58,24 @@ export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthCon
|
|
|
56
58
|
throw new AIError.OnPromptRequiredError(config.providerLabel);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
if (config.authUrl && config.instructions) {
|
|
62
|
+
options.onAuth?.({
|
|
63
|
+
url: config.authUrl,
|
|
64
|
+
instructions: config.instructions,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
63
67
|
|
|
64
|
-
const apiKey =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const apiKey =
|
|
69
|
+
config.emptyKeyFallback === undefined
|
|
70
|
+
? await options.onPrompt({
|
|
71
|
+
message: config.promptMessage,
|
|
72
|
+
placeholder: config.placeholder,
|
|
73
|
+
})
|
|
74
|
+
: await options.onPrompt({
|
|
75
|
+
message: config.promptMessage,
|
|
76
|
+
placeholder: config.placeholder,
|
|
77
|
+
allowEmpty: true,
|
|
78
|
+
});
|
|
68
79
|
|
|
69
80
|
if (options.signal?.aborted) {
|
|
70
81
|
throw new AIError.LoginCancelledError();
|
|
@@ -72,6 +83,9 @@ export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthCon
|
|
|
72
83
|
|
|
73
84
|
const trimmed = apiKey.trim();
|
|
74
85
|
if (!trimmed) {
|
|
86
|
+
if (config.emptyKeyFallback !== undefined) {
|
|
87
|
+
return config.emptyKeyFallback;
|
|
88
|
+
}
|
|
75
89
|
throw new AIError.ApiKeyRequiredError();
|
|
76
90
|
}
|
|
77
91
|
|
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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const AUTH_URL = "https://developers.cloudflare.com/ai-gateway/configuration/authentication/";
|
|
@@ -10,33 +10,14 @@ const AUTH_URL = "https://developers.cloudflare.com/ai-gateway/configuration/aut
|
|
|
10
10
|
* Opens browser to Cloudflare AI Gateway authentication docs and prompts for a gateway token/API key.
|
|
11
11
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"Copy your Cloudflare AI Gateway token/API key. Configure account/gateway base URL in models config.",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const apiKey = await options.onPrompt({
|
|
25
|
-
message: "Paste your Cloudflare AI Gateway token/API key",
|
|
26
|
-
placeholder: "cf-aig-...",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (options.signal?.aborted) {
|
|
30
|
-
throw new AIError.LoginCancelledError();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const trimmed = apiKey.trim();
|
|
34
|
-
if (!trimmed) {
|
|
35
|
-
throw new AIError.ApiKeyRequiredError();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return trimmed;
|
|
39
|
-
}
|
|
13
|
+
export const loginCloudflareAiGateway = createApiKeyLogin({
|
|
14
|
+
providerLabel: "Cloudflare AI Gateway",
|
|
15
|
+
authUrl: AUTH_URL,
|
|
16
|
+
instructions: "Copy your Cloudflare AI Gateway token/API key. Configure account/gateway base URL in models config.",
|
|
17
|
+
promptMessage: "Paste your Cloudflare AI Gateway token/API key",
|
|
18
|
+
placeholder: "cf-aig-...",
|
|
19
|
+
validation: null,
|
|
20
|
+
});
|
|
40
21
|
|
|
41
22
|
export const cloudflareAiGatewayProvider = {
|
|
42
23
|
id: "cloudflare-ai-gateway",
|
package/src/registry/kagi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const AUTH_URL = "https://kagi.com/settings/api";
|
|
@@ -10,33 +10,15 @@ const AUTH_URL = "https://kagi.com/settings/api";
|
|
|
10
10
|
* Opens browser to API settings and prompts user to paste their API key.
|
|
11
11
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const apiKey = await options.onPrompt({
|
|
25
|
-
message: "Paste your Kagi API key",
|
|
26
|
-
placeholder: "KG_...",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (options.signal?.aborted) {
|
|
30
|
-
throw new AIError.LoginCancelledError();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const trimmed = apiKey.trim();
|
|
34
|
-
if (!trimmed) {
|
|
35
|
-
throw new AIError.ApiKeyRequiredError();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return trimmed;
|
|
39
|
-
}
|
|
13
|
+
export const loginKagi = createApiKeyLogin({
|
|
14
|
+
providerLabel: "Kagi",
|
|
15
|
+
authUrl: AUTH_URL,
|
|
16
|
+
instructions:
|
|
17
|
+
"Copy your Kagi Search API key from Kagi API settings. Search API access is beta-only; if unavailable, email support@kagi.com.",
|
|
18
|
+
promptMessage: "Paste your Kagi API key",
|
|
19
|
+
placeholder: "KG_...",
|
|
20
|
+
validation: null,
|
|
21
|
+
});
|
|
40
22
|
|
|
41
23
|
export const kagiProvider = {
|
|
42
24
|
id: "kagi",
|