@gajae-code/ai 0.17.2 → 0.17.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/types/auth-gateway/server.d.ts +23 -1
  3. package/dist/types/auth-storage.d.ts +12 -1
  4. package/dist/types/providers/anthropic.d.ts +1 -1
  5. package/dist/types/providers/cursor.d.ts +10 -0
  6. package/dist/types/providers/openai-completions.d.ts +9 -1
  7. package/dist/types/types.d.ts +16 -0
  8. package/dist/types/utils/discovery/openai-compatible.d.ts +10 -0
  9. package/dist/types/utils/fallback-transport.d.ts +4 -0
  10. package/dist/types/utils/h2-fetch.d.ts +8 -7
  11. package/dist/types/utils/stream-repetition-guard.d.ts +107 -0
  12. package/dist/types/utils/tool-call-healing.d.ts +4 -0
  13. package/dist/types/utils/tool-fence-strip.d.ts +27 -0
  14. package/package.json +3 -3
  15. package/src/auth-gateway/server.ts +48 -9
  16. package/src/auth-storage.ts +185 -48
  17. package/src/model-pricing.ts +22 -0
  18. package/src/model-thinking.ts +40 -4
  19. package/src/models.json +191 -15
  20. package/src/providers/anthropic.d.ts +1 -1
  21. package/src/providers/anthropic.ts +1 -1
  22. package/src/providers/cursor.d.ts +10 -0
  23. package/src/providers/cursor.ts +144 -24
  24. package/src/providers/openai-completions.d.ts +9 -1
  25. package/src/providers/openai-completions.ts +371 -126
  26. package/src/stream.ts +7 -0
  27. package/src/types.d.ts +16 -0
  28. package/src/types.ts +17 -0
  29. package/src/utils/discovery/openai-compatible.ts +16 -2
  30. package/src/utils/fallback-transport.d.ts +4 -0
  31. package/src/utils/fallback-transport.ts +11 -0
  32. package/src/utils/h2-fetch.ts +65 -26
  33. package/src/utils/http-inspector.ts +2 -0
  34. package/src/utils/idle-iterator.ts +109 -96
  35. package/src/utils/json-parse.ts +12 -4
  36. package/src/utils/stream-repetition-guard.d.ts +107 -0
  37. package/src/utils/stream-repetition-guard.ts +290 -0
  38. package/src/utils/tool-call-healing.d.ts +4 -0
  39. package/src/utils/tool-call-healing.ts +4 -0
  40. package/src/utils/tool-fence-strip.d.ts +27 -0
  41. package/src/utils/tool-fence-strip.ts +64 -0
package/src/models.json CHANGED
@@ -116,6 +116,76 @@
116
116
  "maxLevel": "xhigh"
117
117
  }
118
118
  },
119
+ "deepseek-v4-pro-0813": {
120
+ "id": "deepseek-v4-pro-0813",
121
+ "name": "DeepSeek V4 Pro 0813",
122
+ "api": "openai-completions",
123
+ "provider": "alibaba-token-plan",
124
+ "baseUrl": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
125
+ "reasoning": true,
126
+ "input": [
127
+ "text"
128
+ ],
129
+ "cost": {
130
+ "input": 0,
131
+ "output": 0,
132
+ "cacheRead": 0,
133
+ "cacheWrite": 0
134
+ },
135
+ "contextWindow": 1000000,
136
+ "maxTokens": 384000,
137
+ "compat": {
138
+ "supportsDeveloperRole": false,
139
+ "supportsReasoningEffort": true,
140
+ "reasoningContentField": "reasoning_content",
141
+ "requiresReasoningContentForToolCalls": true
142
+ },
143
+ "thinking": {
144
+ "mode": "effort",
145
+ "minLevel": "low",
146
+ "maxLevel": "max",
147
+ "levels": [
148
+ "low",
149
+ "high",
150
+ "max"
151
+ ]
152
+ }
153
+ },
154
+ "deepseek-v4.1-flash": {
155
+ "id": "deepseek-v4.1-flash",
156
+ "name": "DeepSeek V4.1 Flash",
157
+ "api": "openai-completions",
158
+ "provider": "alibaba-token-plan",
159
+ "baseUrl": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
160
+ "reasoning": true,
161
+ "input": [
162
+ "text"
163
+ ],
164
+ "cost": {
165
+ "input": 0,
166
+ "output": 0,
167
+ "cacheRead": 0,
168
+ "cacheWrite": 0
169
+ },
170
+ "contextWindow": 1000000,
171
+ "maxTokens": 384000,
172
+ "compat": {
173
+ "supportsDeveloperRole": false,
174
+ "supportsReasoningEffort": true,
175
+ "reasoningContentField": "reasoning_content",
176
+ "requiresReasoningContentForToolCalls": true
177
+ },
178
+ "thinking": {
179
+ "mode": "effort",
180
+ "minLevel": "low",
181
+ "maxLevel": "max",
182
+ "levels": [
183
+ "low",
184
+ "high",
185
+ "max"
186
+ ]
187
+ }
188
+ },
119
189
  "glm-5": {
120
190
  "id": "glm-5",
121
191
  "name": "GLM-5",
@@ -197,6 +267,42 @@
197
267
  "maxLevel": "xhigh"
198
268
  }
