@gajae-code/ai 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/types/auth-storage.d.ts +2 -2
  3. package/dist/types/model-cache.d.ts +2 -0
  4. package/dist/types/provider-models/special.d.ts +3 -1
  5. package/dist/types/providers/anthropic.d.ts +1 -0
  6. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  7. package/dist/types/providers/cursor/gen/agent_pb.d.ts +3854 -107
  8. package/dist/types/providers/cursor-pi-args.d.ts +119 -0
  9. package/dist/types/providers/cursor.d.ts +8 -1
  10. package/dist/types/providers/openai-codex-responses.d.ts +2 -0
  11. package/dist/types/providers/openai-responses-shared.d.ts +1 -1
  12. package/dist/types/types.d.ts +41 -1
  13. package/dist/types/utils/block-symbols.d.ts +6 -0
  14. package/dist/types/utils/discovery/openai-compatible.d.ts +2 -0
  15. package/dist/types/utils/idle-iterator.d.ts +5 -2
  16. package/dist/types/utils/oauth/kimi.d.ts +3 -9
  17. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  18. package/dist/types/utils/oauth/types.d.ts +1 -1
  19. package/package.json +4 -4
  20. package/src/auth-broker/remote-store.ts +13 -2
  21. package/src/auth-storage.ts +10 -11
  22. package/src/model-cache.ts +78 -0
  23. package/src/model-manager.ts +194 -25
  24. package/src/provider-models/special.ts +67 -4
  25. package/src/providers/anthropic.ts +115 -40
  26. package/src/providers/aws-credential-config.ts +2 -3
  27. package/src/providers/aws-credentials.ts +2 -3
  28. package/src/providers/azure-openai-responses.ts +18 -2
  29. package/src/providers/cursor/exec-modern.ts +497 -0
  30. package/src/providers/cursor/gen/agent_pb.ts +4687 -181
  31. package/src/providers/cursor/proto/agent.proto +1007 -0
  32. package/src/providers/cursor-pi-args.ts +187 -0
  33. package/src/providers/cursor.ts +382 -47
  34. package/src/providers/google-auth.ts +2 -3
  35. package/src/providers/openai-codex-responses.ts +358 -73
  36. package/src/providers/openai-completions.ts +2 -2
  37. package/src/providers/openai-responses-shared.ts +55 -6
  38. package/src/providers/openai-responses.ts +27 -4
  39. package/src/stream.ts +8 -3
  40. package/src/types.ts +55 -0
  41. package/src/utils/block-symbols.ts +11 -0
  42. package/src/utils/discovery/openai-compatible.ts +21 -6
  43. package/src/utils/idle-iterator.ts +22 -4
  44. package/src/utils/oauth/index.ts +6 -0
  45. package/src/utils/oauth/kimi.ts +14 -8
  46. package/src/utils/oauth/kiro.ts +2 -2
  47. package/src/utils/oauth/openrouter.ts +16 -0
  48. package/src/utils/oauth/types.ts +1 -0
@@ -81,6 +81,7 @@ import {
81
81
  parseStreamingJson,
82
82
  } from "../utils/json-parse";
83
83
  import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
84
+ import { GLM_ZCODE_ANTHROPIC_BASE_URL } from "../utils/oauth/glm-zcode";
84
85
  import { notifyProviderResponse } from "../utils/provider-response";
85
86
  import { isCopilotTransientModelError } from "../utils/retry";
86
87
  import { getRetryAfterMsFromHeaders } from "../utils/retry-after";
@@ -1144,6 +1145,29 @@ type FoundryTlsOptions = {
1144
1145
  key?: string;
1145
1146
  };
1146
1147
 
