@gajae-code/ai 0.17.1 → 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 (47) hide show
  1. package/CHANGELOG.md +117 -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/model-thinking.d.ts +10 -6
  5. package/dist/types/provider-models/openai-compat.d.ts +2 -2
  6. package/dist/types/providers/anthropic.d.ts +1 -1
  7. package/dist/types/providers/cursor.d.ts +10 -0
  8. package/dist/types/providers/openai-completions.d.ts +9 -1
  9. package/dist/types/types.d.ts +16 -0
  10. package/dist/types/utils/discovery/openai-compatible.d.ts +10 -0
  11. package/dist/types/utils/fallback-transport.d.ts +4 -0
  12. package/dist/types/utils/h2-fetch.d.ts +8 -2
  13. package/dist/types/utils/stream-repetition-guard.d.ts +107 -0
  14. package/dist/types/utils/tool-call-healing.d.ts +4 -0
  15. package/dist/types/utils/tool-fence-strip.d.ts +27 -0
  16. package/package.json +3 -3
  17. package/src/auth-gateway/server.ts +48 -9
  18. package/src/auth-storage.ts +185 -48
  19. package/src/model-manager.ts +11 -8
  20. package/src/model-pricing.ts +22 -0
  21. package/src/model-thinking.d.ts +10 -6
  22. package/src/model-thinking.ts +93 -11
  23. package/src/models.json +241 -15
  24. package/src/provider-models/openai-compat.ts +27 -19
  25. package/src/providers/anthropic.d.ts +1 -1
  26. package/src/providers/anthropic.ts +10 -2
  27. package/src/providers/cursor.d.ts +10 -0
  28. package/src/providers/cursor.ts +176 -31
  29. package/src/providers/openai-completions.d.ts +9 -1
  30. package/src/providers/openai-completions.ts +379 -128
  31. package/src/providers/openai-opencodex-responses.ts +15 -5
  32. package/src/stream.ts +24 -1
  33. package/src/types.d.ts +16 -0
  34. package/src/types.ts +17 -0
  35. package/src/utils/discovery/openai-compatible.ts +16 -2
  36. package/src/utils/fallback-transport.d.ts +4 -0
  37. package/src/utils/fallback-transport.ts +11 -0
  38. package/src/utils/h2-fetch.ts +70 -7
  39. package/src/utils/http-inspector.ts +4 -2
  40. package/src/utils/idle-iterator.ts +109 -96
  41. package/src/utils/json-parse.ts +12 -4
  42. package/src/utils/stream-repetition-guard.d.ts +107 -0
  43. package/src/utils/stream-repetition-guard.ts +290 -0
  44. package/src/utils/tool-call-healing.d.ts +4 -0
  45. package/src/utils/tool-call-healing.ts +4 -0
  46. package/src/utils/tool-fence-strip.d.ts +27 -0
  47. package/src/utils/tool-fence-strip.ts +64 -0
@@ -676,6 +676,12 @@ const CURSOR_WRITE_DRAIN_TIMEOUT_MS = 5_000;
676
676
  const CURSOR_MAX_PENDING_SHELL_WRITE_BYTES = 1024 * 1024;
677
677
  const pendingCursorWrites = new WeakMap<object, Set<Promise<void>>>();
678
678
  const cursorWriteErrors = new WeakMap<object, unknown>();
679
+ interface CursorWriteListeners {
680
+ finishes: Set<(error?: unknown) => void>;
681
+ onError: (error: unknown) => void;
682
+ onClose: () => void;
683
+ }
684
+ const cursorWriteListeners = new WeakMap<object, CursorWriteListeners>();
679
685
 