199
269
  },
270
+ "glm-5.3": {
271
+ "id": "glm-5.3",
272
+ "name": "GLM-5.3",
273
+ "api": "openai-completions",
274
+ "provider": "alibaba-token-plan",
275
+ "baseUrl": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
276
+ "reasoning": true,
277
+ "input": [
278
+ "text"
279
+ ],
280
+ "cost": {
281
+ "input": 0,
282
+ "output": 0,
283
+ "cacheRead": 0,
284
+ "cacheWrite": 0
285
+ },
286
+ "contextWindow": 1000000,
287
+ "maxTokens": 131072,
288
+ "compat": {
289
+ "supportsDeveloperRole": false,
290
+ "supportsReasoningEffort": true,
291
+ "reasoningContentField": "reasoning_content",
292
+ "requiresReasoningContentForToolCalls": true
293
+ },
294
+ "thinking": {
295
+ "mode": "effort",
296
+ "minLevel": "low",
297
+ "maxLevel": "max",
298
+ "defaultLevel": "max",
299
+ "levels": [
300
+ "low",
301
+ "high",
302
+ "max"
303
+ ]
304
+ }
305
+ },
200
306
  "kimi-k2.5": {
201
307
  "id": "kimi-k2.5",
202
308
  "name": "Kimi K2.5",
@@ -46019,9 +46125,9 @@
46019
46125
  "contextWindow": 1000000,
46020
46126
  "maxTokens": 128000,
46021
46127
  "thinking": {
46022
- "mode": "budget",
46023
- "minLevel": "minimal",
46024
- "maxLevel": "xhigh"
46128
+ "mode": "effort",
46129
+ "minLevel": "high",
46130
+ "maxLevel": "high"
46025
46131
  }
46026
46132
  },
46027
46133
  "MiniMax-M3[1m]": {
@@ -46044,9 +46150,9 @@
46044
46150
  "contextWindow": 1000000,
46045
46151
  "maxTokens": 128000,
46046
46152
  "thinking": {
46047
- "mode": "budget",
46048
- "minLevel": "minimal",
46049
- "maxLevel": "xhigh"
46153
+ "mode": "effort",
46154
+ "minLevel": "high",
46155
+ "maxLevel": "high"
46050
46156
  }
46051
46157
  }
46052
46158
  },
@@ -46239,9 +46345,9 @@
46239
46345
  "contextWindow": 1000000,
46240
46346
  "maxTokens": 128000,
46241
46347
  "thinking": {
46242
- "mode": "budget",
46243
- "minLevel": "minimal",
46244
- "maxLevel": "xhigh"
46348
+ "mode": "effort",
46349
+ "minLevel": "high",
46350
+ "maxLevel": "high"
46245
46351
  }
46246
46352
  },
