@code-yeongyu/senpi-ai 2026.10.21-1 → 2026.10.21-3

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 (54) hide show
  1. package/dist/api/anthropic-messages.js +34 -4
  2. package/dist/api/azure-openai-responses.js +2 -2
  3. package/dist/api/cursor-agent.js +2 -6
  4. package/dist/api/google-generative-ai.js +3 -1
  5. package/dist/api/google-vertex.js +3 -1
  6. package/dist/api/openai-codex-responses.js +4 -4
  7. package/dist/api/openai-completions.js +3 -1
  8. package/dist/api/openai-prompt-cache.d.ts +3 -0
  9. package/dist/api/openai-prompt-cache.js +22 -0
  10. package/dist/api/openai-responses-shared.js +5 -2
  11. package/dist/api/openai-responses.js +4 -7
  12. package/dist/api/pi-messages.js +8 -1
  13. package/dist/auth/pool/kinds.d.ts +17 -0
  14. package/dist/auth/pool/kinds.js +32 -0
  15. package/dist/auth/pool/slots.d.ts +13 -1
  16. package/dist/auth/pool/slots.js +33 -1
  17. package/dist/auth/types.d.ts +7 -0
  18. package/dist/cursor/capability-table.d.ts +10 -0
  19. package/dist/cursor/capability-table.js +183 -0
  20. package/dist/cursor/catalog-grouping.js +4 -26
  21. package/dist/cursor/display-name.d.ts +31 -0
  22. package/dist/cursor/display-name.js +48 -0
  23. package/dist/cursor/model-capabilities.d.ts +3 -10
  24. package/dist/cursor/model-capabilities.js +2 -154
  25. package/dist/cursor/model-input.d.ts +2 -0
  26. package/dist/cursor/model-input.js +11 -0
  27. package/dist/cursor/selection-descriptor.js +1 -0
  28. package/dist/models.js +4 -2
  29. package/dist/providers/data/.manifest.json +1 -1
  30. package/dist/providers/data/fireworks.json +1 -1
  31. package/dist/providers/data/huggingface.json +1 -1
  32. package/dist/providers/data/mistral.json +1 -1
  33. package/dist/providers/data/nvidia.json +1 -1
  34. package/dist/providers/data/opencode-go.json +1 -1
  35. package/dist/providers/data/opencode.json +1 -1
  36. package/dist/providers/data/opengateway.json +1 -1
  37. package/dist/providers/data/openrouter.json +1 -1
  38. package/dist/providers/data/radius.json +1 -1
  39. package/dist/providers/data/venice.json +1 -1
  40. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  41. package/dist/providers/kimi-coding-auth.js +2 -0
  42. package/dist/tool-call-middleware/stream-message-metadata.js +1 -0
  43. package/dist/types.d.ts +43 -0
  44. package/dist/utils/estimate.js +3 -0
  45. package/dist/utils/fable55-launch.d.ts +21 -0
  46. package/dist/utils/fable55-launch.js +37 -0
  47. package/dist/utils/github-copilot-tool-limit.d.ts +1 -1
  48. package/dist/utils/github-copilot-tool-limit.js +24 -1
  49. package/dist/utils/overflow.js +10 -6
  50. package/dist/utils/provider-diagnostic-sources.d.ts +2 -1
  51. package/dist/utils/provider-diagnostic-sources.js +14 -2
  52. package/dist/utils/reported-count.d.ts +6 -0
  53. package/dist/utils/reported-count.js +8 -0
  54. package/package.json +2 -2
@@ -734,6 +734,19 @@ function markSystemMessageCacheCheckpoint(message, cacheControl) {
734
734
  lastBlock.cache_control = cacheControl;
735
735
  }
736
736
  }
737
+ /** Marks the last block that accepts `cache_control` (thinking blocks do not); false when none does. */
738
+ function markAssistantMessageCacheCheckpoint(blocks, cacheControl) {
739
+ for (let index = blocks.length - 1; index >= 0; index--) {
740
+ const block = blocks[index];
741
+ if (block.type === "text" || block.type === "tool_use") {
742
+ block.cache_control = cacheControl;
743
+ return true;
744
+ }
745
+ if (block.type === "thinking" || block.type === "redacted_thinking")
746
+ return false;
747
+ }
748
+ return false;
749
+ }
737
750
  function markUserMessageCacheCheckpoint(message, cacheControl) {
738
751
  if (message.role !== "user")
739
752
  return false;
@@ -1779,6 +1792,12 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1779
1792
  });
1780
1793
  // The prompt travels in `params.system`; the leading system message is not a turn.
1781
1794
  const conversationMessages = initialSystemMessage ? transformedMessages.slice(1) : transformedMessages;
