fix(ai): make custom output token budget configurable

This commit is contained in:
Freud authored and GitHub committed 2026-09-03 21:07:37 +08:00
1 parent 56f85a2c77
commit 1c170396d3
28 files changed
+249 -48

No files matched your search

@@ -56,6 +56,8 @@ import {
useSettingsStore,
AI_PROVIDER_PRESETS,
AI_PROVIDER_PARTNER_PRESETS,
AI_OUTPUT_TOKENS_MAX,
AI_OUTPUT_TOKENS_MIN,
EDITOR_THEMES,
DEFAULT_EDITOR_SETTINGS,
DEFAULT_DESKTOP_SETTINGS,
@@ -3524,6 +3526,7 @@ const aiEditProxyEnabled = ref(false);
const aiEditProxyUrl = ref("");
const aiEditEnableThinking = ref(true);
const aiEditReasoningLevel = ref<AiReasoningLevel>("default");
const aiEditMaxOutputTokens = ref<number | undefined>(undefined);
const aiEditContextWindow = ref<number | undefined>(undefined);
const aiEditCodexCliPath = ref("");
const aiEditCodexCliEnvRows = ref<AiEnvRow[]>([]);
@@ -3808,6 +3811,7 @@ function currentAiEditConfig() {
proxyUrl: aiEditProxyUrl.value,
enableThinking: aiEditEnableThinking.value,
reasoningLevel: aiEditReasoningLevel.value,
maxOutputTokens: aiEditMaxOutputTokens.value || undefined,
contextWindow: aiEditContextWindow.value || undefined,
codexCliPath: aiEditCodexCliPath.value.trim() || undefined,
codexCliEnv: aiIsCodexCli.value ? cliEnvFromRows(aiEditCodexCliEnvRows.value) : {},
@@ -3894,6 +3898,7 @@ function aiEnterEditMode(configId?: string) {
aiEditProxyUrl.value = config.proxyUrl ?? "";
aiEditEnableThinking.value = config.enableThinking ?? true;
aiEditReasoningLevel.value = config.reasoningLevel ?? "default";
aiEditMaxOutputTokens.value = config.maxOutputTokens;
aiEditContextWindow.value = config.contextWindow;
aiEditCodexCliPath.value = config.codexCliPath ?? "";
aiEditCodexCliEnvRows.value = aiEnvRowsFromConfig(config.codexCliEnv);
@@ -3927,6 +3932,7 @@ function aiEnterEditMode(configId?: string) {
aiEditProxyUrl.value = "";
aiEditEnableThinking.value = true;
aiEditReasoningLevel.value = "default";
aiEditMaxOutputTokens.value = undefined;
aiEditContextWindow.value = undefined;
aiEditCodexCliPath.value = "";
aiEditCodexCliEnvRows.value = [];
@@ -7735,6 +7741,16 @@ onUnmounted(() => {
</p>
</div>
</div>
<!-- Maximum Output Tokens -->
<div v-if="!aiIsCliProvider" class="grid grid-cols-3 items-start gap-3">
<Label class="text-right text-xs">{{ t("ai.maxOutputTokens") }}</Label>
<div class="col-span-2">
<Input v-model.number="aiEditMaxOutputTokens" type="number" :min="AI_OUTPUT_TOKENS_MIN" :max="AI_OUTPUT_TOKENS_MAX" step="1000" class="h-8 text-xs" :placeholder="t('ai.maxOutputTokensAuto')" />
<p class="mt-1 text-xs text-muted-foreground">
{{ t("ai.maxOutputTokensHint", { min: AI_OUTPUT_TOKENS_MIN, max: AI_OUTPUT_TOKENS_MAX }) }}
</p>
</div>
</div>
<!-- Proxy -->
<div v-if="!aiIsCliProvider" class="grid grid-cols-3 items-center gap-3">
+3
View File
@@ -2777,6 +2777,9 @@ export default {
contextWindow: "Context Window",
contextWindowAuto: "Auto (detect from model name)",
contextWindowHint: "Tokens. Leave empty to auto-detect. Set manually for local/custom models.",
maxOutputTokens: "Max Output Tokens",
maxOutputTokensAuto: "Auto (per action)",
maxOutputTokensHint: "Optional per-configuration limit sent to the provider. Range: {min}–{max}. Thinking requests still use at least 8192 tokens.",
maxRetries: "Max Retries",
maxRetriesHint: "Retry on rate limits, timeouts, and temporary network errors. 0 = never, max 10.",
maxRetriesGlobal: "Max Retries",
+3
View File
@@ -2847,6 +2847,9 @@ export default withEnglishFallback({
contextWindow: "Ventana de contexto",
contextWindowAuto: "Auto (detectar desde el nombre del modelo)",
contextWindowHint: "Tokens. Déjalo vacío para detección automática. Configúralo manualmente para modelos locales/personalizados.",
maxOutputTokens: "Tokens máximos de salida",
maxOutputTokensAuto: "Auto (por acción)",
maxOutputTokensHint: "Límite opcional por configuración enviado al proveedor. Rango: {min}–{max}. Las solicitudes con razonamiento usan al menos 8192 tokens.",
maxRetriesGlobal: "Reintentos máximos",
maxRetriesGlobalDescription: "Número de reintentos automáticos en errores transitorios de API (límite de tasa, timeout, fallos de red). Se aplica a todos los proveedores de IA por API. Los proveedores CLI no se ven afectados.",
maxRetriesRange: "{min}–{max}, predeterminado {default}",
+3
View File
@@ -2741,6 +2741,9 @@ export default withEnglishFallback({
contextWindow: "Finestra di Contesto",
contextWindowAuto: "Auto (rileva dal nome del modello)",
contextWindowHint: "Token. Lascia vuoto per il rilevamento automatico. Imposta manualmente per modelli locali/personalizzati.",
maxOutputTokens: "Token di output massimi",
maxOutputTokensAuto: "Automatico (per azione)",
maxOutputTokensHint: "Limite opzionale per configurazione inviato al provider. Intervallo: {min}–{max}. Le richieste con ragionamento usano comunque almeno 8192 token.",
maxRetriesGlobal: "Tentativi massimi",
maxRetriesGlobalDescription: "Numero di tentativi automatici su errori API transitori (limite di velocità, timeout, problemi di rete). Si applica a tutti i provider IA via API. I provider CLI non sono interessati.",
maxRetriesRange: "{min}–{max}, predefinito {default}",
+3
View File
@@ -2541,6 +2541,9 @@ export default withEnglishFallback({
contextWindow: "コンテキストウィンドウ",
contextWindowAuto: "自動(モデル名から検出)",
contextWindowHint: "トークン数。空欄で自動検出。ローカル/カスタムモデルは手動設定してください。",
maxOutputTokens: "最大出力トークン",
maxOutputTokensAuto: "自動(アクションごと)",
maxOutputTokensHint: "プロバイダーに送信する設定ごとの任意の出力上限。範囲: {min}–{max}。思考を有効にしたリクエストでは少なくとも8192トークンを使用します。",
maxRetriesGlobal: "最大再試行回数",
maxRetriesGlobalDescription: "一時的なAPIエラー(レート制限、タイムアウト、ネットワーク障害)発生時の自動再試行回数。すべてのAPI対応AIプロバイダーに適用されます。CLIプロバイダーは影響を受けません。",
maxRetriesRange: "{min}–{max}、デフォルト {default}",
+3
View File
@@ -2592,6 +2592,9 @@ export default withEnglishFallback({
contextWindow: "컨텍스트 창",
contextWindowAuto: "자동 (모델 이름에서 감지)",
contextWindowHint: "토큰. 자동 감지하려면 비워 두세요. 로컬/사용자 지정 모델은 수동으로 설정하세요.",
maxOutputTokens: "최대 출력 토큰",
maxOutputTokensAuto: "자동 (모델 기본값)",
maxOutputTokensHint: "공급자에게 전송되는 선택적 구성별 출력 토큰 제한입니다. 범위: {min}–{max}. 사고 요청은 여전히 최소 8192 토큰을 사용합니다.",
codexCliPath: "Codex CLI 경로",
codexCliPathHint: "PATH의 codex를 사용하려면 비워 두세요. codex login으로 별도로 로그인하세요.",
codexCliPathEnvError: "Codex CLI 경로에는 실행 파일 경로만 포함해야 합니다. 환경 변수는 아래에 추가하세요.",
+3
View File
@@ -2845,6 +2845,9 @@ export default withEnglishFallback({
contextWindow: "Janela de Contexto",
contextWindowAuto: "Automático (detectar pelo nome do modelo)",
contextWindowHint: "Tokens. Deixe vazio para detecção automática. Defina manualmente para modelos locais/personalizados.",
maxOutputTokens: "Máximo de tokens de saída",
maxOutputTokensAuto: "Automático (por ação)",
maxOutputTokensHint: "Limite opcional por configuração enviado ao provedor. Intervalo: {min}–{max}. Solicitações com raciocínio ainda usam pelo menos 8192 tokens.",
maxRetriesGlobal: "Máximo de Tentativas",
maxRetriesGlobalDescription: "Número de tentativas automáticas em erros transitórios de API (limite de taxa, timeout, falhas de rede). Aplica-se a todos os provedores de IA via API. Provedores CLI não são afetados.",
maxRetriesRange: "{min}–{max}, padrão {default}",
+3
View File
@@ -2700,6 +2700,9 @@ export default withEnglishFallback({
contextWindow: "上下文窗口",
contextWindowAuto: "自动(根据模型名称推断)",
contextWindowHint: "单位:token。留空自动推断,本地/自定义模型建议手动设置。",
maxOutputTokens: "最大输出 Token 数",
maxOutputTokensAuto: "自动(按操作类型)",
maxOutputTokensHint: "可选的单配置输出上限,会发送给 AI 服务商。范围:{min}–{max}。启用思考时仍至少使用 8192 token。",
maxRetries: "最大重试次数",
maxRetriesHint: "遇到限流、超时或临时网络错误时自动重试。0 = 不重试,最大 10。",
maxRetriesGlobal: "最大重试次数",
+3
View File
@@ -2740,6 +2740,9 @@ export default withEnglishFallback({
contextWindow: "Context Window",
contextWindowAuto: "自動(從模型名稱偵測)",
contextWindowHint: "Token 數。留空以自動偵測。本機/自訂模型請手動設定。",
maxOutputTokens: "最大輸出 Token 數",
maxOutputTokensAuto: "自動(依操作類型)",
maxOutputTokensHint: "可選的單一設定輸出上限,會傳送給 AI 服務商。範圍:{min}–{max}。啟用思考時仍至少使用 8192 token。",
maxRetries: "最大重試次數",
maxRetriesHint: "遇到限流、超時或暫時性網路錯誤時自動重試。0 = 不重試,最大 10。",
maxRetriesGlobal: "最大重試次數",
+38
View File
@@ -158,6 +158,44 @@ describe("AI SQL dialect prompt", () => {
expect(prompt).toContain("Ask mode");
});
it("uses the configured output budget for non-thinking requests", () => {
const request = buildAgentRequest({
config: {
provider: "deepseek",
apiKey: "test",
apiUrl: "https://example.invalid",
model: "deepseek-v4-flash",
enableThinking: false,
maxOutputTokens: 1024,
},
action: "general",
mode: "ask",
instruction: "generate a long migration",
context: context(),
});
expect(request.maxTokens).toBe(1024);
});
it("uses the configured output budget above the thinking minimum", () => {
const request = buildAgentRequest({
config: {
provider: "deepseek",
apiKey: "test",
apiUrl: "https://example.invalid",
model: "deepseek-v4-flash",
enableThinking: true,
maxOutputTokens: 32768,
},
action: "general",
mode: "ask",
instruction: "generate a long migration",
context: context(),
});
expect(request.maxTokens).toBe(32768);
});
it("MongoDB agent mode uses shell commands instead of SQL", () => {
const prompt = buildSystemPrompt("general", context({ databaseType: "mongodb", connectionName: "MongoDB", database: "benchmark" }), "agent");
+2 -1
View File
@@ -179,7 +179,8 @@ export function buildAgentRequest(input: AiRequestInput, history?: api.AiMessage
];
const params = actionParams(input.action);
const maxTokens = input.config.enableThinking ? Math.max(params.maxTokens, 8192) : params.maxTokens;
const baseMaxTokens = input.config.maxOutputTokens ?? params.maxTokens;
const maxTokens = input.config.enableThinking ? Math.max(baseMaxTokens, 8192) : baseMaxTokens;
return { messages, systemPrompt, taskContract, maxTokens };
}
@@ -669,6 +669,12 @@ describe("settingsStore AI API key normalization", () => {
it("trims API keys when normalizing loaded configurations", () => {
expect(normalizeAiConfig({ provider: "openai", apiKey: " secret " }).apiKey).toBe("secret");
});
it("preserves and bounds a configured maximum output token budget", () => {
expect(normalizeAiConfig({ provider: "deepseek", maxOutputTokens: 32768 }).maxOutputTokens).toBe(32768);
expect(normalizeAiConfig({ provider: "deepseek", maxOutputTokens: 1_500_000 }).maxOutputTokens).toBe(1_000_000);
expect(normalizeAiConfig({ provider: "deepseek", maxOutputTokens: 128 }).maxOutputTokens).toBeUndefined();
expect(normalizeAiConfig({ provider: "deepseek", maxOutputTokens: Number.NaN }).maxOutputTokens).toBeUndefined();
});
it("provides Kimi defaults and recognizes legacy Kimi configurations", () => {
expect(AI_PROVIDER_PRESETS.kimi).toMatchObject({
+5
View File
@@ -458,6 +458,8 @@ const defaultConfigs: Record<AiProvider, Omit<AiConfig, "apiKey">> = Object.from
) as Record<AiProvider, Omit<AiConfig, "apiKey">>;
const AI_REASONING_LEVELS: AiReasoningLevel[] = ["default", "minimal", "low", "medium", "high", "xhigh", "max"];
export const AI_OUTPUT_TOKENS_MIN = 256;
export const AI_OUTPUT_TOKENS_MAX = 1_000_000;
const AI_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
function normalizeAiReasoningLevel(value: unknown): AiReasoningLevel {
@@ -488,6 +490,8 @@ export function normalizeAiHeaders(value: unknown): Record<string, string> {
export function normalizeAiConfig(config: Partial<AiConfig> | null | undefined): AiConfig {
const provider = config?.provider && config.provider in AI_PROVIDER_PRESETS ? config.provider : inferAiProviderFromConfig(config);
const rawMaxOutputTokens = config?.maxOutputTokens;
const maxOutputTokens = typeof rawMaxOutputTokens === "number" && Number.isFinite(rawMaxOutputTokens) && rawMaxOutputTokens >= AI_OUTPUT_TOKENS_MIN ? Math.min(AI_OUTPUT_TOKENS_MAX, Math.round(rawMaxOutputTokens)) : undefined;
return {
...defaultConfigs[provider],
...config,
@@ -500,6 +504,7 @@ export function normalizeAiConfig(config: Partial<AiConfig> | null | undefined):
proxyUrl: config?.proxyUrl ?? "",
enableThinking: config?.enableThinking ?? true,
reasoningLevel: normalizeAiReasoningLevel(config?.reasoningLevel),
maxOutputTokens,
contextWindow: config?.contextWindow ?? undefined,
codexCliPath: config?.codexCliPath?.trim() || undefined,
codexCliEnv: normalizeAiEnv(config?.codexCliEnv),
+2
View File
@@ -42,6 +42,8 @@ export interface AiConfig {
proxyUrl?: string;
enableThinking?: boolean;
reasoningLevel?: AiReasoningLevel;
/** Optional per-configuration output budget sent as max_tokens/max_output_tokens. */
maxOutputTokens?: number;
contextWindow?: number;
codexCliPath?: string | null;
codexCliEnv?: Record<string, string>;