46247
46353
  "MiniMax-M3[1m]": {
@@ -46264,9 +46370,9 @@
46264
46370
  "contextWindow": 1000000,
46265
46371
  "maxTokens": 128000,
46266
46372
  "thinking": {
46267
- "mode": "budget",
46268
- "minLevel": "minimal",
46269
- "maxLevel": "xhigh"
46373
+ "mode": "effort",
46374
+ "minLevel": "high",
46375
+ "maxLevel": "high"
46270
46376
  }
46271
46377
  }
46272
46378
  },
@@ -46538,7 +46644,7 @@
46538
46644
  },
46539
46645
  "thinking": {
46540
46646
  "mode": "effort",
46541
- "minLevel": "minimal",
46647
+ "minLevel": "high",
46542
46648
  "maxLevel": "high"
46543
46649
  }
46544
46650
  }
@@ -46811,7 +46917,7 @@
46811
46917
  },
46812
46918
  "thinking": {
46813
46919
  "mode": "effort",
46814
- "minLevel": "minimal",
46920
+ "minLevel": "high",
46815
46921
  "maxLevel": "high"
46816
46922
  }
46817
46923
  }
@@ -65544,6 +65650,76 @@
65544
65650
  },
65545
65651
  "applyPatchToolType": "freeform"
65546
65652
  },
65653
+ "gpt-6-luna": {
65654
+ "id": "gpt-6-luna",
65655
+ "name": "GPT-6-Luna",
65656
+ "api": "openai-codex-responses",
65657
+ "provider": "openai-codex",
65658
+ "baseUrl": "https://chatgpt.com/backend-api",
65659
+ "reasoning": true,
65660
+ "input": [
65661
+ "text",
65662
+ "image"
65663
+ ],
65664
+ "cost": {
65665
+ "input": 0.1,
65666
+ "output": 0.5,
65667
+ "cacheRead": 0.01,
65668
+ "cacheWrite": 0.125
65669
+ },
65670
+ "contextWindow": 272000,
65671
+ "maxTokens": 128000,
65672
+ "preferWebsockets": true,
65673
+ "thinking": {
65674
+ "mode": "effort",
65675
+ "minLevel": "low",
65676
+ "maxLevel": "max"
65677
+ },
65678
+ "longContextPricing": {
65679
+ "threshold": 272000,
65680
+ "cost": {
65681
+ "input": 0.2,
65682
+ "output": 0.75,
65683
+ "cacheRead": 0.02,
65684
+ "cacheWrite": 0.25
65685
+ }
65686
+ }
65687
+ },
65688
+ "gpt-6-sol": {
65689
+ "id": "gpt-6-sol",
65690
+ "name": "GPT-6-Sol",
65691
+ "api": "openai-codex-responses",
65692
+ "provider": "openai-codex",
65693
+ "baseUrl": "https://chatgpt.com/backend-api",
65694
+ "reasoning": true,
65695
+ "input": [
65696
+ "text",
65697
+ "image"
65698
+ ],
65699
+ "cost": {
65700
+ "input": 2,
65701
+ "output": 10,
65702
+ "cacheRead": 0.2,
65703
+ "cacheWrite": 2.5
65704
+ },
65705
+ "contextWindow": 272000,
65706
+ "maxTokens": 128000,
65707
+ "preferWebsockets": true,
65708
+ "thinking": {
65709
+ "mode": "effort",
65710
+ "minLevel": "low",
65711
+ "maxLevel": "max"
65712
+ },
65713
+ "longContextPricing": {
65714
+ "threshold": 272000,
65715
+ "cost": {
65716
+ "input": 4,
65717
+ "output": 15,
65718
+ "cacheRead": 0.4,
65719
+ "cacheWrite": 5
65720
+ }
65721
+ }
65722
+ },
65547
65723
  "gpt-daybreak-blue-latest": {
65548
65724
  "id": "gpt-daybreak-blue-latest",
65549
65725
  "name": "Daybreak Blue",
@@ -95071,4 +95247,4 @@
95071
95247
  }
95072
95248
  }
95073
95249
  }