680
686
  function closeStalledCursorRequest(request: http2.ClientHttp2Stream): void {
681
687
  // A request whose peer stopped reading may never invoke a write callback. Close
@@ -833,25 +839,44 @@ function writeCursorFrame(request: http2.ClientHttp2Stream, frame: Uint8Array):
833
839
  // late transport error cannot surface as an unhandled rejection in the gap.
834
840
  completion.promise.catch(() => {});
835
841
  pending.add(completion.promise);
842
+ let listeners = cursorWriteListeners.get(request);
843
+ if (!listeners) {
844
+ const finishes = new Set<(error?: unknown) => void>();
845
+ const onError = (error: unknown) => {
846
+ for (const finish of [...finishes]) finish(error);
847
+ };
848
+ listeners = {
849
+ finishes,
850
+ onError,
851
+ onClose: () => onError(new Error("Cursor request closed before write completed")),
852
+ };
853
+ cursorWriteListeners.set(request, listeners);
854
+ }
855
+ const shared = listeners;
836
856
  const finish = (error?: unknown) => {
837
857
  if (completed) return;
838
858
  completed = true;
839
859
  pending.delete(completion.promise);
840
860
  if (error != null && !cursorWriteErrors.has(request)) cursorWriteErrors.set(request, error);
841
- if (typeof request.removeListener === "function") {
842
- request.removeListener("close", onClose);
843
- request.removeListener("error", finish);
861
+ shared.finishes.delete(finish);
862
+ if (shared.finishes.size === 0) {
863
+ if (typeof request.removeListener === "function") {
864
+ request.removeListener("close", shared.onClose);
865
+ request.removeListener("error", shared.onError);
866
+ }
867
+ cursorWriteListeners.delete(request);
868
+ // Keep the pending set and first error until the final drain observes them.
844
869
  }
845
870
  if (error == null) completion.resolve();
846
871
  else completion.reject(error);
847
872
  };
848
- const onClose = () => finish(new Error("Cursor request closed before write completed"));
873
+ shared.finishes.add(finish);
849
874
  try {
850
875
  // The real HTTP/2 stream always exposes EventEmitter methods. Keep the
851
876
  // test seam tolerant of a minimal writer stub as well.
852
- if (typeof request.once === "function") {
853
- request.once("close", onClose);
854
- request.once("error", finish);
877
+ if (shared.finishes.size === 1 && typeof request.once === "function") {
878
+ request.once("close", shared.onClose);
879
+ request.once("error", shared.onError);
855
880
  }
856
881
  return request.write(frame, finish) !== false;
857
882
  } catch (error) {
@@ -1263,6 +1288,9 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
1263
1288
  let terminalDrainStarted = false;
1264
1289
  let execQueuePrefix: Promise<void> | undefined;
1265
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>();
1266
1294
  let terminalBoundarySeen = false;
1267
1295
  // Lookahead can validate turnEnded while an exec handler holds the normal
1268
1296
  // parser. Close new exec admission immediately, but leave the validated
@@ -1570,6 +1598,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
1570
1598
  };
1571
1599
  h2RequestErrorHandler = error => {
1572
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);
1573
1607
  terminalize(error, "drainable");
1574
1608
  };
1575
1609
  h2Request.on("error", h2RequestErrorHandler);
@@ -1612,7 +1646,10 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
1612
1646
  return;
1613
1647
  }
1614
1648
  responseEnded = true;
1615
- 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");
1616
1653
  };
1617
1654
  h2RequestCloseHandler = () => handleUnexpectedRequestClose("closed");
1618
1655
  h2RequestAbortedHandler = () => handleUnexpectedRequestClose("aborted");
@@ -2308,6 +2345,15 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
2308
2345
  output.stopReason = callerAbortError || options?.signal?.aborted ? "aborted" : "error";
2309
2346
  output.errorStatus = extractHttpStatusFromError(mappedError);
2310
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
+ }
2311
2357
  output.errorMessage = formatErrorMessageWithRetryAfter(mappedError);
2312
2358
  finalizeCursorUsage(output, usageState);
2313
2359
  calculateCost(model, output.usage);
@@ -4125,6 +4171,42 @@ function cursorNativeToolName(kindKey: string): string {
4125
4171
  /** Hard node budget for one native-payload conversion; bounds hostile or cyclic graphs. */
4126
4172
  const CURSOR_JSON_SAFE_MAX_NODES = 10_000;
4127
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
+ }
4128
4210
 
4129
4211
  /**
4130
4212
  * Total conversion of a Cursor protobuf payload into plain JSON-safe data.
@@ -4137,17 +4219,39 @@ const CURSOR_JSON_SAFE_MAX_DEPTH = 100;
4137
4219
  * all of which require `JSON.stringify`-safe values. Attaching the raw payload
4138
4220
  * is exactly the local-snapshot producer defect class behind issue #4578.
4139
4221
  *
4140
- * Rules: `$typeName` is stripped, safe-range bigints become numbers (decimal
4141
- * strings beyond `Number.MAX_SAFE_INTEGER`), byte arrays become base64
4142
- * strings, dates become ISO strings, functions/symbols are dropped, cycles
4143
- * and over-depth values collapse to null, and containers stop accepting
4144
- * 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.
4145
4229
  */
4146
- 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 {
4147
4241
  const seen = path ?? new Set<object>();
4148
- const nodes = budget ?? { remaining: CURSOR_JSON_SAFE_MAX_NODES };
4149
- if (nodes.remaining-- <= 0) return null;
4150
- 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
+ }
4151
4255
  if (typeof value === "bigint") {
4152
4256
  return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER)