1148
+ export function resolveGlmZcodeAnthropicBaseUrl(): string {
1149
+ const configured = $credentialEnv("ZCODE_PLAN_ANTHROPIC_BASE_URL")?.trim();
1150
+ if (!configured || /[\u0000-\u001f\u007f-\u009f]/u.test(configured)) {
1151
+ return GLM_ZCODE_ANTHROPIC_BASE_URL;
1152
+ }
1153
+ try {
1154
+ const parsed = new URL(configured);
1155
+ if (
1156
+ parsed.protocol !== "https:" ||
1157
+ parsed.hostname.length === 0 ||
1158
+ parsed.username.length > 0 ||
1159
+ parsed.password.length > 0 ||
1160
+ parsed.search.length > 0 ||
1161
+ parsed.hash.length > 0
1162
+ ) {
1163
+ return GLM_ZCODE_ANTHROPIC_BASE_URL;
1164
+ }
1165
+ return normalizeAnthropicBaseUrl(parsed.toString()) ?? GLM_ZCODE_ANTHROPIC_BASE_URL;
1166
+ } catch {
1167
+ return GLM_ZCODE_ANTHROPIC_BASE_URL;
1168
+ }
1169
+ }
1170
+
1147
1171
  function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: string): string | undefined {
1148
1172
  if (model.provider === "github-copilot") {
1149
1173
  return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
@@ -1152,9 +1176,7 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
1152
1176
  // calls api.z.ai directly (no zcode.z.ai gateway, no captcha). Pin the base so dynamic
1153
1177
  // discovery / stale bundled catalogs / model cache can't redirect it elsewhere.
1154
1178
  if (model.provider === "glm-zcode") {
1155
- return (
1156
- normalizeAnthropicBaseUrl($credentialEnv("ZCODE_PLAN_ANTHROPIC_BASE_URL")) ?? "https://api.z.ai/api/anthropic"
1157
- );
1179
+ return resolveGlmZcodeAnthropicBaseUrl();
1158
1180
  }
1159
1181
  if (model.provider === "anthropic" && isFoundryEnabled()) {
1160
1182
  const foundryBaseUrl = normalizeAnthropicBaseUrl($credentialEnv("FOUNDRY_BASE_URL"));
@@ -2023,7 +2045,21 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2023
2045
  const blocks = output.content as Block[];
2024
2046
  const blocksByAnthropicIndex = new Map<number, Block>();
2025
2047
  const truncatedToolCalls = new Set<ToolCall>();
2026
- let sawTerminalStopReason = false;
2048
+ // Bounded diagnostic for degraded primitive increments: at most one
2049
+ // warning per delta type per stream invocation, naming only the
2050
+ // envelope shape (delta type and received typeof) — never the payload.
2051
+ const degradedIncrementDiagnostics = new Set<string>();
2052
+ const noteDegradedIncrement = (deltaType: string, received: unknown): void => {
2053
+ if (degradedIncrementDiagnostics.has(deltaType)) return;
2054
+ degradedIncrementDiagnostics.add(deltaType);
2055
+ logger.warn("anthropic: degraded non-string stream increment to empty string", {
2056
+ model: model.id,
2057
+ provider: model.provider,
2058
+ deltaType,
2059
+ receivedType: received === null ? "null" : typeof received,
2060
+ });
2061
+ };
2062
+
2027
2063
  // Derive from the ACTUAL request shape, not the option default: the request
2028
2064
  // only sends `display: "summarized"` on specific paths (adaptive display is
2029
2065
  // omitted for models where supportsAdaptiveThinkingDisplay is false). Defaulting
@@ -2037,25 +2073,18 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2037
2073
  return { block, contentIndex: blocks.indexOf(block) };
2038
2074
  };
2039
2075
  const trackBlockByAnthropicIndex = (anthropicIndex: number, block: Block) => {
2040
- // A duplicate start for an active index is a provider-envelope violation;
2041
- // finalize the orphaned block so no internal stream fields leak into output.
2042
2076
  const orphaned = blocksByAnthropicIndex.get(anthropicIndex);
2043
2077
  if (orphaned) {
2044
2078
  if (orphaned.type === "toolCall") {
2045
- if (!isCompleteJson(orphaned.partialJson)) {
2046
- orphaned.incompleteArguments = true;
2047
- orphaned.incompleteArgumentsReason = "truncated";
2048
- truncatedToolCalls.add(orphaned);
2049
- }
2050
- if (orphaned.partialJson.trim()) {
2051
- orphaned.arguments = parseStreamingJson(orphaned.partialJson);
2052
- if (findUnnecessaryUnicodeEscape(orphaned.partialJson)) {
2053
- orphaned.escapedNonAsciiArguments = true;
2054
- }
2055
- }
2079
+ orphaned.incompleteArguments = true;
2080
+ orphaned.incompleteArgumentsReason = "ambiguous";
2081
+ truncatedToolCalls.add(orphaned);
2082
+ }
2083
+ if (block.type === "toolCall") {
2084
+ block.incompleteArguments = true;
2085
+ block.incompleteArgumentsReason = "ambiguous";
2056
2086
  }
2057
- delete (orphaned as { index?: number }).index;
2058
- delete (orphaned as { partialJson?: string }).partialJson;
2087
+ throw new Error("Anthropic stream reused an active content block index");
2059
2088
  }
2060
2089
  blocksByAnthropicIndex.set(anthropicIndex, block);
2061
2090
  };
@@ -2070,7 +2099,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2070
2099
  output.stopReason = "stop";
2071
2100
  firstTokenTime = undefined;
2072
2101
  truncatedToolCalls.clear();
2073
- sawTerminalStopReason = false;
2074
2102
  };
2075
2103
  const idleTimeoutMs =
2076
2104
  options?.streamIdleTimeoutMs ??
@@ -2105,7 +2133,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2105
2133
  // Retries reset output.content; drop stale block correlations from the aborted attempt.
2106
2134
  blocksByAnthropicIndex.clear();
2107
2135
  truncatedToolCalls.clear();
2108
- sawTerminalStopReason = false;
2109
2136
  activeAbortTracker = createAbortSourceTracker(options?.signal);
2110
2137
  let firstEventTimeoutAbortError: FirstEventTimeoutError | undefined;
2111
2138
  const idleTimeoutAbortError = new Error("Anthropic stream stalled while waiting for the next event");
@@ -2253,13 +2280,21 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2253
2280
  trackBlockByAnthropicIndex(event.index, block);
2254
2281
  } else if (event.content_block.type === "tool_use") {
2255
2282
  streamedReplayUnsafeContent = true;
2283
+ const initialArguments: unknown = event.content_block.input;
2284
+ if (
2285
+ initialArguments === null ||
2286
+ typeof initialArguments !== "object" ||
2287
+ Array.isArray(initialArguments)
2288
+ ) {
2289
+ throw new Error("Anthropic tool_use started with non-object arguments");
2290
+ }
2256
2291
  const block: Block = {
2257
2292
  type: "toolCall",
2258
2293
  id: event.content_block.id,
2259
2294
  name: isOAuthToken
2260
2295
  ? stripClaudeToolPrefix(event.content_block.name)
2261
2296
  : event.content_block.name,
2262
- arguments: (event.content_block.input as Record<string, unknown>) ?? {},
2297
+ arguments: initialArguments as Record<string, unknown>,
2263
2298
  partialJson: "",
2264
2299
  index: event.index,
2265
2300
  };
@@ -2275,32 +2310,42 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2275
2310
  if (event.delta.type === "text_delta") {
2276
2311
  const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
2277
2312
  if (block && block.type === "text") {
2278
- block.text += event.delta.text;
2313
+ const rawTextDelta: unknown = event.delta.text;
2314
+ if (typeof rawTextDelta !== "string") {
2315
+ noteDegradedIncrement("text_delta", rawTextDelta);
2316
+ }
2317
+ const textDelta = typeof rawTextDelta === "string" ? rawTextDelta : "";
2318
+ block.text += textDelta;
2279
2319
  stream.push({
2280
2320
  type: "text_delta",
2281
2321
  contentIndex: index,
2282
- delta: event.delta.text,
2322
+ delta: textDelta,
2283
2323
  partial: output,
2284
2324
  });
2285
2325
  }
2286
2326
  } else if (event.delta.type === "thinking_delta") {
2287
2327
  const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
2288
2328
  if (block && block.type === "thinking") {
2289
- block.thinking += event.delta.thinking;
2329
+ const rawThinkingDelta: unknown = event.delta.thinking;
2330
+ if (typeof rawThinkingDelta !== "string") {
2331
+ noteDegradedIncrement("thinking_delta", rawThinkingDelta);
2332
+ }
2333
+ const thinkingDelta = typeof rawThinkingDelta === "string" ? rawThinkingDelta : "";
2334
+ block.thinking += thinkingDelta;
2290
2335
  if (summarizedThinking) {
2291
- const summary = (reasoningBuffers.get(block) ?? "") + event.delta.thinking;
2336
+ const summary = (reasoningBuffers.get(block) ?? "") + thinkingDelta;
2292
2337
  reasoningBuffers.set(block, summary);
2293
2338
  stream.push({
2294
2339
  type: "reasoning_summary_delta",
2295
2340
  contentIndex: index,
2296
- delta: event.delta.thinking,
2341
+ delta: thinkingDelta,
2297
2342
  partial: output,
2298
2343
  });
2299
2344
  } else {
2300
2345
  stream.push({
2301
2346
  type: "thinking_delta",
2302
2347
  contentIndex: index,
2303
- delta: event.delta.thinking,
2348
+ delta: thinkingDelta,
2304
2349
  partial: output,
2305
2350
  });
2306
2351
  }
@@ -2308,12 +2353,26 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2308
2353
  } else if (event.delta.type === "input_json_delta") {
2309
2354
  const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
2310
2355
  if (block && block.type === "toolCall") {
2311
- block.partialJson += event.delta.partial_json;
2356
+ const rawJsonDelta: unknown = event.delta.partial_json;
2357
+ if (typeof rawJsonDelta !== "string") {
2358
+ // Tool-argument fragments are positional JSON text: erasing or
2359
+ // coercing any malformed increment (primitive OR object/function)
2360
+ // assembles valid-but-wrong arguments — e.g. `{"n":1` + numeric
2361
+ // primitive erased to "" + `3}` parses as {"n":13} and executes.
2362
+ // Prose/thinking/signature anomalies are safe to degrade; tool
2363
+ // arguments fail the turn closed. The payload never enters the
2364
+ // error.
2365
+ throw new Error(
2366
+ "Anthropic stream sent a non-string input_json_delta tool-argument increment; failing the turn instead of assembling wrong tool arguments",
2367
+ );
2368
+ }
2369
+ const jsonDelta = rawJsonDelta;
2370
+ block.partialJson += jsonDelta;
2312
2371
  block.arguments = parseStreamingJson(block.partialJson);
2313
2372
  stream.push({
2314
2373
  type: "toolcall_delta",
2315
2374
  contentIndex: index,
2316
- delta: event.delta.partial_json,
2375
+ delta: jsonDelta,
2317
2376
  partial: output,
2318
2377
  });
2319
2378
  }
@@ -2321,7 +2380,12 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2321
2380
  const { block } = getBlockByAnthropicIndex(event.index);
2322
2381
  if (block && block.type === "thinking") {
2323
2382
  block.thinkingSignature = block.thinkingSignature || "";
2324
- block.thinkingSignature += event.delta.signature;
2383
+ const rawSignatureDelta: unknown = event.delta.signature;
2384
+ if (typeof rawSignatureDelta === "string") {
2385
+ block.thinkingSignature += rawSignatureDelta;
2386
+ } else {
2387
+ noteDegradedIncrement("signature_delta", rawSignatureDelta);
2388
+ }
2325
2389
  }
2326
2390
  }
2327
2391
  } else if (event.type === "content_block_stop") {
@@ -2359,9 +2423,21 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2359
2423
  partial: output,
2360
2424
  });