95074
- }
95250
+ }
@@ -107,7 +107,7 @@ export interface CpaToolAliasRestoreFailure {
107
107
  */
108
108
  export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
109
109
  export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
110
- export declare const claudeCodeVersion = "2.1.273";
110
+ export declare const claudeCodeVersion = "2.1.280";
111
111
  export declare const claudeCodeEntrypoint = "sdk-cli";
112
112
  export declare const claudeToolPrefix: string;
113
113
  export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -834,7 +834,7 @@ function getCacheControl(
834
834
  }
835
835
 
836
836
  // Stealth mode: Mimic Anthropic Code headers and tool prefixing.
837
- export const claudeCodeVersion = "2.1.273";
837
+ export const claudeCodeVersion = "2.1.280";
838
838
  export const claudeCodeEntrypoint = "sdk-cli";
839
839
  export const claudeToolPrefix: string = "proxy_";
840
840
  export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -73,6 +73,8 @@ export declare function createCursorMessageQueueForTest(onError?: (error: unknow
73
73
  };
74
74
  /** Exported for direct regression coverage of the JSON-safety boundary. */
75
75
  export declare function cursorJsonSafeValueForTest(value: unknown): unknown;
76
+ /** Exported for direct regression coverage of the Cursor serialization boundary. */
77
+ export declare function cursorJsonSafeStringifyForTest(value: unknown): string;
76
78
  export declare function buildNativeToolCallBlock(toolCall: Record<string, unknown>, callId: string, index: number): ToolCallState | null;
77
79
  /** Derive prompt usage from Cursor's whole-conversation checkpoint total. */
78
80
  export declare function finalizeCursorUsage(output: AssistantMessage, usageState: UsageState): void;
@@ -103,6 +105,14 @@ export declare function finalizeCursorUsageForTest(usedTokens: number, outputTok
103
105
  export declare function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined, modelId?: string): string[];
104
106
  /** Exported for regression coverage of the tool usage-cache identity boundary. */
105
107
  export declare function buildCursorUsageToolsKeyForTest(tools: Tool[]): string;
108
+ /** Exported for regression coverage of the generic tool-schema wire boundary. */
109
+ export declare function buildCursorWireToolIdentitiesForTest(tools: Tool[]): Array<{
110
+ name: string;
111
+ description: string;
112
+ inputSchema: JsonValue;
113
+ }>;
114
+ /** Exported for regression coverage of lossless conversation identity hashing. */
115
+ export declare function hashCursorConversationValueForTest(value: unknown): string;
106
116
  /** Exported for tests: decodes Cursor history blobs built from conversation messages. */
107
117
  export declare function buildCursorHistoryForTest(messages: Message[]): {
108
118
  rootPromptMessagesJson: unknown[];
@@ -1288,6 +1288,9 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
1288
1288
  let terminalDrainStarted = false;
1289
1289
  let execQueuePrefix: Promise<void> | undefined;
1290
1290
  let terminalPendingError: unknown;
1291
+ let requestCloseError: (Error & { http2RstCode?: number; nativeErrorCode?: string }) | undefined;
1292
+ // Native errors may be frozen; keep observations separate from their identity.
1293
+ const requestErrorResetCodes = new WeakMap<Error, number | undefined>();
1291
1294
  let terminalBoundarySeen = false;
1292
1295
  // Lookahead can validate turnEnded while an exec handler holds the normal
1293
1296
  // parser. Close new exec admission immediately, but leave the validated
@@ -1595,6 +1598,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
1595
1598
  };
1596
1599
  h2RequestErrorHandler = error => {
1597
1600
  if (terminalBoundarySeen || terminalBoundaryObserved || sawTurnEnded) return;
1601
+ // Enrich only the synthetic reset diagnostic with the first observed
1602
+ // native code. Never replace its message, priority, or retry class.
1603
+ if (requestCloseError && !requestCloseError.nativeErrorCode) {
1604
+ requestCloseError.nativeErrorCode = transportFailureFacts(error)?.nativeErrorCode;
1605
+ }
1606
+ if (!requestErrorResetCodes.has(error)) requestErrorResetCodes.set(error, h2Request?.rstCode);
1598
1607
  terminalize(error, "drainable");
1599
1608
  };
1600
1609
  h2Request.on("error", h2RequestErrorHandler);
@@ -1637,7 +1646,10 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
1637
1646
  return;
1638
1647
  }
1639
1648
  responseEnded = true;
1640
- terminalize(new Error(`Cursor HTTP/2 request ${kind} before turnEnded`), "drainable");
1649
+ requestCloseError = Object.assign(new Error(`Cursor HTTP/2 request ${kind} before turnEnded`), {
1650
+ http2RstCode: h2Request?.rstCode,
1651
+ });
1652
+ terminalize(requestCloseError, "drainable");
1641
1653
  };
