@oh-my-pi/pi-ai 18.0.3 → 18.0.4
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 +9 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/error/flags.d.ts +13 -0
- package/dist/types/providers/cursor.d.ts +1 -0
- package/package.json +5 -5
- package/src/auth-storage.ts +14 -7
- package/src/error/flags.ts +146 -15
- package/src/oneshot-retry.ts +1 -0
- package/src/providers/cursor.ts +43 -21
- package/src/providers/openai-chat-server.ts +24 -10
- package/src/stream.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.0.4] - 2026-08-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Cursor tool calls through OpenAI-compatible authentication gateways losing arguments when complete argument maps are sent without streaming deltas ([#9479](https://github.com/can1357/oh-my-pi/issues/9479)).
|
|
10
|
+
- Fixed Cursor plan entitlement refusals repeatedly selecting ineligible accounts by scoping credential blocks to the requested model during rotation ([#9488](https://github.com/can1357/oh-my-pi/issues/9488)).
|
|
11
|
+
- Improved HTTP 413 error classification to accurately distinguish between payload/media size limits and token context window overflows, preventing inappropriate token compaction attempts and routing to correct recovery/fallback strategies ([#9235](https://github.com/can1357/oh-my-pi/issues/9235)).
|
|
12
|
+
- Fixed Cursor conversation rotation after aborts or mid-turn restarts to properly replay the last user message on a fresh conversation.
|
|
13
|
+
|
|
5
14
|
## [18.0.3] - 2026-08-23
|
|
6
15
|
|
|
7
16
|
### Fixed
|
|
@@ -1151,8 +1151,8 @@ export declare class AuthStorage {
|
|
|
1151
1151
|
* - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
|
|
1152
1152
|
* (temporary block via its own backoff — default plus server usage-report
|
|
1153
1153
|
* reset; sticky left intact so the next resolve re-ranks around the block).
|
|
1154
|
-
* - exact
|
|
1155
|
-
*
|
|
1154
|
+
* - exact model-entitlement denial (Codex ChatGPT account or Cursor plan) →
|
|
1155
|
+
* temporarily block only that requested model, then rotate.
|
|
1156
1156
|
* - other account-scoped policy denial → temporarily block that account
|
|
1157
1157
|
* without marking its credential suspect, then rotate through siblings.
|
|
1158
1158
|
* - otherwise (hard 401 / auth failure) → mark the credential suspect (or
|
|
@@ -23,6 +23,8 @@ export declare const Flag: {
|
|
|
23
23
|
readonly FastModeUnsupported: 536870912;
|
|
24
24
|
/** OAuth refresh failed definitively — the stored grant is dead, re-login required. */
|
|
25
25
|
readonly OAuthExpiry: 1073741824;
|
|
26
|
+
/** HTTP 413 byte/media rejection — token compaction cannot shrink bytes or media budgets (#9235). */
|
|
27
|
+
readonly PayloadRejected: 2147483648;
|
|
26
28
|
};
|
|
27
29
|
export type Flag = (typeof Flag)[keyof typeof Flag];
|
|
28
30
|
export declare const STREAM_READ_ERROR_PATTERN: RegExp;
|
|
@@ -65,6 +67,8 @@ export declare function isAccountPolicyError(error: unknown, api?: Api): boolean
|
|
|
65
67
|
export declare function codexChatGPTAccountPolicyModel(error: unknown, depth?: number): string | undefined;
|
|
66
68
|
/** Whether the exact Codex entitlement denial applies to this provider and requested model. */
|
|
67
69
|
export declare function isCodexChatGPTAccountPolicyError(error: unknown, provider: string, modelId: string | undefined): boolean;
|
|
70
|
+
/** Whether Cursor returned a non-retryable plan entitlement denial for this account. */
|
|
71
|
+
export declare function isCursorPlanAccountPolicyError(error: unknown, provider: string, depth?: number): boolean;
|
|
68
72
|
/**
|
|
69
73
|
* Strict-tool rejection: grammar too large, schema too complex, or structured
|
|
70
74
|
* outputs unsupported by the model/endpoint.
|
|
@@ -93,7 +97,16 @@ export declare function classifyMessage(message: {
|
|
|
93
97
|
errorStatus?: number;
|
|
94
98
|
}): number;
|
|
95
99
|
export declare function attach<E extends object>(error: E, id: number): E;
|
|
100
|
+
/** Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235). */
|
|
101
|
+
export declare function isUsageBackedContextOverflow(message: AssistantMessage, contextWindow?: number): boolean;
|
|
96
102
|
export declare function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean;
|
|
103
|
+
/** HTTP 413 byte/media rejection (#9235); may co-occur with {@link isContextOverflow} for bare `413 (no body)`.
|
|
104
|
+
* Callers with local headroom should skip compaction when this returns true. */
|
|
105
|
+
export declare function isPayloadRejection(message: AssistantMessage): boolean;
|
|
106
|
+
/** Dual-flagged 413 (PayloadRejected + ContextOverflow) with no provider-reported token excess (#9235).
|
|
107
|
+
* The co-flag means a different provider's larger byte/media budget may accept the request.
|
|
108
|
+
* Usage-backed overflows are authoritative window excesses and never ambiguous. */
|
|
109
|
+
export declare function isTextAmbiguousContextOverflow(errorId: number, message: AssistantMessage | undefined, contextWindow?: number): boolean;
|
|
97
110
|
export declare function stringify(id: number | undefined): string;
|
|
98
111
|
/**
|
|
99
112
|
* Transient stream corruption where the response was truncated mid-JSON.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "18.0.
|
|
4
|
+
"version": "18.0.4",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/omptype": "18.0.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "18.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
43
|
-
"@oh-my-pi/pi-wire": "18.0.
|
|
40
|
+
"@oh-my-pi/omptype": "18.0.4",
|
|
41
|
+
"@oh-my-pi/pi-catalog": "18.0.4",
|
|
42
|
+
"@oh-my-pi/pi-utils": "18.0.4",
|
|
43
|
+
"@oh-my-pi/pi-wire": "18.0.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/bun": "^1.3.14"
|
package/src/auth-storage.ts
CHANGED
|
@@ -1028,9 +1028,13 @@ function resolveOpenAICodexPlanRequirement(provider: string, modelId: string | u
|
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
1030
1030
|
const MODEL_ACCOUNT_POLICY_BLOCK_SCOPE_PREFIX = "model-policy:";
|
|
1031
|
+
const MODEL_ACCOUNT_POLICY_PROVIDERS: Readonly<Record<string, true>> = {
|
|
1032
|
+
"openai-codex": true,
|
|
1033
|
+
cursor: true,
|
|
1034
|
+
};
|
|
1031
1035
|
|
|
1032
1036
|
function modelAccountPolicyBlockScope(provider: string, modelId: string | undefined): string | undefined {
|
|
1033
|
-
if (provider
|
|
1037
|
+
if (!Object.hasOwn(MODEL_ACCOUNT_POLICY_PROVIDERS, provider) || typeof modelId !== "string") return undefined;
|
|
1034
1038
|
const separator = modelId.lastIndexOf("/");
|
|
1035
1039
|
const bareModelId = (separator === -1 ? modelId : modelId.slice(separator + 1)).trim().toLowerCase();
|
|
1036
1040
|
if (!bareModelId || bareModelId.includes("\0")) return undefined;
|
|
@@ -6393,8 +6397,8 @@ export class AuthStorage {
|
|
|
6393
6397
|
* - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
|
|
6394
6398
|
* (temporary block via its own backoff — default plus server usage-report
|
|
6395
6399
|
* reset; sticky left intact so the next resolve re-ranks around the block).
|
|
6396
|
-
* - exact
|
|
6397
|
-
*
|
|
6400
|
+
* - exact model-entitlement denial (Codex ChatGPT account or Cursor plan) →
|
|
6401
|
+
* temporarily block only that requested model, then rotate.
|
|
6398
6402
|
* - other account-scoped policy denial → temporarily block that account
|
|
6399
6403
|
* without marking its credential suspect, then rotate through siblings.
|
|
6400
6404
|
* - otherwise (hard 401 / auth failure) → mark the credential suspect (or
|
|
@@ -6411,7 +6415,9 @@ export class AuthStorage {
|
|
|
6411
6415
|
const error = options?.error;
|
|
6412
6416
|
const status = AIError.status(error);
|
|
6413
6417
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
6414
|
-
|
|
6418
|
+
const exactCursorModelPolicy = AIError.isCursorPlanAccountPolicyError(error, provider);
|
|
6419
|
+
const accountPolicy = exactCursorModelPolicy || AIError.isAccountPolicyError(error);
|
|
6420
|
+
if (!accountPolicy && (AIError.isUsageLimit(error) || isUsageLimitOutcome(status, message))) {
|
|
6415
6421
|
// Thread the provider-specified reset window (e.g. Devin "Your limit
|
|
6416
6422
|
// will reset in 13 minutes") into the block duration so the credential
|
|
6417
6423
|
// is not reselected and hammered while the cap remains active.
|
|
@@ -6436,15 +6442,16 @@ export class AuthStorage {
|
|
|
6436
6442
|
const deniedModel = AIError.codexChatGPTAccountPolicyModel(error);
|
|
6437
6443
|
const exactCodexModelPolicy =
|
|
6438
6444
|
deniedModel !== undefined && AIError.isCodexChatGPTAccountPolicyError(error, provider, options?.modelId);
|
|
6445
|
+
const exactModelPolicy = exactCodexModelPolicy || exactCursorModelPolicy;
|
|
6439
6446
|
// The exact sentence is provider-controlled input. A non-Codex provider,
|
|
6440
6447
|
// absent request model, or mismatched model must not turn it into either a
|
|
6441
6448
|
// global block or a hard-auth invalidation.
|
|
6442
6449
|
if (deniedModel !== undefined && !exactCodexModelPolicy) return false;
|
|
6443
|
-
if (
|
|
6444
|
-
const modelPolicyScope =
|
|
6450
|
+
if (exactModelPolicy || accountPolicy) {
|
|
6451
|
+
const modelPolicyScope = exactModelPolicy
|
|
6445
6452
|
? modelAccountPolicyBlockScope(provider, options?.modelId)
|
|
6446
6453
|
: undefined;
|
|
6447
|
-
if (
|
|
6454
|
+
if (exactModelPolicy && modelPolicyScope === undefined) return false;
|
|
6448
6455
|
const routing = this.#credentialBlockRouting(
|
|
6449
6456
|
provider,
|
|
6450
6457
|
sessionCredential.type,
|
package/src/error/flags.ts
CHANGED
|
@@ -40,6 +40,8 @@ export const Flag = {
|
|
|
40
40
|
FastModeUnsupported: 0x2000_0000,
|
|
41
41
|
/** OAuth refresh failed definitively — the stored grant is dead, re-login required. */
|
|
42
42
|
OAuthExpiry: 0x4000_0000,
|
|
43
|
+
/** HTTP 413 byte/media rejection — token compaction cannot shrink bytes or media budgets (#9235). */
|
|
44
|
+
PayloadRejected: 0x8000_0000,
|
|
43
45
|
} as const;
|
|
44
46
|
|
|
45
47
|
export type Flag = (typeof Flag)[keyof typeof Flag];
|
|
@@ -57,6 +59,7 @@ const KIND_MASK =
|
|
|
57
59
|
Flag.AccountPolicy |
|
|
58
60
|
Flag.ContextOverflow |
|
|
59
61
|
Flag.AuthFailed |
|
|
62
|
+
Flag.PayloadRejected |
|
|
60
63
|
Flag.SilentAbort |
|
|
61
64
|
Flag.UserInterrupt |
|
|
62
65
|
Flag.Abort |
|
|
@@ -72,7 +75,7 @@ const RETRIABLE_KINDS =
|
|
|
72
75
|
Flag.ProviderFinishError |
|
|
73
76
|
Flag.EmptyResponse;
|
|
74
77
|
|
|
75
|
-
const
|
|
78
|
+
const CONTEXT_OVERFLOW_EVIDENCE_PATTERNS = [
|
|
76
79
|
/prompt is too long/i, // Anthropic
|
|
77
80
|
/input is too long for requested model/i, // Amazon Bedrock
|
|
78
81
|
/exceeds the context window/i, // OpenAI (Completions & Responses API)
|
|
@@ -80,7 +83,6 @@ const OVERFLOW_PATTERNS = [
|
|
|
80
83
|
/maximum prompt length is \d+/i, // xAI (Grok)
|
|
81
84
|
/reduce the length of the messages/i, // Groq
|
|
82
85
|
/maximum context length is \d+ tokens/i, // OpenRouter (all backends)
|
|
83
|
-
/exceeds the limit of \d+/i, // GitHub Copilot
|
|
84
86
|
/exceeds the available context size/i, // llama.cpp server
|
|
85
87
|
/requested tokens?.*exceed.*context (window|length|size)/i, // llama.cpp / OpenAI-compatible local servers
|
|
86
88
|
/context (window|length|size).*(exceeded|overflow|too small)/i, // Generic local server variants
|
|
@@ -92,16 +94,62 @@ const OVERFLOW_PATTERNS = [
|
|
|
92
94
|
/context[_ ]length[_ ]exceeded/i, // Generic fallback
|
|
93
95
|
/too many tokens/i, // Generic fallback
|
|
94
96
|
/token limit exceeded/i, // Generic fallback
|
|
95
|
-
/request_too_large/i,
|
|
96
|
-
/
|
|
97
|
-
/payload too large/i, // Generic HTTP 413 variant
|
|
98
|
-
/entity too large/i, // Generic HTTP 413 variant
|
|
99
|
-
/\b413\b.*\b(request|payload|entity)\b.*\btoo large\b/i, // "413 Request Entity Too Large" variants
|
|
97
|
+
/request_too_large[^\n]*\btokens?\b/i,
|
|
98
|
+
/\btokens?\b[^\n]*request_too_large/i,
|
|
100
99
|
/model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
|
|
101
100
|
/prompt filled the context window/i, // Ollama OpenAI-compatible empty length completion
|
|
102
|
-
|
|
101
|
+
/exceeds the limit of \d+ tokens?\b/i,
|
|
102
|
+
] as const;
|
|
103
|
+
// Numeric limit pattern — also matches media budgets, so must never veto a payload flag (#9235).
|
|
104
|
+
const GENERIC_LIMIT_OVERFLOW_PATTERN = /exceeds the limit of \d+/i;
|
|
105
|
+
const OVERFLOW_PATTERNS = [...CONTEXT_OVERFLOW_EVIDENCE_PATTERNS, GENERIC_LIMIT_OVERFLOW_PATTERN];
|
|
106
|
+
|
|
107
|
+
/** Token-context evidence only — excludes GENERIC_LIMIT_OVERFLOW_PATTERN which media budgets also match (#9235). */
|
|
108
|
+
function hasTokenContextOverflowEvidence(text: string): boolean {
|
|
109
|
+
return CONTEXT_OVERFLOW_EVIDENCE_PATTERNS.some(p => p.test(text));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Checks every cause-chain link; a wrapper whose nested 413 names the token budget stays overflow-only (#9235). */
|
|
113
|
+
function hasCauseTokenContextOverflowEvidence(error: unknown): boolean {
|
|
114
|
+
const seen = new Set<object>();
|
|
115
|
+
let link: unknown = error;
|
|
116
|
+
while (link !== undefined && link !== null) {
|
|
117
|
+
if (typeof link !== "object") {
|
|
118
|
+
if (typeof link === "string" && hasTokenContextOverflowEvidence(link)) return true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (seen.has(link)) break;
|
|
122
|
+
seen.add(link);
|
|
123
|
+
if ("message" in link) {
|
|
124
|
+
const message: unknown = link.message;
|
|
125
|
+
if (typeof message === "string" && hasTokenContextOverflowEvidence(message)) return true;
|
|
126
|
+
}
|
|
127
|
+
if ("cause" in link) {
|
|
128
|
+
link = link.cause;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
103
135
|
|
|
104
136
|
const OVERFLOW_NO_BODY_PATTERN = /\b4(00|13)\s*(status code)?\s*\(no body\)/i;
|
|
137
|
+
// Bare `413 (no body)` deliberately dual-flagged; maintenance arbitrates against local headroom (#9235).
|
|
138
|
+
const PAYLOAD_REJECTION_PATTERNS = [
|
|
139
|
+
/\b413\s*(?:status code\s*)?\(no body\)/i,
|
|
140
|
+
/\b413\b[^.\n]{0,120}\b(?:request|payload|entity|body)\b[^.\n]{0,60}\b(?:exceed|too large|limit)/i,
|
|
141
|
+
/request_too_large/i,
|
|
142
|
+
/(?:payload|entity) too large/i,
|
|
143
|
+
/request exceeds the maximum (?:size|number of bytes)/i,
|
|
144
|
+
] as const;
|
|
145
|
+
|
|
146
|
+
function matchesPayloadRejectionText(text: string): boolean {
|
|
147
|
+
if (!PAYLOAD_REJECTION_PATTERNS.some(p => p.test(text))) return false;
|
|
148
|
+
// Token-context evidence vetoes; only token wording counts — generic numeric limits also match media budgets.
|
|
149
|
+
if (hasTokenContextOverflowEvidence(text)) return false;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
105
153
|
const TIMEOUT_PATTERN = /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i;
|
|
106
154
|
const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
|
|
107
155
|
const TRANSIENT_ENVELOPE_BEFORE_START_PATTERN = /before message_start/i;
|
|
@@ -118,6 +166,12 @@ const ACCOUNT_POLICY_PATTERN = /\bcyber_policy\b|trusted access for cyber/i;
|
|
|
118
166
|
const CODEX_CHATGPT_ACCOUNT_MODEL_POLICY_PATTERN =
|
|
119
167
|
/\bThe ['"]([^'"\r\n]+)['"] model is not supported when using Codex with a ChatGPT account\./i;
|
|
120
168
|
const CODEX_CHATGPT_ACCOUNT_MODEL_MAX_LENGTH = 256;
|
|
169
|
+
const CURSOR_PLAN_POLICY_MARKER_PATTERN = /\bERROR_RATE_LIMITED_CHANGEABLE\b/i;
|
|
170
|
+
const CURSOR_PLAN_POLICY_PATTERN = /\bNamed models unavailable\b|\bModel unavailable on\b|\bFree plans can only use\b/i;
|
|
171
|
+
|
|
172
|
+
function isCursorPlanPolicyText(text: string): boolean {
|
|
173
|
+
return CURSOR_PLAN_POLICY_MARKER_PATTERN.test(text) && CURSOR_PLAN_POLICY_PATTERN.test(text);
|
|
174
|
+
}
|
|
121
175
|
|
|
122
176
|
function normalizeCodexChatGPTAccountPolicyModel(modelId: string | undefined): string | undefined {
|
|
123
177
|
if (typeof modelId !== "string") return undefined;
|
|
@@ -249,6 +303,7 @@ const ERROR_KIND_LABELS: readonly [Flag, string][] = [
|
|
|
249
303
|
[Flag.ContentBlocked, "content-blocked"],
|
|
250
304
|
[Flag.AccountPolicy, "account-policy"],
|
|
251
305
|
[Flag.ContextOverflow, "context-overflow"],
|
|
306
|
+
[Flag.PayloadRejected, "payload-rejected"],
|
|
252
307
|
[Flag.AuthFailed, "auth-failed"],
|
|
253
308
|
[Flag.SilentAbort, "silent-abort"],
|
|
254
309
|
[Flag.UserInterrupt, "user-interrupt"],
|
|
@@ -275,6 +330,7 @@ export function is(id: number | undefined, flag: Flag): boolean {
|
|
|
275
330
|
|
|
276
331
|
export function retriable(id: number | undefined, opts?: { replayUnsafe?: boolean }): boolean {
|
|
277
332
|
if (is(id, Flag.ContentBlocked)) return false;
|
|
333
|
+
if (is(id, Flag.PayloadRejected)) return false;
|
|
278
334
|
if (opts?.replayUnsafe) return false;
|
|
279
335
|
if (is(id, Flag.MalformedFunctionCall)) return true;
|
|
280
336
|
return ((id ?? 0) & RETRIABLE_KINDS) !== 0;
|
|
@@ -386,6 +442,7 @@ function matchesOverflowText(text: string): boolean {
|
|
|
386
442
|
function classifyText(
|
|
387
443
|
errorMessage: string | undefined,
|
|
388
444
|
errorStatus: number | undefined,
|
|
445
|
+
priorTokenOverflowEvidence = false,
|
|
389
446
|
api?: Api,
|
|
390
447
|
provider?: string,
|
|
391
448
|
modelId?: string,
|
|
@@ -393,13 +450,15 @@ function classifyText(
|
|
|
393
450
|
let kinds = 0;
|
|
394
451
|
if (errorMessage) {
|
|
395
452
|
if (matchesOverflowText(errorMessage)) kinds |= Flag.ContextOverflow;
|
|
453
|
+
if (matchesPayloadRejectionText(errorMessage)) kinds |= Flag.PayloadRejected;
|
|
396
454
|
if (isMalformedFunctionCallText(errorMessage)) kinds |= Flag.MalformedFunctionCall;
|
|
397
455
|
if (isProviderFinishErrorText(errorMessage)) kinds |= Flag.ProviderFinishError;
|
|
398
456
|
if (EMPTY_RESPONSE_PATTERN.test(errorMessage)) kinds |= Flag.EmptyResponse | Flag.Transient;
|
|
399
457
|
if (isContentBlockedText(errorMessage)) kinds |= Flag.ContentBlocked;
|
|
400
458
|
if (
|
|
401
459
|
ACCOUNT_POLICY_PATTERN.test(errorMessage) ||
|
|
402
|
-
isCodexChatGPTAccountPolicyText(errorMessage, provider, modelId)
|
|
460
|
+
isCodexChatGPTAccountPolicyText(errorMessage, provider, modelId) ||
|
|
461
|
+
(provider === "cursor" && isCursorPlanPolicyText(errorMessage))
|
|
403
462
|
) {
|
|
404
463
|
kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
405
464
|
}
|
|
@@ -450,6 +509,15 @@ function classifyText(
|
|
|
450
509
|
if (matchesStrictToolsRejection(cleanMessage, statusClean)) kinds |= Flag.Grammar;
|
|
451
510
|
if (matchesFastModeUnsupported(cleanMessage, statusClean)) kinds |= Flag.FastModeUnsupported;
|
|
452
511
|
}
|
|
512
|
+
// Status-only 413: infer PayloadRejected unless prior classification carries token-context evidence (#9235).
|
|
513
|
+
const statusEvidence = errorStatus ?? (errorMessage ? status({ message: errorMessage }) : undefined);
|
|
514
|
+
if (
|
|
515
|
+
statusEvidence === 413 &&
|
|
516
|
+
!priorTokenOverflowEvidence &&
|
|
517
|
+
!(errorMessage && hasTokenContextOverflowEvidence(errorMessage))
|
|
518
|
+
) {
|
|
519
|
+
kinds |= Flag.PayloadRejected;
|
|
520
|
+
}
|
|
453
521
|
if (kinds !== 0) return create(kinds);
|
|
454
522
|
const fallbackStatus = errorStatus ?? (errorMessage ? status({ message: errorMessage }) : undefined);
|
|
455
523
|
if (fallbackStatus === 401 || fallbackStatus === 403) return create(Flag.AuthFailed);
|
|
@@ -459,6 +527,7 @@ function classifyText(
|
|
|
459
527
|
export function classify(error: unknown, api?: Api): number {
|
|
460
528
|
let kinds = 0;
|
|
461
529
|
const seen = new Set<object>();
|
|
530
|
+
const causeTokenEvidence = hasCauseTokenContextOverflowEvidence(error);
|
|
462
531
|
let link: unknown = error;
|
|
463
532
|
while (link !== undefined && link !== null) {
|
|
464
533
|
if (typeof link === "object") {
|
|
@@ -532,7 +601,7 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
532
601
|
linkMessage = (link as { message: string }).message;
|
|
533
602
|
}
|
|
534
603
|
|
|
535
|
-
const textId = classifyText(linkMessage, status(link), api);
|
|
604
|
+
const textId = classifyText(linkMessage, status(link), causeTokenEvidence, api);
|
|
536
605
|
kinds |= textId & KIND_MASK;
|
|
537
606
|
|
|
538
607
|
link = typeof link === "object" && "cause" in link ? (link as { cause: unknown }).cause : undefined;
|
|
@@ -586,6 +655,22 @@ export function isCodexChatGPTAccountPolicyError(
|
|
|
586
655
|
return provider === "openai-codex" && deniedIdentity !== undefined && deniedIdentity === requestedIdentity;
|
|
587
656
|
}
|
|
588
657
|
|
|
658
|
+
/** Whether Cursor returned a non-retryable plan entitlement denial for this account. */
|
|
659
|
+
export function isCursorPlanAccountPolicyError(error: unknown, provider: string, depth = 0): boolean {
|
|
660
|
+
if (provider !== "cursor" || depth > 6) return false;
|
|
661
|
+
if (typeof error === "string") return isCursorPlanPolicyText(error);
|
|
662
|
+
if (!error || typeof error !== "object") return false;
|
|
663
|
+
if (
|
|
664
|
+
"errorMessage" in error &&
|
|
665
|
+
typeof error.errorMessage === "string" &&
|
|
666
|
+
isCursorPlanPolicyText(error.errorMessage)
|
|
667
|
+
) {
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
if ("message" in error && typeof error.message === "string" && isCursorPlanPolicyText(error.message)) return true;
|
|
671
|
+
return "cause" in error && isCursorPlanAccountPolicyError(error.cause, provider, depth + 1);
|
|
672
|
+
}
|
|
673
|
+
|
|
589
674
|
/**
|
|
590
675
|
* Strict-tool rejection: grammar too large, schema too complex, or structured
|
|
591
676
|
* outputs unsupported by the model/endpoint.
|
|
@@ -648,10 +733,28 @@ export function classifyMessage(message: {
|
|
|
648
733
|
}): number {
|
|
649
734
|
const existingId = message.errorId;
|
|
650
735
|
const currentStatus = message.errorStatus ?? statusFromId(existingId);
|
|
736
|
+
const existingOverflowOnly =
|
|
737
|
+
existingId !== undefined && is(existingId, Flag.ContextOverflow) && !is(existingId, Flag.PayloadRejected);
|
|
651
738
|
const classificationMessage = message.errorClassificationMessage ?? message.errorMessage;
|
|
652
|
-
const textId = classifyText(
|
|
739
|
+
const textId = classifyText(
|
|
740
|
+
classificationMessage,
|
|
741
|
+
currentStatus,
|
|
742
|
+
existingOverflowOnly,
|
|
743
|
+
message.api,
|
|
744
|
+
message.provider,
|
|
745
|
+
message.model,
|
|
746
|
+
);
|
|
653
747
|
|
|
654
748
|
let kinds = ((existingId ?? 0) | textId) & KIND_MASK;
|
|
749
|
+
// Two-phase finalization: drop stale status-inferred payload bit when final text proves token overflow (#9235).
|
|
750
|
+
if (
|
|
751
|
+
currentStatus === 413 &&
|
|
752
|
+
classificationMessage &&
|
|
753
|
+
hasTokenContextOverflowEvidence(classificationMessage) &&
|
|
754
|
+
!(textId & Flag.PayloadRejected)
|
|
755
|
+
) {
|
|
756
|
+
kinds &= ~Flag.PayloadRejected;
|
|
757
|
+
}
|
|
655
758
|
if (classificationMessage && LLAMA_CPP_TOOL_CALL_PARSE_PATTERN.test(classificationMessage)) {
|
|
656
759
|
// Deterministic local-model tool-call JSON parse failure: HTTP 500 is misleading
|
|
657
760
|
// because the same prompt reproduces the same malformed output, so the agent-level
|
|
@@ -669,15 +772,43 @@ export function attach<E extends object>(error: E, id: number): E {
|
|
|
669
772
|
return error;
|
|
670
773
|
}
|
|
671
774
|
|
|
775
|
+
/** Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235). */
|
|
776
|
+
export function isUsageBackedContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
|
|
777
|
+
if (!contextWindow) return false;
|
|
778
|
+
const inputTokens = message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
|
|
779
|
+
return inputTokens > contextWindow;
|
|
780
|
+
}
|
|
781
|
+
|
|
672
782
|
export function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
|
|
673
783
|
if (is(message.errorId, Flag.ContextOverflow)) return true;
|
|
674
|
-
if (contextWindow)
|
|
675
|
-
const inputTokens = message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
|
|
676
|
-
if (inputTokens > contextWindow) return true;
|
|
677
|
-
}
|
|
784
|
+
if (isUsageBackedContextOverflow(message, contextWindow)) return true;
|
|
678
785
|
return message.stopReason === "error" && !!message.errorMessage && matchesOverflowText(message.errorMessage);
|
|
679
786
|
}
|
|
680
787
|
|
|
788
|
+
/** HTTP 413 byte/media rejection (#9235); may co-occur with {@link isContextOverflow} for bare `413 (no body)`.
|
|
789
|
+
* Callers with local headroom should skip compaction when this returns true. */
|
|
790
|
+
export function isPayloadRejection(message: AssistantMessage): boolean {
|
|
791
|
+
if (is(message.errorId, Flag.PayloadRejected)) return true;
|
|
792
|
+
const { errorMessage } = message;
|
|
793
|
+
if (message.stopReason !== "error" || !errorMessage) return false;
|
|
794
|
+
return matchesPayloadRejectionText(errorMessage);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Dual-flagged 413 (PayloadRejected + ContextOverflow) with no provider-reported token excess (#9235).
|
|
798
|
+
* The co-flag means a different provider's larger byte/media budget may accept the request.
|
|
799
|
+
* Usage-backed overflows are authoritative window excesses and never ambiguous. */
|
|
800
|
+
export function isTextAmbiguousContextOverflow(
|
|
801
|
+
errorId: number,
|
|
802
|
+
message: AssistantMessage | undefined,
|
|
803
|
+
contextWindow?: number,
|
|
804
|
+
): boolean {
|
|
805
|
+
const overflowFlagged =
|
|
806
|
+
is(errorId, Flag.ContextOverflow) || (message !== undefined && isContextOverflow(message, contextWindow));
|
|
807
|
+
if (!overflowFlagged) return false;
|
|
808
|
+
if (!is(errorId, Flag.PayloadRejected)) return false;
|
|
809
|
+
return !(message !== undefined && isUsageBackedContextOverflow(message, contextWindow));
|
|
810
|
+
}
|
|
811
|
+
|
|
681
812
|
export function stringify(id: number | undefined): string {
|
|
682
813
|
if (!id) return "none";
|
|
683
814
|
if (!isClassified(id)) return `status:${id}`;
|
package/src/oneshot-retry.ts
CHANGED
|
@@ -106,6 +106,7 @@ function isRetryableOneshotFailure(errorId: number, errorStatus: number | undefi
|
|
|
106
106
|
// identically on every attempt. Retrying burns the caller's deadline instead
|
|
107
107
|
// of reaching the fallback that can actually shrink the input.
|
|
108
108
|
if (AIError.is(errorId, AIError.Flag.ContextOverflow)) return false;
|
|
109
|
+
if (AIError.is(errorId, AIError.Flag.PayloadRejected)) return false;
|
|
109
110
|
return (
|
|
110
111
|
AIError.isTransientStatus(errorStatus) ||
|
|
111
112
|
AIError.is(errorId, AIError.Flag.Transient) ||
|
package/src/providers/cursor.ts
CHANGED
|
@@ -327,13 +327,15 @@ const warnedCursorKimiK3ReplayMessages = new Set<string>();
|
|
|
327
327
|
/**
|
|
328
328
|
* Base conversation id → rotated wire id (#8345). Cursor's backend can pin a
|
|
329
329
|
* per-conversation rejection (bare `resource_exhausted`, zero tokens) to one
|
|
330
|
-
* conversationId forever
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
330
|
+
* conversationId forever. On such a failure the id is rotated and the next
|
|
331
|
+
* attempt rebuilds a fresh conversation from `context` (no cached-state
|
|
332
|
+
* migration). A failed rotation is not repeated, so real account exhaustion
|
|
333
|
+
* is not hidden. After the rotated id completes a turn, a later poison of
|
|
334
|
+
* that id is allowed to rotate again.
|
|
335
335
|
*/
|
|
336
336
|
const rotatedConversationIds = new Map<string, string>();
|
|
337
|
+
const successfulRotatedConversationIds = new Set<string>();
|
|
338
|
+
const freshRotatedConversationIds = new Set<string>();
|
|
337
339
|
|
|
338
340
|
export interface CursorOptions extends StreamOptions {
|
|
339
341
|
customSystemPrompt?: string;
|
|
@@ -355,6 +357,7 @@ interface CursorRequestState {
|
|
|
355
357
|
conversationId: string;
|
|
356
358
|
blobStore: Map<string, Uint8Array>;
|
|
357
359
|
conversationState?: ConversationStateStructure;
|
|
360
|
+
rotatedFresh?: boolean;
|
|
358
361
|
}
|
|
359
362
|
|
|
360
363
|
interface CursorGrpcRequest {
|
|
@@ -680,9 +683,10 @@ function streamCursorWithWireMode(
|
|
|
680
683
|
|
|
681
684
|
baseConversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
|
|
682
685
|
conversationId = rotatedConversationIds.get(baseConversationId) ?? baseConversationId;
|
|
686
|
+
const rotatedFresh = freshRotatedConversationIds.has(conversationId);
|
|
683
687
|
const blobStore = conversationBlobStores.get(conversationId) ?? new Map<string, Uint8Array>();
|
|
684
688
|
conversationBlobStores.set(conversationId, blobStore);
|
|
685
|
-
const cachedState = conversationStateCache.get(conversationId);
|
|
689
|
+
const cachedState = rotatedFresh ? undefined : conversationStateCache.get(conversationId);
|
|
686
690
|
const builtRequest = await buildGrpcRequestForWireMode(
|
|
687
691
|
model,
|
|
688
692
|
context,
|
|
@@ -691,6 +695,7 @@ function streamCursorWithWireMode(
|
|
|
691
695
|
conversationId,
|
|
692
696
|
blobStore,
|
|
693
697
|
conversationState: cachedState,
|
|
698
|
+
rotatedFresh,
|
|
694
699
|
},
|
|
695
700
|
wireMode,
|
|
696
701
|
);
|
|
@@ -929,6 +934,10 @@ function streamCursorWithWireMode(
|
|
|
929
934
|
h2Request.write(frameConnectMessage(requestBytes));
|
|
930
935
|
heartbeatTimer = setInterval(sendHeartbeat, 5000);
|
|
931
936
|
await h2Completion.promise;
|
|
937
|
+
if (conversationId && baseConversationId && conversationId !== baseConversationId) {
|
|
938
|
+
successfulRotatedConversationIds.add(conversationId);
|
|
939
|
+
freshRotatedConversationIds.delete(conversationId);
|
|
940
|
+
}
|
|
932
941
|
// The transport is done, but a handler decoded from the last chunk may
|
|
933
942
|
// still be running: exec handlers and `onToolResult` transformers are
|
|
934
943
|
// async. Pushing `done` now would let the Agent drain its Cursor result
|
|
@@ -1013,25 +1022,31 @@ function streamCursorWithWireMode(
|
|
|
1013
1022
|
const result = await AIError.finalize(error, { api: model.api, signal: options?.signal });
|
|
1014
1023
|
// #8345: a server-side per-conversation rejection surfaces as a bare
|
|
1015
1024
|
// resource_exhausted with zero tokens — the conversation is poisoned,
|
|
1016
|
-
// not the account
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1025
|
+
// not the account. Rotate the wire id and rebuild from `context` on
|
|
1026
|
+
// the next attempt (the caller's retry loop). Do not migrate cached
|
|
1027
|
+
// side-state: pendingToolCalls from a mid-turn checkpoint re-poison
|
|
1028
|
+
// the new id. One rotation per failure streak; a new rotation is
|
|
1029
|
+
// allowed only after the current rotated id completed a turn.
|
|
1030
|
+
const currentRotated =
|
|
1031
|
+
baseConversationId === undefined ? undefined : rotatedConversationIds.get(baseConversationId);
|
|
1032
|
+
const canRotate = currentRotated === undefined || successfulRotatedConversationIds.has(currentRotated);
|
|
1021
1033
|
if (
|
|
1022
1034
|
conversationId !== undefined &&
|
|
1023
1035
|
baseConversationId !== undefined &&
|
|
1024
1036
|
usageState !== undefined &&
|
|
1025
1037
|
!usageState.sawTokenDelta &&
|
|
1026
1038
|
RESOURCE_EXHAUSTED_PATTERN.test(result.message) &&
|
|
1027
|
-
|
|
1039
|
+
canRotate
|
|
1028
1040
|
) {
|
|
1029
1041
|
const rotated = crypto.randomUUID();
|
|
1042
|
+
if (currentRotated) successfulRotatedConversationIds.delete(currentRotated);
|
|
1030
1043
|
rotatedConversationIds.set(baseConversationId, rotated);
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1044
|
+
freshRotatedConversationIds.add(rotated);
|
|
1045
|
+
logger.debug("cursor conversation rotated", {
|
|
1046
|
+
base: baseConversationId,
|
|
1047
|
+
from: conversationId,
|
|
1048
|
+
to: rotated,
|
|
1049
|
+
});
|
|
1035
1050
|
}
|
|
1036
1051
|
output.stopReason = result.stopReason;
|
|
1037
1052
|
output.errorStatus = result.status;
|
|
@@ -4260,9 +4275,8 @@ export function processInteractionUpdate(
|
|
|
4260
4275
|
// what the exec channel pairs its result under. Diverging here would
|
|
4261
4276
|
// name the block one thing and its result another.
|
|
4262
4277
|
name: args.toolName || args.name || "",
|
|
4263
|
-
arguments: {},
|
|
4278
|
+
arguments: decodeMcpArgsMap(args.args) ?? {},
|
|
4264
4279
|
[kStreamingBlockIndex]: output.content.length,
|
|
4265
|
-
[kStreamingPartialJson]: "",
|
|
4266
4280
|
[kStreamingBlockKind]: "mcp",
|
|
4267
4281
|
[kStreamingEnvelopeId]: update.message.value.callId || undefined,
|
|
4268
4282
|
};
|
|
@@ -4385,7 +4399,7 @@ export function processInteractionUpdate(
|
|
|
4385
4399
|
// Authoritative full parse of the accumulated argument buffer; the delta
|
|
4386
4400
|
// path throttles mid-stream parses, so `arguments` may lag the buffer.
|
|
4387
4401
|
const partial = settled[kStreamingPartialJson];
|
|
4388
|
-
if (partial
|
|
4402
|
+
if (partial) {
|
|
4389
4403
|
settled.arguments = parseStreamingJson(partial);
|
|
4390
4404
|
}
|
|
4391
4405
|
const decodedArgs = decodeMcpArgsMap(selectMcpCall(toolCall)?.args?.args);
|
|
@@ -5210,10 +5224,18 @@ async function buildGrpcRequestForWireMode(
|
|
|
5210
5224
|
storeCursorBlob(blobStore, new TextEncoder().encode(json)),
|
|
5211
5225
|
);
|
|
5212
5226
|
|
|
5213
|
-
const
|
|
5227
|
+
const lastUserMessageIndex = findLastUserMessageIndex(context.messages);
|
|
5228
|
+
let activeUserMessageIndex = context.messages.length - 1;
|
|
5214
5229
|
const activeMessage = context.messages[activeUserMessageIndex];
|
|
5215
|
-
|
|
5230
|
+
let activeUserMessage =
|
|
5216
5231
|
activeMessage?.role === "user" || activeMessage?.role === "developer" ? activeMessage : undefined;
|
|
5232
|
+
if (state.rotatedFresh && !activeUserMessage && lastUserMessageIndex >= 0) {
|
|
5233
|
+
activeUserMessageIndex = lastUserMessageIndex;
|
|
5234
|
+
const lastUser = context.messages[lastUserMessageIndex];
|
|
5235
|
+
if (lastUser.role === "user" || lastUser.role === "developer") {
|
|
5236
|
+
activeUserMessage = lastUser;
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5217
5239
|
let userContent: string | (TextContent | ImageContent)[] | undefined;
|
|
5218
5240
|
let userText = "";
|
|
5219
5241
|
let hasUserImages = false;
|
|
@@ -582,9 +582,9 @@ export function encodeStream(
|
|
|
582
582
|
async start(controller) {
|
|
583
583
|
// contentIndex (from pi-ai events) -> tool_calls index on the wire.
|
|
584
584
|
const toolIndexByContentIndex = new Map<number, number>();
|
|
585
|
-
// wire index ->
|
|
586
|
-
//
|
|
587
|
-
const sentToolMeta = new Map<number, { id: string; name: string }>();
|
|
585
|
+
// wire index -> metadata emitted so far, to detect values that need a
|
|
586
|
+
// concatenation-safe corrective chunk before the finish.
|
|
587
|
+
const sentToolMeta = new Map<number, { id: string; name: string; hasArgumentBytes: boolean }>();
|
|
588
588
|
let nextToolIndex = 0;
|
|
589
589
|
let hasToolCalls = false;
|
|
590
590
|
let finishReason: string = "stop";
|
|
@@ -620,7 +620,7 @@ export function encodeStream(
|
|
|
620
620
|
toolIndexByContentIndex.set(event.contentIndex, idx);
|
|
621
621
|
const partial = event.partial.content[event.contentIndex];
|
|
622
622
|
const call = partial && partial.type === "toolCall" ? partial : undefined;
|
|
623
|
-
sentToolMeta.set(idx, { id: call?.id ?? "", name: call?.name ?? "" });
|
|
623
|
+
sentToolMeta.set(idx, { id: call?.id ?? "", name: call?.name ?? "", hasArgumentBytes: false });
|
|
624
624
|
writeSse(
|
|
625
625
|
controller,
|
|
626
626
|
baseChunk(
|
|
@@ -643,6 +643,8 @@ export function encodeStream(
|
|
|
643
643
|
case "toolcall_delta": {
|
|
644
644
|
const idx = toolIndexByContentIndex.get(event.contentIndex);
|
|
645
645
|
if (idx === undefined) break;
|
|
646
|
+
const sent = sentToolMeta.get(idx);
|
|
647
|
+
if (sent && event.delta.length > 0) sent.hasArgumentBytes = true;
|
|
646
648
|
writeSse(
|
|
647
649
|
controller,
|
|
648
650
|
baseChunk({ tool_calls: [{ index: idx, function: { arguments: event.delta } }] }, null),
|
|
@@ -655,14 +657,17 @@ export function encodeStream(
|
|
|
655
657
|
if (idx === undefined) break;
|
|
656
658
|
const sent = sentToolMeta.get(idx);
|
|
657
659
|
if (sent === undefined) break;
|
|
658
|
-
// Upstream
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
660
|
+
// Upstream providers can settle id, name, or arguments after the
|
|
661
|
+
// start chunk. Emit corrections only for fields whose streamed
|
|
662
|
+
// value was empty: accumulating clients concatenate each field,
|
|
663
|
+
// so "" + value is the only safe correction.
|
|
662
664
|
const correctId = sent.id === "" && event.toolCall.id !== "" ? event.toolCall.id : undefined;
|
|
663
665
|
const correctName =
|
|
664
666
|
sent.name === "" && event.toolCall.name !== "" ? event.toolCall.name : undefined;
|
|
665
|
-
|
|
667
|
+
const correctArguments = sent.hasArgumentBytes
|
|
668
|
+
? undefined
|
|
669
|
+
: stringifyArgs(event.toolCall.arguments);
|
|
670
|
+
if (correctId !== undefined || correctName !== undefined || correctArguments !== undefined) {
|
|
666
671
|
writeSse(
|
|
667
672
|
controller,
|
|
668
673
|
baseChunk(
|
|
@@ -671,7 +676,16 @@ export function encodeStream(
|
|
|
671
676
|
{
|
|
672
677
|
index: idx,
|
|
673
678
|
...(correctId !== undefined ? { id: correctId } : {}),
|
|
674
|
-
...(correctName !== undefined
|
|
679
|
+
...(correctName !== undefined || correctArguments !== undefined
|
|
680
|
+
? {
|
|
681
|
+
function: {
|
|
682
|
+
...(correctName !== undefined ? { name: correctName } : {}),
|
|
683
|
+
...(correctArguments !== undefined
|
|
684
|
+
? { arguments: correctArguments }
|
|
685
|
+
: {}),
|
|
686
|
+
},
|
|
687
|
+
}
|
|
688
|
+
: {}),
|
|
675
689
|
},
|
|
676
690
|
],
|
|
677
691
|
},
|
package/src/stream.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic
|
|
|
7
7
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
8
8
|
import { isVertexExpressOpenAIUrl, isVertexRawPredictUrl, resolveVertexEndpointHost } from "@oh-my-pi/pi-catalog/hosts";
|
|
9
9
|
import {
|
|
10
|
+
defaultSupportedEffort,
|
|
10
11
|
mapEffortToAnthropicAdaptiveEffort,
|
|
11
12
|
mapEffortToGoogleThinkingLevel,
|
|
12
|
-
minimumSupportedEffort,
|
|
13
13
|
requireSupportedEffort,
|
|
14
14
|
resolveWireModelId,
|
|
15
15
|
} from "@oh-my-pi/pi-catalog/model-thinking";
|
|
@@ -1890,7 +1890,7 @@ function normalizeMandatoryReasoningOptions<TApi extends Api>(
|
|
|
1890
1890
|
) {
|
|
1891
1891
|
return options;
|
|
1892
1892
|
}
|
|
1893
|
-
const floor =
|
|
1893
|
+
const floor = defaultSupportedEffort(model);
|
|
1894
1894
|
if (floor === undefined) return options;
|
|
1895
1895
|
return { ...options, reasoning: floor, disableReasoning: undefined, forceReasoningOff: undefined };
|
|
1896
1896
|
}
|