@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/types/auth-storage.d.ts +2 -2
  3. package/dist/types/model-cache.d.ts +2 -0
  4. package/dist/types/provider-models/special.d.ts +3 -1
  5. package/dist/types/providers/anthropic.d.ts +1 -0
  6. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  7. package/dist/types/providers/cursor/gen/agent_pb.d.ts +3854 -107
  8. package/dist/types/providers/cursor-pi-args.d.ts +119 -0
  9. package/dist/types/providers/cursor.d.ts +8 -1
  10. package/dist/types/providers/openai-codex-responses.d.ts +2 -0
  11. package/dist/types/providers/openai-responses-shared.d.ts +1 -1
  12. package/dist/types/types.d.ts +41 -1
  13. package/dist/types/utils/block-symbols.d.ts +6 -0
  14. package/dist/types/utils/discovery/openai-compatible.d.ts +2 -0
  15. package/dist/types/utils/idle-iterator.d.ts +5 -2
  16. package/dist/types/utils/oauth/kimi.d.ts +3 -9
  17. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  18. package/dist/types/utils/oauth/types.d.ts +1 -1
  19. package/package.json +4 -4
  20. package/src/auth-broker/remote-store.ts +13 -2
  21. package/src/auth-storage.ts +10 -11
  22. package/src/model-cache.ts +78 -0
  23. package/src/model-manager.ts +194 -25
  24. package/src/provider-models/special.ts +67 -4
  25. package/src/providers/anthropic.ts +115 -40
  26. package/src/providers/aws-credential-config.ts +2 -3
  27. package/src/providers/aws-credentials.ts +2 -3
  28. package/src/providers/azure-openai-responses.ts +18 -2
  29. package/src/providers/cursor/exec-modern.ts +497 -0
  30. package/src/providers/cursor/gen/agent_pb.ts +4687 -181
  31. package/src/providers/cursor/proto/agent.proto +1007 -0
  32. package/src/providers/cursor-pi-args.ts +187 -0
  33. package/src/providers/cursor.ts +382 -47
  34. package/src/providers/google-auth.ts +2 -3
  35. package/src/providers/openai-codex-responses.ts +358 -73
  36. package/src/providers/openai-completions.ts +2 -2
  37. package/src/providers/openai-responses-shared.ts +55 -6
  38. package/src/providers/openai-responses.ts +27 -4
  39. package/src/stream.ts +8 -3
  40. package/src/types.ts +55 -0
  41. package/src/utils/block-symbols.ts +11 -0
  42. package/src/utils/discovery/openai-compatible.ts +21 -6
  43. package/src/utils/idle-iterator.ts +22 -4
  44. package/src/utils/oauth/index.ts +6 -0
  45. package/src/utils/oauth/kimi.ts +14 -8
  46. package/src/utils/oauth/kiro.ts +2 -2
  47. package/src/utils/oauth/openrouter.ts +16 -0
  48. package/src/utils/oauth/types.ts +1 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Translate a Pi frame's args into the local tool kwargs that run it.
