@gajae-code/ai 0.12.0 → 0.12.2
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 +23 -0
- package/dist/types/model-thinking.d.ts +15 -0
- package/dist/types/providers/azure-openai-responses.d.ts +2 -0
- package/dist/types/providers/google-vertex.d.ts +2 -0
- package/dist/types/utils/fallback-transport.d.ts +2 -0
- package/dist/types/utils/http-inspector.d.ts +10 -0
- package/dist/types/utils/idle-iterator.d.ts +4 -0
- package/dist/types/utils/oauth/kimi.d.ts +2 -0
- package/dist/types/utils/oauth/perplexity.d.ts +2 -7
- package/package.json +2 -2
- package/src/auth-storage.ts +92 -15
- package/src/model-thinking.ts +19 -0
- package/src/providers/amazon-bedrock.ts +5 -20
- package/src/providers/anthropic.ts +95 -44
- package/src/providers/azure-openai-responses.ts +28 -16
- package/src/providers/google-vertex.ts +35 -4
- package/src/providers/ollama.ts +32 -4
- package/src/providers/openai-codex-responses.ts +48 -31
- package/src/providers/openai-completions.ts +13 -12
- package/src/providers/openai-responses.ts +9 -11
- package/src/providers/register-builtins.ts +12 -2
- package/src/utils/fallback-transport.ts +17 -10
- package/src/utils/http-inspector.ts +47 -1
- package/src/utils/idle-iterator.ts +12 -3
- package/src/utils/oauth/kimi.ts +17 -2
- package/src/utils/oauth/perplexity.ts +21 -2
- package/src/utils/schema/adapt.ts +2 -2
- package/src/utils/tool-choice-capability.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.12.2] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
## [0.12.1] - 2026-07-29
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Lazy-stream first-event timeouts now abort with `FirstEventTimeoutError` so `transportFailure.providerCode` is `stream_first_event_timeout` on the outer watchdog path shared by all bundled providers via `createLazyStream`. Idle stalls remain bare `Error`s (distinct class intentionally) (#3496).
|
|
12
|
+
|
|
13
|
+
- Provider streams now surface first-event watchdog expiry as a typed timeout so callers can apply bounded retry policy without parsing error prose.
|
|
14
|
+
- Codex websocket first-event timeouts now discard the timed-out connection before the outer retry/fallback layer handles the typed failure, preventing late frames from the abandoned request from being consumed by the replayed turn.
|
|
15
|
+
- Codex named-tool requests now recognize provider `Tool choice '<name>' not found in 'tools' parameter` errors as runtime capability failures and retry once without forcing the choice.
|
|
16
|
+
- The Kimi OAuth host (`KIMI_CODE_OAUTH_HOST` / `KIMI_OAUTH_HOST`) is now resolved from trusted environment sources only. That host receives the device-authorization request, the authorization-code exchange, and the refresh call that carries the existing refresh token, so reading it through the merged view that includes the caller's `cwd/.env` let a repository redirect the login flow and collect the user's Kimi credentials. Resolution now uses the non-project resolver; shell and user-level configuration is unchanged.
|
|
17
|
+
- The documented `GJC_NO_STRICT` environment variable now takes effect. `adaptSchemaForStrict` read only the legacy `PI_NO_STRICT`, so an operator hitting a provider that rejects strict function schemas set the documented name and strict mode stayed on. Both names are honoured, canonical name first, and `GJC_NO_STRICT` is now listed in the environment-variable reference rather than only in the schema-normalisation note.
|
|
18
|
+
- The documented `GJC_AUTH_NO_BORROW` environment variable now takes effect. Only the legacy `PI_AUTH_NO_BORROW` was read, so an operator who followed the documentation to disable macOS native-app token borrowing still had a JWT read out of the Perplexity desktop application during login. Both names are now honoured, and the contract stays presence-based as documented so that setting it to `0` cannot silently re-enable borrowing.
|
|
19
|
+
- The Azure client's `AZURE_OPENAI_API_KEY` fallback is now resolved from trusted environment sources only. It read the merged view that includes the caller's `cwd/.env`, so a repository could supply the credential the client authenticates with; provider credential resolution is documented as excluding the project `.env`, and this fallback now matches. An explicit caller-supplied key still takes precedence, and shell / user-level configuration is unchanged.
|
|
20
|
+
- Anthropic and Ollama tool calls cut off by an output-token limit are now marked incomplete before dispatch, so repaired partial JSON is rejected instead of executing with truncated arguments.
|
|
21
|
+
- The Anthropic "thinking blocks in the latest assistant message cannot be modified" 400 now escalates its one-shot replay repair. The error names the latest assistant message but its cited `messages.N.content.M` path can point at an earlier replayed turn, so the latest-only repair was rejected identically and killed the turn; recovery now retries once more with thinking dropped from every replayed assistant message.
|
|
22
|
+
- Anthropic adaptive-thinking `display` support is now decided by the canonical model-version parser instead of a provider-local `claude-opus-(\d+)-(\d+)` regex. The regex only matched two-component ids, so a single-component alias such as `claude-opus-5` was classified as pre-4.7 while its dated snapshot `claude-opus-5-20260101` was not: the alias sent `thinking: { type: "adaptive" }` without `display: "summarized"`, additionally requested the `interleaved-thinking-2025-05-14` beta, and had its returned thinking blocks recorded as raw rather than summarized. Both Anthropic and Bedrock providers now share `supportsAnthropicAdaptiveThinkingDisplay`, so alias and dated ids of the same model send an identical request shape.
|
|
23
|
+
- Anthropic requests that force a tool choice no longer replay signed thinking blocks. Forcing `tool_choice` strips `thinking` from the request (the API rejects the combination), but the converted history still carried native `thinking`/`redacted_thinking` blocks from thinking-enabled turns, so eager tool-forcing turns (e.g. the todo bootstrap) sent a request whose history contradicted its own thinking setting and drew a 400. The replay now degrades in the same rebuild; the forced request trades its prompt-cache prefix for a shape the API accepts.
|
|
24
|
+
- A definitively failed OAuth refresh can no longer loop forever instead of disabling the credential. The refresh-failure path disables the row with a CAS conditioned on its serialized `data`, and treated a lost CAS as proof that a peer had rotated the token: it reloaded the store and re-resolved, without bound. That predicate also misses when nothing was rotated — an account switcher that replaces the provider's rows leaves the attempted id gone, and an unrelated identity-metadata write leaves the row byte-different — so a revoked credential was never disabled and every subsequent request re-issued the same `invalid_grant` refresh (observed in the wild as ~3k `OAuth token refresh failed` / `disable lost CAS` log pairs in 3.5 hours, one wasted refresh round-trip per request). When the row still holds the refresh token that just failed, it is now disabled by id (no peer rotation exists to clobber); otherwise the reload-and-retry recovery is capped, so resolution terminates instead of recursing until the runtime dies.
|
|
25
|
+
|
|
5
26
|
## [0.12.0] - 2026-07-28
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -26,6 +47,8 @@
|
|
|
26
47
|
- The OpenAI and Azure endpoint decisions are now resolved from trusted environment sources only: `OPENAI_BASE_URL` (streaming responses, completions, and the model manager), `AZURE_OPENAI_BASE_URL`, and `AZURE_OPENAI_RESOURCE_NAME` (the alternate constructor for the same Azure host). `Bun.env` is `process.env` and the env module merges the caller's `cwd/.env` into it, so a repository could previously plant a `.env` that redirected authenticated requests; the two provider paths already reached for `$inheritedEnv` but re-admitted the project `.env` through a fallback. Resolution now goes through the non-project resolver (launching shell plus GJC/user-owned `.env` files); shell and user-level configuration is unchanged.
|
|
27
48
|
- Google credential material is now resolved from trusted environment sources only: the `GOOGLE_APPLICATION_CREDENTIALS` service-account / authorized-user file path used by the ADC loader, and `GOOGLE_CLOUD_API_KEY` used as the Vertex API key. Both were read through the merged view that includes the caller's `cwd/.env`, so a repository could ship a key file and point the agent at it, making it authenticate to Google as an identity the repository chose. `stream.ts` already resolved the same ADC variable through the non-project resolver; the two now agree. An explicit caller-supplied API key still takes precedence.
|
|
28
49
|
- The Grok usage token fallback (`GROK_CLI_OAUTH_TOKEN`) is now resolved from trusted environment sources only. It authenticates the billing/usage call, and reading it through the merged view that includes the caller's `cwd/.env` let a repository decide which account that call ran against. Stored credentials keep precedence, and shell / user-level configuration is unchanged.
|
|
50
|
+
- The Vertex AI location (`GOOGLE_CLOUD_LOCATION`) can no longer redirect authenticated requests off Google. It is interpolated into the request host (`${location}-aiplatform.googleapis.com`), so a value containing `/` terminated the authority component and sent the Google access token to an arbitrary origin — and it was read through the merged view that includes the caller's `cwd/.env`. It now resolves from trusted sources only and must be a region label; `GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT` moved to the same trusted resolver.
|
|
51
|
+
- HTTP 400 request dumps are now bounded. Every 400 wrote a file containing the full sanitized request body and nothing ever removed one, so the directory grew without limit — a developer machine reached 27,249 files totalling 7.0 GB, which was 96% of everything under `~/.gjc`. The newest 50 are retained, matching the bounded retention the rotating application log already uses, and pruning stays best-effort so diagnostics never turn a request failure into a second failure.
|
|
29
52
|
- Anthropic `ping` keepalives no longer reset stream progress, so responses that stop producing content now reach the idle timeout instead of hanging indefinitely.
|
|
30
53
|
- The Anthropic endpoint decision is now resolved from trusted environment sources only: `ANTHROPIC_BASE_URL`, `FOUNDRY_BASE_URL`, `ZCODE_PLAN_ANTHROPIC_BASE_URL`, and the `CLAUDE_CODE_USE_FOUNDRY` mode switch. `Bun.env` is `process.env` and the env module merges the caller's `cwd/.env` into it, so a repository could previously plant a `.env` that redirected authenticated Anthropic requests — the resolved base URL becomes `${baseUrl}/v1/messages` while the headers carry the API key or OAuth token. Resolution now goes through the non-project resolver (launching shell plus GJC/user-owned `.env` files); shell and user-level configuration is unchanged.
|
|
31
54
|
- The documented `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` environment variable now takes effect: the stream-watchdog idle-timeout helpers resolve it GJC-first before the legacy `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` / `PI_STREAM_IDLE_TIMEOUT_MS` aliases (previously only the `PI_`-prefixed names were read, so setting the documented GJC name was a silent no-op).
|
|
@@ -72,3 +72,18 @@ export declare function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(mod
|
|
|
72
72
|
* - Thinking content is omitted by default (needs display: "summarized")
|
|
73
73
|
*/
|
|
74
74
|
export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Adaptive thinking `display` is supported starting with Anthropic Opus 4.7.
|
|
77
|
+
* Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
|
|
78
|
+
* Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to
|
|
79
|
+
* "omitted" — thinking tokens are billed but no content streams back — so it
|
|
80
|
+
* must opt in like Opus 4.7+ (issue #2791).
|
|
81
|
+
*
|
|
82
|
+
* Shares `hasOpus47ApiRestrictions` version parsing on purpose: the two
|
|
83
|
+
* predicates describe the same API generation, and a private `claude-opus-(\d+)-(\d+)`
|
|
84
|
+
* regex silently disagreed with it for single-component aliases (`claude-opus-5`
|
|
85
|
+
* matched nothing while `claude-opus-5-20260101` matched), so the same model sent
|
|
86
|
+
* a different thinking shape and beta set depending on which id string was used.
|
|
87
|
+
* Bedrock region/inference-profile prefixes are handled by the canonical parser.
|
|
88
|
+
*/
|
|
89
|
+
export declare function supportsAnthropicAdaptiveThinkingDisplay(modelId: string): boolean;
|
|
@@ -18,3 +18,5 @@ export declare function resolveAzureConfigForTest(model: Model<"azure-openai-res
|
|
|
18
18
|
baseUrl: string;
|
|
19
19
|
apiVersion: string;
|
|
20
20
|
};
|
|
21
|
+
/** Test seam: the client API key as resolved from a caller value plus trusted env. */
|
|
22
|
+
export declare function resolveAzureClientApiKeyForTest(apiKey: string): string | undefined;
|
|
@@ -7,3 +7,5 @@ export interface GoogleVertexOptions extends GoogleSharedStreamOptions {
|
|
|
7
7
|
export declare const streamGoogleVertex: StreamFunction<"google-vertex">;
|
|
8
8
|
/** Test seam: the Vertex API key as resolved from options plus trusted env. */
|
|
9
9
|
export declare function resolveVertexApiKeyForTest(options?: GoogleVertexOptions): string | undefined;
|
|
10
|
+
/** Test seam: the Vertex location as resolved from options plus trusted env. */
|
|
11
|
+
export declare function resolveVertexLocationForTest(options?: GoogleVertexOptions): string;
|
|
@@ -3,6 +3,8 @@ export interface FallbackTrigger {
|
|
|
3
3
|
class: FallbackTriggerClass;
|
|
4
4
|
retryAfterMs?: number;
|
|
5
5
|
}
|
|
6
|
+
/** Stable code for streams that time out before producing semantic progress. */
|
|
7
|
+
export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
|
|
6
8
|
export type TransportHeaders = Headers | Record<string, string | undefined>;
|
|
7
9
|
/**
|
|
8
10
|
* Structured facts from an upstream HTTP or transport failure. Retry decisions
|
|
@@ -17,6 +17,16 @@ export type CapturedHttpErrorResponse = {
|
|
|
17
17
|
export declare function isModelUnavailableError(message: string, error: unknown): boolean;
|
|
18
18
|
/** Actionable guidance for selecting an available model/provider. */
|
|
19
19
|
export declare function formatModelUnavailableGuidance(dump: RawHttpRequestDump | undefined): string;
|
|
20
|
+
/** Directory holding the retained HTTP 400 dumps. */
|
|
21
|
+
export declare function httpRequestDumpDir(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Drop the oldest dumps beyond the cap. Best-effort: diagnostics must never turn
|
|
24
|
+
* a request failure into a second failure, so every step swallows its error.
|
|
25
|
+
*
|
|
26
|
+
* File names are `${Date.now()}-${hash}.json`, so a lexical sort is chronological
|
|
27
|
+
* for the millisecond timestamps this writer produces.
|
|
28
|
+
*/
|
|
29
|
+
export declare function pruneHttpRequestDumps(dir?: string): Promise<number>;
|
|
20
30
|
export declare function appendRawHttpRequestDumpFor400(message: string, error: unknown, dump: RawHttpRequestDump | undefined): Promise<string>;
|
|
21
31
|
/**
|
|
22
32
|
* Name the failed connection when the request never produced an HTTP status.
|
|
@@ -30,6 +30,10 @@ export declare function getOpenAIStreamIdleTimeoutMs(): number | undefined;
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fallbackMs?: number): number | undefined;
|
|
32
32
|
export type Watchdog = NodeJS.Timeout | undefined;
|
|
33
|
+
export declare class FirstEventTimeoutError extends Error {
|
|
34
|
+
readonly providerCode = "stream_first_event_timeout";
|
|
35
|
+
constructor(message: string);
|
|
36
|
+
}
|
|
33
37
|
/**
|
|
34
38
|
* Starts a watchdog that aborts a request if no first stream event arrives in time.
|
|
35
39
|
* Call `markFirstEventReceived()` as soon as the first event is observed.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Kimi Code OAuth flow (device authorization grant)
|
|
3
3
|
*/
|
|
4
4
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
5
|
+
/** Test seam: the OAuth host as resolved from trusted env. */
|
|
6
|
+
export declare function resolveKimiOAuthHostForTest(): string;
|
|
5
7
|
export declare let getKimiCommonHeaders: () => Readonly<{
|
|
6
8
|
"User-Agent": `KimiCLI/${string}`;
|
|
7
9
|
"X-Msh-Platform": "kimi_cli";
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* Tries auto-extraction from the desktop app, then runs HTTP email OTP login.
|
|
6
|
-
*
|
|
7
|
-
* No browser/manual token paste fallback is used.
|
|
8
|
-
*/
|
|
2
|
+
/** Test seam: the resolved native-app borrowing opt-out. */
|
|
3
|
+
export declare function authBorrowDisabledForTest(): boolean;
|
|
9
4
|
export declare function loginPerplexity(ctrl: OAuthController): Promise<OAuthCredentials>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.2",
|
|
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.12.
|
|
43
|
+
"@gajae-code/utils": "0.12.2",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -572,6 +572,16 @@ const OAUTH_REFRESH_SKEW_MS = 60_000;
|
|
|
572
572
|
* pathological detach-without-reattach loops can't grow memory unboundedly.
|
|
573
573
|
*/
|
|
574
574
|
const MAX_PENDING_DISABLED_EVENTS = 32;
|
|
575
|
+
/**
|
|
576
|
+
* Cap on how many times an OAuth resolution may reload the credential store and
|
|
577
|
+
* re-resolve after a failed refresh. Each retry exists to recover from a peer
|
|
578
|
+
* process rotating (or replacing) the row under us, which is a bounded event:
|
|
579
|
+
* the peer either published a usable credential we pick up on the next pass, or
|
|
580
|
+
* it did not. Without a cap, a credential whose disable can never be applied
|
|
581
|
+
* (row replaced by an account switcher, CAS predicate that can never match)
|
|
582
|
+
* makes the recovery path re-issue the same failing token refresh forever.
|
|
583
|
+
*/
|
|
584
|
+
const MAX_OAUTH_RESOLUTION_RELOADS = 3;
|
|
575
585
|
|
|
576
586
|
type UsageCacheEntry<T> = {
|
|
577
587
|
value: T;
|
|
@@ -1426,6 +1436,34 @@ export class AuthStorage {
|
|
|
1426
1436
|
return true;
|
|
1427
1437
|
}
|
|
1428
1438
|
|
|
1439
|
+
/**
|
|
1440
|
+
* Whether the persisted row `credentialId` is still an OAuth credential holding
|
|
1441
|
+
* `refreshToken`. Used by the refresh-failure path to tell "a peer rotated this
|
|
1442
|
+
* row" (retry is worthwhile) apart from "the row is unchanged but the CAS
|
|
1443
|
+
* predicate cannot match it" (retry replays the same failing refresh).
|
|
1444
|
+
*/
|
|
1445
|
+
#credentialRowHoldsRefreshToken(provider: string, credentialId: number, refreshToken: string): boolean {
|
|
1446
|
+
const row = this.#store.listAuthCredentials(provider).find(entry => entry.id === credentialId);
|
|
1447
|
+
const credential = row?.credential;
|
|
1448
|
+
return credential?.type === "oauth" && credential.refresh === refreshToken;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* Soft-deletes a row by id, bypassing the data-equality CAS. Only safe when the
|
|
1453
|
+
* caller has confirmed the row still holds the credential it attempted to
|
|
1454
|
+
* refresh, so no peer rotation can be clobbered.
|
|
1455
|
+
*/
|
|
1456
|
+
#disableCredentialById(provider: string, credentialId: number, disabledCause: string): void {
|
|
1457
|
+
this.#store.deleteAuthCredential(credentialId, disabledCause);
|
|
1458
|
+
const entries = this.#getStoredCredentials(provider);
|
|
1459
|
+
this.#setStoredCredentials(
|
|
1460
|
+
provider,
|
|
1461
|
+
entries.filter(entry => entry.id !== credentialId),
|
|
1462
|
+
);
|
|
1463
|
+
this.#resetProviderAssignments(provider);
|
|
1464
|
+
this.#emitCredentialDisabled({ provider, disabledCause });
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1429
1467
|
#emitCredentialDisabled(event: CredentialDisabledEvent): void {
|
|
1430
1468
|
if (this.#credentialDisabledListeners.size === 0) {
|
|
1431
1469
|
// No subscribers — buffer for later replay. Cap the backlog so a process that runs
|
|
@@ -2989,7 +3027,15 @@ export class AuthStorage {
|
|
|
2989
3027
|
provider: string,
|
|
2990
3028
|
sessionId?: string,
|
|
2991
3029
|
options?: AuthApiKeyOptions,
|
|
3030
|
+
reloadsUsed = 0,
|
|
2992
3031
|
): Promise<OAuthResolutionResult | undefined> {
|
|
3032
|
+
if (reloadsUsed > MAX_OAUTH_RESOLUTION_RELOADS) {
|
|
3033
|
+
logger.warn("OAuth credential resolution exhausted its reload budget", {
|
|
3034
|
+
provider,
|
|
3035
|
+
reloadsUsed,
|
|
3036
|
+
});
|
|
3037
|
+
return undefined;
|
|
3038
|
+
}
|
|
2993
3039
|
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options);
|
|
2994
3040
|
const selectedOAuthCredential =
|
|
2995
3041
|
selectedCredential?.credential.type === "oauth"
|
|
@@ -3087,18 +3133,27 @@ export class AuthStorage {
|
|
|
3087
3133
|
usagePrechecked: candidate.usageChecked,
|
|
3088
3134
|
enforceProRequirement,
|
|
3089
3135
|
},
|
|
3136
|
+
reloadsUsed,
|
|
3090
3137
|
);
|
|
3091
3138
|
if (resolved) return resolved;
|
|
3092
3139
|
}
|
|
3093
3140
|
|
|
3094
3141
|
if (fallback && this.#isCredentialBlocked(providerKey, fallback.selection.index)) {
|
|
3095
|
-
return this.#tryOAuthCredential(
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3142
|
+
return this.#tryOAuthCredential(
|
|
3143
|
+
provider,
|
|
3144
|
+
fallback.selection,
|
|
3145
|
+
providerKey,
|
|
3146
|
+
sessionId,
|
|
3147
|
+
options,
|
|
3148
|
+
{
|
|
3149
|
+
checkUsage,
|
|
3150
|
+
allowBlocked: true,
|
|
3151
|
+
prefetchedUsage: fallback.usage,
|
|
3152
|
+
usagePrechecked: fallback.usageChecked,
|
|
3153
|
+
enforceProRequirement,
|
|
3154
|
+
},
|
|
3155
|
+
reloadsUsed,
|
|
3156
|
+
);
|
|
3102
3157
|
}
|
|
3103
3158
|
|
|
3104
3159
|
return undefined;
|
|
@@ -3219,6 +3274,7 @@ export class AuthStorage {
|
|
|
3219
3274
|
usagePrechecked?: boolean;
|
|
3220
3275
|
enforceProRequirement?: boolean;
|
|
3221
3276
|
},
|
|
3277
|
+
reloadsUsed = 0,
|
|
3222
3278
|
): Promise<OAuthResolutionResult | undefined> {
|
|
3223
3279
|
const {
|
|
3224
3280
|
checkUsage,
|
|
@@ -3357,7 +3413,7 @@ export class AuthStorage {
|
|
|
3357
3413
|
credentialId: attemptedCredentialId,
|
|
3358
3414
|
});
|
|
3359
3415
|
await this.reload();
|
|
3360
|
-
return this.#resolveOAuthSelection(provider, sessionId, options);
|
|
3416
|
+
return this.#resolveOAuthSelection(provider, sessionId, options, reloadsUsed + 1);
|
|
3361
3417
|
}
|
|
3362
3418
|
}
|
|
3363
3419
|
// Only remove credentials for definitive auth failures
|
|
@@ -3387,18 +3443,39 @@ export class AuthStorage {
|
|
|
3387
3443
|
`oauth refresh failed: ${errorMsg}`,
|
|
3388
3444
|
);
|
|
3389
3445
|
if (!disabled) {
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3446
|
+
// The CAS predicate compares the row's serialized `data`, so it also
|
|
3447
|
+
// misses when nothing was rotated: the row may have been replaced by
|
|
3448
|
+
// a peer (account switcher rewriting the provider's credentials, so
|
|
3449
|
+
// our snapshot's id no longer exists) or updated with unrelated
|
|
3450
|
+
// identity metadata. Reload-and-retry only makes progress in the
|
|
3451
|
+
// rotation case; otherwise the same revoked token is re-refreshed on
|
|
3452
|
+
// every request forever. When the row is still present with the very
|
|
3453
|
+
// refresh token we just tried, disabling by id is safe — there is no
|
|
3454
|
+
// peer rotation to clobber — so apply it directly instead of looping.
|
|
3455
|
+
const stillHoldsAttemptedToken =
|
|
3456
|
+
attemptedCredentialId !== undefined &&
|
|
3457
|
+
this.#credentialRowHoldsRefreshToken(provider, attemptedCredentialId, selection.credential.refresh);
|
|
3458
|
+
if (stillHoldsAttemptedToken && attemptedCredentialId !== undefined) {
|
|
3459
|
+
logger.warn("OAuth refresh disable CAS mismatched an unrotated row; disabling by id", {
|
|
3460
|
+
provider,
|
|
3461
|
+
index: selection.index,
|
|
3462
|
+
credentialId: attemptedCredentialId,
|
|
3463
|
+
});
|
|
3464
|
+
this.#disableCredentialById(provider, attemptedCredentialId, `oauth refresh failed: ${errorMsg}`);
|
|
3465
|
+
} else {
|
|
3466
|
+
logger.debug("OAuth refresh disable lost CAS; reloading after peer rotation", {
|
|
3467
|
+
provider,
|
|
3468
|
+
index: selection.index,
|
|
3469
|
+
});
|
|
3470
|
+
await this.reload();
|
|
3471
|
+
return this.#resolveOAuthSelection(provider, sessionId, options, reloadsUsed + 1);
|
|
3472
|
+
}
|
|
3396
3473
|
}
|
|
3397
3474
|
if (
|
|
3398
3475
|
!this.#getCredentialSelector(provider, options) &&
|
|
3399
3476
|
this.#getCredentialsForProvider(provider).some(credential => credential.type === "oauth")
|
|
3400
3477
|
) {
|
|
3401
|
-
return this.#resolveOAuthSelection(provider, sessionId, options);
|
|
3478
|
+
return this.#resolveOAuthSelection(provider, sessionId, options, reloadsUsed);
|
|
3402
3479
|
}
|
|
3403
3480
|
} else {
|
|
3404
3481
|
// Block temporarily for transient failures (5 minutes)
|
package/src/model-thinking.ts
CHANGED
|
@@ -350,6 +350,25 @@ export function hasOpus47ApiRestrictions(modelId: string): boolean {
|
|
|
350
350
|
return semverGte(parsed.version, "4.7") && parsed.kind === "opus";
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Adaptive thinking `display` is supported starting with Anthropic Opus 4.7.
|
|
355
|
+
* Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
|
|
356
|
+
* Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to
|
|
357
|
+
* "omitted" — thinking tokens are billed but no content streams back — so it
|
|
358
|
+
* must opt in like Opus 4.7+ (issue #2791).
|
|
359
|
+
*
|
|
360
|
+
* Shares `hasOpus47ApiRestrictions` version parsing on purpose: the two
|
|
361
|
+
* predicates describe the same API generation, and a private `claude-opus-(\d+)-(\d+)`
|
|
362
|
+
* regex silently disagreed with it for single-component aliases (`claude-opus-5`
|
|
363
|
+
* matched nothing while `claude-opus-5-20260101` matched), so the same model sent
|
|
364
|
+
* a different thinking shape and beta set depending on which id string was used.
|
|
365
|
+
* Bedrock region/inference-profile prefixes are handled by the canonical parser.
|
|
366
|
+
*/
|
|
367
|
+
export function supportsAnthropicAdaptiveThinkingDisplay(modelId: string): boolean {
|
|
368
|
+
if (/claude-fable-\d/.test(modelId)) return true;
|
|
369
|
+
return hasOpus47ApiRestrictions(modelId);
|
|
370
|
+
}
|
|
371
|
+
|
|
353
372
|
function anthropicModelHasRealXHighEffort<TApi extends Api>(model: ApiModel<TApi>): boolean {
|
|
354
373
|
if (model.api !== "anthropic-messages") return false;
|
|
355
374
|
const parsedModel = parseKnownModel(model.id);
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { $credentialEnv, $env, $flag, extractHttpStatusFromError, fetchWithRetry } from "@gajae-code/utils";
|
|
11
11
|
import type { Effort } from "../model-thinking";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
mapEffortToAnthropicAdaptiveEffort,
|
|
14
|
+
requireSupportedEffort,
|
|
15
|
+
supportsAnthropicAdaptiveThinkingDisplay as supportsAdaptiveThinkingDisplay,
|
|
16
|
+
} from "../model-thinking";
|
|
13
17
|
import { calculateCost } from "../models";
|
|
14
18
|
import type {
|
|
15
19
|
Api,
|
|
@@ -907,25 +911,6 @@ function buildAdditionalModelRequestFields(
|
|
|
907
911
|
return result;
|
|
908
912
|
}
|
|
909
913
|
|
|
910
|
-
/**
|
|
911
|
-
* Adaptive thinking `display` is supported starting with Anthropic model Opus 4.7.
|
|
912
|
-
* Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
|
|
913
|
-
* Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to
|
|
914
|
-
* "omitted" — thinking tokens are billed but no content streams back — so it
|
|
915
|
-
* must opt in like Opus 4.7+ (issue #2791).
|
|
916
|
-
* Bedrock model ids are prefixed with region/inference-profile slugs (e.g.
|
|
917
|
-
* `eu.anthropic.Anthropic model-opus-4-7-...`); the regex matches the `Anthropic model-opus-X-Y`
|
|
918
|
-
* fragment regardless of prefix.
|
|
919
|
-
*/
|
|
920
|
-
function supportsAdaptiveThinkingDisplay(modelId: string): boolean {
|
|
921
|
-
if (/claude-fable-\d/.test(modelId)) return true;
|
|
922
|
-
const match = /claude-opus-(\d+)-(\d+)/.exec(modelId);
|
|
923
|
-
if (!match) return false;
|
|
924
|
-
const major = Number(match[1]);
|
|
925
|
-
const minor = Number(match[2]);
|
|
926
|
-
return major > 4 || (major === 4 && minor >= 7);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
914
|
/**
|
|
930
915
|
* Bedrock's wire format expects the image as `{ source: { bytes: <base64-string> }, format }`.
|
|
931
916
|
* The caller already passes base64-encoded data, so no decode/re-encode round-trip is needed.
|