feat(web): per-model max_tokens defaults

Adds a hand-maintained MODEL_MAX_TOKENS table (Claude 4.5 line → 64k,
mimo-v2.5-pro → 32k) and an effectiveMaxTokens helper layered over the
override field added in 6a3ae5f, so #29's user — and others on supported
models — don't have to discover Settings to avoid mid-stream truncation.

- apps/web/src/state/maxTokens.ts: lookup + helpers
- providers/{anthropic,anthropic-compatible,openai-compatible}.ts:
  forward effectiveMaxTokens(cfg) instead of cfg.maxTokens ?? 8192
- SettingsDialog: input becomes an optional override (blank = default,
  shown as placeholder)
- 10 locale hint strings updated to the new semantics
This commit is contained in:
lefarcen
2026-05-02 12:16:14 +08:00
parent 6a3ae5f7dc
commit 544e67e864
15 changed files with 55 additions and 15 deletions

View File

@@ -10,6 +10,7 @@ import {
renderModelOptions,
} from './modelOptions';
import { KNOWN_PROVIDERS } from '../state/config';
import { modelMaxTokensDefault } from '../state/maxTokens';
import type { AgentInfo, AppConfig, AppVersionInfo, ExecMode } from '../types';
import { MEDIA_PROVIDERS } from '../media/models';
import type { MediaProvider } from '../media/models';
@@ -452,9 +453,15 @@ export function SettingsDialog({
min={1024}
max={200000}
step={1024}
value={cfg.maxTokens ?? 8192}
placeholder={String(modelMaxTokensDefault(cfg.model))}
value={cfg.maxTokens ?? ''}
onChange={(e) => {
const val = parseInt(e.target.value, 10);
const raw = e.target.value.trim();
if (raw === '') {
setCfg({ ...cfg, maxTokens: undefined });
return;
}
const val = parseInt(raw, 10);
setCfg({ ...cfg, maxTokens: Number.isFinite(val) ? val : undefined });
}}
/>

View File

@@ -75,7 +75,7 @@ export const de: Dict = {
'settings.baseUrl': 'Base URL',
'settings.maxTokens': 'Max. Tokens',
'settings.maxTokensHint':
'Obergrenze für die Antwortlänge. Standard: 8192. Erhöhen (z. B. 32768), wenn ein einzelnes Design-Artefakt mitten im Stream abgeschnitten wird.',
'Obergrenze für die Antwortlänge. Jedes Modell hat einen abgestimmten Standardwert (im Platzhalter sichtbar); leer lassen, um ihn zu verwenden, oder eine Zahl eingeben, um ihn zu überschreiben.',
'settings.apiHint':
'Aufrufe gehen direkt von diesem Browser an die festgelegte Base URL. Kein Proxy. Der Key verlässt localStorage nie.',
'settings.skipForNow': 'Vorerst überspringen',

View File

@@ -75,7 +75,7 @@ export const en: Dict = {
'settings.baseUrl': 'Base URL',
'settings.maxTokens': 'Max tokens',
'settings.maxTokensHint':
'Cap on the response length. Defaults to 8192. Raise it (e.g. 32768) when a single design artifact gets cut off mid-stream.',
'Cap on the response length. Each model has a tuned default (shown as a placeholder); leave blank to use it, or enter a number to override.',
'settings.apiHint':
'Calls go directly from this browser to the base URL you set. No proxy. The key never leaves localStorage.',
'settings.skipForNow': 'Skip for now',

View File

@@ -75,7 +75,7 @@ export const esES: Dict = {
'settings.baseUrl': 'URL base',
'settings.maxTokens': 'Tokens máx.',
'settings.maxTokensHint':
'Tope para la longitud de la respuesta. Por defecto 8192. Súbelo (p. ej. 32768) cuando un artefacto de diseño se trunca a mitad del stream.',
'Tope para la longitud de la respuesta. Cada modelo tiene un valor por defecto ajustado (visible en el placeholder); déjalo vacío para usarlo o introduce un número para anularlo.',
'settings.apiHint':
'Las llamadas van directamente desde este navegador a la URL base que indiques. Sin proxy. La clave nunca sale de localStorage.',
'settings.skipForNow': 'Omitir por ahora',

View File

@@ -75,7 +75,7 @@ export const fa: Dict = {
'settings.baseUrl': 'آدرس پایه',
'settings.maxTokens': 'حداکثر توکن',
'settings.maxTokensHint':
'سقف طول پاسخ. مقدار پیش‌فرض ۸۱۹۲. اگر یک artifact طراحی در میانهٔ استریم قطع می‌شود، آن را افزایش دهید (مثلاً ۳۲۷۶۸).',
'سقف طول پاسخ. هر مدل مقدار پیش‌فرض تنظیم‌شدهٔ خود را دارد (در placeholder نمایش داده می‌شود)؛ برای استفاده از آن خالی بگذارید، یا برای جایگزینی، عددی وارد کنید.',
'settings.apiHint':
'فراخوانی‌ها مستقیماً از این مرورگر به آدرس پایه‌ای که تعیین کرده‌اید ارسال می‌شوند. بدون پراکسی. کلید هرگز localStorage را ترک نمی‌کند.',
'settings.skipForNow': 'فعلاً رد کنید',

View File

@@ -75,7 +75,7 @@ export const ja: Dict = {
'settings.baseUrl': 'ベース URL',
'settings.maxTokens': '最大トークン',
'settings.maxTokensHint':
'応答長の上限。デフォルトは 8192。デザインの artifact がストリームの途中で切れる場合は、32768 などに引き上げてください。',
'応答長の上限。各モデルにチューニング済みのデフォルト値があります(プレースホルダーに表示)。空のままにすればそれを使用し、数値を入力すれば上書きされます。',
'settings.apiHint':
'リクエストはこのブラウザから設定したベース URL に直接送信されます。プロキシなし。キーは localStorage から外に出ません。',
'settings.skipForNow': '今はスキップ',

View File

@@ -75,7 +75,7 @@ export const ptBR: Dict = {
'settings.baseUrl': 'URL base',
'settings.maxTokens': 'Tokens máx.',
'settings.maxTokensHint':
'Limite para o comprimento da resposta. Padrão: 8192. Aumente (por exemplo, 32768) quando um artefato de design é truncado no meio do stream.',
'Limite para o comprimento da resposta. Cada modelo tem um valor padrão ajustado (visível no placeholder); deixe em branco para usá-lo ou insira um número para substituí-lo.',
'settings.apiHint':
'As chamadas vão direto deste navegador para a URL base definida. Sem proxy. A chave nunca sai do localStorage.',
'settings.skipForNow': 'Pular por enquanto',

View File

@@ -75,7 +75,7 @@ export const ru: Dict = {
'settings.baseUrl': 'Базовый URL',
'settings.maxTokens': 'Макс. токенов',
'settings.maxTokensHint':
'Ограничение длины ответа. По умолчанию 8192. Увеличьте (например, до 32768), если один артефакт дизайна обрывается посреди стрима.',
'Ограничение длины ответа. У каждой модели свой настроенный дефолт (виден в плейсхолдере); оставьте поле пустым, чтобы использовать его, или введите число, чтобы переопределить.',
'settings.apiHint':
'Вызовы идут напрямую из этого браузера на указанный вами базовый URL. Без прокси. Ключ никогда не покидает localStorage.',
'settings.skipForNow': 'Пропустить сейчас',

View File

@@ -75,7 +75,7 @@ export const tr: Dict = {
'settings.baseUrl': 'Temel URL',
'settings.maxTokens': 'Maks. token',
'settings.maxTokensHint':
'Yanıt uzunluğu sınırı. Varsayılan 8192. Bir tasarım artifact akış ortasında kesildiğinde değeri yükseltin (örn. 32768).',
'Yanıt uzunluğu sınırı. Her modelin ayarlanmış bir varsayılanı vardır (yer tutucuda görünür); kullanmak için boş bırakın, üzerine yazmak için bir sayı girin.',
'settings.apiHint':
'Çağrılar, bu tarayıcıdan doğrudan belirlediğiniz temel URLye yönlendirilir. Vekil sunucu kullanılmaz. Anahtar hiçbir zaman localStoragedan ayrılmaz.',
'settings.skipForNow': 'Şimdilik atla',

View File

@@ -74,7 +74,7 @@ export const zhCN: Dict = {
'settings.baseUrl': 'Base URL',
'settings.maxTokens': '最大 tokens',
'settings.maxTokensHint':
'响应长度上限,默认 8192。当单次生成的 artifact 被截断时,可以调高(例如 32768。',
'响应长度上限。每个 model 有调优过的默认值(在 placeholder 里显示),留空即使用,输入数字则覆盖。',
'settings.apiHint':
'请求会从当前浏览器直连你设置的 Base URL无中转代理。Key 只存放在 localStorage。',
'settings.skipForNow': '暂时跳过',

View File

@@ -74,7 +74,7 @@ export const zhTW: Dict = {
'settings.baseUrl': 'Base URL',
'settings.maxTokens': '最大 tokens',
'settings.maxTokensHint':
'回應長度上限,預設 8192。當單次生成的 artifact 被截斷時,可以調高(例如 32768。',
'回應長度上限。每個 model 有調過的預設值(在 placeholder 顯示),留空即使用,輸入數字則覆蓋。',
'settings.apiHint':
'請求會從當前瀏覽器直連你設定的 Base URL無中轉代理。Key 只存放在 localStorage。',
'settings.skipForNow': '暫時跳過',

View File

@@ -1,3 +1,4 @@
import { effectiveMaxTokens } from '../state/maxTokens';
import type { AppConfig, ChatMessage } from '../types';
import type { StreamHandlers } from './anthropic';
import { parseSseFrame } from './sse';
@@ -26,7 +27,7 @@ export async function streamMessageAnthropicProxy(
model: cfg.model,
systemPrompt: system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
maxTokens: cfg.maxTokens,
maxTokens: effectiveMaxTokens(cfg),
}),
signal,
});

View File

@@ -8,6 +8,7 @@
* your own backend.
*/
import Anthropic from '@anthropic-ai/sdk';
import { effectiveMaxTokens } from '../state/maxTokens';
import type { AppConfig, ChatMessage } from '../types';
import { streamMessageAnthropicProxy } from './anthropic-compatible';
import { isOpenAICompatible, streamMessageOpenAI } from './openai-compatible';
@@ -57,7 +58,7 @@ export async function streamMessage(
const stream = client.messages.stream(
{
model: cfg.model,
max_tokens: cfg.maxTokens ?? 8192,
max_tokens: effectiveMaxTokens(cfg),
system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
},

View File

@@ -5,6 +5,7 @@
* Routes through the daemon proxy to avoid browser CORS issues.
* BYOK — the key stays on the user's machine.
*/
import { effectiveMaxTokens } from '../state/maxTokens';
import type { AppConfig, ChatMessage } from '../types';
import type { StreamHandlers } from './anthropic';
import { parseSseFrame } from './sse';
@@ -33,7 +34,7 @@ export async function streamMessageOpenAI(
model: cfg.model,
systemPrompt: system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
maxTokens: cfg.maxTokens,
maxTokens: effectiveMaxTokens(cfg),
}),
signal,
});

View File

@@ -0,0 +1,30 @@
import type { AppConfig } from '../types';
// Hand-maintained per-model output cap. Keep in sync with SUGGESTED_MODELS
// in SettingsDialog.tsx and any new providers added to KNOWN_PROVIDERS.
//
// Sources:
// - claude-* 4.5: Anthropic model cards (64k default, beta extends further)
// - mimo-v2.5-pro: matches what issue #29 reports as the working ceiling
//
// Custom models or unknown ids fall through to FALLBACK_MAX_TOKENS so the
// previous hardcoded behavior is preserved for anyone we don't recognize.
export const FALLBACK_MAX_TOKENS = 8192;
const MODEL_MAX_TOKENS: Record<string, number> = {
'claude-opus-4-5': 64000,
'claude-sonnet-4-5': 64000,
'claude-haiku-4-5': 64000,
'mimo-v2.5-pro': 32768,
};
export function modelMaxTokensDefault(model: string): number {
return MODEL_MAX_TOKENS[model] ?? FALLBACK_MAX_TOKENS;
}
// Effective cap to send upstream. User override (cfg.maxTokens) wins; if
// unset, we use the per-model default so the average user never has to
// touch Settings to avoid a mid-stream truncation.
export function effectiveMaxTokens(cfg: Pick<AppConfig, 'maxTokens' | 'model'>): number {
return cfg.maxTokens ?? modelMaxTokensDefault(cfg.model);
}