1642
1654
  h2RequestCloseHandler = () => handleUnexpectedRequestClose("closed");
1643
1655
  h2RequestAbortedHandler = () => handleUnexpectedRequestClose("aborted");
@@ -2333,6 +2345,15 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
2333
2345
  output.stopReason = callerAbortError || options?.signal?.aborted ? "aborted" : "error";
2334
2346
  output.errorStatus = extractHttpStatusFromError(mappedError);
2335
2347
  output.transportFailure = transportFailureFacts(mappedError);
2348
+ if (mappedError instanceof Error && requestErrorResetCodes.has(mappedError)) {
2349
+ output.transportFailure = transportFailureFacts({
2350
+ ...output.transportFailure,
2351
+ // The native error can precede the request's reset observation.
2352
+ // Fill a missing observation at settlement, after terminal draining;
2353
+ // local teardown may supply it, so this is not remote-cause evidence.
2354
+ http2RstCode: requestErrorResetCodes.get(mappedError) ?? h2Request?.rstCode,
2355
+ });
2356
+ }
2336
2357
  output.errorMessage = formatErrorMessageWithRetryAfter(mappedError);
2337
2358
  finalizeCursorUsage(output, usageState);
2338
2359
  calculateCost(model, output.usage);
@@ -4150,6 +4171,42 @@ function cursorNativeToolName(kindKey: string): string {
4150
4171
  /** Hard node budget for one native-payload conversion; bounds hostile or cyclic graphs. */
4151
4172
  const CURSOR_JSON_SAFE_MAX_NODES = 10_000;
4152
4173
  const CURSOR_JSON_SAFE_MAX_DEPTH = 100;
4174
+ /** Generic boundaries remain lossless within explicit resource limits. */
4175
+ const CURSOR_GENERIC_JSON_SAFE_MAX_NODES = 100_000;
4176
+ const CURSOR_GENERIC_JSON_SAFE_MAX_DEPTH = 1_000;
4177
+
4178
+ interface CursorJsonSafeOptions {
4179
+ stripTypeName: boolean;
4180
+ maxNodes?: number;
4181
+ maxDepth?: number;
4182
+ throwOnLimit?: boolean;
4183
+ }
4184
+
4185
+ const CURSOR_NATIVE_JSON_SAFE_OPTIONS: CursorJsonSafeOptions = {
4186
+ stripTypeName: true,
4187
+ maxNodes: CURSOR_JSON_SAFE_MAX_NODES,
4188
+ maxDepth: CURSOR_JSON_SAFE_MAX_DEPTH,
4189
+ };
4190
+
4191
+ /** Generic JSON boundaries must preserve every schema/context entry losslessly. */
4192
+ const CURSOR_GENERIC_JSON_SAFE_OPTIONS: CursorJsonSafeOptions = {
4193
+ stripTypeName: false,
4194
+ maxNodes: CURSOR_GENERIC_JSON_SAFE_MAX_NODES,
4195
+ maxDepth: CURSOR_GENERIC_JSON_SAFE_MAX_DEPTH,
4196
+ throwOnLimit: true,
4197
+ };
4198
+
4199
+ class CursorJsonSafeLimitError extends Error {
4200
+ constructor(message: string) {
4201
+ super(message);
4202
+ this.name = "CursorJsonSafeLimitError";
4203
+ }
4204
+ }
4205
+
4206
+ function cursorJsonSafeLimit(options: CursorJsonSafeOptions, message: string): null {
4207
+ if (options.throwOnLimit) throw new CursorJsonSafeLimitError(message);
4208
+ return null;
4209
+ }
4153
4210
 
4154
4211
  /**
4155
4212
  * Total conversion of a Cursor protobuf payload into plain JSON-safe data.
@@ -4162,17 +4219,39 @@ const CURSOR_JSON_SAFE_MAX_DEPTH = 100;
4162
4219
  * all of which require `JSON.stringify`-safe values. Attaching the raw payload
4163
4220
  * is exactly the local-snapshot producer defect class behind issue #4578.
4164
4221
  *
4165
- * Rules: `$typeName` is stripped, safe-range bigints become numbers (decimal
4166
- * strings beyond `Number.MAX_SAFE_INTEGER`), byte arrays become base64
4167
- * strings, dates become ISO strings, functions/symbols are dropped, cycles
4168
- * and over-depth values collapse to null, and containers stop accepting
4169
- * entries once the shared node budget is exhausted.
4222
+ * Native payload rules: `$typeName` is stripped, safe-range bigints become
4223
+ * numbers (decimal strings beyond `Number.MAX_SAFE_INTEGER`), byte arrays
4224
+ * become base64 strings, dates become ISO strings, functions/symbols are
4225
+ * dropped, cycles and over-depth values collapse to null, and containers stop
4226
+ * accepting entries once the shared node budget is exhausted. Generic payload
4227
+ * boundaries use the same conversion with their own explicit limits and reject
4228
+ * limit exhaustion instead of returning a truncated value.
4170
4229
  */
4171
- function cursorJsonSafeValue(value: unknown, path?: Set<object>, budget?: { remaining: number }, depth = 0): unknown {
4230
+ function cursorJsonSafeValue(value: unknown): unknown {
4231
+ return cursorJsonSafeValueWithOptions(value, CURSOR_NATIVE_JSON_SAFE_OPTIONS);
4232
+ }
4233
+
4234
+ function cursorJsonSafeValueWithOptions(
4235
+ value: unknown,
4236
+ options: CursorJsonSafeOptions,
4237
+ path?: Set<object>,
4238
+ budget?: { remaining: number },
4239
+ depth = 0,
4240
+ ): unknown {
4172
4241
  const seen = path ?? new Set<object>();
4173
- const nodes = budget ?? { remaining: CURSOR_JSON_SAFE_MAX_NODES };
4174
- if (nodes.remaining-- <= 0) return null;
4175
- if (depth >= CURSOR_JSON_SAFE_MAX_DEPTH) return null;
4242
+ const nodes = options.maxNodes === undefined ? undefined : (budget ?? { remaining: options.maxNodes });
4243
+ if (nodes && nodes.remaining-- <= 0) {
4244
+ return cursorJsonSafeLimit(
4245
+ options,
4246
+ `Cursor JSON-safe conversion exceeded the maximum node count of ${options.maxNodes?.toLocaleString("en-US")}.`,
4247
+ );
4248
+ }
4249
+ if (options.maxDepth !== undefined && depth >= options.maxDepth) {
4250
+ return cursorJsonSafeLimit(
4251
+ options,
4252
+ `Cursor JSON-safe conversion exceeded the maximum depth of ${options.maxDepth.toLocaleString("en-US")}.`,
4253
+ );
4254
+ }
4176
4255
  if (typeof value === "bigint") {
4177
4256
  return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER)
4178
4257
  ? Number(value)
@@ -4190,19 +4269,37 @@ function cursorJsonSafeValue(value: unknown, path?: Set<object>, budget?: { rema
4190
4269
  if (Array.isArray(value)) {
4191
4270
  const array: unknown[] = [];
4192
4271
  for (const entry of value) {
4193
- if (nodes.remaining <= 0) break;
4194
- array.push(cursorJsonSafeValue(entry, seen, nodes, depth + 1));
4272
+ if (nodes && nodes.remaining <= 0) {
4273
+ cursorJsonSafeLimit(
4274
+ options,
4275
+ `Cursor JSON-safe conversion exceeded the maximum node count of ${options.maxNodes?.toLocaleString("en-US")}.`,
4276
+ );
4277
+ break;
4278
+ }
4279
+ array.push(cursorJsonSafeValueWithOptions(entry, options, seen, nodes, depth + 1));
4195
4280
  }
4196
4281
  return array;
4197
4282
  }
4198
4283
  const record: Record<string, unknown> = {};
4199
4284
  for (const [key, entry] of Object.entries(value)) {
4200
- if (key === "$typeName") continue;
4201
- if (nodes.remaining <= 0) break;
4202
- record[key] = cursorJsonSafeValue(entry, seen, nodes, depth + 1);
4285
+ if (options.stripTypeName && key === "$typeName") continue;
4286
+ if (nodes && nodes.remaining <= 0) {
4287
+ cursorJsonSafeLimit(
4288
+ options,
4289
+ `Cursor JSON-safe conversion exceeded the maximum node count of ${options.maxNodes?.toLocaleString("en-US")}.`,
4290
+ );
4291
+ break;
4292
+ }
4293
+ Object.defineProperty(record, key, {
4294
+ value: cursorJsonSafeValueWithOptions(entry, options, seen, nodes, depth + 1),
4295
+ enumerable: true,
4296
+ configurable: true,
4297
+ writable: true,
4298
+ });
4203
4299
  }
4204
4300
  return record;
4205
- } catch {
4301
+ } catch (error) {
4302
+ if (error instanceof CursorJsonSafeLimitError) throw error;
4206
4303
  return null;
4207
4304
  } finally {
4208
4305
  seen.delete(value);
@@ -4214,6 +4311,16 @@ export function cursorJsonSafeValueForTest(value: unknown): unknown {
4214
4311
  return cursorJsonSafeValue(value);
4215
4312
  }
4216
4313
 
4314
+ /** Serialize a generic Cursor JSON boundary without dropping keys or truncating containers. */
4315
+ function cursorJsonSafeStringify(value: unknown): string {
4316
+ return JSON.stringify(cursorJsonSafeValueWithOptions(value, CURSOR_GENERIC_JSON_SAFE_OPTIONS)) ?? "";
4317
+ }
4318
+
4319
+ /** Exported for direct regression coverage of the Cursor serialization boundary. */
4320
+ export function cursorJsonSafeStringifyForTest(value: unknown): string {
4321
+ return cursorJsonSafeStringify(value);
4322
+ }
4323
+
4217
4324
  function selectMcpToolCall(toolCall: any): any {
4218
4325
  return toolCall?.tool?.case === "mcpToolCall" ? toolCall.tool.value : toolCall?.mcpToolCall;
4219
4326
  }
@@ -4589,7 +4696,10 @@ function buildCursorWireToolIdentities(tools: Tool[] | undefined): CursorWireToo
4589
4696
  return tools
4590
4697
  .filter(tool => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name))
4591
4698
  .map(tool => {
4592
- const jsonSchema = flattenToolRootCombinators(toolWireSchema(tool));
4699
+ const jsonSchema = cursorJsonSafeValueWithOptions(
4700
+ flattenToolRootCombinators(toolWireSchema(tool)),
4701
+ CURSOR_GENERIC_JSON_SAFE_OPTIONS,
4702
+ );
4593
4703
  return {
4594
4704
  name: tool.name,
4595
4705
  description: tool.description || "",
@@ -4742,12 +4852,12 @@ export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | u
4742
4852
  const systemPrompts = normalizeSystemPrompts(systemPrompt);
4743
4853
  const jsons =
4744
4854
  systemPrompts.length === 0
4745
- ? [JSON.stringify({ role: "system", content: "You are a helpful assistant." })]
4746
- : systemPrompts.map(content => JSON.stringify({ role: "system", content }));
4855
+ ? [cursorJsonSafeStringify({ role: "system", content: "You are a helpful assistant." })]
4856
+ : systemPrompts.map(content => cursorJsonSafeStringify({ role: "system", content }));
4747
4857
  // Composer-harness models need anchor/edit discipline pinned ahead of any
4748
4858
  // host/default prompt (see composer-discipline.ts for the observed failure modes).
4749
4859
  if (modelId !== undefined && isComposerHarnessModel(modelId)) {
4750
- jsons.unshift(JSON.stringify({ role: "system", content: CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT }));
4860
+ jsons.unshift(cursorJsonSafeStringify({ role: "system", content: CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT }));
4751
4861
  }
4752
4862
  return jsons;
4753
4863
  }
@@ -4761,7 +4871,7 @@ function buildRootPromptMessagesJson(
4761
4871
  const lastUserIdx = findLastUserMessageIndex(messages);
4762
4872
 
4763
4873
  const pushJson = (obj: unknown) => {
4764
- const bytes = new TextEncoder().encode(JSON.stringify(obj));
4874
+ const bytes = new TextEncoder().encode(cursorJsonSafeStringify(obj));
4765
4875
  entries.push(storeCursorBlob(blobStore, bytes));
4766
4876
  };
4767
4877
 
@@ -4896,6 +5006,18 @@ export function buildCursorUsageToolsKeyForTest(tools: Tool[]): string {
4896
5006
  return buildCursorUsageToolsKey(tools);
4897
5007
  }
4898
5008
 
5009
+ /** Exported for regression coverage of the generic tool-schema wire boundary. */
5010
+ export function buildCursorWireToolIdentitiesForTest(
5011
+ tools: Tool[],
5012
+ ): Array<{ name: string; description: string; inputSchema: JsonValue }> {
5013
+ return buildCursorWireToolIdentities(tools);
5014
+ }
5015
+
5016
+ /** Exported for regression coverage of lossless conversation identity hashing. */
5017
+ export function hashCursorConversationValueForTest(value: unknown): string {
5018
+ return hashCursorConversationValue(value);
5019
+ }
5020
+
4899
5021
  /** Exported for tests: decodes Cursor history blobs built from conversation messages. */
4900
5022
  export function buildCursorHistoryForTest(messages: Message[]): {
4901
5023
  rootPromptMessagesJson: unknown[];
@@ -4977,9 +5099,7 @@ function hashCursorConversationMessage(message: { role: string; content: unknown
4977
5099
  }
4978
5100
 
4979
5101
  function hashCursorConversationValue(value: unknown): string {
4980
- return createHash("sha256")
4981
- .update(JSON.stringify(value) ?? "")
4982
- .digest("hex");
5102
+ return createHash("sha256").update(cursorJsonSafeStringify(value)).digest("hex");
4983
5103
  }
4984
5104
 
4985
5105
  function canReuseCursorConversationContext(
@@ -1,5 +1,5 @@
1
1
  import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
2
- import { type AssistantMessage, type Context, type Model, type ServiceTier, type StreamFunction, type StreamOptions, type ToolChoice } from "../types";
2
+ import { type AssistantMessage, type Context, type Model, type RepetitionGuardOptions, type ServiceTier, type StreamFunction, type StreamOptions, type ToolChoice } from "../types";
3
3
  import { type ResolvedOpenAICompat } from "./openai-completions-compat";
4
4
  /** Test seam: the provider base URL as resolved from trusted env. */
5
5
  export declare function resolveOpenAICompletionsBaseUrlForTest(baseUrl: string | undefined, authCredentialType: "api_key" | "oauth" | undefined): string;
@@ -23,6 +23,14 @@ export interface OpenAICompletionsOptions extends StreamOptions {
23
23
  /** Force-disable reasoning where supported, or request the lowest effort on generic effort endpoints. */
24
24
  disableReasoning?: boolean;
25
25
  serviceTier?: ServiceTier;
26
+ /**
27
+ * Runaway-repetition guard thresholds, per stream channel. A number sets the
28
+ * consecutive-repeat threshold; `false` disables the channel's guard.
29
+ * Defaults: thinking = DEFAULT_REPETITION_THRESHOLD, text = false — visible
30
+ * output is a deliverable and intentional repetition there (logs, fixtures,
31
+ * tables, generated code) must survive byte for byte (#5627).
32
+ */
33
+ repetitionGuard?: RepetitionGuardOptions;
26
34
  }
27
35
  export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
28
36
  export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined): AssistantMessage["usage"];