1795
+ // Context reduction marks the last message its frontier leaves unchanged: on a frontier step both the
1796
+ // old boundary (read) and the new one (written for the next step) are marked. Two checkpoints fit
1797
+ // only when the system block holds one, which then also covers the tools before it (senpi#3242).
1798
+ const stablePrefixMarkers = conversationMessages.filter((message) => message.role === "assistant" && message.promptCacheBreakpoint === true).length;
1799
+ const systemHoldsCheckpoint = isOAuthToken || initialSystemText.length > 0;
1800
+ const stablePrefixCheckpointLimit = stablePrefixMarkers >= 2 && systemHoldsCheckpoint ? 2 : Math.min(1, stablePrefixMarkers);
1782
1801
  // Native tool changes reference tools by name, so a redefined name cannot be expressed,
1783
1802
  // and Anthropic rejects a tool list where every tool is deferred, so there must be an
1784
1803
  // initial active tool to anchor the deferred ones. Otherwise the current tool list is sent.
@@ -1823,7 +1842,7 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1823
1842
  nativeToolChanges,
1824
1843
  refusalFallbacks: sendFallbacks,
1825
1844
  }, options);
1826
- const convertedMessages = convertMessages(conversationMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay, deferredToolNames, normalizeToolName, midConvoEffort ? model.provider : undefined, nativeToolChanges);
1845
+ const convertedMessages = convertMessages(conversationMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay, deferredToolNames, normalizeToolName, midConvoEffort ? model.provider : undefined, nativeToolChanges, stablePrefixCheckpointLimit);
1827
1846
  const messages = midConvoEffort
1828
1847
  ? insertThinkingLevelMessages(convertedMessages, activeEffort)
1829
1848
  : convertedMessages.messages;
@@ -1885,7 +1904,7 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1885
1904
  configurable: true,
1886
1905
  });
1887
1906
  }
1888
- const toolCacheControl = compat.supportsCacheControlOnTools ? cacheControl : undefined;
1907
+ const toolCacheControl = compat.supportsCacheControlOnTools && stablePrefixCheckpointLimit < 2 ? cacheControl : undefined;
1889
1908
  if (nativeToolChanges) {
1890
1909
  // Initial tools stay active with the cache breakpoint on the last one. Every later
1891
1910
  // declaration is deferred and only surfaced by its `tool_addition` block; removed
@@ -2053,10 +2072,11 @@ function convertToolResult(msg, isOAuthToken, deferredToolNames, loadedToolNames
2053
2072
  : convertedContent,
2054
2073
  };
2055
2074
  }
2056
- function convertMessages(transformedMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay = "text", deferredToolNames = new Set(), normalizeToolName = (name) => name, managedProvider, nativeToolChanges = false) {
2075
+ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay = "text", deferredToolNames = new Set(), normalizeToolName = (name) => name, managedProvider, nativeToolChanges = false, stablePrefixCheckpointLimit = 0) {
2057
2076
  const params = [];
2058
2077
  const assistantLevels = new Map();
2059
2078
  const loadedToolNames = new Set();
2079
+ let stablePrefixCheckpoints = 0;
2060
2080
  // Later system messages are held back and emitted directly before the next assistant
2061
2081
  // message (or at the end of the transcript). Anthropic requires `tool_result` blocks to
2062
2082
  // immediately follow their `tool_use`, so a system message between them is rejected; this
@@ -2247,6 +2267,12 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
2247
2267
  role: "assistant",
2248
2268
  content: blocks,
2249
2269
  });
2270
+ if (cacheControl &&
2271
+ msg.promptCacheBreakpoint === true &&
2272
+ stablePrefixCheckpoints < stablePrefixCheckpointLimit &&
2273
+ markAssistantMessageCacheCheckpoint(blocks, cacheControl)) {
2274
+ stablePrefixCheckpoints++;
2275
+ }
2250
2276
  if (managedProvider !== undefined &&
2251
2277
  msg.api === "anthropic-messages" &&
2252
2278
  msg.provider === managedProvider &&
@@ -2285,7 +2311,11 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
2285
2311
  markSystemMessageCacheCheckpoint(lastParam, cacheControl);
2286
2312
  return { messages: params, assistantLevels };
2287
2313
  }
