fix(ai): make custom output token budget configurable
This commit is contained in:
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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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 경로에는 실행 파일 경로만 포함해야 합니다. 환경 변수는 아래에 추가하세요.",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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: "最大重试次数",
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in new issue
Block a user