4153
4257
  ? Number(value)
@@ -4165,19 +4269,37 @@ function cursorJsonSafeValue(value: unknown, path?: Set<object>, budget?: { rema
4165
4269
  if (Array.isArray(value)) {
4166
4270
  const array: unknown[] = [];
4167
4271
  for (const entry of value) {
4168
- if (nodes.remaining <= 0) break;
4169
- 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));
4170
4280
  }
4171
4281
  return array;
4172
4282
  }
4173
4283
  const record: Record<string, unknown> = {};
4174
4284
  for (const [key, entry] of Object.entries(value)) {
4175
- if (key === "$typeName") continue;
4176
- if (nodes.remaining <= 0) break;
4177
- 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
+ });
4178
4299
  }
4179
4300
  return record;
4180
- } catch {
4301
+ } catch (error) {
4302
+ if (error instanceof CursorJsonSafeLimitError) throw error;
4181
4303
  return null;
4182
4304
  } finally {
4183
4305
  seen.delete(value);
@@ -4189,6 +4311,16 @@ export function cursorJsonSafeValueForTest(value: unknown): unknown {
4189
4311
  return cursorJsonSafeValue(value);
4190
4312
  }
4191
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
+
4192
4324
  function selectMcpToolCall(toolCall: any): any {
4193
4325
  return toolCall?.tool?.case === "mcpToolCall" ? toolCall.tool.value : toolCall?.mcpToolCall;
4194
4326
  }
@@ -4564,7 +4696,10 @@ function buildCursorWireToolIdentities(tools: Tool[] | undefined): CursorWireToo
4564
4696
  return tools
4565
4697
  .filter(tool => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name))
4566
4698
  .map(tool => {
4567
- const jsonSchema = flattenToolRootCombinators(toolWireSchema(tool));
4699
+ const jsonSchema = cursorJsonSafeValueWithOptions(
4700
+ flattenToolRootCombinators(toolWireSchema(tool)),
4701
+ CURSOR_GENERIC_JSON_SAFE_OPTIONS,
4702
+ );
4568
4703
  return {
4569
4704
  name: tool.name,
4570
4705
  description: tool.description || "",
@@ -4717,12 +4852,12 @@ export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | u
4717
4852
  const systemPrompts = normalizeSystemPrompts(systemPrompt);
4718
4853
  const jsons =
4719
4854
  systemPrompts.length === 0
4720
- ? [JSON.stringify({ role: "system", content: "You are a helpful assistant." })]
4721
- : 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 }));
4722
4857
  // Composer-harness models need anchor/edit discipline pinned ahead of any
4723
4858
  // host/default prompt (see composer-discipline.ts for the observed failure modes).
4724
4859
  if (modelId !== undefined && isComposerHarnessModel(modelId)) {
4725
- jsons.unshift(JSON.stringify({ role: "system", content: CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT }));
4860
+ jsons.unshift(cursorJsonSafeStringify({ role: "system", content: CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT }));
4726
4861
  }
4727
4862
  return jsons;
4728
4863
  }
@@ -4736,7 +4871,7 @@ function buildRootPromptMessagesJson(
4736
4871
  const lastUserIdx = findLastUserMessageIndex(messages);
4737
4872
 
4738
4873
  const pushJson = (obj: unknown) => {
4739
- const bytes = new TextEncoder().encode(JSON.stringify(obj));
4874
+ const bytes = new TextEncoder().encode(cursorJsonSafeStringify(obj));
4740
4875
  entries.push(storeCursorBlob(blobStore, bytes));
4741
4876
  };
4742
4877
 
@@ -4871,6 +5006,18 @@ export function buildCursorUsageToolsKeyForTest(tools: Tool[]): string {
4871
5006
  return buildCursorUsageToolsKey(tools);
4872
5007
  }
4873
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
+
4874
5021
  /** Exported for tests: decodes Cursor history blobs built from conversation messages. */
4875
5022
  export function buildCursorHistoryForTest(messages: Message[]): {
4876
5023
  rootPromptMessagesJson: unknown[];
@@ -4952,9 +5099,7 @@ function hashCursorConversationMessage(message: { role: string; content: unknown
4952
5099
  }
4953
5100
 
4954
5101
  function hashCursorConversationValue(value: unknown): string {
4955
- return createHash("sha256")
4956
- .update(JSON.stringify(value) ?? "")
4957
- .digest("hex");
5102
+ return createHash("sha256").update(cursorJsonSafeStringify(value)).digest("hex");
4958
5103
  }
4959
5104
 
4960
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"];