3
+ *
4
+ * Shared deliberately by three consumers: the provider synthesizes a display
5
+ * block from these, the coding-agent bridge executes with them, and the legacy
6
+ * pi shim performs the identical translation for the old wire. Separate
7
+ * hand-rolled copies drift, and the drift is invisible — the transcript shows
8
+ * one operation while a different one runs.
9
+ *
10
+ * Kept apart from `cursor/exec-modern.ts` on purpose: these are pure
11
+ * string/path functions with no protobuf coupling, while that module pulls in
12
+ * `@bufbuild/protobuf` and the generated `agent_pb` graph. The legacy shim is
13
+ * compiled into the bundled virtual module registry, so importing it from a
14
+ * nested path would drag the whole exec implementation in with it — and
15
+ * `./providers/*` is a single-segment wildcard export that cannot serve a
16
+ * nested specifier under bunfs (issue #3442).
17
+ *
18
+ * Every `optional int32` here is presence-sensitive: `0` is a supplied value,
19
+ * not "unset", so it must never be folded into a default.
20
+ */
21
+ /**
22
+ * A `pi_read` range composed onto the path as `read`'s inline `:raw:N+K`
23
+ * selector.
24
+ *
25
+ * `read` exposes no range kwargs, so an uncomposed range reads the whole file.
26
+ * `offset` is a 1-indexed start clamped like the reference's
27
+ * `Math.max(0, offset - 1)` over 0-indexed lines; `limit` is a line count.
28
+ * `null` marks a present `limit: 0` — zero lines, which no selector expresses
29
+ * and which must not degrade into a whole-file read.
30
+ *
31
+ * The range is `raw` because a plain `:N+K` deliberately pads with one leading
32
+ * and three trailing context lines: helpful for a human reading a snippet,
33
+ * wrong for a caller that asked for exactly `limit` lines from `offset`. The
34
+ * wire result is an opaque `output` string, so the hashline and line-number
35
+ * gutter that `raw` also drops carry nothing the frame's contract needs.
36
+ * A range-free read keeps the ordinary form — whole-file reads want them.
37
+ */
38
+ export declare function piReadPath(readPath: string, offset?: number, limit?: number): string | null;
39
+ /**
40
+ * Whether a read path ends in an OMP line selector, including compound `raw`
41
+ * forms. Cursor uses this only to describe the operation already executed by
42
+ * the coding-agent read tool; the selector remains embedded in the path.
43
+ */
44
+ export declare function piReadPathHasRange(readPath: string): boolean;
45
+ /**
46
+ * The same range as {@link piReadPath}, rendered for a transcript block rather
47
+ * than for execution.
48
+ *
49
+ * Differs only at `limit: 0`, where `piReadPath` returns `null` because no
50
+ * selector reads zero lines and the frame is answered with empty output
51
+ * directly. The block still has to say so: falling back to the bare path there
52
+ * would record a whole-file read whose result is empty, which is the widest
53
+ * possible gap between what a rebuilt transcript shows and what happened.
54
+ * `+0` is never executed — it exists to be read.
55
+ */
56
+ export declare function piReadDisplayPath(readPath: string, offset?: number, limit?: number): string;
57
+ /**
58
+ * A legacy `grep` frame's pagination `offset` as the local tool's file `skip`.
59
+ *
60
+ * `grep` paginates by file and reports "use skip=N for the next page" in that
61
+ * same unit, so the offset maps across directly. A present `0` means "start at
62
+ * the beginning", which is the unskipped search rather than a skip of zero.
63
+ *
64
+ * Shared because both the executing bridge and the provider's transcript
65
+ * synthesis need it: a block showing an unskipped search beside a result from
66
+ * a later file window misreports what was searched.
67
+ */
68
+ export declare function piGrepSkip(offset?: number): number | undefined;
69
+ /**
70
+ * Join a Pi frame's optional `path` with the `glob`/`pattern` it scopes.
71
+ *
72
+ * The local `grep`/`glob` tools take one combined path spec. An absolute
73
+ * pattern ignores the path, and an absent or `.` path leaves the pattern
74
+ * standing alone rather than building a `./`- or `//`-prefixed spec.
75
+ *
76
+ * Uses `node:path` rather than string surgery so Windows absolutes (`C:\…`,
77
+ * UNC) are recognised and separators stay normalized.
78
+ */
79
+ export declare function piJoinPath(basePath: string | undefined, pattern: string): string;
80
+ /**
81
+ * The path a `pi_ls` frame lists.
82
+ *
83
+ * The frame's `limit` is deliberately NOT mapped. It caps directory *entries*
84
+ * (the reference does a flat `readdir` and slices the entry array), while the
85
+ * local `read` tool renders a depth-2 tree with per-directory caps and elision
86
+ * summaries and applies a selector as a *rendered line* slice. Nested rows,
87
+ * headers and "N more" lines all count toward that slice, so `:1+K` would cap
88
+ * a different unit while looking honored — worse than leaving it unset, which
89
+ * at least reports the local listing's own truncation faithfully.
90
+ */
91
+ export declare function piLsPath(basePath: string | undefined): string;
92
+ /** Escape a literal string so the regex-only local `grep` tool matches it verbatim. */
93
+ export declare function piEscapeRegexLiteral(value: string): string;
94
+ /** Clamp a present `optional int32` result cap the way the reference does; `undefined` stays unset. */
95
+ export declare function piLimit(limit: number | undefined): number | undefined;
96
+ /**
97
+ * A `pi_bash` frame's timeout as the local `bash` tool's kwarg.
98
+ *
99
+ * Presence-sensitive like every other `optional int32` here, and unusually
100
+ * load-bearing: `bash` documents `timeout: 0` as "disables the command
101
+ * deadline", so folding a supplied `0` into `undefined` applies the 300s
102
+ * default and kills exactly the long-running command that asked not to be.
103
+ * Negative values have no local meaning and fall back to the default.
104
+ */
105
+ export declare function piTimeout(timeout: number | undefined): number | undefined;
106
+ /**
107
+ * Drop keys whose value is `undefined` so optional local-tool kwargs stay
108
+ * absent rather than present-as-undefined.
109
+ *
110
+ * The Cursor exec bridge historically wrote forms like
111
+ * `cwd: workingDirectory || undefined` and
112
+ * `case: caseInsensitive === true ? false : undefined`. ArkType rejects a
113
+ * present `undefined` on an optional field (`was undefined`) even though
114
+ * omitting the key is valid — which flooded Cursor sessions with bash/grep
115
+ * validation errors for otherwise fine frames.
116
+ */
117
+ export declare function omitUndefinedArgs<T extends Record<string, unknown>>(args: T): {
118
+ [K in keyof T]?: Exclude<T[K], undefined>;
119
+ };
@@ -1,5 +1,6 @@
1
1
  import { type JsonValue } from "@bufbuild/protobuf";
2
2
  import type { CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, StreamFunction, StreamOptions, ToolCall, ToolResultMessage } from "../types";
3
+ import { kCursorExecResolved } from "../utils/block-symbols";
3
4
  import { CURSOR_CLIENT_VERSION } from "./cursor/client-version";
4
5
  export declare const CURSOR_API_URL = "https://api2.cursor.sh";
5
6
  export { CURSOR_CLIENT_VERSION };
@@ -15,13 +16,19 @@ export declare const streamCursor: StreamFunction<"cursor-agent">;
15
16
  type ToolCallState = ToolCall & {
16
17
  index: number;
17
18
  partialJson?: string;
18
- kind: "mcp" | "todo_write" | "native";
19
+ kind: "mcp" | "todo_write" | "native" | "cursor-exec";
20
+ [kCursorExecResolved]?: true;
19
21
  };
20
22
  /** Exported for tests: verifies handler is invoked with correct `this` when passed as bound. */
21
23
  export declare function resolveExecHandler<TArgs, TResult>(args: TArgs, handler: ((args: TArgs) => Promise<CursorExecHandlerResult<TResult>>) | undefined, onToolResult: CursorToolResultHandler | undefined, buildFromToolResult: (toolResult: ToolResultMessage) => TResult, buildRejected: (reason: string) => TResult, buildError: (error: string) => TResult): Promise<{
22
24
  execResult: TResult;
23
25
  toolResult?: ToolResultMessage;
24
26
  }>;
27
+ /** Exported for deterministic coverage of ordered server-message handling. */
28
+ export declare function createCursorMessageQueueForTest(onError?: (error: unknown) => void): {
29
+ enqueue(handler: () => void | Promise<void>): Promise<void>;
30
+ drain(): Promise<void>;
31
+ };
25
32
  /** Exported for direct regression coverage of the JSON-safety boundary. */
26
33
  export declare function cursorJsonSafeValueForTest(value: unknown): unknown;
27
34
  export declare function buildNativeToolCallBlock(toolCall: Record<string, unknown>, callId: string, index: number): ToolCallState | null;
@@ -21,6 +21,8 @@ export interface OpenAICodexWebSocketDebugStats {
21
21
  }
22
22
  /** @internal Exported for tests. */
23
23
  export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined, tools?: Tool[], model?: Model<"openai-codex-responses">): string | Record<string, unknown> | undefined;
