@gajae-code/ai 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/model-cache.d.ts +2 -0
- package/dist/types/provider-models/special.d.ts +3 -1
- package/dist/types/providers/anthropic.d.ts +1 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +3854 -107
- package/dist/types/providers/cursor-pi-args.d.ts +119 -0
- package/dist/types/providers/cursor.d.ts +8 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -0
- package/dist/types/providers/openai-responses-shared.d.ts +1 -1
- package/dist/types/types.d.ts +41 -1
- package/dist/types/utils/block-symbols.d.ts +6 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +2 -0
- package/dist/types/utils/idle-iterator.d.ts +5 -2
- package/dist/types/utils/oauth/kimi.d.ts +3 -9
- package/dist/types/utils/oauth/openrouter.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +13 -2
- package/src/auth-storage.ts +10 -11
- package/src/model-cache.ts +78 -0
- package/src/model-manager.ts +194 -25
- package/src/provider-models/special.ts +67 -4
- package/src/providers/anthropic.ts +115 -40
- package/src/providers/aws-credential-config.ts +2 -3
- package/src/providers/aws-credentials.ts +2 -3
- package/src/providers/azure-openai-responses.ts +18 -2
- package/src/providers/cursor/exec-modern.ts +497 -0
- package/src/providers/cursor/gen/agent_pb.ts +4687 -181
- package/src/providers/cursor/proto/agent.proto +1007 -0
- package/src/providers/cursor-pi-args.ts +187 -0
- package/src/providers/cursor.ts +382 -47
- package/src/providers/google-auth.ts +2 -3
- package/src/providers/openai-codex-responses.ts +358 -73
- package/src/providers/openai-completions.ts +2 -2
- package/src/providers/openai-responses-shared.ts +55 -6
- package/src/providers/openai-responses.ts +27 -4
- package/src/stream.ts +8 -3
- package/src/types.ts +55 -0
- package/src/utils/block-symbols.ts +11 -0
- package/src/utils/discovery/openai-compatible.ts +21 -6
- package/src/utils/idle-iterator.ts +22 -4
- package/src/utils/oauth/index.ts +6 -0
- package/src/utils/oauth/kimi.ts +14 -8
- package/src/utils/oauth/kiro.ts +2 -2
- package/src/utils/oauth/openrouter.ts +16 -0
- package/src/utils/oauth/types.ts +1 -0
|
@@ -525,7 +525,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
525
525
|
|
|
526
526
|
try {
|
|
527
527
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
528
|
-
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.provider);
|
|
528
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.provider, model.id);
|
|
529
529
|
const {
|
|
530
530
|
client,
|
|
531
531
|
copilotPremiumRequests,
|
|
@@ -1239,7 +1239,7 @@ async function createClient(
|
|
|
1239
1239
|
// The OpenAI SDK's default is 10 minutes per attempt × `maxRetries`, which
|
|
1240
1240
|
// turns a stalled-before-headers fetch into a multi-minute hang invisible
|
|
1241
1241
|
// to the agent loop (the iterator watchdog only arms AFTER `create()` returns).
|
|
1242
|
-
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(model.provider, streamFirstEventTimeoutOverride);
|
|
1242
|
+
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(model.provider, streamFirstEventTimeoutOverride, model.id);
|
|
1243
1243
|
return {
|
|
1244
1244
|
client: new OpenAI({
|
|
1245
1245
|
apiKey,
|
|
@@ -327,13 +327,57 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
327
327
|
|
|
328
328
|
export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
329
329
|
messages: ResponseInput,
|
|
330
|
-
|
|
330
|
+
toolResults: readonly ToolResultMessage[],
|
|
331
331
|
model: Model<TApi>,
|
|
332
332
|
strictResponsesPairing: boolean,
|
|
333
333
|
knownCallIds: ReadonlySet<string>,
|
|
334
334
|
customCallIds?: ReadonlySet<string>,
|
|
335
335
|
): void {
|
|
336
336
|
const supportsImages = model.input.includes("image");
|
|
337
|
+
const imageParts: ResponseInputContent[] = [];
|
|
338
|
+
|
|
339
|
+
for (const toolResult of toolResults) {
|
|
340
|
+
appendResponsesToolResultOutput(
|
|
341
|
+
messages,
|
|
342
|
+
imageParts,
|
|
343
|
+
toolResult,
|
|
344
|
+
supportsImages,
|
|
345
|
+
strictResponsesPairing,
|
|
346
|
+
knownCallIds,
|
|
347
|
+
customCallIds,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (imageParts.length === 0) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
messages.push({ role: "user", content: imageParts });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Append the Responses items for one tool result of a batch (#4807).
|
|
360
|
+
*
|
|
361
|
+
* Emits the paired `function_call_output` / `custom_tool_call_output` in
|
|
362
|
+
* `messages` — keeping every output of the batch contiguous — and collects
|
|
363
|
+
* supported image blocks into `imageParts` instead of emitting a standalone
|
|
364
|
+
* user message per result. A per-result image user message interleaves with
|
|
365
|
+
* sibling outputs of the same assistant tool-call turn; once an OpenAI
|
|
366
|
+
* Responses → Anthropic Messages proxy groups consecutive outputs into the
|
|
367
|
+
* single user message carrying `tool_result` blocks, the interleaved image
|
|
368
|
+
* user message splits that group and leaves a `tool_use` without its
|
|
369
|
+
* immediately-following `tool_result`, which Anthropic rejects with a 400 on
|
|
370
|
+
* every replay of the poisoned tail.
|
|
371
|
+
*/
|
|
372
|
+
function appendResponsesToolResultOutput(
|
|
373
|
+
messages: ResponseInput,
|
|
374
|
+
imageParts: ResponseInputContent[],
|
|
375
|
+
toolResult: ToolResultMessage,
|
|
376
|
+
supportsImages: boolean,
|
|
377
|
+
strictResponsesPairing: boolean,
|
|
378
|
+
knownCallIds: ReadonlySet<string>,
|
|
379
|
+
customCallIds?: ReadonlySet<string>,
|
|
380
|
+
): void {
|
|
337
381
|
const textResult = toolResult.content
|
|
338
382
|
.filter((block): block is TextContent => block.type === "text")
|
|
339
383
|
.map(block => block.text)
|
|
@@ -370,19 +414,24 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
|
370
414
|
return;
|
|
371
415
|
}
|
|
372
416
|
|
|
373
|
-
|
|
374
|
-
{ type: "input_text", text: "Attached image(s) from tool result:" } satisfies ResponseInputText
|
|
375
|
-
|
|
417
|
+
if (imageParts.length === 0) {
|
|
418
|
+
imageParts.push({ type: "input_text", text: "Attached image(s) from tool result:" } satisfies ResponseInputText);
|
|
419
|
+
}
|
|
420
|
+
// Label each result's image group with its call id so parallel results keep
|
|
421
|
+
// image-to-call attribution inside the single collected user message (#4807).
|
|
422
|
+
imageParts.push({
|
|
423
|
+
type: "input_text",
|
|
424
|
+
text: `call_id=${normalized.callId}`,
|
|
425
|
+
} satisfies ResponseInputText);
|
|
376
426
|
for (const block of toolResult.content) {
|
|
377
427
|
if (block.type === "image") {
|
|
378
|
-
|
|
428
|
+
imageParts.push({
|
|
379
429
|
type: "input_image",
|
|
380
430
|
detail: "auto",
|
|
381
431
|
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
382
432
|
} satisfies ResponseInputImage);
|
|
383
433
|
}
|
|
384
434
|
}
|
|
385
|
-
messages.push({ role: "user", content: contentParts });
|
|
386
435
|
}
|
|
387
436
|
|
|
388
437
|
export interface ProcessResponsesStreamOptions {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type StreamOptions,
|
|
23
23
|
type Tool,
|
|
24
24
|
type ToolChoice,
|
|
25
|
+
type ToolResultMessage,
|
|
25
26
|
} from "../types";
|
|
26
27
|
import {
|
|
27
28
|
createOpenAIResponsesHistoryPayload,
|
|
@@ -389,7 +390,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
389
390
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
390
391
|
const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
|
|
391
392
|
const { params } = buildParams(model, context, options, providerSessionState, cacheRetention, baseUrl);
|
|
392
|
-
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.provider);
|
|
393
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.provider, model.id);
|
|
393
394
|
options?.onPayload?.(params, undefined, options?.attemptScope);
|
|
394
395
|
rawRequestDump = {
|
|
395
396
|
provider: model.provider,
|
|
@@ -623,7 +624,7 @@ function createClient(
|
|
|
623
624
|
);
|
|
624
625
|
// Bound HTTP request timeout to the first-event window so a stalled-before-headers
|
|
625
626
|
// fetch cannot wait the SDK's 10-minute default before the transport watchdog arms.
|
|
626
|
-
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(model.provider, streamFirstEventTimeoutOverride);
|
|
627
|
+
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(model.provider, streamFirstEventTimeoutOverride, model.id);
|
|
627
628
|
return {
|
|
628
629
|
client: new OpenAI({
|
|
629
630
|
apiKey,
|
|
@@ -800,7 +801,30 @@ function convertConversationMessages(
|
|
|
800
801
|
const transformedMessages = transformMessages(context.messages, model, normalizeResponsesToolCallIdForTransform);
|
|
801
802
|
|
|
802
803
|
let msgIndex = 0;
|
|
804
|
+
// Consecutive tool results are batched into one append call so every output
|
|
805
|
+
// of the turn stays contiguous before the collected image user message;
|
|
806
|
+
// per-result image user messages interleave with sibling outputs and break
|
|
807
|
+
// tool_use→tool_result adjacency through Anthropic-translating proxies (#4807).
|
|
808
|
+
let pendingToolResults: ToolResultMessage[] = [];
|
|
809
|
+
const flushPendingToolResults = (): void => {
|
|
810
|
+
if (pendingToolResults.length === 0) return;
|
|
811
|
+
appendResponsesToolResultMessages(
|
|
812
|
+
messages,
|
|
813
|
+
pendingToolResults,
|
|
814
|
+
model,
|
|
815
|
+
strictResponsesPairing,
|
|
816
|
+
knownCallIds,
|
|
817
|
+
customCallIds,
|
|
818
|
+
);
|
|
819
|
+
pendingToolResults = [];
|
|
820
|
+
};
|
|
803
821
|
for (const msg of transformedMessages) {
|
|
822
|
+
if (msg.role === "toolResult") {
|
|
823
|
+
pendingToolResults.push(msg);
|
|
824
|
+
msgIndex++;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
flushPendingToolResults();
|
|
804
828
|
if (msg.role === "user" || msg.role === "developer") {
|
|
805
829
|
const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
806
830
|
const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
|
|
@@ -851,11 +875,10 @@ function convertConversationMessages(
|
|
|
851
875
|
);
|
|
852
876
|
if (outputItems.length === 0) continue;
|
|
853
877
|
messages.push(...outputItems);
|
|
854
|
-
} else if (msg.role === "toolResult") {
|
|
855
|
-
appendResponsesToolResultMessages(messages, msg, model, strictResponsesPairing, knownCallIds, customCallIds);
|
|
856
878
|
}
|
|
857
879
|
msgIndex++;
|
|
858
880
|
}
|
|
881
|
+
flushPendingToolResults();
|
|
859
882
|
|
|
860
883
|
return repairOrphanResponsesToolOutputs(messages);
|
|
861
884
|
}
|
package/src/stream.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
$credentialEnv,
|
|
5
|
+
$env,
|
|
6
|
+
$pickCredentialEnv,
|
|
7
|
+
extractHttpStatusFromError,
|
|
8
|
+
getTrustedHomeDir,
|
|
9
|
+
} from "@gajae-code/utils";
|
|
5
10
|
import { assertManagedAttempt, classifyFallbackTrigger, type TransportFailureFacts } from "./utils/fallback-transport";
|
|
6
11
|
|
|
7
12
|
const managedAttemptValidated = Symbol("managedAttemptValidated");
|
|
@@ -77,7 +82,7 @@ function hasVertexAdcCredentials(): boolean {
|
|
|
77
82
|
cachedVertexAdcCredentialsExists = fs.existsSync(gacPath);
|
|
78
83
|
} else {
|
|
79
84
|
cachedVertexAdcCredentialsExists = fs.existsSync(
|
|
80
|
-
path.join(
|
|
85
|
+
path.join(getTrustedHomeDir(), ".config", "gcloud", "application_default_credentials.json"),
|
|
81
86
|
);
|
|
82
87
|
}
|
|
83
88
|
}
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,20 @@ import type {
|
|
|
13
13
|
LsArgs,
|
|
14
14
|
LsResult,
|
|
15
15
|
McpResult,
|
|
16
|
+
PiBashExecArgs,
|
|
17
|
+
PiBashExecResult,
|
|
18
|
+
PiEditExecArgs,
|
|
19
|
+
PiEditExecResult,
|
|
20
|
+
PiFindExecArgs,
|
|
21
|
+
PiFindExecResult,
|
|
22
|
+
PiGrepExecArgs,
|
|
23
|
+
PiGrepExecResult,
|
|
24
|
+
PiLsExecArgs,
|
|
25
|
+
PiLsExecResult,
|
|
26
|
+
PiReadExecArgs,
|
|
27
|
+
PiReadExecResult,
|
|
28
|
+
PiWriteExecArgs,
|
|
29
|
+
PiWriteExecResult,
|
|
16
30
|
ReadArgs,
|
|
17
31
|
ReadResult,
|
|
18
32
|
ShellArgs,
|
|
@@ -669,6 +683,26 @@ export interface Usage {
|
|
|
669
683
|
|
|
670
684
|
export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
|
|
671
685
|
export type AssistantErrorKind = "provider_safety_stop" | "local_snapshot_failure" | "local_buffer_overflow";
|
|
686
|
+
/**
|
|
687
|
+
* Structured, shape-only staging-buffer overflow diagnostic carried on the
|
|
688
|
+
* terminal `AssistantMessage`. Attached only by the agent runtime from its own
|
|
689
|
+
* identity-checked overflow error; every field is a closed vocabulary literal
|
|
690
|
+
* or a locally synthesized number.
|
|
691
|
+
*/
|
|
692
|
+
export interface AssistantBufferOverflowDiagnostic {
|
|
693
|
+
/** Rejecting stage from the closed managed-local-failure vocabulary. */
|
|
694
|
+
stage: string;
|
|
695
|
+
/** Which provisional cap tripped. */
|
|
696
|
+
exceeded: "events" | "bytes" | "both";
|
|
697
|
+
/** Events retained in the batch at rejection (post-compaction). */
|
|
698
|
+
stagedEventCount: number;
|
|
699
|
+
/** Bytes retained in the batch at rejection (post-compaction). */
|
|
700
|
+
stagedBytes: number;
|
|
701
|
+
/** Serialized size of the event that was rejected. */
|
|
702
|
+
incomingEventBytes: number;
|
|
703
|
+
maxStagedEvents: number;
|
|
704
|
+
maxStagedBytes: number;
|
|
705
|
+
}
|
|
672
706
|
|
|
673
707
|
export interface OpenAIResponsesHistoryPayload {
|
|
674
708
|
type: "openaiResponsesHistory";
|
|
@@ -712,6 +746,15 @@ export interface AssistantMessage {
|
|
|
712
746
|
stopReason: StopReason;
|
|
713
747
|
errorMessage?: string;
|
|
714
748
|
errorKind?: AssistantErrorKind;
|
|
749
|
+
/**
|
|
750
|
+
* Structured, shape-only diagnostic for a terminal local staging-buffer
|
|
751
|
+
* overflow (`errorKind: "local_buffer_overflow"`). Attached only by the
|
|
752
|
+
* agent runtime from its own identity-checked overflow error, so a
|
|
753
|
+
* foreign, self-labeled error cannot populate it. Every field is a closed
|
|
754
|
+
* vocabulary literal or a locally synthesized number — parent surfaces
|
|
755
|
+
* render this instead of trusting the free-form `errorMessage`.
|
|
756
|
+
*/
|
|
757
|
+
bufferOverflow?: AssistantBufferOverflowDiagnostic;
|
|
715
758
|
/** HTTP status surfaced by the provider when the request failed. Populated by every provider's catch block alongside `errorMessage` so consumers (auth retry, telemetry, UI) can branch without regex-scraping the message. */
|
|
716
759
|
errorStatus?: number;
|
|
717
760
|
/** Typed upstream failure facts retained for retry classification without parsing errorMessage. */
|
|
@@ -767,6 +810,11 @@ export interface CursorShellStreamCallbacks {
|
|
|
767
810
|
onStderr(data: string): void;
|
|
768
811
|
}
|
|
769
812
|
|
|
813
|
+
export interface CursorPiCall<TArgs> {
|
|
814
|
+
args: TArgs;
|
|
815
|
+
toolCallId: string;
|
|
816
|
+
}
|
|
817
|
+
|
|
770
818
|
export interface CursorExecHandlers {
|
|
771
819
|
read?: (args: ReadArgs) => Promise<CursorExecHandlerResult<ReadResult>>;
|
|
772
820
|
ls?: (args: LsArgs) => Promise<CursorExecHandlerResult<LsResult>>;
|
|
@@ -780,6 +828,13 @@ export interface CursorExecHandlers {
|
|
|
780
828
|
) => Promise<CursorExecHandlerResult<ShellResult>>;
|
|
781
829
|
diagnostics?: (args: DiagnosticsArgs) => Promise<CursorExecHandlerResult<DiagnosticsResult>>;
|
|
782
830
|
mcp?: (call: CursorMcpCall) => Promise<CursorExecHandlerResult<McpResult>>;
|
|
831
|
+
piRead?: (call: CursorPiCall<PiReadExecArgs>) => Promise<CursorExecHandlerResult<PiReadExecResult>>;
|
|
832
|
+
piBash?: (call: CursorPiCall<PiBashExecArgs>) => Promise<CursorExecHandlerResult<PiBashExecResult>>;
|
|
833
|
+
piEdit?: (call: CursorPiCall<PiEditExecArgs>) => Promise<CursorExecHandlerResult<PiEditExecResult>>;
|
|
834
|
+
piWrite?: (call: CursorPiCall<PiWriteExecArgs>) => Promise<CursorExecHandlerResult<PiWriteExecResult>>;
|
|
835
|
+
piGrep?: (call: CursorPiCall<PiGrepExecArgs>) => Promise<CursorExecHandlerResult<PiGrepExecResult>>;
|
|
836
|
+
piFind?: (call: CursorPiCall<PiFindExecArgs>) => Promise<CursorExecHandlerResult<PiFindExecResult>>;
|
|
837
|
+
piLs?: (call: CursorPiCall<PiLsExecArgs>) => Promise<CursorExecHandlerResult<PiLsExecResult>>;
|
|
783
838
|
onToolResult?: CursorToolResultHandler;
|
|
784
839
|
}
|
|
785
840
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const kCursorExecResolved = Symbol("provider.block.cursorExecResolved");
|
|
2
|
+
|
|
3
|
+
export type CursorExecResolvedCarrier = object & { [kCursorExecResolved]?: true };
|
|
4
|
+
|
|
5
|
+
export function isCursorExecResolved(block: CursorExecResolvedCarrier | null | undefined): boolean {
|
|
6
|
+
return block?.[kCursorExecResolved] === true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function copyCursorExecResolved(target: CursorExecResolvedCarrier, source: CursorExecResolvedCarrier): void {
|
|
10
|
+
if (source[kCursorExecResolved] === true) target[kCursorExecResolved] = true;
|
|
11
|
+
}
|
|
@@ -5,6 +5,17 @@ import { toNumber } from "../../utils";
|
|
|
5
5
|
|
|
6
6
|
const MODELS_PATH = "/models";
|
|
7
7
|
const MAX_MODELS_RESPONSE_BYTES = 1_000_000;
|
|
8
|
+
const MAX_CATALOG_MODEL_ID_LENGTH = 200;
|
|
9
|
+
|
|
10
|
+
/** Catalog identities are rendered and used for routing; unsafe values are dropped, never rewritten. */
|
|
11
|
+
export function isSafeCatalogModelId(value: unknown): value is string {
|
|
12
|
+
return (
|
|
13
|
+
typeof value === "string" &&
|
|
14
|
+
value.trim().length > 0 &&
|
|
15
|
+
value.length <= MAX_CATALOG_MODEL_ID_LENGTH &&
|
|
16
|
+
!/[\u0000-\u001f\u007f-\u009f]/u.test(value)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
8
19
|
|
|
9
20
|
/**
|
|
10
21
|
* Minimal OpenAI-style model entry shape consumed by discovery.
|
|
@@ -186,6 +197,9 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
186
197
|
|
|
187
198
|
const deduped = new Map<string, Model<TApi>>();
|
|
188
199
|
for (const entry of entries) {
|
|
200
|
+
if (!isSafeCatalogModelId(entry.id)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
189
203
|
const rawContextWindow = firstPositiveModelNumber(
|
|
190
204
|
UNK_CONTEXT_WINDOW,
|
|
191
205
|
entry.max_model_len,
|
|
@@ -211,7 +225,7 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
211
225
|
};
|
|
212
226
|
|
|
213
227
|
const mapped = options.mapModel?.(entry, defaults, context) ?? defaults;
|
|
214
|
-
if (!mapped ||
|
|
228
|
+
if (!mapped || !isSafeCatalogModelId(mapped.id)) {
|
|
215
229
|
continue;
|
|
216
230
|
}
|
|
217
231
|
if (options.filterModel && !options.filterModel(entry, mapped)) {
|
|
@@ -314,16 +328,17 @@ function extractModelEntriesFromNode(node: unknown): ParsedOpenAICompatibleModel
|
|
|
314
328
|
}
|
|
315
329
|
|
|
316
330
|
/**
|
|
317
|
-
* First
|
|
331
|
+
* First positive safe integer among candidates, else the fallback.
|
|
318
332
|
*
|
|
319
|
-
* Rejects non-numbers, non-finite values (JSON `1e400` parses to
|
|
320
|
-
*
|
|
321
|
-
* poison compaction thresholds or output
|
|
333
|
+
* Rejects non-numbers, non-finite values (JSON `1e400` parses to `Infinity`),
|
|
334
|
+
* fractions, values outside the safe integer range, zero, and negatives so a
|
|
335
|
+
* malformed catalog field can never poison compaction thresholds or output
|
|
336
|
+
* budgets.
|
|
322
337
|
*/
|
|
323
338
|
function firstPositiveModelNumber(fallback: number, ...candidates: readonly unknown[]): number {
|
|
324
339
|
for (const candidate of candidates) {
|
|
325
340
|
const value = toNumber(candidate);
|
|
326
|
-
if (value !== undefined && value > 0
|
|
341
|
+
if (value !== undefined && Number.isSafeInteger(value) && value > 0) {
|
|
327
342
|
return value;
|
|
328
343
|
}
|
|
329
344
|
}
|
|
@@ -15,6 +15,19 @@ export function getProviderStreamIdleTimeoutFallbackMs(provider: string): number
|
|
|
15
15
|
return undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Grok models behind OpenAI-compatible hosts keep the model id (or an
|
|
19
|
+
// `x-ai/`-prefixed OpenRouter id) even when `provider` is openrouter, kilo,
|
|
20
|
+
// litellm, zenmux, venice, and similar. The long-reasoning silence that
|
|
21
|
+
// motivates the 300s floor is a property of the model, not the account
|
|
22
|
+
// used to reach it, so the floor keys on the model when the provider alone
|
|
23
|
+
// does not already grant it (#4797).
|
|
24
|
+
const GROK_MODEL_ID_PATTERN = /(?:^|[/._-])grok(?:[/._-]|$)/i;
|
|
25
|
+
|
|
26
|
+
export function isGrokModelId(modelId: string | undefined): boolean {
|
|
27
|
+
if (!modelId) return false;
|
|
28
|
+
return GROK_MODEL_ID_PATTERN.test(modelId);
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export function getProviderFirstEventTimeoutFallbackMs(provider: string): number | undefined {
|
|
19
32
|
if (provider === "alibaba-token-plan") return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MS;
|
|
20
33
|
return provider === "kimi-code" ? KIMI_CODE_FIRST_EVENT_TIMEOUT_MS : undefined;
|
|
@@ -50,11 +63,15 @@ export function getStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_
|
|
|
50
63
|
*
|
|
51
64
|
* Honors `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` first (`PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is the legacy alias). Set `=0` to disable.
|
|
52
65
|
* When `provider` is given, long-reasoning hosts (xAI Grok and Grok Build) use that floor instead of the 120s default.
|
|
66
|
+
* Grok models reached through other OpenAI-compatible hosts (`openrouter/x-ai/grok-*`, kilo, litellm, …) get the
|
|
67
|
+
* same floor keyed on the model id, because long-reasoning silence is a property of the model (#4797).
|
|
53
68
|
*/
|
|
54
|
-
export function getOpenAIStreamIdleTimeoutMs(provider?: string): number | undefined {
|
|
69
|
+
export function getOpenAIStreamIdleTimeoutMs(provider?: string, modelId?: string): number | undefined {
|
|
55
70
|
return normalizeIdleTimeoutMs(
|
|
56
|
-
$env.GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.
|
|
57
|
-
getProviderStreamIdleTimeoutFallbackMs(provider ?? "") ??
|
|
71
|
+
$env.GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS,
|
|
72
|
+
getProviderStreamIdleTimeoutFallbackMs(provider ?? "") ??
|
|
73
|
+
(isGrokModelId(modelId) ? ANTHROPIC_STREAM_IDLE_TIMEOUT_MS : undefined) ??
|
|
74
|
+
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
58
75
|
);
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -97,10 +114,11 @@ export function getStreamFirstEventTimeoutMs(
|
|
|
97
114
|
export function resolveOpenAISdkRequestTimeoutMs(
|
|
98
115
|
provider: string,
|
|
99
116
|
streamFirstEventTimeoutOverride?: number,
|
|
117
|
+
modelId?: string,
|
|
100
118
|
): number | undefined {
|
|
101
119
|
const providerFirstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(provider);
|
|
102
120
|
const envSdkTimeoutMs = getStreamFirstEventTimeoutMs(
|
|
103
|
-
getOpenAIStreamIdleTimeoutMs(provider),
|
|
121
|
+
getOpenAIStreamIdleTimeoutMs(provider, modelId),
|
|
104
122
|
providerFirstEventFallbackMs,
|
|
105
123
|
);
|
|
106
124
|
if (streamFirstEventTimeoutOverride === 0) return undefined;
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -180,6 +180,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
180
180
|
name: "OpenCode Go",
|
|
181
181
|
available: true,
|
|
182
182
|
},
|
|
183
|
+
{
|
|
184
|
+
id: "openrouter",
|
|
185
|
+
name: "OpenRouter",
|
|
186
|
+
available: true,
|
|
187
|
+
},
|
|
183
188
|
{
|
|
184
189
|
id: "zai",
|
|
185
190
|
name: "Z.AI (GLM Coding Plan)",
|
|
@@ -408,6 +413,7 @@ export async function refreshOAuthToken(
|
|
|
408
413
|
case "zai":
|
|
409
414
|
case "qianfan":
|
|
410
415
|
case "venice":
|
|
416
|
+
case "openrouter":
|
|
411
417
|
case "minimax-code":
|
|
412
418
|
case "minimax-code-cn":
|
|
413
419
|
case "moonshot":
|
package/src/utils/oauth/kimi.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { scheduler } from "node:timers/promises";
|
|
10
|
-
import { $pickCredentialEnv, getAgentDir, isEnoent } from "@gajae-code/utils";
|
|
10
|
+
import { $pickCredentialEnv, getAgentDir, isEnoent, sanitizeHeaderComponent } from "@gajae-code/utils";
|
|
11
11
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
12
12
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
13
13
|
|
|
@@ -90,18 +90,24 @@ let getDeviceId = (): string => {
|
|
|
90
90
|
return deviceId;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
/** @internal Exported for tests. Builds unsanitized-input-safe Kimi common headers. */
|
|
94
|
+
export function buildKimiCommonHeaders(): Readonly<Record<string, string>> {
|
|
95
|
+
return Object.freeze({
|
|
95
96
|
"User-Agent": `KimiCLI/${packageJson.version}`,
|
|
96
97
|
"X-Msh-Platform": "kimi_cli",
|
|
97
98
|
"X-Msh-Version": packageJson.version,
|
|
98
|
-
"X-Msh-Device-Name": os.hostname(),
|
|
99
|
-
"X-Msh-Device-Model": getDeviceModel(),
|
|
100
|
-
"X-Msh-Os-Version": os.version(),
|
|
99
|
+
"X-Msh-Device-Name": sanitizeHeaderComponent(os.hostname()),
|
|
100
|
+
"X-Msh-Device-Model": sanitizeHeaderComponent(getDeviceModel()),
|
|
101
|
+
"X-Msh-Os-Version": sanitizeHeaderComponent(os.version()),
|
|
101
102
|
"X-Msh-Device-Id": getDeviceId(),
|
|
102
103
|
});
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let memoizedKimiCommonHeaders: Readonly<Record<string, string>> | undefined;
|
|
107
|
+
|
|
108
|
+
export const getKimiCommonHeaders = () => {
|
|
109
|
+
memoizedKimiCommonHeaders ??= buildKimiCommonHeaders();
|
|
110
|
+
return memoizedKimiCommonHeaders;
|
|
105
111
|
};
|
|
106
112
|
|
|
107
113
|
async function requestDeviceAuthorization(): Promise<{
|
package/src/utils/oauth/kiro.ts
CHANGED
|
@@ -382,6 +382,7 @@ export async function loginKiro(options: KiroLoginOptions): Promise<OAuthCredent
|
|
|
382
382
|
|
|
383
383
|
import * as fs from "node:fs";
|
|
384
384
|
import * as path from "node:path";
|
|
385
|
+
import { getTrustedHomeDir } from "@gajae-code/utils";
|
|
385
386
|
|
|
386
387
|
interface SsoCachedAccessToken {
|
|
387
388
|
accessToken: string;
|
|
@@ -398,8 +399,7 @@ interface SsoCachedAccessToken {
|
|
|
398
399
|
* credential store.
|
|
399
400
|
*/
|
|
400
401
|
export function importSsoCacheToken(): OAuthCredentials | undefined {
|
|
401
|
-
const homeDir =
|
|
402
|
-
if (!homeDir) return undefined;
|
|
402
|
+
const homeDir = getTrustedHomeDir();
|
|
403
403
|
const cacheDir = path.join(homeDir, ".aws", "sso", "cache");
|
|
404
404
|
|
|
405
405
|
let files: string[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** OpenRouter login flow (API key paste against https://openrouter.ai/api/v1). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginOpenRouter = createApiKeyLogin({
|
|
5
|
+
providerLabel: "OpenRouter",
|
|
6
|
+
authUrl: "https://openrouter.ai/keys",
|
|
7
|
+
instructions: "Copy your API key from the OpenRouter dashboard",
|
|
8
|
+
promptMessage: "Paste your OpenRouter API key",
|
|
9
|
+
placeholder: "sk-or-v1-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "chat-completions",
|
|
12
|
+
provider: "OpenRouter",
|
|
13
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
14
|
+
model: "openrouter/auto",
|
|
15
|
+
},
|
|
16
|
+
});
|