qwenproxy-cli 1.0.0 → 1.0.1
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/LICENSE +14 -14
- package/README.md +906 -906
- package/bin/qwenproxy.js +5 -1
- package/package.json +78 -78
- package/src/api/error-classifier.ts +159 -159
- package/src/api/error-helpers.ts +118 -118
- package/src/api/models.ts +261 -261
- package/src/api/server.ts +860 -859
- package/src/cache/memory-cache.ts +385 -385
- package/src/clean-cache.ts +204 -204
- package/src/core/account-concurrency.ts +671 -671
- package/src/core/account-manager.ts +301 -297
- package/src/core/account-priority.ts +163 -163
- package/src/core/accounts.ts +186 -186
- package/src/core/config.ts +383 -383
- package/src/core/crypto-utils.ts +79 -79
- package/src/core/database.ts +276 -276
- package/src/core/errors.ts +118 -118
- package/src/core/logger.ts +269 -269
- package/src/core/memory-usage.ts +84 -84
- package/src/core/metrics.ts +291 -291
- package/src/core/model-alias.ts +77 -77
- package/src/core/model-registry.ts +544 -544
- package/src/core/mutex.ts +119 -119
- package/src/core/paths.ts +199 -199
- package/src/core/prompt-limits.ts +214 -214
- package/src/core/reasoning-effort.ts +102 -102
- package/src/core/stream-registry.ts +96 -96
- package/src/core/waf-isolation.ts +117 -117
- package/src/core/watchdog.ts +195 -195
- package/src/delete-chats.ts +23 -23
- package/src/index.ts +65 -64
- package/src/login.ts +147 -147
- package/src/reset-cooldowns.ts +11 -11
- package/src/routes/anthropic/index.ts +355 -355
- package/src/routes/anthropic/translate.ts +522 -522
- package/src/routes/anthropic/types.ts +154 -154
- package/src/routes/anthropic/validation.ts +144 -144
- package/src/routes/chat/account.ts +1817 -1817
- package/src/routes/chat/context.ts +241 -241
- package/src/routes/chat/errors.ts +85 -85
- package/src/routes/chat/helpers.ts +268 -268
- package/src/routes/chat/index.ts +618 -618
- package/src/routes/chat/media.ts +285 -285
- package/src/routes/chat/retry-policy.ts +754 -754
- package/src/routes/chat/stop.ts +98 -98
- package/src/routes/chat/streaming.ts +2710 -2710
- package/src/routes/chat/validation.ts +526 -526
- package/src/routes/chat.ts +2 -2
- package/src/routes/completions.ts +290 -290
- package/src/routes/images.ts +139 -139
- package/src/routes/responses/adapter.ts +503 -503
- package/src/routes/responses/index.ts +405 -405
- package/src/routes/responses/state.ts +230 -230
- package/src/routes/responses/streaming.ts +528 -528
- package/src/routes/responses/types.ts +285 -285
- package/src/routes/responses/validation.ts +202 -202
- package/src/routes/upload.ts +731 -731
- package/src/routes/videos.ts +214 -214
- package/src/services/auth-playwright.ts +173 -173
- package/src/services/captcha-coordinator.ts +161 -161
- package/src/services/captcha-solver.ts +553 -553
- package/src/services/chat-cleanup.ts +80 -80
- package/src/services/context-meter.ts +317 -317
- package/src/services/fingerprint.ts +242 -242
- package/src/services/human-behavior.ts +173 -173
- package/src/services/media-generation.ts +1748 -1748
- package/src/services/playwright.ts +2800 -2800
- package/src/services/qwen-chat-pool.ts +345 -345
- package/src/services/qwen-errors.ts +133 -133
- package/src/services/qwen-headers.ts +79 -79
- package/src/services/qwen-thread-state.ts +393 -393
- package/src/services/qwen-url.ts +19 -19
- package/src/services/qwen.ts +3126 -3126
- package/src/services/session-keeper.ts +88 -88
- package/src/services/token-estimation-metrics.ts +118 -118
- package/src/sync/claude-code.ts +75 -75
- package/src/sync/codex.ts +123 -123
- package/src/sync/index.ts +362 -362
- package/src/sync/omp.ts +105 -105
- package/src/sync/opencode.ts +214 -214
- package/src/sync/types.ts +53 -53
- package/src/sync/utils.ts +27 -27
- package/src/sync-clients.ts +189 -189
- package/src/tools/instructions.ts +137 -137
- package/src/tools/manifest.ts +81 -81
- package/src/tools/parser.ts +2989 -2989
- package/src/tools/toolcall-tags.ts +142 -142
- package/src/tui/app.ts +264 -264
- package/src/tui/index.ts +61 -61
- package/src/tui/markdown.ts +258 -258
- package/src/tui/proxy-client.ts +331 -326
- package/src/tui/screen.ts +294 -278
- package/src/tui/server-manager.ts +270 -270
- package/src/tui/theme.ts +432 -432
- package/src/tui/types.ts +33 -33
- package/src/tui/views/accounts-view.ts +656 -656
- package/src/tui/views/chat-view.ts +1018 -823
- package/src/tui/views/logs-view.ts +479 -413
- package/src/tui/views/status-view.ts +204 -204
- package/src/tui/views/storage-view.ts +304 -291
- package/src/tui/views/sync-view.ts +409 -409
- package/src/types/ali-oss.d.ts +32 -32
- package/src/utils/context-truncation.ts +84 -84
- package/src/utils/json.ts +380 -380
- package/src/utils/session-id.ts +37 -37
- package/src/utils/tool-call-guard.ts +84 -84
- package/src/utils/types.ts +109 -109
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
import { config } from "./config.ts";
|
|
2
|
-
import { ContextLengthExceededError } from "./errors.ts";
|
|
3
|
-
import {
|
|
4
|
-
getModelCapabilities,
|
|
5
|
-
getModelContextWindow,
|
|
6
|
-
} from "./model-registry.ts";
|
|
7
|
-
import { estimateTokenCount } from "../utils/context-truncation.ts";
|
|
8
|
-
|
|
9
|
-
const INPUT_TOKEN_SAFETY_MARGIN = 4_096;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Maximum number of recent messages to keep when truncating.
|
|
13
|
-
* Older messages are dropped with a truncation notice.
|
|
14
|
-
*/
|
|
15
|
-
const TRUNCATION_KEEP_RECENT_MESSAGES = 20;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Notice added when history is truncated.
|
|
19
|
-
*/
|
|
20
|
-
const TRUNCATION_NOTICE =
|
|
21
|
-
"\n\n[Context truncated: older messages were removed to fit within model limits. Recent messages preserved.]\n\n";
|
|
22
|
-
|
|
23
|
-
export interface PromptLimitStats {
|
|
24
|
-
bytes: number;
|
|
25
|
-
estimatedTokens: number;
|
|
26
|
-
modelContextWindow: number;
|
|
27
|
-
usableInputTokens: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface PromptLimitOptions {
|
|
31
|
-
/** Skip the model-token check until live model metadata has been synced. */
|
|
32
|
-
checkModelContext?: boolean;
|
|
33
|
-
/** Use metadata synchronized from this account, never another account. */
|
|
34
|
-
accountId?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getUtf8ByteLength(value: string): number {
|
|
38
|
-
return Buffer.byteLength(value, "utf8");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getPromptLimitStats(
|
|
42
|
-
prompt: string,
|
|
43
|
-
modelId: string,
|
|
44
|
-
accountId?: string,
|
|
45
|
-
): PromptLimitStats {
|
|
46
|
-
const modelContextWindow = getModelContextWindow(modelId, accountId);
|
|
47
|
-
const capabilities = getModelCapabilities(modelId, accountId);
|
|
48
|
-
const reservedOutputTokens = Math.max(
|
|
49
|
-
INPUT_TOKEN_SAFETY_MARGIN,
|
|
50
|
-
capabilities.maxOutputTokens,
|
|
51
|
-
capabilities.maxThinkingTokens,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
bytes: getUtf8ByteLength(prompt),
|
|
56
|
-
estimatedTokens: estimateTokenCount(prompt),
|
|
57
|
-
modelContextWindow,
|
|
58
|
-
usableInputTokens: Math.max(1, modelContextWindow - reservedOutputTokens),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Reject input that Qwen's web endpoint is unlikely to accept before allocating
|
|
64
|
-
* an upstream chat or retrying it on other accounts.
|
|
65
|
-
*/
|
|
66
|
-
export function assertPromptWithinLimits(
|
|
67
|
-
prompt: string,
|
|
68
|
-
modelId: string,
|
|
69
|
-
options: PromptLimitOptions = {},
|
|
70
|
-
): PromptLimitStats {
|
|
71
|
-
const maxPromptBytes = config.qwen.maxPromptBytes;
|
|
72
|
-
const checkModelContext = options.checkModelContext !== false;
|
|
73
|
-
|
|
74
|
-
// Nothing to enforce: skip the byte scan and the O(n) token estimation
|
|
75
|
-
// entirely (the default QWEN_MAX_PROMPT_BYTES=0 makes the byte check a
|
|
76
|
-
// no-op, and callers pass checkModelContext:false before the live model
|
|
77
|
-
// context window is known).
|
|
78
|
-
if (maxPromptBytes <= 0 && !checkModelContext) {
|
|
79
|
-
return {
|
|
80
|
-
bytes: 0,
|
|
81
|
-
estimatedTokens: 0,
|
|
82
|
-
modelContextWindow: getModelContextWindow(modelId, options.accountId),
|
|
83
|
-
usableInputTokens: 1,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const stats = getPromptLimitStats(prompt, modelId, options.accountId);
|
|
88
|
-
|
|
89
|
-
if (maxPromptBytes > 0 && stats.bytes > maxPromptBytes) {
|
|
90
|
-
throw new ContextLengthExceededError(
|
|
91
|
-
`Input is too large for QwenProxy (${stats.bytes} UTF-8 bytes; limit ${maxPromptBytes}). Reduce or summarize the conversation before retrying.`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
options.checkModelContext !== false &&
|
|
97
|
-
stats.estimatedTokens > stats.usableInputTokens
|
|
98
|
-
) {
|
|
99
|
-
throw new ContextLengthExceededError(
|
|
100
|
-
`Input exceeds the usable context for ${modelId} (${stats.estimatedTokens} estimated tokens; limit ${stats.usableInputTokens}). Reduce or summarize the conversation before retrying.`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return stats;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Personalization is account-global Qwen state. Keep its settings payload below
|
|
109
|
-
* a separate cap so a large tool schema cannot trigger a WAF challenge there.
|
|
110
|
-
*/
|
|
111
|
-
export function isRequestPersonalizationWithinLimit(
|
|
112
|
-
instruction: string,
|
|
113
|
-
): boolean {
|
|
114
|
-
const maxPersonalizationBytes = config.qwen.maxPersonalizationBytes;
|
|
115
|
-
return (
|
|
116
|
-
maxPersonalizationBytes <= 0 ||
|
|
117
|
-
getUtf8ByteLength(instruction) <= maxPersonalizationBytes
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface TruncationResult {
|
|
122
|
-
prompt: string;
|
|
123
|
-
wasTruncated: boolean;
|
|
124
|
-
originalTokens: number;
|
|
125
|
-
truncatedTokens: number;
|
|
126
|
-
messagesKept: number;
|
|
127
|
-
messagesDropped: number;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Intelligently truncate a prompt to fit within model context limits.
|
|
132
|
-
*
|
|
133
|
-
* Strategy:
|
|
134
|
-
* 1. Keep system prompt + tools (instructions) intact
|
|
135
|
-
* 2. Keep recent messages (up to TRUNCATION_KEEP_RECENT_MESSAGES)
|
|
136
|
-
* 3. Drop older messages with a truncation notice
|
|
137
|
-
* 4. If still too large, progressively drop more messages
|
|
138
|
-
*/
|
|
139
|
-
export function truncatePromptToIntelligentLimit(
|
|
140
|
-
prompt: string,
|
|
141
|
-
modelId: string,
|
|
142
|
-
accountId?: string,
|
|
143
|
-
messages?: Array<{ role: string; content: string | null }>,
|
|
144
|
-
): TruncationResult {
|
|
145
|
-
const stats = getPromptLimitStats(prompt, modelId, accountId);
|
|
146
|
-
const originalTokens = stats.estimatedTokens;
|
|
147
|
-
|
|
148
|
-
// Within limits, no truncation needed
|
|
149
|
-
if (stats.estimatedTokens <= stats.usableInputTokens) {
|
|
150
|
-
return {
|
|
151
|
-
prompt,
|
|
152
|
-
wasTruncated: false,
|
|
153
|
-
originalTokens,
|
|
154
|
-
truncatedTokens: originalTokens,
|
|
155
|
-
messagesKept: messages?.length ?? 0,
|
|
156
|
-
messagesDropped: 0,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// If no messages provided, do simple character-based truncation
|
|
161
|
-
if (!messages || messages.length === 0) {
|
|
162
|
-
const maxChars = Math.floor(stats.usableInputTokens * 4); // ~4 chars per token
|
|
163
|
-
const truncated = prompt.slice(0, maxChars);
|
|
164
|
-
return {
|
|
165
|
-
prompt: truncated + TRUNCATION_NOTICE,
|
|
166
|
-
wasTruncated: true,
|
|
167
|
-
originalTokens,
|
|
168
|
-
truncatedTokens: estimateTokenCount(truncated),
|
|
169
|
-
messagesKept: 0,
|
|
170
|
-
messagesDropped: 0,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Intelligent truncation: keep recent messages, drop older ones
|
|
175
|
-
const totalMessages = messages.length;
|
|
176
|
-
let keepCount = Math.min(TRUNCATION_KEEP_RECENT_MESSAGES, totalMessages);
|
|
177
|
-
let droppedCount = totalMessages - keepCount;
|
|
178
|
-
|
|
179
|
-
// Build truncated prompt from recent messages
|
|
180
|
-
const buildTruncatedPrompt = (keep: number): string => {
|
|
181
|
-
const recentMessages = messages.slice(-keep);
|
|
182
|
-
const messageText = recentMessages
|
|
183
|
-
.map((m) => `${m.role}: ${m.content ?? ""}`)
|
|
184
|
-
.join("\n\n");
|
|
185
|
-
return messageText + TRUNCATION_NOTICE;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
let truncatedPrompt = buildTruncatedPrompt(keepCount);
|
|
189
|
-
let truncatedTokens = estimateTokenCount(truncatedPrompt);
|
|
190
|
-
|
|
191
|
-
// Progressively drop more messages if still too large
|
|
192
|
-
while (truncatedTokens > stats.usableInputTokens && keepCount > 1) {
|
|
193
|
-
keepCount = Math.max(1, Math.floor(keepCount / 2));
|
|
194
|
-
droppedCount = totalMessages - keepCount;
|
|
195
|
-
truncatedPrompt = buildTruncatedPrompt(keepCount);
|
|
196
|
-
truncatedTokens = estimateTokenCount(truncatedPrompt);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Final fallback: hard character limit
|
|
200
|
-
if (truncatedTokens > stats.usableInputTokens) {
|
|
201
|
-
const maxChars = Math.floor(stats.usableInputTokens * 4);
|
|
202
|
-
truncatedPrompt = truncatedPrompt.slice(0, maxChars) + TRUNCATION_NOTICE;
|
|
203
|
-
truncatedTokens = estimateTokenCount(truncatedPrompt);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
prompt: truncatedPrompt,
|
|
208
|
-
wasTruncated: true,
|
|
209
|
-
originalTokens,
|
|
210
|
-
truncatedTokens,
|
|
211
|
-
messagesKept: keepCount,
|
|
212
|
-
messagesDropped: droppedCount,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
1
|
+
import { config } from "./config.ts";
|
|
2
|
+
import { ContextLengthExceededError } from "./errors.ts";
|
|
3
|
+
import {
|
|
4
|
+
getModelCapabilities,
|
|
5
|
+
getModelContextWindow,
|
|
6
|
+
} from "./model-registry.ts";
|
|
7
|
+
import { estimateTokenCount } from "../utils/context-truncation.ts";
|
|
8
|
+
|
|
9
|
+
const INPUT_TOKEN_SAFETY_MARGIN = 4_096;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maximum number of recent messages to keep when truncating.
|
|
13
|
+
* Older messages are dropped with a truncation notice.
|
|
14
|
+
*/
|
|
15
|
+
const TRUNCATION_KEEP_RECENT_MESSAGES = 20;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Notice added when history is truncated.
|
|
19
|
+
*/
|
|
20
|
+
const TRUNCATION_NOTICE =
|
|
21
|
+
"\n\n[Context truncated: older messages were removed to fit within model limits. Recent messages preserved.]\n\n";
|
|
22
|
+
|
|
23
|
+
export interface PromptLimitStats {
|
|
24
|
+
bytes: number;
|
|
25
|
+
estimatedTokens: number;
|
|
26
|
+
modelContextWindow: number;
|
|
27
|
+
usableInputTokens: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PromptLimitOptions {
|
|
31
|
+
/** Skip the model-token check until live model metadata has been synced. */
|
|
32
|
+
checkModelContext?: boolean;
|
|
33
|
+
/** Use metadata synchronized from this account, never another account. */
|
|
34
|
+
accountId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getUtf8ByteLength(value: string): number {
|
|
38
|
+
return Buffer.byteLength(value, "utf8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getPromptLimitStats(
|
|
42
|
+
prompt: string,
|
|
43
|
+
modelId: string,
|
|
44
|
+
accountId?: string,
|
|
45
|
+
): PromptLimitStats {
|
|
46
|
+
const modelContextWindow = getModelContextWindow(modelId, accountId);
|
|
47
|
+
const capabilities = getModelCapabilities(modelId, accountId);
|
|
48
|
+
const reservedOutputTokens = Math.max(
|
|
49
|
+
INPUT_TOKEN_SAFETY_MARGIN,
|
|
50
|
+
capabilities.maxOutputTokens,
|
|
51
|
+
capabilities.maxThinkingTokens,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
bytes: getUtf8ByteLength(prompt),
|
|
56
|
+
estimatedTokens: estimateTokenCount(prompt),
|
|
57
|
+
modelContextWindow,
|
|
58
|
+
usableInputTokens: Math.max(1, modelContextWindow - reservedOutputTokens),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reject input that Qwen's web endpoint is unlikely to accept before allocating
|
|
64
|
+
* an upstream chat or retrying it on other accounts.
|
|
65
|
+
*/
|
|
66
|
+
export function assertPromptWithinLimits(
|
|
67
|
+
prompt: string,
|
|
68
|
+
modelId: string,
|
|
69
|
+
options: PromptLimitOptions = {},
|
|
70
|
+
): PromptLimitStats {
|
|
71
|
+
const maxPromptBytes = config.qwen.maxPromptBytes;
|
|
72
|
+
const checkModelContext = options.checkModelContext !== false;
|
|
73
|
+
|
|
74
|
+
// Nothing to enforce: skip the byte scan and the O(n) token estimation
|
|
75
|
+
// entirely (the default QWEN_MAX_PROMPT_BYTES=0 makes the byte check a
|
|
76
|
+
// no-op, and callers pass checkModelContext:false before the live model
|
|
77
|
+
// context window is known).
|
|
78
|
+
if (maxPromptBytes <= 0 && !checkModelContext) {
|
|
79
|
+
return {
|
|
80
|
+
bytes: 0,
|
|
81
|
+
estimatedTokens: 0,
|
|
82
|
+
modelContextWindow: getModelContextWindow(modelId, options.accountId),
|
|
83
|
+
usableInputTokens: 1,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const stats = getPromptLimitStats(prompt, modelId, options.accountId);
|
|
88
|
+
|
|
89
|
+
if (maxPromptBytes > 0 && stats.bytes > maxPromptBytes) {
|
|
90
|
+
throw new ContextLengthExceededError(
|
|
91
|
+
`Input is too large for QwenProxy (${stats.bytes} UTF-8 bytes; limit ${maxPromptBytes}). Reduce or summarize the conversation before retrying.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
options.checkModelContext !== false &&
|
|
97
|
+
stats.estimatedTokens > stats.usableInputTokens
|
|
98
|
+
) {
|
|
99
|
+
throw new ContextLengthExceededError(
|
|
100
|
+
`Input exceeds the usable context for ${modelId} (${stats.estimatedTokens} estimated tokens; limit ${stats.usableInputTokens}). Reduce or summarize the conversation before retrying.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return stats;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Personalization is account-global Qwen state. Keep its settings payload below
|
|
109
|
+
* a separate cap so a large tool schema cannot trigger a WAF challenge there.
|
|
110
|
+
*/
|
|
111
|
+
export function isRequestPersonalizationWithinLimit(
|
|
112
|
+
instruction: string,
|
|
113
|
+
): boolean {
|
|
114
|
+
const maxPersonalizationBytes = config.qwen.maxPersonalizationBytes;
|
|
115
|
+
return (
|
|
116
|
+
maxPersonalizationBytes <= 0 ||
|
|
117
|
+
getUtf8ByteLength(instruction) <= maxPersonalizationBytes
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface TruncationResult {
|
|
122
|
+
prompt: string;
|
|
123
|
+
wasTruncated: boolean;
|
|
124
|
+
originalTokens: number;
|
|
125
|
+
truncatedTokens: number;
|
|
126
|
+
messagesKept: number;
|
|
127
|
+
messagesDropped: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Intelligently truncate a prompt to fit within model context limits.
|
|
132
|
+
*
|
|
133
|
+
* Strategy:
|
|
134
|
+
* 1. Keep system prompt + tools (instructions) intact
|
|
135
|
+
* 2. Keep recent messages (up to TRUNCATION_KEEP_RECENT_MESSAGES)
|
|
136
|
+
* 3. Drop older messages with a truncation notice
|
|
137
|
+
* 4. If still too large, progressively drop more messages
|
|
138
|
+
*/
|
|
139
|
+
export function truncatePromptToIntelligentLimit(
|
|
140
|
+
prompt: string,
|
|
141
|
+
modelId: string,
|
|
142
|
+
accountId?: string,
|
|
143
|
+
messages?: Array<{ role: string; content: string | null }>,
|
|
144
|
+
): TruncationResult {
|
|
145
|
+
const stats = getPromptLimitStats(prompt, modelId, accountId);
|
|
146
|
+
const originalTokens = stats.estimatedTokens;
|
|
147
|
+
|
|
148
|
+
// Within limits, no truncation needed
|
|
149
|
+
if (stats.estimatedTokens <= stats.usableInputTokens) {
|
|
150
|
+
return {
|
|
151
|
+
prompt,
|
|
152
|
+
wasTruncated: false,
|
|
153
|
+
originalTokens,
|
|
154
|
+
truncatedTokens: originalTokens,
|
|
155
|
+
messagesKept: messages?.length ?? 0,
|
|
156
|
+
messagesDropped: 0,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If no messages provided, do simple character-based truncation
|
|
161
|
+
if (!messages || messages.length === 0) {
|
|
162
|
+
const maxChars = Math.floor(stats.usableInputTokens * 4); // ~4 chars per token
|
|
163
|
+
const truncated = prompt.slice(0, maxChars);
|
|
164
|
+
return {
|
|
165
|
+
prompt: truncated + TRUNCATION_NOTICE,
|
|
166
|
+
wasTruncated: true,
|
|
167
|
+
originalTokens,
|
|
168
|
+
truncatedTokens: estimateTokenCount(truncated),
|
|
169
|
+
messagesKept: 0,
|
|
170
|
+
messagesDropped: 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Intelligent truncation: keep recent messages, drop older ones
|
|
175
|
+
const totalMessages = messages.length;
|
|
176
|
+
let keepCount = Math.min(TRUNCATION_KEEP_RECENT_MESSAGES, totalMessages);
|
|
177
|
+
let droppedCount = totalMessages - keepCount;
|
|
178
|
+
|
|
179
|
+
// Build truncated prompt from recent messages
|
|
180
|
+
const buildTruncatedPrompt = (keep: number): string => {
|
|
181
|
+
const recentMessages = messages.slice(-keep);
|
|
182
|
+
const messageText = recentMessages
|
|
183
|
+
.map((m) => `${m.role}: ${m.content ?? ""}`)
|
|
184
|
+
.join("\n\n");
|
|
185
|
+
return messageText + TRUNCATION_NOTICE;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
let truncatedPrompt = buildTruncatedPrompt(keepCount);
|
|
189
|
+
let truncatedTokens = estimateTokenCount(truncatedPrompt);
|
|
190
|
+
|
|
191
|
+
// Progressively drop more messages if still too large
|
|
192
|
+
while (truncatedTokens > stats.usableInputTokens && keepCount > 1) {
|
|
193
|
+
keepCount = Math.max(1, Math.floor(keepCount / 2));
|
|
194
|
+
droppedCount = totalMessages - keepCount;
|
|
195
|
+
truncatedPrompt = buildTruncatedPrompt(keepCount);
|
|
196
|
+
truncatedTokens = estimateTokenCount(truncatedPrompt);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Final fallback: hard character limit
|
|
200
|
+
if (truncatedTokens > stats.usableInputTokens) {
|
|
201
|
+
const maxChars = Math.floor(stats.usableInputTokens * 4);
|
|
202
|
+
truncatedPrompt = truncatedPrompt.slice(0, maxChars) + TRUNCATION_NOTICE;
|
|
203
|
+
truncatedTokens = estimateTokenCount(truncatedPrompt);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
prompt: truncatedPrompt,
|
|
208
|
+
wasTruncated: true,
|
|
209
|
+
originalTokens,
|
|
210
|
+
truncatedTokens,
|
|
211
|
+
messagesKept: keepCount,
|
|
212
|
+
messagesDropped: droppedCount,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalize OpenAI / Codex / provider reasoning.effort values.
|
|
3
|
-
*
|
|
4
|
-
* Provider-compatible names (common across OpenAI, Codex, Cursor, agents):
|
|
5
|
-
* - Max / high / xhigh → thinking ON (Qwen thinking_mode: "Thinking")
|
|
6
|
-
* - Fast / none / low → thinking OFF (Qwen thinking_mode: "Fast")
|
|
7
|
-
* - medium → thinking ON (default mid-tier agents)
|
|
8
|
-
*
|
|
9
|
-
* Qwen upstream only has a boolean + Thinking|Fast — no true medium gradient.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { stripFastSuffix } from "./model-registry.ts";
|
|
13
|
-
|
|
14
|
-
export type NormalizedEffort = "low" | "medium" | "high";
|
|
15
|
-
|
|
16
|
-
/** Human-facing / provider aliases → normalized. */
|
|
17
|
-
const EFFORT_ALIASES: Record<string, NormalizedEffort> = {
|
|
18
|
-
// Fast path (thinking off)
|
|
19
|
-
none: "low",
|
|
20
|
-
off: "low",
|
|
21
|
-
disable: "low",
|
|
22
|
-
disabled: "low",
|
|
23
|
-
minimal: "low",
|
|
24
|
-
min: "low",
|
|
25
|
-
low: "low",
|
|
26
|
-
fast: "low",
|
|
27
|
-
quick: "low",
|
|
28
|
-
"thinking-off": "low",
|
|
29
|
-
thinking_off: "low",
|
|
30
|
-
"no-thinking": "low",
|
|
31
|
-
no_thinking: "low",
|
|
32
|
-
|
|
33
|
-
// Mid
|
|
34
|
-
medium: "medium",
|
|
35
|
-
med: "medium",
|
|
36
|
-
default: "medium",
|
|
37
|
-
|
|
38
|
-
// Max path (thinking on)
|
|
39
|
-
high: "high",
|
|
40
|
-
xhigh: "high",
|
|
41
|
-
"x-high": "high",
|
|
42
|
-
max: "high",
|
|
43
|
-
maximum: "high",
|
|
44
|
-
ultra: "high",
|
|
45
|
-
deep: "high",
|
|
46
|
-
thinking: "high",
|
|
47
|
-
"thinking-on": "high",
|
|
48
|
-
thinking_on: "high",
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Map any client-provided effort string to low|medium|high.
|
|
53
|
-
* Unknown values fall back to "high" (prefer thinking for agentic clients like Codex).
|
|
54
|
-
*/
|
|
55
|
-
export function normalizeReasoningEffort(
|
|
56
|
-
value: unknown,
|
|
57
|
-
): NormalizedEffort | undefined {
|
|
58
|
-
if (value === null || value === undefined || value === "") return undefined;
|
|
59
|
-
|
|
60
|
-
const key = String(value).trim().toLowerCase();
|
|
61
|
-
if (!key) return undefined;
|
|
62
|
-
|
|
63
|
-
if (EFFORT_ALIASES[key]) return EFFORT_ALIASES[key];
|
|
64
|
-
|
|
65
|
-
// numeric 0-100 style (rare)
|
|
66
|
-
const n = Number(key);
|
|
67
|
-
if (Number.isFinite(n)) {
|
|
68
|
-
if (n <= 33) return "low";
|
|
69
|
-
if (n <= 66) return "medium";
|
|
70
|
-
return "high";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.warn(
|
|
74
|
-
`[Effort] Unknown reasoning effort '${value}' — defaulting to high`,
|
|
75
|
-
);
|
|
76
|
-
return "high";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Returns the clean, canonical model id, stripping any legacy suffix.
|
|
81
|
-
*/
|
|
82
|
-
export function applyEffortToModel(
|
|
83
|
-
model: string,
|
|
84
|
-
_effort: NormalizedEffort | undefined,
|
|
85
|
-
): string {
|
|
86
|
-
return model.replace(/-(?:low|medium|high|fast|no-thinking|thinking)$/, "");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Chat-completions mapping: effort -> Qwen reasoning mode.
|
|
91
|
-
* - low → "fast" (thinking OFF / Fast mode)
|
|
92
|
-
* - medium → "auto" (thinking AUTO / Qwen decides dynamically)
|
|
93
|
-
* - high → "thinking" (thinking ON / forced reasoning mode)
|
|
94
|
-
*/
|
|
95
|
-
export function effortToReasoningMode(
|
|
96
|
-
effort: NormalizedEffort | undefined,
|
|
97
|
-
): "fast" | "auto" | "thinking" | undefined {
|
|
98
|
-
if (effort === "low") return "fast";
|
|
99
|
-
if (effort === "medium") return "auto";
|
|
100
|
-
if (effort === "high") return "thinking";
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Normalize OpenAI / Codex / provider reasoning.effort values.
|
|
3
|
+
*
|
|
4
|
+
* Provider-compatible names (common across OpenAI, Codex, Cursor, agents):
|
|
5
|
+
* - Max / high / xhigh → thinking ON (Qwen thinking_mode: "Thinking")
|
|
6
|
+
* - Fast / none / low → thinking OFF (Qwen thinking_mode: "Fast")
|
|
7
|
+
* - medium → thinking ON (default mid-tier agents)
|
|
8
|
+
*
|
|
9
|
+
* Qwen upstream only has a boolean + Thinking|Fast — no true medium gradient.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { stripFastSuffix } from "./model-registry.ts";
|
|
13
|
+
|
|
14
|
+
export type NormalizedEffort = "low" | "medium" | "high";
|
|
15
|
+
|
|
16
|
+
/** Human-facing / provider aliases → normalized. */
|
|
17
|
+
const EFFORT_ALIASES: Record<string, NormalizedEffort> = {
|
|
18
|
+
// Fast path (thinking off)
|
|
19
|
+
none: "low",
|
|
20
|
+
off: "low",
|
|
21
|
+
disable: "low",
|
|
22
|
+
disabled: "low",
|
|
23
|
+
minimal: "low",
|
|
24
|
+
min: "low",
|
|
25
|
+
low: "low",
|
|
26
|
+
fast: "low",
|
|
27
|
+
quick: "low",
|
|
28
|
+
"thinking-off": "low",
|
|
29
|
+
thinking_off: "low",
|
|
30
|
+
"no-thinking": "low",
|
|
31
|
+
no_thinking: "low",
|
|
32
|
+
|
|
33
|
+
// Mid
|
|
34
|
+
medium: "medium",
|
|
35
|
+
med: "medium",
|
|
36
|
+
default: "medium",
|
|
37
|
+
|
|
38
|
+
// Max path (thinking on)
|
|
39
|
+
high: "high",
|
|
40
|
+
xhigh: "high",
|
|
41
|
+
"x-high": "high",
|
|
42
|
+
max: "high",
|
|
43
|
+
maximum: "high",
|
|
44
|
+
ultra: "high",
|
|
45
|
+
deep: "high",
|
|
46
|
+
thinking: "high",
|
|
47
|
+
"thinking-on": "high",
|
|
48
|
+
thinking_on: "high",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Map any client-provided effort string to low|medium|high.
|
|
53
|
+
* Unknown values fall back to "high" (prefer thinking for agentic clients like Codex).
|
|
54
|
+
*/
|
|
55
|
+
export function normalizeReasoningEffort(
|
|
56
|
+
value: unknown,
|
|
57
|
+
): NormalizedEffort | undefined {
|
|
58
|
+
if (value === null || value === undefined || value === "") return undefined;
|
|
59
|
+
|
|
60
|
+
const key = String(value).trim().toLowerCase();
|
|
61
|
+
if (!key) return undefined;
|
|
62
|
+
|
|
63
|
+
if (EFFORT_ALIASES[key]) return EFFORT_ALIASES[key];
|
|
64
|
+
|
|
65
|
+
// numeric 0-100 style (rare)
|
|
66
|
+
const n = Number(key);
|
|
67
|
+
if (Number.isFinite(n)) {
|
|
68
|
+
if (n <= 33) return "low";
|
|
69
|
+
if (n <= 66) return "medium";
|
|
70
|
+
return "high";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.warn(
|
|
74
|
+
`[Effort] Unknown reasoning effort '${value}' — defaulting to high`,
|
|
75
|
+
);
|
|
76
|
+
return "high";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns the clean, canonical model id, stripping any legacy suffix.
|
|
81
|
+
*/
|
|
82
|
+
export function applyEffortToModel(
|
|
83
|
+
model: string,
|
|
84
|
+
_effort: NormalizedEffort | undefined,
|
|
85
|
+
): string {
|
|
86
|
+
return model.replace(/-(?:low|medium|high|fast|no-thinking|thinking)$/, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Chat-completions mapping: effort -> Qwen reasoning mode.
|
|
91
|
+
* - low → "fast" (thinking OFF / Fast mode)
|
|
92
|
+
* - medium → "auto" (thinking AUTO / Qwen decides dynamically)
|
|
93
|
+
* - high → "thinking" (thinking ON / forced reasoning mode)
|
|
94
|
+
*/
|
|
95
|
+
export function effortToReasoningMode(
|
|
96
|
+
effort: NormalizedEffort | undefined,
|
|
97
|
+
): "fast" | "auto" | "thinking" | undefined {
|
|
98
|
+
if (effort === "low") return "fast";
|
|
99
|
+
if (effort === "medium") return "auto";
|
|
100
|
+
if (effort === "high") return "thinking";
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|