2288
- const retainPrecedingCheckpoint = isToolLoopContinuation(params);
2314
+ // Four breakpoints at most: system, tools or a second stable-prefix checkpoint, a stable-prefix
2315
+ // checkpoint and the final user turn. A stable-prefix checkpoint takes the preceding tool-loop
2316
+ // checkpoint's slot; the final checkpoint's lookback already reaches the previous request's entry
2317
+ // two blocks back (senpi#3242).
2318
+ const retainPrecedingCheckpoint = stablePrefixCheckpoints === 0 && isToolLoopContinuation(params);
2289
2319
  if (cacheControl && params.length > 0 && markUserMessageCacheCheckpoint(params[params.length - 1], cacheControl)) {
2290
2320
  if (retainPrecedingCheckpoint) {
2291
2321
  for (let index = params.length - 2; index >= 0; index--) {
@@ -8,7 +8,7 @@ import { getProviderEnvValue } from "../utils/provider-env.js";
8
8
  import { retryProviderRequest } from "../utils/provider-retry.js";
9
9
  import { getDeclaredTools, resolveTranscript, resolveTranscriptTools } from "../utils/transcript.js";
10
10
  import { createGrammarToolInputProperties } from "./constrained-sampling.js";
11
- import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
11
+ import { deriveOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
12
12
  import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
13
13
  import { buildBaseOptions } from "./simple-options.js";
14
14
  const DEFAULT_AZURE_API_VERSION = "v1";
@@ -233,7 +233,7 @@ function buildParams(model, context, options, deploymentName, grammarToolInputPr
233
233
  stream: true,
234
234
  ...((options?.cacheRetention ?? model.cacheRetention) === "none"
235
235
  ? {}
236
- : { prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId) }),
236
+ : { prompt_cache_key: deriveOpenAIPromptCacheKey(model.id, context) }),
237
237
  store: false,
238
238
  };
239
239
  if (options?.maxTokens) {
@@ -20,6 +20,7 @@ import * as http2 from "node:http2";
20
20
  import { create, fromBinary, fromJson, toBinary, toJson } from "@bufbuild/protobuf";
21
21
  import { ValueSchema } from "@bufbuild/protobuf/wkt";
22
22
  import { CURSOR_COMPOSER_PROMPT, isCursorComposerModel } from "../cursor/composer-prompt.js";
23
+ import { cursorDiscoveredInput } from "../cursor/model-input.js";
23
24
  import { calculateCost } from "../models.js";
24
25
  import { registerSessionResourceCleanup } from "../session-resources.js";
25
26
  import { clearStreamingPartialJson, kCursorExecResolved, kStreamingBlockIndex, kStreamingBlockKind, kStreamingEnvelopeId, kStreamingLastParseLen, kStreamingPartialJson, } from "../utils/block-symbols.js";
@@ -3788,11 +3789,6 @@ const CURSOR_DEFAULT_MAX_TOKENS = 64_000;
3788
3789
  const CURSOR_1M_CONTEXT_WINDOW = 1_000_000;
3789
3790
  const CURSOR_1M_NAME_PATTERN = /\b1m\b/i;
3790
3791
  const CURSOR_MAX_MODE_1M_ID_PATTERN = /claude|gemini/;
3791
- /**
3792
- * Model-id families whose native catalogs are multimodal. Cursor-only or
3793
- * text-only families (`composer-*`, `grok-code-*`) stay outside this pattern.
3794
- */
3795
- const CURSOR_MULTIMODAL_ID_PATTERN = /claude|gemini|gpt-|codex/;
3796
3792
  /**
3797
3793
  * Fetches Cursor models through `GetUsableModels` (unary protobuf over
3798
3794
  * HTTP/2; the ALB rejects HTTP/1.1 with 464) and normalizes them.
@@ -3882,7 +3878,7 @@ export async function fetchCursorUsableModels(options) {
3882
3878
  id,
3883
3879
  name,
3884
3880
  reasoning: Boolean(model.thinkingDetails),
3885
- input: CURSOR_MULTIMODAL_ID_PATTERN.test(id.toLowerCase()) ? ["text", "image"] : ["text"],
3881
+ input: cursorDiscoveredInput(id),
3886
3882
  contextWindow,
3887
3883
  maxTokens: CURSOR_DEFAULT_MAX_TOKENS,
3888
3884
  cursorMaxMode: maxMode,
@@ -4,6 +4,7 @@ import { formatProviderError, normalizeProviderError } from "../utils/error-body
4
4
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
5
  import { providerHeadersToRecord } from "../utils/headers.js";
6
6
  import { getPiUserAgent } from "../utils/pi-user-agent.js";
7
+ import { reportedCount } from "../utils/reported-count.js";
7
8
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
8
9
  import { getSystemMessageText } from "../utils/text.js";
9
10
  import { collapseSystemMessages, getCurrentTools, getInitialSystemMessage } from "../utils/transcript.js";
@@ -191,12 +192,13 @@ export const stream = (model, context, options) => {
191
192
  }
192
193
  }
193
194
  if (chunk.usageMetadata) {
195
+ const reasoning = reportedCount(chunk.usageMetadata.thoughtsTokenCount);
194
196
  output.usage = {
195
197
  input: (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0),
196
198
  output: (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
197
199
  cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
198
200
  cacheWrite: 0,
199
- reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
201
+ ...(reasoning === undefined ? {} : { reasoning }),
200
202
  totalTokens: chunk.usageMetadata.totalTokenCount || 0,
201
203
  cost: {
202
204
  input: 0,
@@ -5,6 +5,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
5
  import { providerHeadersToRecord } from "../utils/headers.js";
6
6
  import { getPiUserAgent } from "../utils/pi-user-agent.js";
7
7
  import { getProviderEnvValue } from "../utils/provider-env.js";
8
+ import { reportedCount } from "../utils/reported-count.js";
8
9
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
9
10
  import { getSystemMessageText } from "../utils/text.js";
10
11
  import { collapseSystemMessages, getCurrentTools, getInitialSystemMessage } from "../utils/transcript.js";
@@ -195,12 +196,13 @@ export const stream = (model, context, options) => {
195
196
  }
196
197
  }
197
198
  if (chunk.usageMetadata) {
199
+ const reasoning = reportedCount(chunk.usageMetadata.thoughtsTokenCount);
198
200
  output.usage = {
199
201
  input: (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0),
200
202
  output: (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
201
203
  cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
202
204
  cacheWrite: 0,
203
- reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
205
+ ...(reasoning === undefined ? {} : { reasoning }),
204
206
  totalTokens: chunk.usageMetadata.totalTokenCount || 0,
205
207
  cost: {
206
208
  input: 0,
@@ -28,7 +28,7 @@ import { createGrammarToolInputProperties } from "./constrained-sampling.js";
28
28
  import { clearWebSocketFallbackState, getOrCreateWebSocketDebugStats, getWebSocketDebugStats, isWebSocketSseFallbackActive, recordWebSocketFailure, recordWebSocketSseFallback, recordWebSocketSuccess, } from "./openai-codex-responses/fallback-state.js";
29
29
  import { codexEventProviderDiagnostic, codexHttpProviderDiagnostic, } from "./openai-codex-responses/provider-diagnostic.js";
30
30
  import { buildCodexReasoning } from "./openai-codex-responses/reasoning.js";
31
- import { applyChatGptSubscriptionCacheAffinityHeaders, clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
31
+ import { applyChatGptSubscriptionCacheAffinityHeaders, clampOpenAIPromptCacheKey, deriveOpenAIPromptCacheKey, } from "./openai-prompt-cache.js";
32
32
  import { convertResponsesMessages, convertResponsesTools, processResponsesStream, resolveResponsesDeferredToolsMode, resolveResponsesToolPlacement, } from "./openai-responses-shared.js";
33
33
  import { applyExtraBody, buildBaseOptions, clampMaxForOpenAI, OPENAI_RESPONSES_RESERVED_BODY_KEYS, } from "./simple-options.js";
34
34
  import { startWebSocketLiveness } from "./websocket-liveness.js";
@@ -171,7 +171,7 @@ export const stream = (model, context, options) => {
171
171
  const grammarToolInputProperties = createGrammarToolInputProperties(getDeclaredTools(normalizedContext.messages), model.compat?.supportsOpenAIGrammarTools ?? false);
172
172
  const cacheSessionId = options?.cacheRetention === "none" ? undefined : options?.sessionId;
173
173
  const codexSessionId = clampOpenAIPromptCacheKey(cacheSessionId);
174
- let body = buildRequestBody(model, normalizedContext, options, codexSessionId, grammarToolInputProperties);
174
+ let body = buildRequestBody(model, normalizedContext, options, grammarToolInputProperties);
175
175
  const nextBody = await options?.onPayload?.(body, model);
176
176
  if (nextBody !== undefined) {
177
177
  body = nextBody;
@@ -402,7 +402,7 @@ export const streamSimple = (model, context, options) => {
402
402
  // ============================================================================
403
403
  // Request Building
404
404
  // ============================================================================
405
- function buildRequestBody(model, context, options, cacheSessionId, grammarToolInputProperties = createGrammarToolInputProperties(getDeclaredTools(context.messages), model.compat?.supportsOpenAIGrammarTools ?? false)) {
405
+ function buildRequestBody(model, context, options, grammarToolInputProperties = createGrammarToolInputProperties(getDeclaredTools(context.messages), model.compat?.supportsOpenAIGrammarTools ?? false)) {
406
406
  const requestedReasoningEffort = options?.reasoningEffort;
407
407
  const mappedReasoningEffort = requestedReasoningEffort === undefined
408
408
  ? undefined
@@ -436,7 +436,7 @@ function buildRequestBody(model, context, options, cacheSessionId, grammarToolIn
436
436
  input: messages,
437
437
  text: { verbosity: options?.textVerbosity || "low" },
438
438
  include: ["reasoning.encrypted_content"],
439
- prompt_cache_key: cacheSessionId,
439
+ prompt_cache_key: options?.cacheRetention === "none" ? undefined : deriveOpenAIPromptCacheKey(model.id, context),
440
440
  tool_choice: options?.toolChoice ?? "auto",
441
441
  parallel_tool_calls: true,
442
442
  };
@@ -12,6 +12,7 @@ import { getOpenAICompletionsCompat as getCompat, } from "../utils/prompt-cache-
12
12
  import { awaitProviderTransport, iterateProviderTransport, openAICompatibleProviderDiagnosticFromError, } from "../utils/provider-diagnostic-sources.js";
13
13
  import { getProviderEnvValue } from "../utils/provider-env.js";
14
14
  import { retryProviderStreamRequest } from "../utils/provider-retry.js";
15
+ import { reportedCount } from "../utils/reported-count.js";
15
16
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
16
17
  import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
17
18
  import { primeStreamUntilContent, sendWithForcedToolChoiceFallback } from "../utils/tool-choice-fallback.js";
@@ -1569,12 +1570,13 @@ function parseChunkUsage(rawUsage, model) {
1569
1570
  const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
1570
1571
  // OpenAI completion_tokens already includes reasoning_tokens.
1571
1572
  const outputTokens = rawUsage.completion_tokens || 0;
1573
+ const reasoning = reportedCount(rawUsage.completion_tokens_details?.reasoning_tokens);
1572
1574
  const usage = {
1573
1575
  input,
1574
1576
  output: outputTokens,
1575
1577
  cacheRead: cacheReadTokens,
1576
1578
  cacheWrite: cacheWriteTokens,
1577
- reasoning: rawUsage.completion_tokens_details?.reasoning_tokens || 0,
1579
+ ...(reasoning === undefined ? {} : { reasoning }),
1578
1580
  totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
1579
1581
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1580
1582
  };
@@ -1,4 +1,7 @@
1
+ import type { TranscriptContext } from "../types.ts";
1
2
  export declare const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
3
+ /** Cache affinity belongs to the reusable leading prefix, never to a conversation UUID. */
4
+ export declare function deriveOpenAIPromptCacheKey(modelId: string, context: TranscriptContext): string | undefined;
2
5
  export declare function clampOpenAIPromptCacheKey(key: string | undefined): string | undefined;
3
6
  export declare function applyChatGptSubscriptionCacheAffinityHeaders(headers: Headers, sessionId: string | undefined): void;
4
7
  //# sourceMappingURL=openai-prompt-cache.d.ts.map
@@ -1,4 +1,26 @@
1
+ import { shortHash } from "../utils/hash.js";
2
+ import { getSystemMessageText } from "../utils/text.js";
3
+ import { getInitialSystemMessage } from "../utils/transcript.js";
1
4
  export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
5
+ const PREFIX_VERSION = "senpi-responses-prefix-v1";
6
+ /** Cache affinity belongs to the reusable leading prefix, never to a conversation UUID. */
7
+ export function deriveOpenAIPromptCacheKey(modelId, context) {
8
+ const initial = getInitialSystemMessage(context.messages);
9
+ const tools = [...(initial?.toolsAdded ?? [])].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
10
+ const prefix = JSON.stringify([
11
+ PREFIX_VERSION,
12
+ modelId,
13
+ initial === undefined ? "" : getSystemMessageText(initial),
14
+ tools.map(({ name, description, parameters, freeform, constrainedSampling }) => ({
15
+ name,
16
+ description,
17
+ parameters,
18
+ freeform,
19
+ constrainedSampling,
20
+ })),
21
+ ]);
22
+ return clampOpenAIPromptCacheKey(`${PREFIX_VERSION}-${shortHash(prefix)}`);
23
+ }
2
24
  export function clampOpenAIPromptCacheKey(key) {
3
25
  if (key === undefined)
4
26
  return undefined;
@@ -3,6 +3,7 @@ import { calculateCost, supportsConfigurationUpdate } from "../models.js";
3
3
  import { splitDeferredTools } from "../utils/deferred-tools.js";
4
4
  import { shortHash } from "../utils/hash.js";
5
5
  import { parseStreamingJson } from "../utils/json-parse.js";
6
+ import { reportedCount } from "../utils/reported-count.js";
6
7
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
7
8
  import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
8
9
  import { getCurrentTools, getDeclaredTools, resolveTranscript, resolveTranscriptTools } from "../utils/transcript.js";
@@ -480,7 +481,8 @@ export function convertResponsesTools(tools, options) {
480
481
  const defaultStrict = options?.strict === undefined ? false : options.strict;
481
482
  const supportsStrictMode = options?.supportsStrictMode ?? true;
482
483
  const supportsOpenAIGrammarTools = options?.supportsOpenAIGrammarTools ?? false;
483
- return tools.map((tool) => {
484
+ const sortedTools = [...tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
485
+ return sortedTools.map((tool) => {
484
486
  const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
485
487
  if (grammar) {
486
488
  return {
@@ -772,13 +774,14 @@ export async function processResponsesStream(openaiStream, output, stream, model
772
774
  const cachedTokens = inputDetails?.cached_tokens || 0;
773
775
  // OpenAI platform reports cache_write_tokens; some compatible gateways use cache_creation_tokens.
774
776
  const cacheWriteTokens = inputDetails?.cache_write_tokens ?? inputDetails?.cache_creation_tokens ?? 0;
777
+ const reasoning = reportedCount(response.usage.output_tokens_details?.reasoning_tokens);
775
778
  output.usage = {
776
779
  // OpenAI includes cached and cache-write tokens in input_tokens, so subtract both.
777
780
  input: Math.max(0, (response.usage.input_tokens || 0) - cachedTokens - cacheWriteTokens),
778
781
  output: response.usage.output_tokens || 0,
779
782
  cacheRead: cachedTokens,
780
783
  cacheWrite: cacheWriteTokens,
781
- reasoning: response.usage.output_tokens_details?.reasoning_tokens || 0,
784
+ ...(reasoning === undefined ? {} : { reasoning }),
782
785
  totalTokens: response.usage.total_tokens || 0,
783
786
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
784
787
  };
@@ -17,7 +17,7 @@ import { createGrammarToolInputProperties } from "./constrained-sampling.js";
17
17
  import { withGitHubCopilotFailureNote } from "./github-copilot-errors.js";
18
18
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
19
19
  import { resolveOpenAIClientAuth } from "./openai-client-auth.js";
20
- import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
20
+ import { deriveOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
21
21
  import { applyAllowedToolsChoice, hasRefusedAllowedToolsChoice, isAllowedToolsChoiceRefusal, rememberAllowedToolsChoiceRefusal, restrictToAllowedTools, } from "./openai-responses-allowed-tools.js";
22
22
  import { findPromptCacheComparisonResponseId, withPromptCacheComparison, } from "./openai-responses-prompt-cache.js";
23
23
  import { convertResponsesMessages, convertResponsesTools, processResponsesStream, resolveResponsesDeferredToolsMode, resolveResponsesToolPlacement, } from "./openai-responses-shared.js";
@@ -195,7 +195,7 @@ export const stream = (model, context, options) => {
195
195
  params = applyAllowedToolsChoice(params, normalizedContext, context.activeToolNames, compat);
196
196
  if (hasRefusedAllowedToolsChoice(model))
197
197
  params = restrictToAllowedTools(params);
198
- const limitedTools = limitGitHubCopilotTools(model.provider, params.tools, params.tool_choice);
198
+ const limitedTools = limitGitHubCopilotTools(model.provider, params.tools, params.tool_choice, getDeclaredTools(normalizedContext.messages).map((tool) => tool.name));
199
199
  if (limitedTools.omittedCount > 0) {
200
200
  params = { ...params, tools: limitedTools.tools };
201
201
  recordGitHubCopilotToolLimit(output, limitedTools.omittedCount);
@@ -348,7 +348,7 @@ export async function warmOpenAIResponsesPromptCache(model, context, options) {
348
348
  if (nextParams !== undefined)
349
349
  params = nextParams;
350
350
  params = sanitizeUnsupportedNativeTools(params, compat);
351
- const limitedTools = limitGitHubCopilotTools(model.provider, params.tools, params.tool_choice);
351
+ const limitedTools = limitGitHubCopilotTools(model.provider, params.tools, params.tool_choice, getDeclaredTools(prefix.messages).map((tool) => tool.name));
352
352
  if (limitedTools.omittedCount > 0)
353
353
  params = { ...params, tools: limitedTools.tools };
354
354
  const body = {
@@ -443,10 +443,7 @@ function buildParams(model, context, options, compat = getCompat(model, options?
443
443
  model: model.id,
444
444
  input: messages,
445
445
  stream: true,
446
- prompt_cache_key: cacheRetention === "none" ||
447
- (isNativeEndpoint && (compat.supportsExplicitPromptCacheMode || model.cost.cacheWrite > 0))
448
- ? undefined
449
- : clampOpenAIPromptCacheKey(options?.sessionId),
446
+ prompt_cache_key: cacheRetention === "none" ? undefined : deriveOpenAIPromptCacheKey(model.id, context),
450
447
  prompt_cache_retention: getPromptCacheRetention(compat, cacheRetention),
451
448
  prompt_cache_options: withPromptCacheComparison(getPromptCacheOptions(compat, cacheRetention), comparisonResponseId),
452
449
  store: false,
@@ -21,6 +21,11 @@ export class PiMessagesResponseError extends Error {
21
21
  this.diagnosticDetails = diagnosticDetails;
22
22
  }
23
23
  }
24
+ /** `promptCacheBreakpoint` is a request-local hint for adapters that place cache breakpoints; it is not part of the pi-messages wire. */
25
+ function withoutPromptCacheBreakpoint(message) {
26
+ const { promptCacheBreakpoint: _hint, ...rest } = message;
27
+ return rest;
28
+ }
24
29
  function parsePiMessagesErrorBody(body) {
25
30
  try {
26
31
  const parsed = JSON.parse(body);
@@ -266,7 +271,9 @@ export const stream = (model, context, options) => {
266
271
  }
267
272
  : part),
268
273
  }
269
- : message),
274
+ : message.role === "assistant" && message.promptCacheBreakpoint
275
+ ? withoutPromptCacheBreakpoint(message)
276
+ : message),
270
277
  },
271
278
  options: {
272
279
  temperature: options?.temperature,
@@ -0,0 +1,17 @@
1
+ import type { Credential } from "../types.ts";
2
+ import { type CredentialSlot, type PooledCredential } from "./slots.ts";
3
+ export type CredentialKind = Credential["type"];
4
+ /**
5
+ * A slot's kind from its own material: OAuth tokens make an OAuth login, a key alone makes an API
6
+ * key. A slot with neither is not classified, so a caller never drops or reports it by guess.
7
+ */
8
+ export declare function slotKind(slot: Pick<CredentialSlot, "access" | "refresh" | "key">): CredentialKind | undefined;
9
+ /**
10
+ * Kinds held by a credential's slots that its top-level type cannot use: slots are projected
11
+ * through that type, so a slot of the other kind is never used (senpi#3448). Builds before the
12
+ * fix appended a login of the other kind this way.
13
+ */
14
+ export declare function unusedSlotKinds(credential: PooledCredential | undefined): ReadonlyArray<CredentialKind>;
15
+ /** The slots a credential can actually use: a slot of the other kind never projects (senpi#3486). */
16
+ export declare function slotsOfOwnKind(credential: PooledCredential): CredentialSlot[];
17
+ //# sourceMappingURL=kinds.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { listSlots } from "./slots.js";
2
+ /**
3
+ * A slot's kind from its own material: OAuth tokens make an OAuth login, a key alone makes an API
4
+ * key. A slot with neither is not classified, so a caller never drops or reports it by guess.
5
+ */
6
+ export function slotKind(slot) {
7
+ if (slot.access !== undefined || slot.refresh !== undefined)
8
+ return "oauth";
9
+ return slot.key !== undefined ? "api_key" : undefined;
10
+ }
11
+ /**
12
+ * Kinds held by a credential's slots that its top-level type cannot use: slots are projected
13
+ * through that type, so a slot of the other kind is never used (senpi#3448). Builds before the
14
+ * fix appended a login of the other kind this way.
15
+ */
16
+ export function unusedSlotKinds(credential) {
17
+ if (!credential || !Array.isArray(credential.accounts))
18
+ return [];
19
+ const kinds = new Set();
20
+ for (const slot of listSlots(credential)) {
21
+ const kind = slotKind(slot);
22
+ if (kind !== undefined && kind !== credential.type)
23
+ kinds.add(kind);
24
+ }
25
+ return [...kinds];
26
+ }
27
+ /** The slots a credential can actually use: a slot of the other kind never projects (senpi#3486). */
28
+ export function slotsOfOwnKind(credential) {
29
+ return listSlots(credential).filter((slot) => slotKind(slot) !== otherKind(credential.type));
30
+ }
31
+ const otherKind = (kind) => (kind === "oauth" ? "api_key" : "oauth");
32
+ //# sourceMappingURL=kinds.js.map
@@ -72,6 +72,18 @@ export declare function pinSlot(credential: PooledCredential, name: string): Poo
72
72
  * plain credential and never write pool state back through the projection.
73
73
  */
74
74
  export declare function projectSlot(credential: PooledCredential | undefined, name: string): Credential | undefined;
75
+ /** A login tried to store an API-key credential whose `key` is blank or missing (#3867). */
76
+ export declare class EmptyApiKeyError extends Error {
77
+ readonly name = "EmptyApiKeyError";
78
+ constructor();
79
+ }
80
+ /** Drops a blank or missing `key` from an API-key credential; used only for providers that declare `keyOptional`. */
81
+ export declare function withoutBlankKey(credential: Credential): Credential;
82
+ /**
83
+ * Throws `EmptyApiKeyError` when an API-key credential names a `key` that is not a non-empty
84
+ * string. Env-only logins (AWS profile or credential chain, Vertex ADC) omit the field and pass.
85
+ */
86
+ export declare function assertLoginKeyPresent(credential: Credential): void;
75
87
  /**
76
88
  * Appends an unnamed flat credential to a pool as a generated `login-N` slot.
77
89
  * An absent current entry keeps today's whole-write shape; a flat current entry
@@ -89,7 +101,7 @@ export declare function projectSlot(credential: PooledCredential | undefined, na
89
101
  * `onAllocated` reports the account this login committed, so a caller can name
90
102
  * it in a receipt without re-reading storage.
91
103
  */
92
- export declare function appendLoginSlot(current: PooledCredential | undefined, flat: Credential, onAllocated?: (name: string, origin: "generated" | "provider") => void): Credential;
104
+ export declare function appendLoginSlot(stored: PooledCredential | undefined, flat: Credential, onAllocated?: (name: string, origin: "generated" | "provider") => void): Credential;
93
105
  /**
94
106
  * Material a provider writes into the flat credential when the real token lives
95
107
  * outside auth.json (an SDK subprocess or a vendor CLI owns it): the literal
@@ -289,6 +289,34 @@ function mergeProvidedPool(current, existing, provided) {
289
289
  function hasNewerMaterial(candidate, stored) {
290
290
  return candidate.expires !== undefined && (stored.expires === undefined || candidate.expires > stored.expires);
291
291
  }
292
+ /** A login tried to store an API-key credential whose `key` is blank or missing (#3867). */
293
+ export class EmptyApiKeyError extends Error {
294
+ constructor() {
295
+ super("Refusing to save an empty API key");
296
+ this.name = "EmptyApiKeyError";
297
+ }
298
+ }
299
+ /** True when an API-key credential names a `key` that is blank, whitespace or not a string. */
300
+ function hasBlankKey(credential) {
301
+ if (credential.type !== "api_key" || !Object.hasOwn(credential, "key"))
302
+ return false;
303
+ return typeof credential.key !== "string" || credential.key.trim() === "";
304
+ }
305
+ /** Drops a blank or missing `key` from an API-key credential; used only for providers that declare `keyOptional`. */
306
+ export function withoutBlankKey(credential) {
307
+ if (credential.type !== "api_key" || !hasBlankKey(credential))
308
+ return credential;
309
+ const { key: _blank, ...rest } = credential;
310
+ return rest;
311
+ }
312
+ /**
313
+ * Throws `EmptyApiKeyError` when an API-key credential names a `key` that is not a non-empty
314
+ * string. Env-only logins (AWS profile or credential chain, Vertex ADC) omit the field and pass.
315
+ */
316
+ export function assertLoginKeyPresent(credential) {
317
+ if (hasBlankKey(credential))
318
+ throw new EmptyApiKeyError();
319
+ }
292
320
  /**
293
321
  * Appends an unnamed flat credential to a pool as a generated `login-N` slot.
294
322
  * An absent current entry keeps today's whole-write shape; a flat current entry
@@ -306,7 +334,11 @@ function hasNewerMaterial(candidate, stored) {
306
334
  * `onAllocated` reports the account this login committed, so a caller can name
307
335
  * it in a receipt without re-reading storage.
308
336
  */
309
- export function appendLoginSlot(current, flat, onAllocated) {
337
+ export function appendLoginSlot(stored, flat, onAllocated) {
338
+ assertLoginKeyPresent(flat);
339
+ // A pool holds one kind: a login of the other kind replaces the credential instead of joining it
340
+ // as a slot nothing can project (senpi#3448). The caller backs up what it replaces.
341
+ const current = stored?.type === flat.type ? stored : undefined;
310
342
  const provided = providedSlots(flat);
311
343
  if (provided) {
312
344
  // Provider-owned envelopes identify an addition by immutable ID, never by
@@ -188,6 +188,13 @@ export interface ApiKeyAuth {
188
188
  name: string;
189
189
  /** Ambient compatibility adapter that must not outrank a stored OAuth credential. */
190
190
  ambientOnly?: boolean;
191
+ /**
192
+ * A login may leave the key blank: the provider needs none (a local server such as llama.cpp) or its
193
+ * `resolve` falls back to an ambient key while the login stores other env (Kimi's region + KIMI_API_KEY).
194
+ * `Models.login` then drops the blank key; without this flag it refuses one with `EmptyApiKeyError` (#3867).
195
+ * Only `Models.login` reads it: `AuthStorage.set` and RPC `login_api_key` always require a key.
196
+ */
197
+ keyOptional?: boolean;
191
198
  /** Interactive setup (prompt for key/provider env). Absent = ambient-only. */
192
199
  login?(interaction: ProviderAuthInteraction): Promise<ApiKeyCredential>;
193
200
  /**
@@ -0,0 +1,10 @@
1
+ import type { CursorModelCapability } from "./model-capabilities.ts";
2
+ /**
3
+ * Static cursor capability table. Values come from the live
4
+ * aiserver.v1 AvailableModels capture of 2026-08-18 (evidence:
5
+ * local-ignore/qa-evidence/20260818-cursor-reasoning-levels/available-models-catalog.json).
6
+ * `GetUsableModels` carries no window or parameter field, so this table is the
7
+ * authoritative source; see .omo/plans/cursor-reasoning-levels.md §3.1/§6.
8
+ */
9
+ export declare const CURSOR_MODEL_CAPABILITIES: Record<string, CursorModelCapability>;
10
+ //# sourceMappingURL=capability-table.d.ts.map