@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.
- package/dist/api/anthropic-messages.js +34 -4
- package/dist/api/azure-openai-responses.js +2 -2
- package/dist/api/cursor-agent.js +2 -6
- package/dist/api/google-generative-ai.js +3 -1
- package/dist/api/google-vertex.js +3 -1
- package/dist/api/openai-codex-responses.js +4 -4
- package/dist/api/openai-completions.js +3 -1
- package/dist/api/openai-prompt-cache.d.ts +3 -0
- package/dist/api/openai-prompt-cache.js +22 -0
- package/dist/api/openai-responses-shared.js +5 -2
- package/dist/api/openai-responses.js +4 -7
- package/dist/api/pi-messages.js +8 -1
- package/dist/auth/pool/kinds.d.ts +17 -0
- package/dist/auth/pool/kinds.js +32 -0
- package/dist/auth/pool/slots.d.ts +13 -1
- package/dist/auth/pool/slots.js +33 -1
- package/dist/auth/types.d.ts +7 -0
- package/dist/cursor/capability-table.d.ts +10 -0
- package/dist/cursor/capability-table.js +183 -0
- package/dist/cursor/catalog-grouping.js +4 -26
- package/dist/cursor/display-name.d.ts +31 -0
- package/dist/cursor/display-name.js +48 -0
- package/dist/cursor/model-capabilities.d.ts +3 -10
- package/dist/cursor/model-capabilities.js +2 -154
- package/dist/cursor/model-input.d.ts +2 -0
- package/dist/cursor/model-input.js +11 -0
- package/dist/cursor/selection-descriptor.js +1 -0
- package/dist/models.js +4 -2
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/fireworks.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/kimi-coding-auth.js +2 -0
- package/dist/tool-call-middleware/stream-message-metadata.js +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/utils/estimate.js +3 -0
- package/dist/utils/fable55-launch.d.ts +21 -0
- package/dist/utils/fable55-launch.js +37 -0
- package/dist/utils/github-copilot-tool-limit.d.ts +1 -1
- package/dist/utils/github-copilot-tool-limit.js +24 -1
- package/dist/utils/overflow.js +10 -6
- package/dist/utils/provider-diagnostic-sources.d.ts +2 -1
- package/dist/utils/provider-diagnostic-sources.js +14 -2
- package/dist/utils/reported-count.d.ts +6 -0
- package/dist/utils/reported-count.js +8 -0
- 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
|
-
|
|
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 {
|
|
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:
|
|
236
|
+
: { prompt_cache_key: deriveOpenAIPromptCacheKey(model.id, context) }),
|
|
237
237
|
store: false,
|
|
238
238
|
};
|
|
239
239
|
if (options?.maxTokens) {
|
package/dist/api/cursor-agent.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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,
|
package/dist/api/pi-messages.js
CHANGED
|
@@ -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(
|
|
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
|
package/dist/auth/pool/slots.js
CHANGED
|
@@ -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(
|
|
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
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -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
|