2361
2425
  } else if (block.type === "toolCall") {
2362
- if (!isCompleteJson(block.partialJson)) truncatedToolCalls.add(block);
2426
+ if (!isCompleteJson(block.partialJson)) {
2427
+ truncatedToolCalls.add(block);
2428
+ block.incompleteArguments = true;
2429
+ block.incompleteArgumentsReason = "truncated";
2430
+ }
2363
2431
  if (block.partialJson.trim()) {
2364
- block.arguments = parseStreamingJson(block.partialJson);
2432
+ const parsedArguments: unknown = parseStreamingJson(block.partialJson);
2433
+ if (
2434
+ parsedArguments === null ||
2435
+ typeof parsedArguments !== "object" ||
2436
+ Array.isArray(parsedArguments)
2437
+ ) {
2438
+ throw new Error("Anthropic tool_use completed with non-object arguments");
2439
+ }
2440
+ block.arguments = parsedArguments as Record<string, unknown>;
2365
2441
  if (findUnnecessaryUnicodeEscape(block.partialJson)) {
2366
2442
  block.escapedNonAsciiArguments = true;
2367
2443
  }
@@ -2383,7 +2459,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2383
2459
  if (rawStopReason) {
2384
2460
  output.stopReason = isProviderSafetyStop ? "error" : mapStopReason(rawStopReason);
2385
2461
  sawTerminalEnvelope = true;
2386
- sawTerminalStopReason = true;
2387
2462
  }
2388
2463
  if (isProviderSafetyStop) {
2389
2464
  sawProviderSafetyStop = true;
@@ -2852,6 +2927,8 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2852
2927
  delete (block as { index?: number }).index;
2853
2928
  if (block.type === "toolCall") {
2854
2929
  truncatedToolCalls.add(block);
2930
+ block.incompleteArguments = true;
2931
+ block.incompleteArgumentsReason = "truncated";
2855
2932
  if (block.partialJson.trim()) {
2856
2933
  block.arguments = parseStreamingJson(block.partialJson);
2857
2934
  if (findUnnecessaryUnicodeEscape(block.partialJson)) {
@@ -2862,12 +2939,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2862
2939
  }
2863
2940
  }
2864
2941
  blocksByAnthropicIndex.clear();
2865
- if (output.stopReason === "length" || !sawTerminalStopReason) {
2866
- for (const block of output.content) {
2867
- if (block.type === "toolCall" && truncatedToolCalls.has(block)) {
2868
- block.incompleteArguments = true;
2869
- block.incompleteArgumentsReason = "truncated";
2870
- }
2942
+ for (const block of output.content) {
2943
+ if (block.type === "toolCall" && truncatedToolCalls.has(block)) {
2944
+ block.incompleteArguments = true;
2945
+ block.incompleteArgumentsReason = "truncated";
2871
2946
  }
2872
2947
  }
2873
2948
  output.duration = Date.now() - startTime;
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
- import { $credentialEnv } from "@gajae-code/utils";
3
+ import { $credentialEnv, getTrustedHomeDir } from "@gajae-code/utils";
5
4
  import type { AwsCredentials } from "./aws-sigv4";
6
5
 
7
6
  export type AwsIniFile = Record<string, Record<string, string>>;
@@ -64,7 +63,7 @@ export function parseAwsIni(text: string): AwsIniFile {
64
63
 
65
64
  export function resolveAwsCredentialSource(options: AwsCredentialSourceOptions = {}): AwsCredentialSource {
66
65
  const profile = options.profile || $credentialEnv("AWS_PROFILE") || "default";
67
- const home = os.homedir();
66
+ const home = getTrustedHomeDir();
68
67
  return {
69
68
  profile,
70
69
  credentialsPath: path.resolve(
@@ -20,9 +20,8 @@
20
20
  */
21
21
 
22
22
  import * as fs from "node:fs";
23
- import * as os from "node:os";
24
23
  import * as path from "node:path";
25
- import { $env, isEnoent, logger } from "@gajae-code/utils";
24
+ import { $env, getTrustedHomeDir, isEnoent, logger } from "@gajae-code/utils";
26
25
  import {
27
26
  type AwsIniFile,
28
27
  classifyAwsProfileCapability,
@@ -201,7 +200,7 @@ async function loadSsoCachedToken(
201
200
  startUrl: string,
202
201
  sessionName: string | undefined,
203
202
  ): Promise<SsoCachedToken | undefined> {
204
- const cacheDir = path.join(os.homedir(), ".aws", "sso", "cache");
203
+ const cacheDir = path.join(getTrustedHomeDir(), ".aws", "sso", "cache");
205
204
  let entries: string[];
206
205
  try {
207
206
  entries = await fs.promises.readdir(cacheDir);
@@ -15,6 +15,7 @@ import type {
15
15
  StreamOptions,
16
16
  Tool,
17
17
  ToolChoice,
18
+ ToolResultMessage,
18
19
  } from "../types";
19
20
  import { normalizeSystemPrompts } from "../utils";
20
21
  import { createAbortSourceTracker } from "../utils/abort";
@@ -396,7 +397,23 @@ function convertMessages(
396
397
  }
397
398
 
398
399
  let msgIndex = 0;
400
+ // Consecutive tool results are batched into one append call so every output
401
+ // of the turn stays contiguous before the collected image user message;
402
+ // per-result image user messages interleave with sibling outputs and break
403
+ // tool_use→tool_result adjacency through Anthropic-translating proxies (#4807).
404
+ let pendingToolResults: ToolResultMessage[] = [];
405
+ const flushPendingToolResults = (): void => {
406
+ if (pendingToolResults.length === 0) return;
407
+ appendResponsesToolResultMessages(messages, pendingToolResults, model, strictResponsesPairing, knownCallIds);
408
+ pendingToolResults = [];
409
+ };
399
410
  for (const msg of transformedMessages) {
411
+ if (msg.role === "toolResult") {
412
+ pendingToolResults.push(msg);
413
+ msgIndex++;
414
+ continue;
415
+ }
416
+ flushPendingToolResults();
400
417
  if (msg.role === "user" || msg.role === "developer") {
401
418
  const content = convertResponsesInputContent(msg.content, model.input.includes("image"));
402
419
  if (!content) continue;
@@ -408,11 +425,10 @@ function convertMessages(
408
425
  const outputItems = convertResponsesAssistantMessage(msg as AssistantMessage, model, msgIndex, knownCallIds);
409
426
  if (outputItems.length === 0) continue;
410
427
  messages.push(...outputItems);
411
- } else if (msg.role === "toolResult") {
412
- appendResponsesToolResultMessages(messages, msg, model, strictResponsesPairing, knownCallIds);
413
428
  }
414
429
  msgIndex++;
415
430
  }
431
+ flushPendingToolResults();
416
432
 
417
433
  return messages;
418
434
  }