@gajae-code/ai 0.12.21 → 0.13.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 +8 -1
- package/dist/types/auth-storage.d.ts +46 -4
- package/dist/types/context-cap-policy.d.ts +46 -1
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-responses.d.ts +2 -1
- package/dist/types/utils/fallback-transport.d.ts +2 -0
- package/dist/types/utils/idle-iterator.d.ts +1 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +560 -91
- package/src/context-cap-policy.ts +52 -0
- package/src/providers/anthropic.ts +22 -2
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-responses.ts +34 -2
- package/src/utils/fallback-transport.ts +5 -2
- package/src/utils/idle-iterator.ts +6 -0
- package/src/utils/overflow.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Anthropic thinking-replay repairs caused by a deterministic rejection now stay in force for the rest of the session instead of being released as soon as the repaired retry succeeds. `thinking`/`redacted_thinking` blocks that draw a 400 (`blocks ... cannot be modified`, `Invalid \`signature\` in \`thinking\` block`) stay in the session history, so releasing the repair made the very next turn replay the same blocks and spend another rejected round trip — every turn, indefinitely. Observed against a proxied Claude Code session as a sustained ~50% 400 rate that never converged. The speculative masked-`api_error` probe is still released on the first completed stream, since that one may have been a transient blip (#4011).
|
|
8
|
+
|
|
5
9
|
## [0.12.21] - 2026-08-09
|
|
6
10
|
|
|
7
11
|
## [0.12.20] - 2026-08-09
|
|
@@ -9,7 +13,7 @@
|
|
|
9
13
|
### Fixed
|
|
10
14
|
|
|
11
15
|
- OpenAI Responses transports no longer send tool declarations whose names the provider reserves for its own built-ins. OpenCode Zen/Go reject `web_search` as a custom function with `invalid tools in request: custom function name "web_search" is reserved`, and that rejection is request-scoped — one colliding declaration failed the entire tools array before any token streamed, so the bundled `critic`, `planner`, and `architect` agents failed 100% of the time on those providers (#4104). The collision is dropped rather than renamed, because a renamed function tool returns as a `function_call` under the wire alias and that path does not populate `Tool.customWireName`, which would leave the agent-loop dispatcher unable to route the call. `compat.reservedToolNames` overrides the per-provider default.
|
|
12
|
-
|
|
16
|
+
- Anthropic streams now use a 300-second default idle window so long extended-thinking gaps do not trip the shared 120-second watchdog; explicit stream timeout overrides still take precedence.
|
|
13
17
|
## [0.12.19] - 2026-08-08
|
|
14
18
|
|
|
15
19
|
## [0.12.18] - 2026-08-08
|
|
@@ -29,8 +33,11 @@
|
|
|
29
33
|
### Fixed
|
|
30
34
|
|
|
31
35
|
- Codex named-tool fallback now keeps its downgraded request body across later same-turn provider retries and uses an independent one-shot budget, so retries cannot reintroduce `tool_choice` or suppress a later capability downgrade (#3669).
|
|
36
|
+
- Stored command-backed API keys that resolve empty now fall through to another usable API key or OAuth credential instead of suppressing it. AuthStorage also exposes the credential type its request dispatcher will select, with selector validation, runtime/config overrides, session OAuth stickiness, blocked/expired OAuth retry, command-key recovery, environment fallback, and `openai-codex-device` normalization sharing one precedence contract.
|
|
32
37
|
- `google-generative-ai` and `google-vertex` generate-content streams now consume newline-delimited JSON responses when the response media type declares NDJSON or JSONL, while preserving standard event-stream parsing and diagnostics.
|
|
33
38
|
- Anthropic requests rejected with `A maximum of 4 blocks with cache_control may be provided. Found N.` now step their generated breakpoints down instead of dying on the first attempt (#3934, supersedes #3943). An Anthropic-compatible gateway may attach its own block-level cache markers before forwarding, and those never appear in the params we serialize, so the total is unpredictable locally and the rejection itself is the only usable signal. Because that rejection says "too many", not "none allowed", recovery gives up one breakpoint at a time: explicit mode normally emits two (a conversation-prefix anchor plus a current-turn refresh point), so the first retry keeps the prefix anchor — the higher-value marker — and only a second rejection disables generated caching entirely. The reduced budget persists for the provider session so later turns neither re-trigger the 400 nor lose more caching than the endpoint requires. Only a genuine breakpoint-overflow `invalid_request_error` is claimed — other `cache_control` complaints, unrelated 400s, non-400 statuses, and our own pre-flight validation failure still surface immediately. The classifier is exported as `isAnthropicCacheBreakpointOverflowError`.
|
|
39
|
+
- A stale in-memory OAuth snapshot can no longer replay an already-rotated refresh token at the provider's token endpoint. Several gjc processes share one credential store, but each holds its own in-memory snapshot; when a peer rotated the token, the existing recovery only re-read the row AFTER the failed refresh — by which point the stale token had already been replayed upstream. Providers with refresh-token rotation plus reuse detection (Anthropic) treat that replay as token theft and can revoke the entire grant family, killing the peer's freshly rotated, still-valid tokens mid-request (observed in the wild as a live session dying with 401 `OAuth access token has been revoked` while sibling processes flooded the log with `invalid_grant` / `Refresh token not found or invalid` refresh failures all day). The local refresh path now re-reads the persisted row (via the canonical storage provider, so the `openai-codex-device` alias is covered) before dialing out: if a peer already rotated and the rotated access token is still fresh, it is adopted with no upstream call at all; if the rotated token has also expired — or the caller demanded a force refresh — the refresh spends the newest persisted refresh token instead of the stale snapshot one, and dispatch re-checks the adopted credential's MCP binding so a bound refresh token is only ever sent to its bound token endpoint. Failure recovery now compares the row against the refresh token that was actually sent (not the caller's stale snapshot), so an adoption that fails is classified and disabled instead of looping reload-retry, and a locally failed (credential, token) attempt is memoized for 30s so automatic refresh retries never immediately replay a token the provider may already have consumed after an ambiguous failure (timeout, lost response) — explicit force refreshes are the deliberate exception: they bypass the memo check so an operator/broker retry always reaches the endpoint, while their failures are still recorded. Broker-routed refreshes are unaffected, and the post-failure reload recovery remains as the backstop for rotations that land inside the now-milliseconds-wide window.
|
|
40
|
+
- Codex GPT-5.6 OAuth context windows now follow an explicit acceptance contract: live `/codex/models` `context_window` metadata is the authoritative runtime source, the bundled 272K fallback/ceiling remains the evidence-gated product guard, and explicit user `contextWindow` overrides survive the final cap instead of being silently re-clamped (#3987).
|
|
34
41
|
## [0.12.15] - 2026-08-06
|
|
35
42
|
|
|
36
43
|
### Fixed
|
|
@@ -149,6 +149,24 @@ export interface AuthCredentialSnapshot {
|
|
|
149
149
|
* a remote broker; mutating methods (`replace*`, `upsert*`, `delete*ForProvider`)
|
|
150
150
|
* throw because login flows route through the broker, not the client.
|
|
151
151
|
*/
|
|
152
|
+
export type OAuthRefreshLease = {
|
|
153
|
+
credentialId: number;
|
|
154
|
+
owner: string;
|
|
155
|
+
tokenFingerprint: string;
|
|
156
|
+
};
|
|
157
|
+
export type OAuthRefreshLeaseClaim = {
|
|
158
|
+
kind: "claimed";
|
|
159
|
+
credential: OAuthCredential;
|
|
160
|
+
lease: OAuthRefreshLease;
|
|
161
|
+
} | {
|
|
162
|
+
kind: "adopted";
|
|
163
|
+
credential: OAuthCredential;
|
|
164
|
+
} | {
|
|
165
|
+
kind: "busy";
|
|
166
|
+
expiresAt: number;
|
|
167
|
+
} | {
|
|
168
|
+
kind: "missing";
|
|
169
|
+
};
|
|
152
170
|
export interface AuthCredentialStore {
|
|
153
171
|
close(): void;
|
|
154
172
|
listAuthCredentials(provider?: string): StoredAuthCredential[];
|
|
@@ -179,6 +197,17 @@ export interface AuthCredentialStore {
|
|
|
179
197
|
refreshOAuthCredential?(provider: Provider, credentialId: number, credential: OAuthCredential, signal?: AbortSignal): Promise<OAuthCredentials>;
|
|
180
198
|
/** Broker-backed MCP refresh using the broker's stored token endpoint and refresh secret. */
|
|
181
199
|
refreshMCPOAuthCredential?(credentialId: number, credential: OAuthCredential, client: MCPOAuthRefreshClient, signal?: AbortSignal): Promise<OAuthCredential>;
|
|
200
|
+
/**
|
|
201
|
+
* Atomically adopts a fresh row or claims the current refresh token for one
|
|
202
|
+
* local provider dial. SQLite-backed stores use this to prevent another
|
|
203
|
+
* process from replaying a rotating refresh token between a pre-read and
|
|
204
|
+
* the provider request.
|
|
205
|
+
*/
|
|
206
|
+
claimOAuthRefreshLease?(credentialId: number, expectedRefresh: string, force: boolean, owner: string, nowMs: number, leaseMs: number): OAuthRefreshLeaseClaim;
|
|
207
|
+
/** Atomically persists a successful claimed refresh and releases its lease. */
|
|
208
|
+
completeOAuthRefreshLease?(lease: OAuthRefreshLease, credential: OAuthCredential): boolean;
|
|
209
|
+
/** Releases an uncompleted refresh lease owned by this process. */
|
|
210
|
+
releaseOAuthRefreshLease?(lease: OAuthRefreshLease): void;
|
|
182
211
|
/**
|
|
183
212
|
* Optional async pre-read hook invoked after AuthStorage selects a stored
|
|
184
213
|
* credential but before it returns that credential for an outbound request.
|
|
@@ -434,6 +463,8 @@ export declare class AuthStorage {
|
|
|
434
463
|
removeRuntimeApiKey(provider: string): void;
|
|
435
464
|
/** Whether a provider is currently authenticated by a runtime API-key override. */
|
|
436
465
|
hasRuntimeApiKey(provider: string): boolean;
|
|
466
|
+
/** Whether a provider is currently authenticated by a config API-key override. */
|
|
467
|
+
hasConfigApiKey(provider: string): boolean;
|
|
437
468
|
/**
|
|
438
469
|
* Whether credential selection for a provider is pinned to one stored row by
|
|
439
470
|
* a runtime selector (`--credential`).
|
|
@@ -510,6 +541,12 @@ export declare class AuthStorage {
|
|
|
510
541
|
*/
|
|
511
542
|
has(provider: string): boolean;
|
|
512
543
|
hasAuth(provider: string): boolean;
|
|
544
|
+
/**
|
|
545
|
+
* Credential type that a provider/session will dispatch first without performing I/O.
|
|
546
|
+
* Mirrors getApiKey selector validation, overrides, session OAuth stickiness,
|
|
547
|
+
* cached command-key usability, OAuth retry, and environment fallback order.
|
|
548
|
+
*/
|
|
549
|
+
getEffectiveCredentialType(provider: string, sessionId?: string): AuthCredential["type"] | undefined;
|
|
513
550
|
/**
|
|
514
551
|
* Check whether configured auth is currently usable without resolving credentials.
|
|
515
552
|
*/
|
|
@@ -603,10 +640,12 @@ export declare class AuthStorage {
|
|
|
603
640
|
* Priority:
|
|
604
641
|
* 1. Runtime override (CLI --api-key)
|
|
605
642
|
* 2. Config override (models.yml `providers.<name>.apiKey`)
|
|
606
|
-
* 3.
|
|
607
|
-
* 4.
|
|
608
|
-
* 5.
|
|
609
|
-
* 6.
|
|
643
|
+
* 3. Session-selected OAuth credential, when present
|
|
644
|
+
* 4. Usable or unresolved API key from storage
|
|
645
|
+
* 5. OAuth token from storage (auto-refreshed)
|
|
646
|
+
* 6. Previously unusable command-backed API key retry
|
|
647
|
+
* 7. Environment variable
|
|
648
|
+
* 8. Fallback resolver (models.yml custom providers, last-resort)
|
|
610
649
|
*/
|
|
611
650
|
getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
|
|
612
651
|
/**
|
|
@@ -697,6 +736,9 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
697
736
|
constructor(db: Database);
|
|
698
737
|
static open(dbPath?: string): Promise<SqliteAuthCredentialStore>;
|
|
699
738
|
listAuthCredentials(provider?: string): StoredAuthCredential[];
|
|
739
|
+
claimOAuthRefreshLease(credentialId: number, expectedRefresh: string, force: boolean, owner: string, nowMs: number, leaseMs: number): OAuthRefreshLeaseClaim;
|
|
740
|
+
completeOAuthRefreshLease(lease: OAuthRefreshLease, credential: OAuthCredential): boolean;
|
|
741
|
+
releaseOAuthRefreshLease(lease: OAuthRefreshLease): void;
|
|
700
742
|
replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[];
|
|
701
743
|
upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[];
|
|
702
744
|
upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult;
|
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import type { Api, Model } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Codex GPT-5.6 OAuth context-window policy.
|
|
4
|
+
*
|
|
5
|
+
* Authoritative source and ownership path
|
|
6
|
+
* ---------------------------------------
|
|
7
|
+
* The authenticated Codex backend discovery endpoint (`GET {base}/codex/models`
|
|
8
|
+
* with an OAuth bearer token) is the authoritative runtime source of Codex OAuth
|
|
9
|
+
* model context-window metadata (`models[].context_window`). OpenAI owns that
|
|
10
|
+
* value; GJC consumes it read-only via `fetchCodexModels` and must not silently
|
|
11
|
+
* invent a larger limit without upstream evidence.
|
|
12
|
+
*
|
|
13
|
+
* This bundled policy is a client-side conservative guard, not a primary source.
|
|
14
|
+
* It exists because OpenAI temporarily reverted the GPT-5.6 Sol product context
|
|
15
|
+
* limit from 372K to 272K on 2026-07-13 (staff announcement quoted in
|
|
16
|
+
* gajae-code issues #2240 / #2260), while planning to restore 372K later.
|
|
17
|
+
* `ceiling` must only be raised with an upstream evidence citation; an
|
|
18
|
+
* unverified report that "372K is live again" is not sufficient.
|
|
19
|
+
*
|
|
20
|
+
* Precedence (highest -> lowest)
|
|
21
|
+
* ------------------------------
|
|
22
|
+
* 1. Explicit user per-model override (`contextWindow` in `modelOverrides`).
|
|
23
|
+
* The model registry merges it into `model.contextWindow` before this cap and
|
|
24
|
+
* passes it here; it is honored when a positive finite number, with
|
|
25
|
+
* diagnostics emitted at the registry. Never silently discarded.
|
|
26
|
+
* 2. Live OAuth discovery metadata (`context_window`), forced to the enforced
|
|
27
|
+
* product window for the GPT-5.6 tier.
|
|
28
|
+
* 3. Bundled conservative generic window (`CODEX_GENERIC_CONTEXT_WINDOW`),
|
|
29
|
+
* used when discovery metadata is absent or invalid.
|
|
30
|
+
*/
|
|
2
31
|
export interface CodexGpt56ContextCapPolicy {
|
|
3
32
|
/**
|
|
4
33
|
* Usable prompt budget forced for the GPT-5.6 tier on the Codex product
|
|
@@ -19,5 +48,21 @@ export declare const CODEX_GPT_5_6_CONTEXT_CAP: CodexGpt56ContextCapPolicy;
|
|
|
19
48
|
export declare const CODEX_GENERIC_CONTEXT_WINDOW = 272000;
|
|
20
49
|
export declare function isCodexProductTransport(model: Pick<Model<Api>, "api" | "provider">): boolean;
|
|
21
50
|
export declare function isCodexGpt56Tier(model: Pick<Model<Api>, "id">): boolean;
|
|
51
|
+
export declare function codexContextOverrideKey(provider: string, modelId: string): string;
|
|
22
52
|
export declare function resolveCodexGpt56DiscoveryContext(model: Pick<Model<Api>, "api" | "id" | "provider">, rawContextWindow: unknown, policy?: CodexGpt56ContextCapPolicy): number;
|
|
23
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Applies the final Codex GPT-5.6 context ceiling, honoring explicit user
|
|
55
|
+
* overrides.
|
|
56
|
+
*
|
|
57
|
+
* `userContextWindowOverrides` maps provider-qualified composite keys
|
|
58
|
+
* (`provider:modelId`, both lowercased, built by
|
|
59
|
+
* {@link codexContextOverrideKey}) to the user's explicit `contextWindow` value
|
|
60
|
+
* (already merged into `model.contextWindow` by the model registry). A tier
|
|
61
|
+
* model present with a positive finite value keeps its value even above
|
|
62
|
+
* `enforced` — the user's explicit, diagnosed choice. Every other tier
|
|
63
|
+
* model is forced to `enforced`, so a stale larger live/cached observation
|
|
64
|
+
* (e.g. a pre-rollback 373K cache) cannot resurface without an override.
|
|
65
|
+
* Because the key is provider-qualified, an override only exempts the exact
|
|
66
|
+
* provider+model pair it was configured for.
|
|
67
|
+
*/
|
|
68
|
+
export declare function applyFinalCodexGpt56ContextCap<TApi extends Api>(models: readonly Model<TApi>[], policy?: CodexGpt56ContextCapPolicy, userContextWindowOverrides?: ReadonlyMap<string, number>): Model<TApi>[];
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* // Inspect calls afterwards.
|
|
42
42
|
* expect(mock.calls).toHaveLength(2);
|
|
43
43
|
*/
|
|
44
|
-
import type { Api, Context, Model, SimpleStreamOptions, StopReason, Usage } from "../types";
|
|
44
|
+
import type { Api, AssistantMessage, Context, Model, SimpleStreamOptions, StopReason, Usage } from "../types";
|
|
45
45
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
46
46
|
/** The API string this provider serves. */
|
|
47
47
|
export declare const MOCK_API: "mock";
|
|
@@ -75,6 +75,8 @@ export interface MockResponse {
|
|
|
75
75
|
};
|
|
76
76
|
/** Pre-set responseId. */
|
|
77
77
|
responseId?: string;
|
|
78
|
+
/** Optional typed provider failure metadata for retry/fallback tests. */
|
|
79
|
+
transportFailure?: AssistantMessage["transportFailure"];
|
|
78
80
|
/** If set, the stream emits a terminal error event instead of completing. */
|
|
79
81
|
throw?: string | Error;
|
|
80
82
|
/** Delay before any event is emitted. Honors the call's AbortSignal. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Tool as OpenAITool } from "openai/resources/responses/responses";
|
|
2
|
-
import { type Model, type ServiceTier, type StreamFunction, type StreamOptions, type Tool, type ToolChoice } from "../types";
|
|
2
|
+
import { type AssistantMessage, type Model, type ServiceTier, type StreamFunction, type StreamOptions, type Tool, type ToolChoice } from "../types";
|
|
3
3
|
import { type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
4
4
|
export declare function normalizeOpenAIResponsesPromptCacheKey(sessionId: string | undefined): string | undefined;
|
|
5
5
|
export interface OpenAIResponsesOptions extends StreamOptions {
|
|
@@ -15,6 +15,7 @@ export interface OpenAIResponsesOptions extends StreamOptions {
|
|
|
15
15
|
}
|
|
16
16
|
/** Test seam: the provider base URL as resolved from trusted env. */
|
|
17
17
|
export declare function resolveOpenAIProviderBaseUrlForTest(baseUrl: string | undefined, authCredentialType: "api_key" | "oauth" | undefined): string;
|
|
18
|
+
export declare function isOpenCodeGoEmptyCompletedResponse(model: Model<"openai-responses">, output: AssistantMessage, nativeOutputItemCount: number): boolean;
|
|
18
19
|
/**
|
|
19
20
|
* Generate function for OpenAI Responses API
|
|
20
21
|
*/
|
|
@@ -21,6 +21,8 @@ export interface FallbackTrigger {
|
|
|
21
21
|
}
|
|
22
22
|
/** Stable code for streams that time out before producing semantic progress. */
|
|
23
23
|
export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
|
|
24
|
+
/** Stable code for a nominally successful response with no content or token usage. */
|
|
25
|
+
export declare const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
|
|
24
26
|
export type TransportHeaders = Headers | Record<string, string | undefined>;
|
|
25
27
|
/**
|
|
26
28
|
* Structured facts from an upstream HTTP or transport failure. Retry decisions
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function getProviderStreamIdleTimeoutFallbackMs(provider: string): number | undefined;
|
|
1
2
|
export declare function getProviderFirstEventTimeoutFallbackMs(provider: string): number | undefined;
|
|
2
3
|
/**
|
|
3
4
|
* Returns the idle timeout used for provider streaming transports.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.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,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@gajae-code/utils": "0.
|
|
43
|
+
"@gajae-code/utils": "0.13.0",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|