@code-yeongyu/senpi-ai 2026.10.10-6 → 2026.10.10-9
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 +97 -37
- package/dist/api/anthropic-tool-references.js +24 -18
- package/dist/api/bedrock-converse-stream.js +5 -1
- package/dist/api/lazy.js +13 -0
- package/dist/api/mistral-conversations.js +18 -3
- package/dist/auth/oauth/anthropic.js +4 -3
- package/dist/auth/oauth/chatgpt-subscription.js +3 -2
- package/dist/auth/oauth/cursor.js +3 -2
- package/dist/auth/oauth/devin-token.js +2 -1
- package/dist/auth/oauth/github-copilot.js +3 -2
- package/dist/auth/oauth/kimi-coding.js +5 -4
- package/dist/auth/oauth/openai-chatgpt.js +2 -1
- package/dist/auth/oauth/openrouter.js +2 -1
- package/dist/auth/oauth/radius.js +5 -5
- package/dist/auth/oauth/xai.js +7 -4
- package/dist/auth/oauth-refresh.js +10 -0
- package/dist/auth/resolve.js +5 -0
- package/dist/providers/all.d.ts +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/anthropic.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.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.models.d.ts +1 -1
- package/dist/providers/kimi-coding.models.js +1 -1
- package/dist/providers/opengateway-refresh.js +2 -1
- package/dist/utils/oauth-refresh-error.d.ts +16 -0
- package/dist/utils/oauth-refresh-error.js +67 -0
- package/dist/utils/prompt-cache-ttl.js +3 -1
- package/dist/utils/retry.d.ts +6 -0
- package/dist/utils/retry.js +10 -0
- package/package.json +2 -2
|
@@ -71,7 +71,7 @@ function getCacheControl(model, cacheRetention, env) {
|
|
|
71
71
|
// The bundled Claude Code version and the floor of the advertised `claude-cli/<version>`
|
|
72
72
|
// (see utils/claude-code-version.ts). Keep this exact declaration: a downstream installer
|
|
73
73
|
// (oh-my-openagent) rewrites it byte-for-byte in the installed dist and bundle.
|
|
74
|
-
const claudeCodeVersion = "2.1.
|
|
74
|
+
const claudeCodeVersion = "2.1.292";
|
|
75
75
|
// Claude Code 2.x tool names (canonical casing)
|
|
76
76
|
// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md
|
|
77
77
|
// To update: https://github.com/badlogic/cchistory
|
|
@@ -164,6 +164,7 @@ const ADAPTIVE_THINKING_MODEL_MARKERS = [
|
|
|
164
164
|
"opus-5",
|
|
165
165
|
"sonnet-4-6",
|
|
166
166
|
"sonnet-5",
|
|
167
|
+
"haiku-5-5",
|
|
167
168
|
"fable-5",
|
|
168
169
|
"mythos-5",
|
|
169
170
|
];
|
|
@@ -176,14 +177,20 @@ const NATIVE_XHIGH_EFFORT_MODEL_MARKERS = [
|
|
|
176
177
|
"opus-4-8",
|
|
177
178
|
"opus-5",
|
|
178
179
|
"sonnet-5",
|
|
180
|
+
"haiku-5-5",
|
|
179
181
|
"fable-5",
|
|
180
182
|
"mythos-5",
|
|
181
183
|
];
|
|
182
184
|
/**
|
|
183
|
-
*
|
|
184
|
-
* `"thinking.type.disabled" is not supported for this model`)
|
|
185
|
-
*
|
|
186
|
-
*
|
|
185
|
+
* Families senpi never sends `thinking: {type: "disabled"}` to. Fable 5, Opus 5.5 and Sonnet 5.5 reject it
|
|
186
|
+
* outright (verified live 400: `"thinking.type.disabled" is not supported for this model`); Mythos 5 is
|
|
187
|
+
* listed by family with Fable. Claude Haiku 5.5 is listed by choice, not because it rejects `disabled`: its
|
|
188
|
+
* effort docs accept `disabled` at effort `high` or below (400 only at `xhigh` / `max`) and reject a
|
|
189
|
+
* differing per-message effort while disabled, so a real thinking-off needs marker handling first
|
|
190
|
+
* (senpi#2927). Until then a thinking-off turn sends no `thinking` and effort `low`, which is valid on all of
|
|
191
|
+
* them. The generated catalog also encodes this as `compat.supportsDisabledThinking: false`, but
|
|
192
|
+
* `models.json` entries and third-party gateway rows carry no generated compat, so the family fact has to
|
|
193
|
+
* live here as well.
|
|
187
194
|
*/
|
|
188
195
|
const DISABLED_THINKING_REJECTING_MODEL_MARKERS = [
|
|
189
196
|
"fable-5",
|
|
@@ -192,6 +199,8 @@ const DISABLED_THINKING_REJECTING_MODEL_MARKERS = [
|
|
|
192
199
|
"opus-5.5",
|
|
193
200
|
"sonnet-5-5",
|
|
194
201
|
"sonnet-5.5",
|
|
202
|
+
"haiku-5-5",
|
|
203
|
+
"haiku-5.5",
|
|
195
204
|
];
|
|
196
205
|
const UNSUPPORTED_NATIVE_COMPUTER_TOOL_MODEL_MARKERS = [
|
|
197
206
|
"opus-4-6",
|
|
@@ -922,7 +931,7 @@ export const stream = (model, context, options) => {
|
|
|
922
931
|
const normalizedContext = resolveTranscript(context, supportsMidConvoSystemMessages(model));
|
|
923
932
|
const currentTools = getCurrentTools(normalizedContext.messages);
|
|
924
933
|
(async () => {
|
|
925
|
-
const providerThinkingLevel = model
|
|
934
|
+
const providerThinkingLevel = managedEffortForRequest(model, options);
|
|
926
935
|
const output = {
|
|
927
936
|
role: "assistant",
|
|
928
937
|
content: [],
|
|
@@ -972,7 +981,7 @@ export const stream = (model, context, options) => {
|
|
|
972
981
|
const cacheRetention = resolveCacheRetention(options?.cacheRetention ?? model.cacheRetention, options?.env, "short");
|
|
973
982
|
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
974
983
|
openClient = () => {
|
|
975
|
-
const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined, optionsHeaders, options?.fetch, copilotDynamicHeaders, cacheSessionId, options?.env, federation);
|
|
984
|
+
const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined && configuredBetaHeader(model, options) !== null, optionsHeaders, options?.fetch, copilotDynamicHeaders, cacheSessionId, options?.env, federation);
|
|
976
985
|
client = created.client;
|
|
977
986
|
isOAuth = created.isOAuthToken;
|
|
978
987
|
advertisedClaudeCodeVersion = created.claudeCodeVersion;
|
|
@@ -1459,6 +1468,25 @@ function cannotDisableThinking(model, compat) {
|
|
|
1459
1468
|
return true;
|
|
1460
1469
|
return matchesModelMarker(model, DISABLED_THINKING_REJECTING_MODEL_MARKERS);
|
|
1461
1470
|
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Effort a per-message-effort request runs at, which is also the active marker and the level recorded
|
|
1473
|
+
* on the reply: the caller's effort with thinking on, `low` for a thinking-off turn on a family that
|
|
1474
|
+
* cannot disable thinking, and none when thinking is actually disabled (senpi#2912).
|
|
1475
|
+
*/
|
|
1476
|
+
function managedEffortForRequest(model, options) {
|
|
1477
|
+
if (model.compat?.supportsMidConvoEffort !== true)
|
|
1478
|
+
return undefined;
|
|
1479
|
+
if (options?.thinkingEnabled === false) {
|
|
1480
|
+
return cannotDisableThinking(model, getAnthropicCompat(model)) ? "low" : undefined;
|
|
1481
|
+
}
|
|
1482
|
+
return options?.effort ?? "high";
|
|
1483
|
+
}
|
|
1484
|
+
function isEffortMarker(message) {
|
|
1485
|
+
return (message.role === "system" &&
|
|
1486
|
+
Array.isArray(message.content) &&
|
|
1487
|
+
message.content.length === 0 &&
|
|
1488
|
+
message.output_config?.effort !== undefined);
|
|
1489
|
+
}
|
|
1462
1490
|
function disableThinkingForRequest(params, model, compat) {
|
|
1463
1491
|
// A degraded/disabled turn must not retain the caller's higher effort.
|
|
1464
1492
|
delete params.output_config;
|
|
@@ -1470,6 +1498,8 @@ function disableThinkingForRequest(params, model, compat) {
|
|
|
1470
1498
|
return;
|
|
1471
1499
|
}
|
|
1472
1500
|
params.thinking = { type: "disabled" };
|
|
1501
|
+
// Per-message effort is rejected alongside disabled thinking (senpi#1399).
|
|
1502
|
+
params.messages = params.messages.filter((message) => !isEffortMarker(message));
|
|
1473
1503
|
}
|
|
1474
1504
|
function supportsAdaptiveThinking(model) {
|
|
1475
1505
|
if (model.compat?.forceAdaptiveThinking !== undefined) {
|
|
@@ -1665,27 +1695,46 @@ export function buildAnthropicWarmPromptCacheParams(model, context, options) {
|
|
|
1665
1695
|
max_tokens: 0,
|
|
1666
1696
|
});
|
|
1667
1697
|
}
|
|
1668
|
-
function
|
|
1669
|
-
let
|
|
1698
|
+
function configuredBetaHeader(model, options) {
|
|
1699
|
+
let configured;
|
|
1670
1700
|
for (const headers of [model.headers, options?.headers]) {
|
|
1671
1701
|
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
1672
1702
|
if (name.toLowerCase() === "anthropic-beta")
|
|
1673
|
-
|
|
1703
|
+
configured = value;
|
|
1674
1704
|
}
|
|
1675
1705
|
}
|
|
1676
|
-
|
|
1706
|
+
return configured;
|
|
1707
|
+
}
|
|
1708
|
+
function getBetaFeatures(model, context, isOAuthToken, shape, options) {
|
|
1709
|
+
const configured = configuredBetaHeader(model, options);
|
|
1710
|
+
if (configured === null)
|
|
1677
1711
|
return [];
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
...new Set(configuredFeatures
|
|
1681
|
-
.split(",")
|
|
1682
|
-
.map((feature) => feature.trim())
|
|
1683
|
-
.filter(Boolean)),
|
|
1684
|
-
];
|
|
1685
|
-
}
|
|
1686
|
-
const features = [];
|
|
1712
|
+
// Betas the request body depends on: the API rejects the request without them.
|
|
1713
|
+
const required = [];
|
|
1687
1714
|
if (isOAuthToken)
|
|
1688
|
-
|
|
1715
|
+
required.push("claude-code-20250219", "oauth-2025-04-20");
|
|
1716
|
+
if (shape.midConvoEffort)
|
|
1717
|
+
required.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA);
|
|
1718
|
+
if (shape.thinkingBinding)
|
|
1719
|
+
required.push(THINKING_BINDING_CONTROLS_BETA);
|
|
1720
|
+
if (shape.nativeToolChanges)
|
|
1721
|
+
required.push(MID_CONVERSATION_TOOL_CHANGES_BETA);
|
|
1722
|
+
if (shape.refusalFallbacks)
|
|
1723
|
+
required.push(SERVER_SIDE_FALLBACK_BETA);
|
|
1724
|
+
// A configured header owns the optional betas and is merged with the required ones, never replacing them: a
|
|
1725
|
+
// proxy route's own header used to drop the effort beta, and Claude 5.5 models then 400ed (senpi#2957).
|
|
1726
|
+
// Interleaved thinking is stripped on adaptive models here too, as it is from the client headers.
|
|
1727
|
+
const optional = configured === undefined
|
|
1728
|
+
? defaultOptionalBetas(model, context, options)
|
|
1729
|
+
: configured
|
|
1730
|
+
.split(",")
|
|
1731
|
+
.map((feature) => feature.trim())
|
|
1732
|
+
.filter(Boolean)
|
|
1733
|
+
.filter((feature) => !(feature === INTERLEAVED_THINKING_BETA && supportsAdaptiveThinking(model)));
|
|
1734
|
+
return [...new Set([...optional, ...required])];
|
|
1735
|
+
}
|
|
1736
|
+
function defaultOptionalBetas(model, context, options) {
|
|
1737
|
+
const features = [];
|
|
1689
1738
|
if (shouldUseFineGrainedToolStreamingBeta(model, context))
|
|
1690
1739
|
features.push(FINE_GRAINED_TOOL_STREAMING_BETA);
|
|
1691
1740
|
if (model.reasoning &&
|
|
@@ -1696,12 +1745,7 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
|
|
|
1696
1745
|
}
|
|
1697
1746
|
if (shouldUseServerSideFallbackBeta(model))
|
|
1698
1747
|
features.push(SERVER_SIDE_FALLBACK_BETA);
|
|
1699
|
-
|
|
1700
|
-
features.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA, THINKING_BINDING_CONTROLS_BETA);
|
|
1701
|
-
}
|
|
1702
|
-
if (nativeToolChanges)
|
|
1703
|
-
features.push(MID_CONVERSATION_TOOL_CHANGES_BETA);
|
|
1704
|
-
return [...new Set(features)];
|
|
1748
|
+
return features;
|
|
1705
1749
|
}
|
|
1706
1750
|
function buildParams(model, context, isOAuthToken, options, unsignedThinkingReplay = getAnthropicCompat(model).unsignedThinkingReplay) {
|
|
1707
1751
|
const compat = getAnthropicCompat(model);
|
|
@@ -1718,7 +1762,16 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1718
1762
|
// and Anthropic rejects a tool list where every tool is deferred, so there must be an
|
|
1719
1763
|
// initial active tool to anchor the deferred ones. Otherwise the current tool list is sent.
|
|
1720
1764
|
const initialTools = initialSystemMessage?.toolsAdded ?? [];
|
|
1721
|
-
|
|
1765
|
+
// A configured `anthropic-beta: null` asks for no betas at all, so the request takes a shape that needs none:
|
|
1766
|
+
// the current tool list instead of native tool changes, top-level effort instead of per-message markers, and
|
|
1767
|
+
// no server-side fallbacks (senpi#2957). OAuth still needs its identity betas; getBetaFeatures names them.
|
|
1768
|
+
const betasSuppressed = configuredBetaHeader(model, options) === null;
|
|
1769
|
+
if (betasSuppressed && isOAuthToken) {
|
|
1770
|
+
throw new Error(`The anthropic-beta header is set to null, but ${model.provider}/${model.id} is called with an OAuth token, which needs the claude-code-20250219 and oauth-2025-04-20 betas. Remove the null, or list the betas you want instead; senpi adds the ones a request needs.`);
|
|
1771
|
+
}
|
|
1772
|
+
const midConvoEffort = model.compat?.supportsMidConvoEffort === true && !betasSuppressed;
|
|
1773
|
+
const nativeToolChanges = !betasSuppressed &&
|
|
1774
|
+
supportsMidConvoSystemMessages(model) &&
|
|
1722
1775
|
supportsMidConvoToolChanges(model) &&
|
|
1723
1776
|
initialTools.length > 0 &&
|
|
1724
1777
|
!hasToolRedefinitions(context.messages);
|
|
@@ -1741,15 +1794,21 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1741
1794
|
deferredTools = [];
|
|
1742
1795
|
}
|
|
1743
1796
|
const deferredToolNames = new Set(deferredTools.map((tool) => normalizeToolName(tool.name)));
|
|
1744
|
-
const activeEffort = options
|
|
1745
|
-
const
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1797
|
+
const activeEffort = managedEffortForRequest(model, options) ?? "high";
|
|
1798
|
+
const sendFallbacks = options?.refusalFallbacks !== undefined && !betasSuppressed;
|
|
1799
|
+
const betaFeatures = getBetaFeatures(model, context, isOAuthToken, {
|
|
1800
|
+
midConvoEffort,
|
|
1801
|
+
thinkingBinding: midConvoEffort && options?.thinkingEnabled !== false,
|
|
1802
|
+
nativeToolChanges,
|
|
1803
|
+
refusalFallbacks: sendFallbacks,
|
|
1804
|
+
}, options);
|
|
1805
|
+
const convertedMessages = convertMessages(conversationMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay, deferredToolNames, normalizeToolName, midConvoEffort ? model.provider : undefined, nativeToolChanges);
|
|
1806
|
+
const messages = midConvoEffort
|
|
1748
1807
|
? insertThinkingLevelMessages(convertedMessages, activeEffort)
|
|
1749
1808
|
: convertedMessages.messages;
|
|
1750
1809
|
// Effort markers are appended after history; re-run the fork's final-user cache checkpoint
|
|
1751
1810
|
// against the actual last user/tool-result message so the marker cannot displace cache_control.
|
|
1752
|
-
if (
|
|
1811
|
+
if (midConvoEffort && cacheControl) {
|
|
1753
1812
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1754
1813
|
const message = messages[index];
|
|
1755
1814
|
// A native system update after the last turn already holds the checkpoint;
|
|
@@ -1826,9 +1885,10 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1826
1885
|
];
|
|
1827
1886
|
}
|
|
1828
1887
|
// Managed effort models always use adaptive thinking so prefix mismatches can
|
|
1829
|
-
// be dropped instead of surfacing as persistent 400 responses.
|
|
1830
|
-
//
|
|
1831
|
-
|
|
1888
|
+
// be dropped instead of surfacing as persistent 400 responses. A thinking-off turn
|
|
1889
|
+
// skips this branch: `disableThinkingForRequest` sends `disabled` where the family
|
|
1890
|
+
// accepts it and pins effort `low` where it does not (Opus/Sonnet/Haiku 5.5, Fable, Mythos).
|
|
1891
|
+
if (midConvoEffort && options?.thinkingEnabled !== false) {
|
|
1832
1892
|
params.thinking = {
|
|
1833
1893
|
type: "adaptive",
|
|
1834
1894
|
display: options?.thinkingDisplay ?? "summarized",
|
|
@@ -1897,7 +1957,7 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1897
1957
|
}
|
|
1898
1958
|
}
|
|
1899
1959
|
applyExtraBodyToAnthropicParams(params, options?.extraBody);
|
|
1900
|
-
if (options?.refusalFallbacks !== undefined) {
|
|
1960
|
+
if (sendFallbacks && options?.refusalFallbacks !== undefined) {
|
|
1901
1961
|
// AnthropicRefusalFallback models a readonly list; the SDK's BetaFallbacksParam
|
|
1902
1962
|
// is mutable, so copy rather than widen the fork's public type.
|
|
1903
1963
|
params.fallbacks = options.refusalFallbacks === "default" ? "default" : [...options.refusalFallbacks];
|
|
@@ -73,6 +73,27 @@ export function demoteUnavailableToolReferences(params) {
|
|
|
73
73
|
}
|
|
74
74
|
let changed = false;
|
|
75
75
|
const availableToolNames = [...available];
|
|
76
|
+
// A native search pair whose every reference stopped resolving is demoted as a
|
|
77
|
+
// unit: the result decides, and its `server_tool_use` follows. The pair can span
|
|
78
|
+
// two assistant messages (a deferred server tool resumes in the continuation), so
|
|
79
|
+
// the decision is collected over the whole request before any message is rewritten.
|
|
80
|
+
const droppedSearchUseIds = new Set();
|
|
81
|
+
const droppedSearchNames = new Map();
|
|
82
|
+
for (const message of messages) {
|
|
83
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
84
|
+
continue;
|
|
85
|
+
for (const block of message.content) {
|
|
86
|
+
if (!isNativeToolSearchResultBlock(block))
|
|
87
|
+
continue;
|
|
88
|
+
const names = block.content.tool_references
|
|
89
|
+
.filter((item) => isRecord(item) && item.type === "tool_reference")
|
|
90
|
+
.map((item) => (typeof item.tool_name === "string" ? item.tool_name : ""));
|
|
91
|
+
if (names.length > 0 && names.every((name) => resolve(name) === undefined)) {
|
|
92
|
+
droppedSearchUseIds.add(block.tool_use_id);
|
|
93
|
+
droppedSearchNames.set(block.tool_use_id, names);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
76
97
|
const seenDemotedCallNames = new Set();
|
|
77
98
|
const rewrittenMessages = [];
|
|
78
99
|
for (const message of messages) {
|
|
@@ -81,23 +102,6 @@ export function demoteUnavailableToolReferences(params) {
|
|
|
81
102
|
continue;
|
|
82
103
|
}
|
|
83
104
|
let messageChanged = false;
|
|
84
|
-
// A native search pair whose every reference stopped resolving is demoted
|
|
85
|
-
// as a unit: the result decides, and its `server_tool_use` follows.
|
|
86
|
-
const droppedSearchUseIds = new Set();
|
|
87
|
-
const droppedSearchNames = new Map();
|
|
88
|
-
if (message.role === "assistant") {
|
|
89
|
-
for (const block of message.content) {
|
|
90
|
-
if (!isNativeToolSearchResultBlock(block))
|
|
91
|
-
continue;
|
|
92
|
-
const names = block.content.tool_references
|
|
93
|
-
.filter((item) => isRecord(item) && item.type === "tool_reference")
|
|
94
|
-
.map((item) => (typeof item.tool_name === "string" ? item.tool_name : ""));
|
|
95
|
-
if (names.length > 0 && names.every((name) => resolve(name) === undefined)) {
|
|
96
|
-
droppedSearchUseIds.add(block.tool_use_id);
|
|
97
|
-
droppedSearchNames.set(block.tool_use_id, names);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
105
|
const content = [];
|
|
102
106
|
for (const block of message.content) {
|
|
103
107
|
if (message.role === "assistant" && isRecord(block) && block.type === "tool_use") {
|
|
@@ -168,7 +172,9 @@ export function demoteUnavailableToolReferences(params) {
|
|
|
168
172
|
}
|
|
169
173
|
content.push(block);
|
|
170
174
|
}
|
|
171
|
-
|
|
175
|
+
// Only a message this pass emptied is dropped; a message that arrived empty (a per-message
|
|
176
|
+
// effort marker carries `content: []` by design) passes through untouched (senpi#2912).
|
|
177
|
+
if (messageChanged && content.length === 0) {
|
|
172
178
|
changed = true;
|
|
173
179
|
continue;
|
|
174
180
|
}
|
|
@@ -590,6 +590,7 @@ function supportsAdaptiveThinking(modelId, modelName) {
|
|
|
590
590
|
s.includes("opus-5") ||
|
|
591
591
|
s.includes("sonnet-4-6") ||
|
|
592
592
|
s.includes("sonnet-5") ||
|
|
593
|
+
s.includes("haiku-5-5") ||
|
|
593
594
|
s.includes("fable-5") ||
|
|
594
595
|
s.includes("mythos-5"));
|
|
595
596
|
}
|
|
@@ -599,6 +600,7 @@ function supportsNativeXhighEffort(model) {
|
|
|
599
600
|
s.includes("opus-4-8") ||
|
|
600
601
|
s.includes("opus-5") ||
|
|
601
602
|
s.includes("sonnet-5") ||
|
|
603
|
+
s.includes("haiku-5-5") ||
|
|
602
604
|
s.includes("fable-5") ||
|
|
603
605
|
s.includes("mythos-5"));
|
|
604
606
|
}
|
|
@@ -616,7 +618,9 @@ function rejectsDisabledThinking(model) {
|
|
|
616
618
|
s.includes("opus-5-5") ||
|
|
617
619
|
s.includes("opus-5.5") ||
|
|
618
620
|
s.includes("sonnet-5-5") ||
|
|
619
|
-
s.includes("sonnet-5.5")
|
|
621
|
+
s.includes("sonnet-5.5") ||
|
|
622
|
+
s.includes("haiku-5-5") ||
|
|
623
|
+
s.includes("haiku-5.5"));
|
|
620
624
|
}
|
|
621
625
|
function mapThinkingLevelToEffort(model, level) {
|
|
622
626
|
if (level === "xhigh" && supportsNativeXhighEffort(model))
|
package/dist/api/lazy.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
2
|
+
import { isOAuthRefreshUnavailableError } from "../utils/oauth-refresh-error.js";
|
|
2
3
|
import { describeReplacedInstall } from "../utils/provider-failure-description.js";
|
|
4
|
+
import { OAUTH_REFRESH_UNAVAILABLE_DIAGNOSTIC } from "../utils/retry.js";
|
|
3
5
|
function createSetupErrorMessage(model, error) {
|
|
4
6
|
return {
|
|
5
7
|
role: "assistant",
|
|
@@ -17,6 +19,17 @@ function createSetupErrorMessage(model, error) {
|
|
|
17
19
|
},
|
|
18
20
|
stopReason: "error",
|
|
19
21
|
errorMessage: describeReplacedInstall(error) ?? (error instanceof Error ? error.message : String(error)),
|
|
22
|
+
...(isOAuthRefreshUnavailableError(error)
|
|
23
|
+
? {
|
|
24
|
+
diagnostics: [
|
|
25
|
+
{
|
|
26
|
+
type: OAUTH_REFRESH_UNAVAILABLE_DIAGNOSTIC,
|
|
27
|
+
timestamp: Date.now(),
|
|
28
|
+
details: { provider: model.provider },
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
}
|
|
32
|
+
: {}),
|
|
20
33
|
timestamp: Date.now(),
|
|
21
34
|
};
|
|
22
35
|
}
|
|
@@ -612,6 +612,21 @@ function stripSymbolKeys(value) {
|
|
|
612
612
|
}
|
|
613
613
|
return value;
|
|
614
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Consecutive user turns become one user message, as the OpenAI Chat converter does for
|
|
617
|
+
* non-OpenAI endpoints: a word message rebuilt after tool results (senpi#2920) is often followed
|
|
618
|
+
* by a steer or the next prompt, and Mistral's role-order validation is not documented to accept
|
|
619
|
+
* two user messages in a row.
|
|
620
|
+
*/
|
|
621
|
+
function appendUserMessage(result, content) {
|
|
622
|
+
const previous = result[result.length - 1];
|
|
623
|
+
if (previous?.role !== "user") {
|
|
624
|
+
result.push({ role: "user", content });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const chunks = (value) => typeof value === "string" ? [{ type: "text", text: value }] : (value ?? []);
|
|
628
|
+
previous.content = [...chunks(previous.content), ...chunks(content)];
|
|
629
|
+
}
|
|
615
630
|
function toChatMessages(messages, supportsImages) {
|
|
616
631
|
const result = [];
|
|
617
632
|
for (const [index, msg] of messages.entries()) {
|
|
@@ -625,7 +640,7 @@ function toChatMessages(messages, supportsImages) {
|
|
|
625
640
|
}
|
|
626
641
|
if (msg.role === "user") {
|
|
627
642
|
if (typeof msg.content === "string") {
|
|
628
|
-
result
|
|
643
|
+
appendUserMessage(result, sanitizeSurrogates(msg.content));
|
|
629
644
|
continue;
|
|
630
645
|
}
|
|
631
646
|
const hadImages = msg.content.some((item) => item.type === "image");
|
|
@@ -637,11 +652,11 @@ function toChatMessages(messages, supportsImages) {
|
|
|
637
652
|
return { type: "image_url", imageUrl: `data:${item.mimeType};base64,${item.data}` };
|
|
638
653
|
});
|
|
639
654
|
if (content.length > 0) {
|
|
640
|
-
result
|
|
655
|
+
appendUserMessage(result, content);
|
|
641
656
|
continue;
|
|
642
657
|
}
|
|
643
658
|
if (hadImages && !supportsImages) {
|
|
644
|
-
result
|
|
659
|
+
appendUserMessage(result, "(image omitted: model does not support images)");
|
|
645
660
|
}
|
|
646
661
|
continue;
|
|
647
662
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* NOTE: This module uses Node.js http.createServer for the OAuth callback server.
|
|
5
5
|
* It is only intended for CLI use, not browser environments.
|
|
6
6
|
*/
|
|
7
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
7
8
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
8
9
|
import { PREFERRED_CALLBACK_PORT, startCallbackListener, } from "./anthropic-callback-listener.js";
|
|
9
10
|
import { parseAuthorizationInput } from "./authorization-input.js";
|
|
@@ -57,7 +58,7 @@ async function postJson(url, body, signal) {
|
|
|
57
58
|
});
|
|
58
59
|
const responseBody = await response.text();
|
|
59
60
|
if (!response.ok) {
|
|
60
|
-
throw new
|
|
61
|
+
throw new OAuthTokenEndpointError(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`, response.status);
|
|
61
62
|
}
|
|
62
63
|
return responseBody;
|
|
63
64
|
}
|
|
@@ -74,7 +75,7 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri, sig
|
|
|
74
75
|
}, signal);
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
|
-
throw new Error(`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}
|
|
78
|
+
throw new Error(`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}`, { cause: error });
|
|
78
79
|
}
|
|
79
80
|
let tokenData;
|
|
80
81
|
try {
|
|
@@ -240,7 +241,7 @@ async function refreshAnthropicToken(refreshToken, signal) {
|
|
|
240
241
|
}, signal);
|
|
241
242
|
}
|
|
242
243
|
catch (error) {
|
|
243
|
-
throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}
|
|
244
|
+
throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`, { cause: error });
|
|
244
245
|
}
|
|
245
246
|
let data;
|
|
246
247
|
try {
|
|
@@ -12,6 +12,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
import { extractChatGptSubscriptionAccountId } from "../../utils/chatgpt-subscription-auth.js";
|
|
15
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
15
16
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
16
17
|
import { getWireIdentity } from "../../wire-identity.js";
|
|
17
18
|
import { startOAuthCallbackServer, waitForCallbackOrManualInput } from "./callback-server.js";
|
|
@@ -80,7 +81,7 @@ async function fetchWithLoginCancellation(input, init) {
|
|
|
80
81
|
async function readTokenResponse(response, operation) {
|
|
81
82
|
if (!response.ok) {
|
|
82
83
|
const text = await response.text().catch(() => "");
|
|
83
|
-
throw new
|
|
84
|
+
throw new OAuthTokenEndpointError(`ChatGPT Subscription token ${operation} failed (${response.status}): ${text || response.statusText}`, response.status);
|
|
84
85
|
}
|
|
85
86
|
const rawJson = await response.json();
|
|
86
87
|
const json = rawJson;
|
|
@@ -123,7 +124,7 @@ async function refreshAccessToken(refreshToken, signal) {
|
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
catch (error) {
|
|
126
|
-
throw new Error(`ChatGPT Subscription token refresh error: ${error instanceof Error ? error.message : String(error)}
|
|
127
|
+
throw new Error(`ChatGPT Subscription token refresh error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
127
128
|
}
|
|
128
129
|
return readTokenResponse(response, "refresh");
|
|
129
130
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* (or a dashboard user API key) at `auth/exchange_user_api_key` for a fresh
|
|
11
11
|
* session JWT.
|
|
12
12
|
*/
|
|
13
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
13
14
|
import { generatePKCE } from "./pkce.js";
|
|
14
15
|
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
15
16
|
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
@@ -205,12 +206,12 @@ async function refreshCursorCredential(credential, signal) {
|
|
|
205
206
|
}
|
|
206
207
|
catch (error) {
|
|
207
208
|
if (signal.aborted)
|
|
208
|
-
throw new Error("Cursor token refresh cancelled");
|
|
209
|
+
throw new Error("Cursor token refresh cancelled", { cause: signal.reason });
|
|
209
210
|
throw error;
|
|
210
211
|
}
|
|
211
212
|
const body = await readJsonBody(response);
|
|
212
213
|
if (!response.ok) {
|
|
213
|
-
throw new
|
|
214
|
+
throw new OAuthTokenEndpointError(`Cursor token refresh failed (${describeFailure(response.status, body)})`, response.status);
|
|
214
215
|
}
|
|
215
216
|
const access = requiredString(body ?? {}, "accessToken");
|
|
216
217
|
const rotated = body?.refreshToken;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* "token" that serves as both the access and the refresh credential. Expiry is
|
|
8
8
|
* read from the JWT itself because the response carries no expires_in.
|
|
9
9
|
*/
|
|
10
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
10
11
|
export const DEVIN_TOKEN_URL = "https://api.devin.ai/auth/cli/token";
|
|
11
12
|
export const DEVIN_API_ENDPOINT = "https://api.devin.ai";
|
|
12
13
|
/** Devin issues long-lived CLI tokens; used when the JWT carries no usable exp. */
|
|
@@ -91,7 +92,7 @@ export async function exchangeDevinAuthorizationCode(code, verifier, signal) {
|
|
|
91
92
|
}
|
|
92
93
|
if (!response.ok) {
|
|
93
94
|
const detail = errorDetail(body);
|
|
94
|
-
throw new
|
|
95
|
+
throw new OAuthTokenEndpointError(`Devin OAuth token exchange failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`, response.status);
|
|
95
96
|
}
|
|
96
97
|
if (typeof body.token !== "string" || body.token.length === 0) {
|
|
97
98
|
throw new Error('Devin OAuth response carries no "token"');
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { GITHUB_COPILOT_INDIVIDUAL_BASE_URL, githubCopilotTokenField, parseGitHubCopilotApiEndpoint, resolveGitHubCopilotBaseUrl, } from "../../api/github-copilot-endpoint.js";
|
|
5
5
|
import { GITHUB_COPILOT_REJECTED_TOKEN_STATUSES } from "../../api/github-copilot-headers.js";
|
|
6
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
6
7
|
import { sleep } from "../../utils/sleep.js";
|
|
7
8
|
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
|
|
8
9
|
import { parseGitHubCopilotModelCatalog } from "./github-copilot-model-catalog.js";
|
|
@@ -79,7 +80,7 @@ async function fetchGitHubCopilotModels(copilotToken, enterpriseDomain, signal,
|
|
|
79
80
|
},
|
|
80
81
|
}, signal, retryPolicy);
|
|
81
82
|
if (!response.ok) {
|
|
82
|
-
throw new
|
|
83
|
+
throw new OAuthTokenEndpointError(`${response.status} ${response.statusText}: ${await response.text()}`, response.status);
|
|
83
84
|
}
|
|
84
85
|
return parseGitHubCopilotModelCatalog(await response.json(), allowPolicyFallback);
|
|
85
86
|
}
|
|
@@ -87,7 +88,7 @@ async function fetchJson(url, init) {
|
|
|
87
88
|
const response = await fetch(url, init);
|
|
88
89
|
if (!response.ok) {
|
|
89
90
|
const text = await response.text();
|
|
90
|
-
throw new
|
|
91
|
+
throw new OAuthTokenEndpointError(`${response.status} ${response.statusText}: ${text}`, response.status);
|
|
91
92
|
}
|
|
92
93
|
return response.json();
|
|
93
94
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* access token authenticates requests to that region's coding API as an
|
|
7
7
|
* `Authorization: Bearer` header; see kimi-region.ts for the host table.
|
|
8
8
|
*/
|
|
9
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
9
10
|
import { sleep } from "../../utils/sleep.js";
|
|
10
11
|
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
|
|
11
12
|
import { kimiCodeIdentityHeaders } from "./kimi-identity.js";
|
|
@@ -178,7 +179,7 @@ async function refreshToken(oauthHost, refreshTokenValue, signal) {
|
|
|
178
179
|
await sleep(1000 * 2 ** (attempt - 1), signal);
|
|
179
180
|
}
|
|
180
181
|
if (signal.aborted) {
|
|
181
|
-
throw new Error("Kimi Code token refresh aborted");
|
|
182
|
+
throw new Error("Kimi Code token refresh aborted", { cause: signal.reason });
|
|
182
183
|
}
|
|
183
184
|
let response;
|
|
184
185
|
try {
|
|
@@ -208,14 +209,14 @@ async function refreshToken(oauthHost, refreshTokenValue, signal) {
|
|
|
208
209
|
// Unauthorized: the stored credential is dead; Models clears it and prompts re-login.
|
|
209
210
|
if (response.status === 401 || response.status === 403 || json?.error === "invalid_grant") {
|
|
210
211
|
const description = typeof json?.error_description === "string" ? `: ${json.error_description}` : "";
|
|
211
|
-
throw new
|
|
212
|
+
throw new OAuthTokenEndpointError(`Kimi Code token refresh unauthorized (status ${response.status})${description}`, response.status);
|
|
212
213
|
}
|
|
213
214
|
if (isRetryableRefreshFailure(response) && attempt < REFRESH_MAX_RETRIES) {
|
|
214
|
-
lastError = new
|
|
215
|
+
lastError = new OAuthTokenEndpointError(`Kimi Code token refresh failed with status ${response.status}`, response.status);
|
|
215
216
|
continue;
|
|
216
217
|
}
|
|
217
218
|
const text = JSON.stringify(json);
|
|
218
|
-
throw new
|
|
219
|
+
throw new OAuthTokenEndpointError(`Kimi Code token refresh failed with status ${response.status}${text ? `: ${text}` : ""}`, response.status);
|
|
219
220
|
}
|
|
220
221
|
throw lastError ?? new Error("Kimi Code token refresh failed");
|
|
221
222
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
9
|
import { oauthErrorHtml, oauthSuccessHtml } from "../../utils/oauth-page.js";
|
|
10
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
10
11
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
11
12
|
import { generatePKCE } from "./pkce.js";
|
|
12
13
|
// every login registers a new client with this ID; OpenAI returns the issued client ID in the callback
|
|
@@ -119,7 +120,7 @@ async function requestToken(body, signal) {
|
|
|
119
120
|
});
|
|
120
121
|
if (!response.ok) {
|
|
121
122
|
const responseBody = await response.text().catch(() => "");
|
|
122
|
-
throw new
|
|
123
|
+
throw new OAuthTokenEndpointError(`OpenAI OAuth token request failed (${response.status}): ${responseBody || response.statusText}`, response.status);
|
|
123
124
|
}
|
|
124
125
|
const data = await response.json();
|
|
125
126
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* NOTE: This module uses node:http (via callback-server.ts) for the OAuth callback server.
|
|
11
11
|
* It is only intended for CLI use, not browser environments.
|
|
12
12
|
*/
|
|
13
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
13
14
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
14
15
|
import { startOAuthCallbackServer, waitForCallbackOrManualInput } from "./callback-server.js";
|
|
15
16
|
import { generatePKCE } from "./pkce.js";
|
|
@@ -88,7 +89,7 @@ async function exchangeAuthorizationCode(code, verifier, signal) {
|
|
|
88
89
|
}
|
|
89
90
|
if (!response.ok) {
|
|
90
91
|
const detail = errorDetail(body);
|
|
91
|
-
throw new
|
|
92
|
+
throw new OAuthTokenEndpointError(`OpenRouter OAuth key exchange failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`, response.status);
|
|
92
93
|
}
|
|
93
94
|
if (typeof body.key !== "string" || body.key.length === 0) {
|
|
94
95
|
throw new Error('OpenRouter OAuth response carries no "key"');
|