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

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/dist/api/azure-openai-responses.js +2 -2
  2. package/dist/api/cursor-agent.js +2 -6
  3. package/dist/api/google-generative-ai.js +3 -1
  4. package/dist/api/google-vertex.js +3 -1
  5. package/dist/api/openai-codex-responses.js +4 -4
  6. package/dist/api/openai-completions.js +3 -1
  7. package/dist/api/openai-prompt-cache.d.ts +3 -0
  8. package/dist/api/openai-prompt-cache.js +22 -0
  9. package/dist/api/openai-responses-shared.js +5 -2
  10. package/dist/api/openai-responses.js +4 -7
  11. package/dist/auth/pool/kinds.d.ts +17 -0
  12. package/dist/auth/pool/kinds.js +32 -0
  13. package/dist/auth/pool/slots.d.ts +1 -1
  14. package/dist/auth/pool/slots.js +4 -1
  15. package/dist/cursor/capability-table.d.ts +10 -0
  16. package/dist/cursor/capability-table.js +183 -0
  17. package/dist/cursor/catalog-grouping.js +4 -26
  18. package/dist/cursor/display-name.d.ts +31 -0
  19. package/dist/cursor/display-name.js +48 -0
  20. package/dist/cursor/model-capabilities.d.ts +3 -10
  21. package/dist/cursor/model-capabilities.js +2 -154
  22. package/dist/cursor/model-input.d.ts +2 -0
  23. package/dist/cursor/model-input.js +11 -0
  24. package/dist/cursor/selection-descriptor.js +1 -0
  25. package/dist/providers/data/.manifest.json +1 -1
  26. package/dist/providers/data/huggingface.json +1 -1
  27. package/dist/providers/data/nvidia.json +1 -1
  28. package/dist/providers/data/opencode-go.json +1 -1
  29. package/dist/providers/data/opencode.json +1 -1
  30. package/dist/providers/data/openrouter.json +1 -1
  31. package/dist/providers/data/radius.json +1 -1
  32. package/dist/providers/data/venice.json +1 -1
  33. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  34. package/dist/types.d.ts +26 -0
  35. package/dist/utils/github-copilot-tool-limit.d.ts +1 -1
  36. package/dist/utils/github-copilot-tool-limit.js +24 -1
  37. package/dist/utils/provider-diagnostic-sources.d.ts +2 -1
  38. package/dist/utils/provider-diagnostic-sources.js +14 -2
  39. package/dist/utils/reported-count.d.ts +6 -0
  40. package/dist/utils/reported-count.js +8 -0
  41. package/package.json +2 -2
