@gajae-code/ai 0.18.5 → 0.18.7
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 +73 -0
- package/dist/types/auth-storage.d.ts +8 -1
- package/dist/types/providers/cursor.d.ts +6 -3
- package/dist/types/providers/kiro-api-key.d.ts +1 -0
- package/dist/types/providers/openai-codex-responses.d.ts +3 -0
- package/dist/types/types.d.ts +16 -1
- package/dist/types/utils/fallback-transport.d.ts +6 -0
- package/dist/types/utils/idle-iterator.d.ts +2 -0
- package/dist/types/utils/oauth/glm-zcode.d.ts +5 -0
- package/dist/types/utils/oauth/index.d.ts +4 -0
- package/package.json +3 -3
- package/src/auth-storage.ts +115 -39
- package/src/model-thinking.ts +8 -0
- package/src/providers/aws-eventstream.ts +2 -1
- package/src/providers/cursor.d.ts +6 -3
- package/src/providers/cursor.ts +73 -64
- package/src/providers/google-gemini-cli.ts +49 -3
- package/src/providers/kiro-api-key.d.ts +1 -0
- package/src/providers/kiro-api-key.ts +1 -1
- package/src/providers/kiro-codewhisperer.ts +164 -68
- package/src/providers/openai-codex-responses.d.ts +3 -0
- package/src/providers/openai-codex-responses.ts +589 -41
- package/src/providers/openai-opencodex-responses.ts +6 -1
- package/src/providers/openai-responses.ts +1 -1
- package/src/providers/register-builtins.ts +53 -5
- package/src/stream.ts +20 -0
- package/src/types.d.ts +16 -1
- package/src/types.ts +19 -0
- package/src/utils/fallback-transport.d.ts +6 -0
- package/src/utils/fallback-transport.ts +6 -0
- package/src/utils/idle-iterator.d.ts +2 -0
- package/src/utils/idle-iterator.ts +13 -3
- package/src/utils/oauth/glm-zcode.d.ts +5 -0
- package/src/utils/oauth/glm-zcode.ts +8 -1
- package/src/utils/oauth/index.ts +11 -1
- package/src/utils/validation.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,79 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.18.7] - 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Explicitly emit `strict: false` for non-strict OpenAI Responses and Codex Responses function tools.
|
|
10
|
+
|
|
11
|
+
- Salvage finalized Codex tool calls when an empty assistant message item opens before a transient stream close, while still failing closed on non-empty text.
|
|
12
|
+
|
|
13
|
+
- Preserve finalized Codex tool calls when the stream ends without a terminal event, including an empty trailing message; unfinished calls and visible text still fail closed.
|
|
14
|
+
|
|
15
|
+
- Add Codex request-timeout salvage refusal diagnostics and replay coverage for complete `todo_write` calls.
|
|
16
|
+
|
|
17
|
+
- Salvage complete Codex function calls when the SSE or WebSocket idle watchdog ends a stalled stream.
|
|
18
|
+
|
|
19
|
+
- **Kiro OAuth CodeWhisperer wire protocol**: Corrected critical wire protocol issues in the streaming transport:
|
|
20
|
+
- Fixed typo in request header `amzn-X-amz-target` → `x-amz-target`
|
|
21
|
+
- Changed streaming service target from `AmazonCodeWhispererService.GenerateAssistantResponse` to `AmazonCodeWhispererStreamingService.GenerateAssistantResponse`
|
|
22
|
+
- Fixed content-type from `application/json` to `application/x-amz-json-1.0`
|
|
23
|
+
- Moved `profileArn` from header to request body `conversationState`
|
|
24
|
+
- Flattened `userInputMessageContext.tools` from nested `{tools: [...]}` to direct array
|
|
25
|
+
- Fixed event payload handling for flat `{content}` structure (not nested `{assistantResponseEvent: {content}}`)
|
|
26
|
+
- Implemented accumulation of streaming tool input fragments per `toolUseId` until `stop` signal
|
|
27
|
+
- Cross-verified against kiro-api-key.ts headers implementation
|
|
28
|
+
- Reported by: nomo via wire protocol validation failures
|
|
29
|
+
|
|
30
|
+
## [0.18.6] - 2026-10-03
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `AuthStorage.hasLiteralConfigApiKey(provider, owner?)` reports whether a provider's config override is a literal `apiKey` rather than an `apiKeyEnv` indirection.
|
|
35
|
+
|
|
36
|
+
- Bundled GPT-6.1 Sol for the Codex provider: `openai-codex/gpt-6.1-sol` (272K context, 128K output, reasoning effort low..max) with $2/$10 per MTok and $0.10 cached-input pricing (#6147).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- GLM Coding Plan (`glm-zcode`) GLM-5.3 and GLM-5.3-Flash now expose the `max` thinking level on top of the existing `minimal`–`xhigh` budget ladder. These models ride the Anthropic Messages-compatible endpoint where thinking is a token budget, and the generic anthropic-messages fallback capped non-Anthropic models at `xhigh` (32768 tokens). The endpoint accepts budgets up to 65536 and reasoning volume scales with the budget, so the previous ceiling silently limited the GLM-5.3 generation.
|
|
41
|
+
|
|
42
|
+
- The `glm-zcode` OAuth login guidance now names the exact `zcode://oauth/callback?code=…&state=…` redirect shape and tells users to copy it from the browser DevTools Network tab, because a custom-protocol redirect never appears in the address bar; the manual-code prompt is now `glm-zcode`-specific instead of the generic authorization-code wording, which previously led users to paste the address-bar URL and stall the login.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Anthropic-adaptive models with high/xhigh/max reasoning now receive sufficient `max_tokens` to accommodate both thinking output and message completion. Previously, adaptive requests used the default 32,000-token budget, consuming all tokens for thinking and leaving no output. When reasoning is enabled with adaptive thinking, the budget is now raised to `model.maxTokens` to allow room for both thinking and output. Explicit or configured `maxTokens` values remain authoritative and are not overridden. The fix also treats `maxTokens: 0` (documented as unspecified) the same as `undefined`, matching the behavior of `resolveDefaultRequestMaxTokens` (#6156).
|
|
47
|
+
|
|
48
|
+
- A Kiro CodeWhisperer HTTP 200 response whose body is not an AWS event stream (for example a JSON or HTML error) now reports its status, content type, and the first 1000 characters of the body. Previously the body was parsed as event-stream frames and surfaced only as `eventstream: truncated message at end of stream`. The event-stream media type is matched case-insensitively, and the truncation error now reports how many trailing bytes were left (#6158). The error carries the `provider_protocol_mismatch` code, so session retry and managed fallback surface it instead of replaying the request.
|
|
49
|
+
|
|
50
|
+
- Kiro (OAuth CodeWhisperer transport) now sets `userInputMessage.origin: AI_EDITOR`. Without it the service ignored `modelId` and answered every request with `auto`, so selecting a specific Kiro model had no effect.
|
|
51
|
+
- Kiro (OAuth CodeWhisperer transport) now sends every trailing result of a parallel tool batch in `currentMessage`. The earlier results were left as a separate history entry, so the turn after two or more parallel tool calls failed with HTTP 400 `TOOL_USE_RESULT_MISMATCH`.
|
|
52
|
+
|
|
53
|
+
- Salvage Codex function calls when complete JSON arguments arrive but the stream closes before `response.output_item.done`, including empty-object no-argument calls and idle SSE stalls.
|
|
54
|
+
- Fail closed on mismatched argument events, explicit non-transient errors, and unauthoritative whitespace-only deltas; close open reasoning blocks before salvage.
|
|
55
|
+
|
|
56
|
+
- Codex SSE progress classification reads each event's `delta` once, so the idle-watchdog classifier added in #6226 can no longer observe a different value than the stream handler assembles.
|
|
57
|
+
|
|
58
|
+
- End a silent Codex WebSocket stream at its idle bound after a completed `todo_write` tool-call start with zero usage.
|
|
59
|
+
- Preserve Codex transport errors and eligible SSE recovery when provisional tool arguments parse to null or another non-record value.
|
|
60
|
+
|
|
61
|
+
- Recover expired Codex websocket continuation anchors with one anchor-free full-context replay on the same model when fallback is managed, while still honoring disabled provider retries.
|
|
62
|
+
|
|
63
|
+
- Preserve bounded session retries for content-free Codex `server_is_overloaded` stream errors with generic or absent prose.
|
|
64
|
+
|
|
65
|
+
- Codex websocket streams with an idle or first-event timeout above `2147483647` ms (about 24.9 days), such as `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS=99999999999` meant as "effectively never", no longer time out right after the request. `setTimeout` treats longer delays as 1 ms; the wait is now capped at the timer maximum.
|
|
66
|
+
|
|
67
|
+
- Treat trailing whitespace after complete Codex function-call arguments as idle SSE events without changing WebSocket progress or replay semantics.
|
|
68
|
+
|
|
69
|
+
- Prevent Cursor inline MCP, todo, and native tool announcements from being replayed locally after a completed or aborted turn. Keep one canonical provider-owned MCP call for its inline execution.
|
|
70
|
+
|
|
71
|
+
- Release the cross-process OAuth refresh lease when a token refresh fails, and keep credentials active when their OAuth provider has not registered yet.
|
|
72
|
+
|
|
73
|
+
- Start SQLite auth-storage read-then-write transactions immediately to prevent concurrent OAuth refresh processes from failing with `database is locked`.
|
|
74
|
+
- Advance the OAuth lease clock by time spent waiting for the immediate write reservation so busy-timeout waits do not shorten leases.
|
|
75
|
+
|
|
76
|
+
- Stream idle and first-event watchdogs with a timeout above `2147483647` ms (about 24.9 days), such as `PI_STREAM_IDLE_TIMEOUT_MS=99999999999` meant as "effectively never", no longer abort the stream right away. `setTimeout` treats longer delays as 1 ms; the watchdog delay is now capped at the timer maximum. `0` still disables the watchdog.
|
|
77
|
+
|
|
5
78
|
## [0.18.5] - 2026-09-30
|
|
6
79
|
|
|
7
80
|
## [0.18.4] - 2026-09-30
|
|
@@ -329,7 +329,8 @@ export interface AuthCredentialStore {
|
|
|
329
329
|
* Atomically adopts a fresh row or claims the current refresh token for one
|
|
330
330
|
* local provider dial. SQLite-backed stores use this to prevent another
|
|
331
331
|
* process from replaying a rotating refresh token between a pre-read and
|
|
332
|
-
* the provider request.
|
|
332
|
+
* the provider request. The supplied clock is advanced by any time spent
|
|
333
|
+
* waiting for the immediate write reservation.
|
|
333
334
|
*/
|
|
334
335
|
claimOAuthRefreshLease?(credentialId: number, expectedRefresh: string, force: boolean, owner: string, nowMs: number, leaseMs: number): OAuthRefreshLeaseClaim;
|
|
335
336
|
/** Atomically persists a successful claimed refresh and releases its lease. */
|
|
@@ -702,6 +703,12 @@ export declare class AuthStorage {
|
|
|
702
703
|
hasRuntimeApiKey(provider: string): boolean;
|
|
703
704
|
/** Whether a provider is currently authenticated by a config API-key override. */
|
|
704
705
|
hasConfigApiKey(provider: string, owner?: object): boolean;
|
|
706
|
+
/**
|
|
707
|
+
* Whether a provider's config override is a literal `apiKey`, not an `apiKeyEnv`
|
|
708
|
+
* indirection. Startup pin policy treats only a literal key as explicit, because
|
|
709
|
+
* an env-sourced override yields to a stored api_key account (see {@link getApiKey}).
|
|
710
|
+
*/
|
|
711
|
+
hasLiteralConfigApiKey(provider: string, owner?: object): boolean;
|
|
705
712
|
/**
|
|
706
713
|
* Whether credential selection for a provider is pinned to one stored row by
|
|
707
714
|
* a runtime selector (`--credential`).
|
|
@@ -2,8 +2,10 @@ import http2 from "node:http2";
|
|
|
2
2
|
import { type JsonValue } from "@bufbuild/protobuf";
|
|
3
3
|
import type { AssistantMessage, CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, Model, StreamFunction, StreamOptions, Tool, ToolCall, ToolResultMessage, Usage } from "../types";
|
|
4
4
|
import { kProviderResolvedToolCall } from "../utils/block-symbols";
|
|
5
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
5
6
|
import { CURSOR_CLIENT_VERSION } from "./cursor/client-version";
|
|
6
7
|
import type { CursorRule, RequestedModel_ModelParameterbytes } from "./cursor/gen/agent_pb";
|
|
8
|
+
import { type AgentServerMessage } from "./cursor/gen/agent_pb";
|
|
7
9
|
export declare const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
8
10
|
export { CURSOR_CLIENT_VERSION };
|
|
9
11
|
/** Drop all cached state + blob bytes for a conversation (F15 bound + session-teardown hook). */
|
|
@@ -46,9 +48,8 @@ export declare function mapH2TransportError(error: unknown, baseUrl: string): un
|
|
|
46
48
|
export declare const streamCursor: StreamFunction<"cursor-agent">;
|
|
47
49
|
type ToolCallState = ToolCall & {
|
|
48
50
|
index: number;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[kProviderResolvedToolCall]?: true;
|
|
51
|
+
kind: "todo_write" | "native" | "cursor-exec";
|
|
52
|
+
[kProviderResolvedToolCall]: true;
|
|
52
53
|
};
|
|
53
54
|
interface UsageState {
|
|
54
55
|
sawTokenDelta: boolean;
|
|
@@ -56,6 +57,8 @@ interface UsageState {
|
|
|
56
57
|
checkpointOutputTokens: number;
|
|
57
58
|
hasConversationCheckpoint: boolean;
|
|
58
59
|
}
|
|
60
|
+
/** Offline test seam for the same server-message boundary used by streamCursor. */
|
|
61
|
+
export declare function createCursorServerMessageHandlerForTest(output: AssistantMessage, stream: AssistantMessageEventStream, write: (frame: Uint8Array, done: () => void) => boolean, options?: CursorOptions): (message: AgentServerMessage) => Promise<void>;
|
|
59
62
|
export declare function storeCursorBlobForTest(blobStore: Map<string, Uint8Array>, blobId: Uint8Array, blobData: Uint8Array, limits: {
|
|
60
63
|
maxBytes: number;
|
|
61
64
|
}): boolean;
|
|
@@ -5,6 +5,7 @@ export declare function kiroApiRegion(options?: {
|
|
|
5
5
|
}): string;
|
|
6
6
|
export declare function kiroApiBaseUrl(region: string): string;
|
|
7
7
|
export declare function toKiroModelId(modelId: string): string;
|
|
8
|
+
export declare function sanitizeKiroError(value: unknown, secret?: string): string;
|
|
8
9
|
export declare function kiroApiStaticModels(): Model<"kiro-codewhisperer-stream">[];
|
|
9
10
|
/** Discover models this API key can use. Returns null when the key is missing. */
|
|
10
11
|
export declare function fetchKiroApiModels(apiKey: string, region?: string): Promise<Model<"kiro-codewhisperer-stream">[]>;
|
|
@@ -11,6 +11,8 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
|
11
11
|
preferWebsockets?: boolean;
|
|
12
12
|
serviceTier?: ServiceTier;
|
|
13
13
|
}
|
|
14
|
+
/** @internal Exported for tests. */
|
|
15
|
+
export declare function createCodexStreamProgressClassifier(): (event: unknown) => boolean;
|
|
14
16
|
type CodexTransport = "sse" | "websocket";
|
|
15
17
|
export interface OpenAICodexWebSocketDebugStats {
|
|
16
18
|
fullContextRequests: number;
|
|
@@ -75,3 +77,4 @@ type CodexToolPayload = {
|
|
|
75
77
|
/** @internal Exported for tests. */
|
|
76
78
|
export declare function convertOpenAICodexResponsesTools(tools: Tool[], model: Model<"openai-codex-responses">): CodexToolPayload[];
|
|
77
79
|
declare function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean;
|
|
80
|
+
export declare function isExplicitCodexTerminalVeto(code?: string, providerMessage?: string): boolean;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -640,6 +640,8 @@ export interface AssistantMessage {
|
|
|
640
640
|
* supplied no trustworthy structured evidence.
|
|
641
641
|
*/
|
|
642
642
|
providerDiagnostic?: ProviderDiagnostic;
|
|
643
|
+
/** Redacted first-event transport timing and chunk-shape diagnostics for Google Gemini CLI. */
|
|
644
|
+
googleGeminiCliDiagnostics?: GoogleGeminiCliDiagnostics;
|
|
643
645
|
/** Typed upstream failure facts retained for retry classification without parsing errorMessage. */
|
|
644
646
|
transportFailure?: TransportFailureFacts;
|
|
645
647
|
/**
|
|
@@ -658,6 +660,19 @@ export interface AssistantMessage {
|
|
|
658
660
|
/** Prompt-prefix fingerprint of the request that produced this message. */
|
|
659
661
|
promptPrefix?: PromptPrefixTelemetry;
|
|
660
662
|
}
|
|
663
|
+
export interface GoogleGeminiCliDiagnostics {
|
|
664
|
+
responseAtMs?: number;
|
|
665
|
+
firstRawSseAtMs?: number;
|
|
666
|
+
chunkCounts: {
|
|
667
|
+
content: number;
|
|
668
|
+
thinking: number;
|
|
669
|
+
functionCall: number;
|
|
670
|
+
usageOnly: number;
|
|
671
|
+
other: number;
|
|
672
|
+
};
|
|
673
|
+
firstEventTimeoutMs?: number;
|
|
674
|
+
firstEventTimeoutSource?: "stream-option" | "env" | "idle-timeout" | "provider-fallback" | "default";
|
|
675
|
+
}
|
|
661
676
|
export interface ToolResultMessage<TDetails = any> {
|
|
662
677
|
role: "toolResult";
|
|
663
678
|
toolCallId: string;
|
|
@@ -746,7 +761,7 @@ export type TSchema = ZodType | TJsonSchema;
|
|
|
746
761
|
export type Static<S> = S extends ZodType ? z.infer<S> : S extends {
|
|
747
762
|
static: infer T;
|
|
748
763
|
} ? T : unknown;
|
|
749
|
-
export type RawArgumentRejectionCode = "ask-deep-interview-question-body-required" | "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
|
|
764
|
+
export type RawArgumentRejectionCode = "ask-question-body-required" | "ask-deep-interview-metadata-required" | "ask-deep-interview-single-question-required" | "ask-deep-interview-question-body-required" | "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
|
|
750
765
|
/**
|
|
751
766
|
* Optional structured detail attached to a raw-argument rejection. The fixed
|
|
752
767
|
* per-code guidance in `RAW_ARGUMENT_REJECTION_MESSAGES` explains the shape;
|
|
@@ -23,6 +23,12 @@ export interface FallbackTrigger {
|
|
|
23
23
|
export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
|
|
24
24
|
/** Stable code for a nominally successful response with no content or token usage. */
|
|
25
25
|
export declare const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
|
|
26
|
+
/**
|
|
27
|
+
* Terminal error code for a successful HTTP response whose body is not the protocol the
|
|
28
|
+
* provider speaks (for example a JSON or HTML error returned with 200). Deterministic for
|
|
29
|
+
* the request, so session retry and managed fallback must surface it instead of replaying.
|
|
30
|
+
*/
|
|
31
|
+
export declare const PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE = "provider_protocol_mismatch";
|
|
26
32
|
/**
|
|
27
33
|
* OpenAI's typed capacity-overload code. It arrives without an HTTP status —
|
|
28
34
|
* inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
|
|
@@ -72,6 +72,7 @@ export interface FirstEventTimeoutFacts {
|
|
|
72
72
|
requestBytes?: number;
|
|
73
73
|
firstEventElapsedMs?: number;
|
|
74
74
|
firstEventTimeoutMs?: number;
|
|
75
|
+
firstEventTimeoutSource?: "stream-option" | "env" | "idle-timeout" | "provider-fallback" | "default";
|
|
75
76
|
endpointClass?: "canonical" | "custom";
|
|
76
77
|
retryMaxAttempts?: number;
|
|
77
78
|
}
|
|
@@ -80,6 +81,7 @@ export declare class FirstEventTimeoutError extends Error {
|
|
|
80
81
|
readonly requestBytes?: number;
|
|
81
82
|
readonly firstEventElapsedMs?: number;
|
|
82
83
|
readonly firstEventTimeoutMs?: number;
|
|
84
|
+
readonly firstEventTimeoutSource?: FirstEventTimeoutFacts["firstEventTimeoutSource"];
|
|
83
85
|
readonly endpointClass?: "canonical" | "custom";
|
|
84
86
|
readonly retryMaxAttempts?: number;
|
|
85
87
|
constructor(message: string, facts?: FirstEventTimeoutFacts);
|
|
@@ -57,6 +57,11 @@ export declare class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
|
|
|
57
57
|
}>;
|
|
58
58
|
exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Manual-input prompt for the paste fallback. Repeats the redirect-URL shape and
|
|
62
|
+
* the DevTools pointer so a re-prompt after a rejected paste stays self-contained.
|
|
63
|
+
*/
|
|
64
|
+
export declare const GLM_ZCODE_MANUAL_INPUT_PROMPT = "Paste the full zcode://oauth/callback?code=\u2026&state=\u2026 URL (copy it from browser DevTools \u2192 Network tab; the address bar never shows it) or the authorization code:";
|
|
60
65
|
export declare function loginGlmZcode(ctrl: OAuthController, options?: GlmZcodeOAuthFlowOptions): Promise<OAuthCredentials>;
|
|
61
66
|
export interface GlmZcodeRefreshOptions {
|
|
62
67
|
signal?: AbortSignal;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { OAuthCredentials, OAuthProvider, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types";
|
|
2
|
+
export declare class UnknownOAuthProviderError extends Error {
|
|
3
|
+
readonly provider: string;
|
|
4
|
+
constructor(provider: string);
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
7
|
* Register a custom OAuth provider.
|
|
4
8
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.7",
|
|
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",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
42
42
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
43
43
|
"@bufbuild/protobuf": "^2.12.0",
|
|
44
|
-
"@gajae-code/natives": "0.18.
|
|
45
|
-
"@gajae-code/utils": "0.18.
|
|
44
|
+
"@gajae-code/natives": "0.18.7",
|
|
45
|
+
"@gajae-code/utils": "0.18.7",
|
|
46
46
|
"openai": "^6.36.0",
|
|
47
47
|
"partial-json": "^0.1.7",
|
|
48
48
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -26,7 +26,13 @@ import type {
|
|
|
26
26
|
UsageReport,
|
|
27
27
|
} from "./usage";
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
getOAuthApiKey,
|
|
31
|
+
getOAuthProvider,
|
|
32
|
+
refreshOAuthToken,
|
|
33
|
+
resolveOAuthStorageProvider,
|
|
34
|
+
UnknownOAuthProviderError,
|
|
35
|
+
} from "./utils/oauth";
|
|
30
36
|
import { loginDeepInfra } from "./utils/oauth/deepinfra";
|
|
31
37
|
import { loginDeepSeek } from "./utils/oauth/deepseek";
|
|
32
38
|
import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex";
|
|
@@ -531,7 +537,8 @@ export interface AuthCredentialStore {
|
|
|
531
537
|
* Atomically adopts a fresh row or claims the current refresh token for one
|
|
532
538
|
* local provider dial. SQLite-backed stores use this to prevent another
|
|
533
539
|
* process from replaying a rotating refresh token between a pre-read and
|
|
534
|
-
* the provider request.
|
|
540
|
+
* the provider request. The supplied clock is advanced by any time spent
|
|
541
|
+
* waiting for the immediate write reservation.
|
|
535
542
|
*/
|
|
536
543
|
claimOAuthRefreshLease?(
|
|
537
544
|
credentialId: number,
|
|
@@ -2181,6 +2188,16 @@ export class AuthStorage {
|
|
|
2181
2188
|
return Boolean(this.#configOverrideRegistration(provider, owner)?.apiKey);
|
|
2182
2189
|
}
|
|
2183
2190
|
|
|
2191
|
+
/**
|
|
2192
|
+
* Whether a provider's config override is a literal `apiKey`, not an `apiKeyEnv`
|
|
2193
|
+
* indirection. Startup pin policy treats only a literal key as explicit, because
|
|
2194
|
+
* an env-sourced override yields to a stored api_key account (see {@link getApiKey}).
|
|
2195
|
+
*/
|
|
2196
|
+
hasLiteralConfigApiKey(provider: string, owner?: object): boolean {
|
|
2197
|
+
const registration = this.#configOverrideRegistration(provider, owner);
|
|
2198
|
+
return Boolean(registration?.apiKey) && !registration?.envSourced;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2184
2201
|
/**
|
|
2185
2202
|
* Whether credential selection for a provider is pinned to one stored row by
|
|
2186
2203
|
* a runtime selector (`--credential`).
|
|
@@ -3676,10 +3693,11 @@ export class AuthStorage {
|
|
|
3676
3693
|
break;
|
|
3677
3694
|
}
|
|
3678
3695
|
case "glm-zcode": {
|
|
3679
|
-
const { loginGlmZcode } = await import("./utils/oauth/glm-zcode");
|
|
3696
|
+
const { loginGlmZcode, GLM_ZCODE_MANUAL_INPUT_PROMPT } = await import("./utils/oauth/glm-zcode");
|
|
3680
3697
|
credentials = await loginGlmZcode({
|
|
3681
3698
|
...ctrl,
|
|
3682
|
-
onManualCodeInput:
|
|
3699
|
+
onManualCodeInput:
|
|
3700
|
+
ctrl.onManualCodeInput ?? (() => ctrl.onPrompt({ message: GLM_ZCODE_MANUAL_INPUT_PROMPT })),
|
|
3683
3701
|
});
|
|
3684
3702
|
break;
|
|
3685
3703
|
}
|
|
@@ -3872,7 +3890,7 @@ export class AuthStorage {
|
|
|
3872
3890
|
default: {
|
|
3873
3891
|
const customProvider = getOAuthProvider(provider);
|
|
3874
3892
|
if (!customProvider) {
|
|
3875
|
-
throw new
|
|
3893
|
+
throw new UnknownOAuthProviderError(provider);
|
|
3876
3894
|
}
|
|
3877
3895
|
const customLoginResult = await customProvider.login({
|
|
3878
3896
|
onAuth: info => ctrl.onAuth(info),
|
|
@@ -5538,6 +5556,21 @@ export class AuthStorage {
|
|
|
5538
5556
|
let refreshPromise: Promise<OAuthCredentials>;
|
|
5539
5557
|
let localDial = false;
|
|
5540
5558
|
let refreshLease: OAuthRefreshLease | undefined;
|
|
5559
|
+
let refreshLeaseCompleted = false;
|
|
5560
|
+
const releaseRefreshLease = (): void => {
|
|
5561
|
+
if (!refreshLease || refreshLeaseCompleted) return;
|
|
5562
|
+
refreshLeaseCompleted = true;
|
|
5563
|
+
try {
|
|
5564
|
+
const releaseLease = this.#store.releaseOAuthRefreshLease?.bind(this.#store);
|
|
5565
|
+
releaseLease?.(refreshLease);
|
|
5566
|
+
} catch (error) {
|
|
5567
|
+
logger.warn("OAuth refresh lease release failed", {
|
|
5568
|
+
provider,
|
|
5569
|
+
credentialId: refreshLease.credentialId,
|
|
5570
|
+
error: scrubHealthReason(error, [credential.access, credential.refresh]),
|
|
5571
|
+
});
|
|
5572
|
+
}
|
|
5573
|
+
};
|
|
5541
5574
|
|
|
5542
5575
|
// Caller override > store-level hook > local per-provider refresh.
|
|
5543
5576
|
// `RemoteAuthCredentialStore` exposes the hook so a broker-backed gateway
|
|
@@ -5571,14 +5604,20 @@ export class AuthStorage {
|
|
|
5571
5604
|
const deadline = Date.now() + OAUTH_REFRESH_LEASE_MS;
|
|
5572
5605
|
for (;;) {
|
|
5573
5606
|
if (signal?.aborted) throw new Error("OAuth token refresh aborted by caller");
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5607
|
+
let claim: OAuthRefreshLeaseClaim;
|
|
5608
|
+
try {
|
|
5609
|
+
claim = claimLease(
|
|
5610
|
+
credentialId,
|
|
5611
|
+
credential.refresh,
|
|
5612
|
+
force,
|
|
5613
|
+
owner,
|
|
5614
|
+
Date.now(),
|
|
5615
|
+
OAUTH_REFRESH_LEASE_MS,
|
|
5616
|
+
);
|
|
5617
|
+
} catch (error) {
|
|
5618
|
+
releaseRefreshLease();
|
|
5619
|
+
throw error;
|
|
5620
|
+
}
|
|
5582
5621
|
if (claim.kind === "missing") throw new Error("OAuth refresh credential disappeared");
|
|
5583
5622
|
|
|
5584
5623
|
if (claim.kind === "claimed") {
|
|
@@ -5628,6 +5667,7 @@ export class AuthStorage {
|
|
|
5628
5667
|
const memoKey = `${credentialId}:${credential.refresh}`;
|
|
5629
5668
|
const memo = this.#recentOAuthRefreshFailures.get(memoKey);
|
|
5630
5669
|
if (memo && memo.expiresAt > Date.now()) {
|
|
5670
|
+
releaseRefreshLease();
|
|
5631
5671
|
throw memo.error;
|
|
5632
5672
|
}
|
|
5633
5673
|
}
|
|
@@ -5637,16 +5677,32 @@ export class AuthStorage {
|
|
|
5637
5677
|
// and its refresh token must only ever be sent to the bound token
|
|
5638
5678
|
// endpoint.
|
|
5639
5679
|
if (credential.mcpBinding) {
|
|
5640
|
-
|
|
5680
|
+
try {
|
|
5681
|
+
refreshPromise = refreshBoundMCPOAuthCredential(credential, mcpClient, signal);
|
|
5682
|
+
} catch (error) {
|
|
5683
|
+
releaseRefreshLease();
|
|
5684
|
+
throw error;
|
|
5685
|
+
}
|
|
5641
5686
|
} else {
|
|
5642
5687
|
const customProvider = getOAuthProvider(provider);
|
|
5643
5688
|
if (customProvider) {
|
|
5644
5689
|
if (!customProvider.refreshToken) {
|
|
5690
|
+
releaseRefreshLease();
|
|
5645
5691
|
throw new Error(`OAuth provider "${provider}" does not support token refresh`);
|
|
5646
5692
|
}
|
|
5647
|
-
|
|
5693
|
+
try {
|
|
5694
|
+
refreshPromise = customProvider.refreshToken(credential);
|
|
5695
|
+
} catch (error) {
|
|
5696
|
+
releaseRefreshLease();
|
|
5697
|
+
throw error;
|
|
5698
|
+
}
|
|
5648
5699
|
} else {
|
|
5649
|
-
|
|
5700
|
+
try {
|
|
5701
|
+
refreshPromise = refreshOAuthToken(provider as OAuthProvider, credential);
|
|
5702
|
+
} catch (error) {
|
|
5703
|
+
releaseRefreshLease();
|
|
5704
|
+
throw error;
|
|
5705
|
+
}
|
|
5650
5706
|
}
|
|
5651
5707
|
}
|
|
5652
5708
|
}
|
|
@@ -5701,6 +5757,7 @@ export class AuthStorage {
|
|
|
5701
5757
|
if (!completeLease?.(refreshLease, persisted)) {
|
|
5702
5758
|
throw new Error("OAuth token refresh ownership was lost before persistence");
|
|
5703
5759
|
}
|
|
5760
|
+
refreshLeaseCompleted = true;
|
|
5704
5761
|
authority.persistedByLease = true;
|
|
5705
5762
|
}
|
|
5706
5763
|
return authority;
|
|
@@ -5716,17 +5773,20 @@ export class AuthStorage {
|
|
|
5716
5773
|
// pair is immediately eligible for a second refresh, replaying the token
|
|
5717
5774
|
// and tripping provider reuse detection. Skip the guard update only for a
|
|
5718
5775
|
// caller-owned abort, never for an internal timeout.
|
|
5719
|
-
|
|
5720
|
-
|
|
5776
|
+
const callerAbort = signal?.aborted && !isTimeoutAbort(signal);
|
|
5777
|
+
const unknownProvider = error instanceof UnknownOAuthProviderError;
|
|
5778
|
+
const taggedError = callerAbort || unknownProvider ? error : tagRefreshAttempt(error, credential.refresh);
|
|
5779
|
+
if (!callerAbort && !unknownProvider && localDial && credentialId !== undefined) {
|
|
5721
5780
|
for (const [key, entry] of this.#recentOAuthRefreshFailures) {
|
|
5722
5781
|
if (entry.expiresAt <= Date.now()) this.#recentOAuthRefreshFailures.delete(key);
|
|
5723
5782
|
}
|
|
5724
5783
|
this.#recentOAuthRefreshFailures.set(`${credentialId}:${credential.refresh}`, {
|
|
5725
5784
|
expiresAt: Date.now() + OAUTH_REFRESH_FAILURE_REPLAY_GUARD_MS,
|
|
5726
|
-
error,
|
|
5785
|
+
error: taggedError,
|
|
5727
5786
|
});
|
|
5728
5787
|
}
|
|
5729
|
-
|
|
5788
|
+
releaseRefreshLease();
|
|
5789
|
+
throw taggedError;
|
|
5730
5790
|
} finally {
|
|
5731
5791
|
if (timeout) clearTimeout(timeout);
|
|
5732
5792
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
@@ -7188,7 +7248,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7188
7248
|
`);
|
|
7189
7249
|
this.#db.run("DROP TABLE auth_credentials_v0");
|
|
7190
7250
|
});
|
|
7191
|
-
migrate();
|
|
7251
|
+
migrate.immediate();
|
|
7192
7252
|
}
|
|
7193
7253
|
|
|
7194
7254
|
#migrateAuthSchemaV1OrV2ToV3(): void {
|
|
@@ -7210,7 +7270,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7210
7270
|
`);
|
|
7211
7271
|
this.#db.run("DROP TABLE auth_credentials_legacy");
|
|
7212
7272
|
});
|
|
7213
|
-
migrate();
|
|
7273
|
+
migrate.immediate();
|
|
7214
7274
|
}
|
|
7215
7275
|
|
|
7216
7276
|
#migrateAuthSchemaV3ToV4(): void {
|
|
@@ -7232,7 +7292,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7232
7292
|
`);
|
|
7233
7293
|
this.#db.run("DROP TABLE auth_credentials_v3");
|
|
7234
7294
|
});
|
|
7235
|
-
migrate();
|
|
7295
|
+
migrate.immediate();
|
|
7236
7296
|
}
|
|
7237
7297
|
#migrateAuthSchemaV4ToV5(): void {
|
|
7238
7298
|
const columns = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
|
|
@@ -7335,7 +7395,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7335
7395
|
.run(`usage_cache:report:${provider}:`.length, `usage_cache:report:${provider}:`);
|
|
7336
7396
|
return { kind: "removed", ids: unique.map(target => target.id) };
|
|
7337
7397
|
});
|
|
7338
|
-
return remove();
|
|
7398
|
+
return remove.immediate();
|
|
7339
7399
|
}
|
|
7340
7400
|
claimOAuthRefreshLease(
|
|
7341
7401
|
credentialId: number,
|
|
@@ -7345,7 +7405,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7345
7405
|
nowMs: number,
|
|
7346
7406
|
leaseMs: number,
|
|
7347
7407
|
): OAuthRefreshLeaseClaim {
|
|
7408
|
+
const enteredAtMs = Date.now();
|
|
7348
7409
|
const claim = this.#db.transaction((): OAuthRefreshLeaseClaim => {
|
|
7410
|
+
const effectiveNowMs = nowMs + Math.max(0, Date.now() - enteredAtMs);
|
|
7349
7411
|
const row = this.#db
|
|
7350
7412
|
.prepare(
|
|
7351
7413
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE id = ? AND disabled_cause IS NULL",
|
|
@@ -7353,17 +7415,21 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7353
7415
|
.get(credentialId) as AuthRow | undefined;
|
|
7354
7416
|
const credential = row ? deserializeCredential(row) : null;
|
|
7355
7417
|
if (credential?.type !== "oauth") return { kind: "missing" };
|
|
7356
|
-
if (
|
|
7418
|
+
if (
|
|
7419
|
+
!force &&
|
|
7420
|
+
credential.refresh !== expectedRefresh &&
|
|
7421
|
+
effectiveNowMs + OAUTH_REFRESH_SKEW_MS < credential.expires
|
|
7422
|
+
) {
|
|
7357
7423
|
return { kind: "adopted", credential };
|
|
7358
7424
|
}
|
|
7359
7425
|
const active = this.#db
|
|
7360
7426
|
.prepare("SELECT owner, expires_at FROM oauth_refresh_leases WHERE credential_id = ?")
|
|
7361
7427
|
.get(credentialId) as { owner?: string; expires_at?: number } | undefined;
|
|
7362
|
-
if (typeof active?.expires_at === "number" && active.expires_at >
|
|
7428
|
+
if (typeof active?.expires_at === "number" && active.expires_at > effectiveNowMs) {
|
|
7363
7429
|
if (active.owner === owner) {
|
|
7364
7430
|
this.#db
|
|
7365
7431
|
.prepare("UPDATE oauth_refresh_leases SET expires_at = ? WHERE credential_id = ? AND owner = ?")
|
|
7366
|
-
.run(
|
|
7432
|
+
.run(effectiveNowMs + leaseMs, credentialId, owner);
|
|
7367
7433
|
return {
|
|
7368
7434
|
kind: "claimed",
|
|
7369
7435
|
credential,
|
|
@@ -7383,10 +7449,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7383
7449
|
.prepare(
|
|
7384
7450
|
"INSERT INTO oauth_refresh_leases (credential_id, owner, token_fingerprint, expires_at) VALUES (?, ?, ?, ?)",
|
|
7385
7451
|
)
|
|
7386
|
-
.run(credentialId, owner, tokenFingerprint,
|
|
7452
|
+
.run(credentialId, owner, tokenFingerprint, effectiveNowMs + leaseMs);
|
|
7387
7453
|
return { kind: "claimed", credential, lease: { credentialId, owner, tokenFingerprint } };
|
|
7388
7454
|
});
|
|
7389
|
-
return claim();
|
|
7455
|
+
return claim.immediate();
|
|
7390
7456
|
}
|
|
7391
7457
|
|
|
7392
7458
|
completeOAuthRefreshLease(lease: OAuthRefreshLease, credential: OAuthCredential): boolean {
|
|
@@ -7421,7 +7487,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7421
7487
|
.run(lease.credentialId, lease.owner);
|
|
7422
7488
|
return true;
|
|
7423
7489
|
});
|
|
7424
|
-
return complete();
|
|
7490
|
+
return complete.immediate();
|
|
7425
7491
|
}
|
|
7426
7492
|
|
|
7427
7493
|
releaseOAuthRefreshLease(lease: OAuthRefreshLease): void {
|
|
@@ -7483,7 +7549,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7483
7549
|
return result;
|
|
7484
7550
|
});
|
|
7485
7551
|
|
|
7486
|
-
const result = replace(provider, credentials);
|
|
7552
|
+
const result = replace.immediate(provider, credentials);
|
|
7487
7553
|
this.#purgeSupersededDisabledRows(provider, result);
|
|
7488
7554
|
return result;
|
|
7489
7555
|
}
|
|
@@ -7530,7 +7596,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7530
7596
|
return result;
|
|
7531
7597
|
});
|
|
7532
7598
|
|
|
7533
|
-
const result = upsert(provider, credential);
|
|
7599
|
+
const result = upsert.immediate(provider, credential);
|
|
7534
7600
|
this.#purgeSupersededDisabledRows(provider, result);
|
|
7535
7601
|
return result;
|
|
7536
7602
|
}
|
|
@@ -7727,12 +7793,22 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7727
7793
|
|
|
7728
7794
|
tryAcquireUsageFetchLease(key: string, owner: string, nowMs: number, leaseMs: number): boolean | undefined {
|
|
7729
7795
|
try {
|
|
7730
|
-
const
|
|
7731
|
-
const
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7796
|
+
const enteredAtMs = Date.now();
|
|
7797
|
+
const claim = this.#db.transaction(() => {
|
|
7798
|
+
const effectiveNowMs = nowMs + Math.max(0, Date.now() - enteredAtMs);
|
|
7799
|
+
const nowSec = Math.floor(effectiveNowMs / 1000);
|
|
7800
|
+
const expiresAtSec = Math.ceil((effectiveNowMs + leaseMs) / 1000);
|
|
7801
|
+
const result = this.#claimUsageFetchLeaseStmt.run(
|
|
7802
|
+
`usage_fetch_lease:${key}`,
|
|
7803
|
+
owner,
|
|
7804
|
+
expiresAtSec,
|
|
7805
|
+
nowSec,
|
|
7806
|
+
) as {
|
|
7807
|
+
changes: number;
|
|
7808
|
+
};
|
|
7809
|
+
return result.changes === 1;
|
|
7810
|
+
});
|
|
7811
|
+
return claim.immediate();
|
|
7736
7812
|
} catch {
|
|
7737
7813
|
return undefined;
|
|
7738
7814
|
}
|
|
@@ -7754,7 +7830,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7754
7830
|
this.#upsertCacheStmt.run(key, String(next), expiresAtSec);
|
|
7755
7831
|
return next;
|
|
7756
7832
|
});
|
|
7757
|
-
return allocate();
|
|
7833
|
+
return allocate.immediate();
|
|
7758
7834
|
}
|
|
7759
7835
|
|
|
7760
7836
|
deleteCachePrefix(prefix: string): void {
|
package/src/model-thinking.ts
CHANGED
|
@@ -939,6 +939,14 @@ function inferSupportedEfforts<TApi extends Api>(parsedModel: ParsedModel, model
|
|
|
939
939
|
if (model.provider === "alibaba-token-plan" && model.id === "glm-5.3") {
|
|
940
940
|
return ALIBABA_GLM_53_EFFORTS;
|
|
941
941
|
}
|
|
942
|
+
if (model.provider === "glm-zcode" && (model.id === "glm-5.3" || model.id === "glm-5.3-flash")) {
|
|
943
|
+
// The GLM Coding Plan serves these over the Anthropic Messages-compatible
|
|
944
|
+
// endpoint, where thinking rides the budget ladder. The generic
|
|
945
|
+
// anthropic-messages fallback stops at xhigh (32768 tokens), but the
|
|
946
|
+
// endpoint accepts budgets up to 65536 and reasoning volume scales with
|
|
947
|
+
// the budget (verified 2026-10-02), so expose the full ladder including max.
|
|
948
|
+
return DEFAULT_REASONING_EFFORTS_WITH_XHIGH_AND_MAX;
|
|
949
|
+
}
|
|
942
950
|
switch (parsedModel.family) {
|
|
943
951
|
case "openai":
|
|
944
952
|
return inferOpenAISupportedEfforts(parsedModel);
|
|
@@ -178,7 +178,8 @@ export async function* decodeEventStream(source: ReadableStream<Uint8Array>): As
|
|
|
178
178
|
if (offset > 0) buf = buf.slice(offset);
|
|
179
179
|
if (done) break;
|
|
180
180
|
}
|
|
181
|
-
if (buf.length > 0)
|
|
181
|
+
if (buf.length > 0)
|
|
182
|
+
throw new Error(`eventstream: truncated message at end of stream (${buf.length} trailing bytes)`);
|
|
182
183
|
} finally {
|
|
183
184
|
reader.releaseLock();
|
|
184
185
|
}
|