24
+ /** @internal Exported for tests. */
25
+ export declare function formatCodexUserAgent(platform: string, release: string, arch: string): string;
24
26
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
25
27
  export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState">): Promise<void>;
26
28
  export interface OpenAICodexTransportDetails {
@@ -39,7 +39,7 @@ export declare function collectCustomCallIds(messages: ResponseInput): Set<strin
39
39
  export declare function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput;
40
40
  export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean): ResponseInputContent[] | undefined;
41
41
  export declare function convertResponsesAssistantMessage<TApi extends Api>(assistantMsg: AssistantMessage, model: Model<TApi>, msgIndex: number, knownCallIds: Set<string>, includeThinkingSignatures?: boolean, customCallIds?: Set<string>): ResponseInput;
42
- export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>): void;
42
+ export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResults: readonly ToolResultMessage[], model: Model<TApi>, strictResponsesPairing: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>): void;
43
43
  export interface ProcessResponsesStreamOptions {
44
44
  onFirstToken?: () => void;
45
45
  onOutputItemDone?: (item: ResponseOutputItem) => void;
@@ -3,7 +3,7 @@ import type { BedrockOptions } from "./providers/amazon-bedrock";
3
3
  import type { AnthropicOptions } from "./providers/anthropic";
4
4
  import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses";
5
5
  import type { CursorOptions } from "./providers/cursor";
6
- import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "./providers/cursor/gen/agent_pb";
6
+ import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, PiBashExecArgs, PiBashExecResult, PiEditExecArgs, PiEditExecResult, PiFindExecArgs, PiFindExecResult, PiGrepExecArgs, PiGrepExecResult, PiLsExecArgs, PiLsExecResult, PiReadExecArgs, PiReadExecResult, PiWriteExecArgs, PiWriteExecResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "./providers/cursor/gen/agent_pb";
7
7
  import type { GoogleOptions } from "./providers/google";
8
8
  import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
9
9
  import type { GoogleVertexOptions } from "./providers/google-vertex";
@@ -461,6 +461,26 @@ export interface Usage {
461
461
  }
462
462
  export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
463
463
  export type AssistantErrorKind = "provider_safety_stop" | "local_snapshot_failure" | "local_buffer_overflow";
464
+ /**
465
+ * Structured, shape-only staging-buffer overflow diagnostic carried on the
466
+ * terminal `AssistantMessage`. Attached only by the agent runtime from its own
467
+ * identity-checked overflow error; every field is a closed vocabulary literal
468
+ * or a locally synthesized number.
469
+ */
470
+ export interface AssistantBufferOverflowDiagnostic {
471
+ /** Rejecting stage from the closed managed-local-failure vocabulary. */
472
+ stage: string;
473
+ /** Which provisional cap tripped. */
474
+ exceeded: "events" | "bytes" | "both";
475
+ /** Events retained in the batch at rejection (post-compaction). */
476
+ stagedEventCount: number;
477
+ /** Bytes retained in the batch at rejection (post-compaction). */
478
+ stagedBytes: number;
479
+ /** Serialized size of the event that was rejected. */
480
+ incomingEventBytes: number;
481
+ maxStagedEvents: number;
482
+ maxStagedBytes: number;
483
+ }
464
484
  export interface OpenAIResponsesHistoryPayload {
465
485
  type: "openaiResponsesHistory";
466
486
  provider?: string;
@@ -499,6 +519,15 @@ export interface AssistantMessage {
499
519
  stopReason: StopReason;
500
520
  errorMessage?: string;
501
521
  errorKind?: AssistantErrorKind;
522
+ /**
523
+ * Structured, shape-only diagnostic for a terminal local staging-buffer
524
+ * overflow (`errorKind: "local_buffer_overflow"`). Attached only by the
525
+ * agent runtime from its own identity-checked overflow error, so a
526
+ * foreign, self-labeled error cannot populate it. Every field is a closed
527
+ * vocabulary literal or a locally synthesized number — parent surfaces
528
+ * render this instead of trusting the free-form `errorMessage`.
529
+ */
530
+ bufferOverflow?: AssistantBufferOverflowDiagnostic;
502
531
  /** 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. */
503
532
  errorStatus?: number;
504
533
  /** Typed upstream failure facts retained for retry classification without parsing errorMessage. */
@@ -548,6 +577,10 @@ export interface CursorShellStreamCallbacks {
548
577
  onStdout(data: string): void;
549
578
  onStderr(data: string): void;
550
579
  }
580
+ export interface CursorPiCall<TArgs> {
581
+ args: TArgs;
582
+ toolCallId: string;
583
+ }
551
584
  export interface CursorExecHandlers {
552
585
  read?: (args: ReadArgs) => Promise<CursorExecHandlerResult<ReadResult>>;
553
586
  ls?: (args: LsArgs) => Promise<CursorExecHandlerResult<LsResult>>;
@@ -558,6 +591,13 @@ export interface CursorExecHandlers {
558
591
  shellStream?: (args: ShellArgs, callbacks: CursorShellStreamCallbacks) => Promise<CursorExecHandlerResult<ShellResult>>;
559
592
  diagnostics?: (args: DiagnosticsArgs) => Promise<CursorExecHandlerResult<DiagnosticsResult>>;
560
593
  mcp?: (call: CursorMcpCall) => Promise<CursorExecHandlerResult<McpResult>>;
594
+ piRead?: (call: CursorPiCall<PiReadExecArgs>) => Promise<CursorExecHandlerResult<PiReadExecResult>>;
595
+ piBash?: (call: CursorPiCall<PiBashExecArgs>) => Promise<CursorExecHandlerResult<PiBashExecResult>>;
596
+ piEdit?: (call: CursorPiCall<PiEditExecArgs>) => Promise<CursorExecHandlerResult<PiEditExecResult>>;
597
+ piWrite?: (call: CursorPiCall<PiWriteExecArgs>) => Promise<CursorExecHandlerResult<PiWriteExecResult>>;
598
+ piGrep?: (call: CursorPiCall<PiGrepExecArgs>) => Promise<CursorExecHandlerResult<PiGrepExecResult>>;
599
+ piFind?: (call: CursorPiCall<PiFindExecArgs>) => Promise<CursorExecHandlerResult<PiFindExecResult>>;
600
+ piLs?: (call: CursorPiCall<PiLsExecArgs>) => Promise<CursorExecHandlerResult<PiLsExecResult>>;
561
601
  onToolResult?: CursorToolResultHandler;
562
602
  }
563
603
  /**
@@ -0,0 +1,6 @@
1
+ export declare const kCursorExecResolved: unique symbol;
2
+ export type CursorExecResolvedCarrier = object & {
3
+ [kCursorExecResolved]?: true;
4
+ };
5
+ export declare function isCursorExecResolved(block: CursorExecResolvedCarrier | null | undefined): boolean;
6
+ export declare function copyCursorExecResolved(target: CursorExecResolvedCarrier, source: CursorExecResolvedCarrier): void;
@@ -1,4 +1,6 @@
1
1
  import type { Api, FetchImpl, Model, Provider } from "../../types";
2
+ /** Catalog identities are rendered and used for routing; unsafe values are dropped, never rewritten. */
3
+ export declare function isSafeCatalogModelId(value: unknown): value is string;
2
4
  /**
3
5
  * Minimal OpenAI-style model entry shape consumed by discovery.
4
6
  *
@@ -1,4 +1,5 @@
1
1
  export declare function getProviderStreamIdleTimeoutFallbackMs(provider: string): number | undefined;
2
+ export declare function isGrokModelId(modelId: string | undefined): boolean;
2
3
  export declare function getProviderFirstEventTimeoutFallbackMs(provider: string): number | undefined;
3
4
  /**
4
5
  * Returns the idle timeout used for provider streaming transports.
@@ -16,8 +17,10 @@ export declare function getStreamIdleTimeoutMs(fallbackMs?: number): number | un
16
17
  *
17
18
  * Honors `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` first (`PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is the legacy alias). Set `=0` to disable.
18
19
  * When `provider` is given, long-reasoning hosts (xAI Grok and Grok Build) use that floor instead of the 120s default.
20
+ * Grok models reached through other OpenAI-compatible hosts (`openrouter/x-ai/grok-*`, kilo, litellm, …) get the
21
+ * same floor keyed on the model id, because long-reasoning silence is a property of the model (#4797).
19
22
  */
20
- export declare function getOpenAIStreamIdleTimeoutMs(provider?: string): number | undefined;
23
+ export declare function getOpenAIStreamIdleTimeoutMs(provider?: string, modelId?: string): number | undefined;
21
24
  /**
22
25
  * Returns the timeout used while waiting for the first stream event.
23
26
  * The first token can legitimately take longer than later inter-event gaps,
@@ -47,7 +50,7 @@ export declare function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fal
47
50
  * window so a short post-connect first-event budget cannot kill legitimate
48
51
  * slow setup.
49
52
  */
50
- export declare function resolveOpenAISdkRequestTimeoutMs(provider: string, streamFirstEventTimeoutOverride?: number): number | undefined;
53
+ export declare function resolveOpenAISdkRequestTimeoutMs(provider: string, streamFirstEventTimeoutOverride?: number, modelId?: string): number | undefined;
51
54
  /**
52
55
  * Resolves the Anthropic SDK client `timeout` so stalled-before-headers requests
53
56
  * are bounded. The Anthropic first-event watchdog deliberately arms only once
@@ -4,15 +4,9 @@
4
4
  import type { OAuthController, OAuthCredentials } from "./types";
5
5
  /** Test seam: the OAuth host as resolved from trusted env. */
6
6
  export declare function resolveKimiOAuthHostForTest(): string;
7
- export declare let getKimiCommonHeaders: () => Readonly<{
8
- "User-Agent": `KimiCLI/${string}`;
9
- "X-Msh-Platform": "kimi_cli";
10
- "X-Msh-Version": string;
11
- "X-Msh-Device-Name": string;
12
- "X-Msh-Device-Model": string;
13
- "X-Msh-Os-Version": string;
14
- "X-Msh-Device-Id": string;
15
- }>;
7
+ /** @internal Exported for tests. Builds unsanitized-input-safe Kimi common headers. */
8
+ export declare function buildKimiCommonHeaders(): Readonly<Record<string, string>>;
9
+ export declare const getKimiCommonHeaders: () => Readonly<Record<string, string>>;
16
10
  /**
17
11
  * Login with Kimi Code OAuth (device code flow).
18
12
  */
@@ -0,0 +1 @@
1
+ export declare const loginOpenRouter: (options: import("./types").OAuthController) => Promise<string>;
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  };
10
- export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "opencodex" | "zai";
10
+ export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "opencodex" | "zai";
11
11
  export type OAuthProviderId = OAuthProvider | (string & {});
12
12
  export type OAuthPrompt = {
13
13
  message: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.14.2",
4
+ "version": "0.15.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/natives": "0.14.2",
44
- "@gajae-code/utils": "0.14.2",
43
+ "@gajae-code/natives": "0.15.0",
44
+ "@gajae-code/utils": "0.15.0",
45
45
  "openai": "^6.36.0",
46
46
  "partial-json": "^0.1.7",
47
47
  "zod": "4.4.3"
@@ -50,7 +50,7 @@
50
50
  "@types/bun": "^1.3.14"
51
51
  },
52
52
  "engines": {
53
- "bun": ">=1.3.14"
53
+ "bun": ">=1.4.0"
54
54
  },
55
55
  "files": [
56
56
  "src",
@@ -75,7 +75,18 @@ const BACKGROUND_BACKOFF_MAX_MS = 30_000;
75
75
  const PRESENTATION_FRESH_MS = 5 * 60_000;
76
76
  const PRESENTATION_RETENTION_MS = 24 * 60 * 60_000;
77
77
  const PRESENTATION_SIDECAR_VERSION = 1;
78
- const DEFAULT_PRESENTATION_SIDECAR = path.join(getConfigRootDir(), "auth-broker-presentations.json");
78
+ /**
79
+ * Default location of the redacted presentation sidecar.
80
+ *
81
+ * Derived when a store is constructed, never at import time: the trusted
82
+ * config root is call-time state (#4761, #4772), so an import-time constant
83
+ * keeps pointing at the home that was in effect when this module first loaded
84
+ * and a process can read and write one logical profile through two different
85
+ * roots (#4786).
86
+ */
87
+ function defaultPresentationSidecarPath(): string {
88
+ return path.join(getConfigRootDir(), "auth-broker-presentations.json");
89
+ }
79
90
 
80
91
  function emptySnapshot(): SnapshotResponse {
81
92
  return {
@@ -194,7 +205,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
194
205
  constructor(opts: RemoteAuthCredentialStoreOptions) {
195
206
  this.#client = opts.client;
196
207
  this.#streamSnapshots = opts.streamSnapshots ?? true;
197
- this.#presentationPath = opts.presentationPath ?? DEFAULT_PRESENTATION_SIDECAR;
208
+ this.#presentationPath = opts.presentationPath ?? defaultPresentationSidecarPath();
198
209
  this.#presentationAuthority = createHash("sha256").update(this.#client.baseUrl).digest("hex");
199
210
  this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0, false);
200
211
  this.#setInventoryState("pending", this.#generation, {
@@ -1676,12 +1676,12 @@ export class AuthStorage {
1676
1676
  return this.#store.listCredentialInventory?.(provider) ?? [];
1677
1677
  }
1678
1678
 
1679
- /** Return local OAuth hard-removal action targets, including disabled rows. */
1679
+ /** Return local credential hard-removal action targets, including disabled rows. */
1680
1680
  listCredentialRemovalTargets(provider?: string): CredentialRemovalTarget[] {
1681
1681
  return this.#store.listCredentialRemovalTargets?.(provider) ?? [];
1682
1682
  }
1683
1683
 
1684
- /** Remove selected local OAuth rows atomically; conflict leaves all rows intact. */
1684
+ /** Remove selected local credential rows atomically; conflict leaves all rows intact. */
1685
1685
  removeAuthCredentialsHard(
1686
1686
  provider: string,
1687
1687
  targets: readonly CredentialRemovalTarget[],
@@ -3065,6 +3065,12 @@ export class AuthStorage {
3065
3065
  await saveApiKeyCredential(apiKey);
3066
3066
  return;
3067
3067
  }
3068
+ case "openrouter": {
3069
+ const { loginOpenRouter } = await import("./utils/oauth/openrouter");
3070
+ const apiKey = await loginOpenRouter(ctrl);
3071
+ await saveApiKeyCredential(apiKey);
3072
+ return;
3073
+ }
3068
3074
  case "litellm": {
3069
3075
  const { loginLiteLLM } = await import("./utils/oauth/litellm");
3070
3076
  const apiKey = await loginLiteLLM(ctrl);
@@ -6153,9 +6159,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
6153
6159
  provider === undefined
6154
6160
  ? (this.#listAllStmt.all() as AuthRow[])
6155
6161
  : (this.#listAllByProviderStmt.all(provider) as AuthRow[]);
6156
- return rows
6157
- .filter(row => row.credential_type === "oauth")
6158
- .map(row => ({ id: row.id, provider: row.provider, expectedRevision: row.revision }));
6162
+ return rows.map(row => ({ id: row.id, provider: row.provider, expectedRevision: row.revision }));
6159
6163
  }
6160
6164
  removeAuthCredentialsHard(
6161
6165
  provider: string,
@@ -6170,12 +6174,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
6170
6174
  .get(target.id) as
6171
6175
  | { id?: number; provider?: string; credential_type?: string; revision?: number }
6172
6176
  | undefined;
6173
- if (
6174
- !row ||
6175
- row.provider !== provider ||
6176
- row.credential_type !== "oauth" ||
6177
- row.revision !== target.expectedRevision
6178
- ) {
6177
+ if (!row || row.provider !== provider || row.revision !== target.expectedRevision) {
6179
6178
  currentIds.push(row?.id ?? target.id);
6180
6179
  }
6181
6180
  }
@@ -156,3 +156,81 @@ export function writeModelCache<TApi extends Api>(
156
156
  // Cache writes are best-effort; failures should not break model resolution.
157
157
  }
158
158
  }
159
+
160
+ export function insertModelCacheIfAbsent<TApi extends Api>(
161
+ providerId: string,
162
+ updatedAt: number,
163
+ models: Model<TApi>[],
164
+ authoritative: boolean,
165
+ staticFingerprint: string,
166
+ dbPath?: string,
167
+ dynamicModelIds?: readonly string[],
168
+ dynamicModelProvenance?: string,
169
+ ): boolean {
170
+ try {
171
+ const result = getDb(dbPath).run(
172
+ `INSERT INTO model_cache (provider_id, version, updated_at, authoritative, static_fingerprint, dynamic_model_ids, dynamic_model_provenance, models)
173
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
174
+ ON CONFLICT(provider_id) DO NOTHING`,
175
+ [
176
+ providerId,
177
+ CACHE_SCHEMA_VERSION,
178
+ updatedAt,
179
+ authoritative ? 1 : 0,
180
+ staticFingerprint,
181
+ dynamicModelIds === undefined ? null : JSON.stringify(dynamicModelIds),
182
+ dynamicModelProvenance ?? null,
183
+ JSON.stringify(models),
184
+ ],
185
+ );
186
+ return result.changes === 1;
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+
192
+ export function updateModelCacheIfUnchanged<TApi extends Api>(
193
+ providerId: string,
194
+ expectedUpdatedAt: number,
195
+ expectedDynamicModelIds: readonly string[] | undefined,
196
+ expectedDynamicModelProvenance: string | undefined,
197
+ expectedModels: readonly Model<TApi>[],
198
+ updatedAt: number,
199
+ models: Model<TApi>[],
200
+ authoritative: boolean,
201
+ staticFingerprint: string,
202
+ dbPath?: string,
203
+ dynamicModelIds?: readonly string[],
204
+ dynamicModelProvenance?: string,
205
+ ): boolean {
206
+ try {
207
+ const expectedIds = expectedDynamicModelIds === undefined ? null : JSON.stringify(expectedDynamicModelIds);
208
+ const provenance = expectedDynamicModelProvenance ?? null;
209
+ const nextIds = dynamicModelIds === undefined ? null : JSON.stringify(dynamicModelIds);
210
+ const nextProvenance = dynamicModelProvenance ?? null;
211
+ const expectedModelsJson = JSON.stringify(expectedModels);
212
+ const result = getDb(dbPath).run(
213
+ `UPDATE model_cache
214
+ SET updated_at = ?, authoritative = ?, static_fingerprint = ?, dynamic_model_ids = ?,
215
+ dynamic_model_provenance = ?, models = ?
216
+ WHERE provider_id = ? AND updated_at = ?
217
+ AND dynamic_model_ids IS ? AND dynamic_model_provenance IS ? AND models = ?`,
218
+ [
219
+ updatedAt,
220
+ authoritative ? 1 : 0,
221
+ staticFingerprint,
222
+ nextIds,
223
+ nextProvenance,
224
+ JSON.stringify(models),
225
+ providerId,
226
+ expectedUpdatedAt,
227
+ expectedIds,
228
+ provenance,
229
+ expectedModelsJson,
230
+ ],
231
+ );
232
+ return result.changes === 1;
233
+ } catch {
234
+ return false;
235
+ }
236
+ }