@@ -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,
@@ -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
@@ -89,7 +89,7 @@ export declare function projectSlot(credential: PooledCredential | undefined, na
89
89
  * `onAllocated` reports the account this login committed, so a caller can name
90
90
  * it in a receipt without re-reading storage.
91
91
  */
92
- export declare function appendLoginSlot(current: PooledCredential | undefined, flat: Credential, onAllocated?: (name: string, origin: "generated" | "provider") => void): Credential;
92
+ export declare function appendLoginSlot(stored: PooledCredential | undefined, flat: Credential, onAllocated?: (name: string, origin: "generated" | "provider") => void): Credential;
93
93
  /**
94
94
  * Material a provider writes into the flat credential when the real token lives
95
95
  * outside auth.json (an SDK subprocess or a vendor CLI owns it): the literal
@@ -306,7 +306,10 @@ function hasNewerMaterial(candidate, stored) {
306
306
  * `onAllocated` reports the account this login committed, so a caller can name
307
307
  * it in a receipt without re-reading storage.
308
308
  */
309
- export function appendLoginSlot(current, flat, onAllocated) {
309
+ export function appendLoginSlot(stored, flat, onAllocated) {
310
+ // A pool holds one kind: a login of the other kind replaces the credential instead of joining it
311
+ // as a slot nothing can project (senpi#3448). The caller backs up what it replaces.
312
+ const current = stored?.type === flat.type ? stored : undefined;
310
313
  const provided = providedSlots(flat);
311
314
  if (provided) {
312
315
  // Provider-owned envelopes identify an addition by immutable ID, never by
@@ -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
@@ -0,0 +1,183 @@
1
+ const P = "parameters";
2
+ const V = "variant-id";
3
+ function ladder(values, encoding = P) {
4
+ const out = {};
5
+ for (const value of values)
6
+ out[value === "none" ? "off" : value] = { value, encoding };
7
+ return out;
8
+ }
9
+ const CLAUDE_ORDER = ["thinking", "context", "effort"];
10
+ const GPT_ORDER = ["context", "reasoning", "fast"];
11
+ function claude(window, maxWindow, levels, defaultContext) {
12
+ return {
13
+ evidence: "available-models",
14
+ window,
15
+ maxWindow,
16
+ parameterOrder: CLAUDE_ORDER,
17
+ defaultContext,
18
+ requestContext: window >= 1_000_000 ? "1m" : defaultContext,
19
+ levels: ladder(levels),
20
+ };
21
+ }
22
+ function gpt(levels, order = GPT_ORDER, window = 400000, maxWindow = 400000) {
23
+ return {
24
+ evidence: "available-models",
25
+ window,
26
+ maxWindow,
27
+ parameterOrder: order,
28
+ defaultContext: "272k",
29
+ requestContext: order.includes("context") && window >= 1_000_000 ? "1m" : undefined,
30
+ levels: ladder(levels),
31
+ };
32
+ }
33
+ const LEVELS_LOW_MAX = ["low", "medium", "high", "xhigh", "max"];
34
+ const CONTEXT_TOKEN = { 300000: "300k", 256000: "256k" };
35
+ /** A family whose window is the default context option the server reported serving. */
36
+ function served(window, maxWindow, order, levels) {
37
+ const context = order.includes("context") ? CONTEXT_TOKEN[window] : undefined;
38
+ return {
39
+ evidence: "served-checkpoint",
40
+ window,
41
+ ...(maxWindow !== undefined ? { maxWindow } : {}),
42
+ parameterOrder: order,
43
+ ...(context !== undefined ? { defaultContext: context, requestContext: context } : {}),
44
+ levels: ladder(levels),
45
+ };
46
+ }
47
+ /**
48
+ * Static cursor capability table. Values come from the live
49
+ * aiserver.v1 AvailableModels capture of 2026-08-18 (evidence:
50
+ * local-ignore/qa-evidence/20260818-cursor-reasoning-levels/available-models-catalog.json).
51
+ * `GetUsableModels` carries no window or parameter field, so this table is the
52
+ * authoritative source; see .omo/plans/cursor-reasoning-levels.md §3.1/§6.
53
+ */
54
+ export const CURSOR_MODEL_CAPABILITIES = {
55
+ "claude-fable-5": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"),
56
+ "claude-sonnet-5": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"),
57
+ "claude-opus-4-7": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"),
58
+ "claude-opus-4-8": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"),
59
+ "claude-opus-5": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"),
60
+ "claude-4.6-opus": claude(1000000, 1000000, ["high", "max"], "200k"),
61
+ "claude-4.6-sonnet": claude(1000000, 1000000, ["medium"], "200k"),
62
+ "claude-4.5-opus": {
63
+ evidence: "available-models",
64
+ window: 200000,
65
+ parameterOrder: ["thinking"],
66
+ levels: { high: { value: "high", encoding: V } },
67
+ },
68
+ "gpt-5.6-sol": gpt(["none", "low", "medium", "high", "xhigh", "max"], GPT_ORDER, 1000000, 1000000),
69
+ "gpt-5.6-luna": gpt(["none", "low", "medium", "high", "xhigh", "max"], GPT_ORDER, 1000000, 1000000),
70
+ "gpt-5.6-terra": gpt(["none", "low", "medium", "high", "xhigh", "max"], GPT_ORDER, 272000, 272000),
71
+ "gpt-5.5": {
72
+ ...gpt(["none", "low", "medium", "high"], GPT_ORDER, 1000000, 1000000),
73
+ levels: { ...ladder(["none", "low", "medium", "high"]), xhigh: { value: "extra-high", encoding: P } },
74
+ },
75
+ "gpt-5.3-codex": {
76
+ ...gpt([], ["reasoning", "fast"]),
77
+ maxWindow: undefined,
78
+ defaultContext: undefined,
79
+ levels: { ...ladder(["low", "medium", "high"]), xhigh: { value: "extra-high", encoding: P } },
80
+ },
81
+ "gpt-5.1": {
82
+ ...gpt([], ["reasoning"]),
83
+ maxWindow: undefined,
84
+ defaultContext: undefined,
85
+ levels: ladder(["low", "high"]),
86
+ },
87
+ "gpt-5.2": {
88
+ ...gpt([], ["reasoning"]),
89
+ maxWindow: undefined,
90
+ defaultContext: undefined,
91
+ levels: { ...ladder(["low", "high"]), xhigh: { value: "xhigh", encoding: V } },
92
+ },
93
+ "gpt-5.4": {
94
+ ...gpt([], ["reasoning"]),
95
+ maxWindow: undefined,
96
+ defaultContext: undefined,
97
+ levels: { ...ladder(["low", "medium", "high"]), xhigh: { value: "xhigh", encoding: V } },
98
+ },
99
+ "gpt-5.4-mini": {
100
+ evidence: "available-models",
101
+ window: 400_000,
102
+ parameterOrder: [],
103
+ levels: ladder(["none", "low", "medium", "high", "xhigh"], V),
104
+ },
105
+ "gpt-5.4-nano": {
106
+ ...gpt([], ["reasoning"]),
107
+ maxWindow: undefined,
108
+ defaultContext: undefined,
109
+ levels: ladder(["none", "low", "medium", "high", "xhigh"]),
110
+ },
111
+ "gemini-3.7-flash": {
112
+ evidence: "cli-live",
113
+ window: 1_048_576,
114
+ parameterOrder: ["effort"],
115
+ levels: ladder(["low", "medium", "high"]),
116
+ },
117
+ "gemini-3.6-flash": {
118
+ evidence: "suffix-only",
119
+ window: 1_048_576,
120
+ parameterOrder: [],
121
+ levels: ladder(["minimal", "low", "medium", "high"], V),
122
+ },
123
+ "cursor-grok-4.6": {
124
+ evidence: "available-models",
125
+ window: 500_000,
126
+ parameterOrder: ["effort", "fast"],
127
+ levels: ladder(["low", "medium", "high", "xhigh"]),
128
+ },
129
+ "cursor-grok-4.5": {
130
+ evidence: "suffix-only",
131
+ window: 500_000,
132
+ parameterOrder: [],
133
+ levels: ladder(["low", "medium", "high"], V),
134
+ },
135
+ "glm-5.2": {
136
+ evidence: "available-models",
137
+ window: 1000000,
138
+ parameterOrder: ["reasoning"],
139
+ levels: ladder(["high", "max"]),
140
+ },
141
+ "kimi-k3": {
142
+ evidence: "available-models",
143
+ window: 1048576,
144
+ parameterOrder: ["reasoning"],
145
+ levels: ladder(["low", "high", "max"]),
146
+ },
147
+ "composer-2.5": { evidence: "available-models", window: 200000, parameterOrder: ["fast"], levels: {} },
148
+ "claude-haiku-4-5": { evidence: "available-models", window: 200000, parameterOrder: ["thinking"], levels: {} },
149
+ "claude-4-sonnet": {
150
+ evidence: "available-models",
151
+ window: 200000,
152
+ parameterOrder: ["thinking", "context"],
153
+ defaultContext: "200k",
154
+ levels: {},
155
+ },
156
+ "claude-4.5-sonnet": {
157
+ evidence: "available-models",
158
+ window: 200_000,
159
+ parameterOrder: ["thinking", "context"],
160
+ defaultContext: "200k",
161
+ levels: {},
162
+ },
163
+ "kimi-k2.7-code": { evidence: "available-models", window: 262_144, parameterOrder: [], levels: {} },
164
+ "gemini-3-flash": { evidence: "available-models", window: 1000000, parameterOrder: [], levels: {} },
165
+ "gemini-3.1-pro": { evidence: "available-models", window: 1000000, parameterOrder: [], levels: {} },
166
+ "gemini-3.5-flash": { evidence: "available-models", window: 1_048_576, parameterOrder: [], levels: {} },
167
+ "gpt-5-mini": { evidence: "available-models", window: 400_000, parameterOrder: [], levels: {} },
168
+ // Families published after the 2026-08-18 capture (senpi#3620). Windows are the
169
+ // ceilings the server reported on the first checkpoint on 2026-10-18 for the
170
+ // suffix variant senpi sends (no context parameter, so Cursor's default option);
171
+ // maxWindow is the largest context option AvailableModels offers.
172
+ "claude-opus-5-5": served(300_000, 1_000_000, ["context", "effort", "fast"], LEVELS_LOW_MAX),
173
+ "claude-fable-5-1": served(300_000, 1_000_000, ["thinking", "context", "effort"], LEVELS_LOW_MAX),
174
+ "claude-sonnet-5-5": served(300_000, 1_000_000, ["context", "reasoning_effort"], LEVELS_LOW_MAX),
175
+ "claude-haiku-5-5": served(300_000, 1_000_000, ["thinking", "context", "reasoning_effort"], LEVELS_LOW_MAX),
176
+ "muse-wonder-1.0": served(300_000, 1_000_000, ["context", "reasoning_effort"], ["minimal", ...LEVELS_LOW_MAX]),
177
+ "muse-spark-1.3": served(300_000, 1_000_000, ["context", "effort"], ["minimal", ...LEVELS_LOW_MAX]),
178
+ "grok-4.7": served(256_000, 500_000, ["context", "reasoning_effort", "fast"], ["low", "medium", "high", "xhigh"]),
179
+ "gemini-4-argon": served(200_000, undefined, ["reasoning_effort"], ["low", "medium", "high"]),
180
+ "gemini-3.8-flash": served(200_000, undefined, ["reasoning_effort"], ["low", "medium", "high"]),
181
+ default: served(256_000, undefined, [], []),
182
+ };
183
+ //# sourceMappingURL=capability-table.js.map
@@ -1,32 +1,10 @@
1
1
  import aliasData from "./cursor-variant-aliases.json" with { type: "json" };
2
+ import { cleanCursorGroupName, dropUnservedSizeToken, sanitizeCursorDisplayName } from "./display-name.js";
2
3
  import { CURSOR_MODEL_CAPABILITIES, getCursorVariantAlias, parseCursorVariantId, } from "./model-capabilities.js";
3
4
  export { parseCursorVariantId } from "./model-capabilities.js";
4
5
  const ALL_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
5
6
  const FALLBACK_WINDOW = 200000;
6
7
  const STATIC_TARGETS = new Set(Object.values(aliasData.aliases).map((alias) => alias.targetId));
7
- function isClaude(baseId) {
8
- return baseId.startsWith("claude-");
9
- }
10
- function cleanName(members, baseId, thinkingMode) {
11
- const levelWords = /(Minimal|Low|Medium|High|Extra High|XHigh|Max|None)/g;
12
- const representative = members.find((member) => member.level === "high") ??
13
- members.find((member) => member.level === "medium") ??
14
- members.find((member) => member.level === "low") ??
15
- members[0];
16
- let name = representative.raw.name;
17
- name = name.replace(/\s*\(NO ZDR\)\s*/g, " \u0001").trim();
18
- name = name.replace(new RegExp(`\\s+${levelWords.source}\\b`, "g"), "");
19
- name = name.replace(/\s+Fast\b/g, "");
20
- if (thinkingMode !== true)
21
- name = name.replace(/\s+Thinking\b/g, "");
22
- name = name
23
- .replace(/\u0001/g, "(NO ZDR)")
24
- .replace(/\s+/g, " ")
25
- .trim();
26
- if (name.length === 0)
27
- name = baseId;
28
- return name;
29
- }
30
8
  function buildLevelMap(members, capability) {
31
9
  const observed = new Set(members.map((member) => member.level).filter((level) => level !== undefined));
32
10
  const map = {};
@@ -196,10 +174,10 @@ export function normalizeCursorCatalog(rawEntries) {
196
174
  const isGrouped = members.length > 1 || first.alias.targetId !== first.raw.id;
197
175
  const efforts = members.filter((member) => member.level !== undefined && member.level !== "none");
198
176
  if (isGrouped && efforts.length > 0 && !first.fast) {
199
- const thinkingMode = isClaude(baseId) ? first.thinking === true : undefined;
177
+ const thinkingMode = baseId.startsWith("claude-") ? first.thinking === true : undefined;
200
178
  out.push({
201
179
  id: first.alias.targetId,
202
- name: cleanName(members, baseId, thinkingMode),
180
+ name: dropUnservedSizeToken(cleanCursorGroupName(members, baseId, thinkingMode), capability),
203
181
  reasoning: true,
204
182
  thinkingLevelMap: derivedGroup ? buildDerivedLevelMap(members) : buildLevelMap(members, capability),
205
183
  window: capability?.window ?? FALLBACK_WINDOW,
@@ -219,7 +197,7 @@ export function normalizeCursorCatalog(rawEntries) {
219
197
  const memberCapability = CURSOR_MODEL_CAPABILITIES[memberBase];
220
198
  out.push({
221
199
  id: member.raw.id,
222
- name: member.raw.name,
200
+ name: dropUnservedSizeToken(sanitizeCursorDisplayName(member.raw.name), memberCapability),
223
201
  reasoning: false,
224
202
  window: memberCapability?.window ?? FALLBACK_WINDOW,
225
203
  ...(memberCapability?.maxWindow !== undefined ? { maxWindow: memberCapability.maxWindow } : {}),
@@ -0,0 +1,31 @@
1
+ /** Cursor display-name cleanup for catalog identities (senpi#3620). */
2
+ export interface CursorNamedMember {
3
+ readonly raw: {
4
+ readonly name: string;
5
+ };
6
+ readonly level: string | undefined;
7
+ }
8
+ /** Drops zero-width characters and collapses whitespace in a raw Cursor label. */
9
+ export declare function sanitizeCursorDisplayName(name: string): string;
10
+ /**
11
+ * Names a grouped identity after its representative variant, without the level, fast and
12
+ * thinking words that the identity's level map and thinking flag now carry. Cursor labels
13
+ * the Haiku 5.5 split the other way round ("No Thinking" on the plain variants, nothing on
14
+ * the thinking ones), so the plain group drops "No Thinking" whole and a thinking group
15
+ * whose label never said so gains "Thinking".
16
+ */
17
+ export declare function cleanCursorGroupName(members: readonly CursorNamedMember[], baseId: string, thinkingMode: boolean | undefined): string;
18
+ /** A window the server was measured serving; `window` is what senpi sends. */
19
+ export interface CursorSizedCapability {
20
+ readonly evidence?: string;
21
+ readonly window: number;
22
+ }
23
+ /**
24
+ * A size token may stay in a display name only while the served window backs it. Cursor
25
+ * labels the 2026-10 generation "1M" for the extended-context tier; the checkpoint the
26
+ * catalog measures reports 300k for the default option senpi requests, so the token
27
+ * misleads (senpi#3620 review LOW-1). Entries whose window is only advertised
28
+ * (`available-models`) keep Cursor's label; their mismatch is senpi#3621's call.
29
+ */
30
+ export declare function dropUnservedSizeToken(name: string, capability: CursorSizedCapability | undefined): string;
31
+ //# sourceMappingURL=display-name.d.ts.map