mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-08 07:19:17 +08:00
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:
@@ -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 });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -75,7 +75,7 @@ export const fa: Dict = {
|
||||
'settings.baseUrl': 'آدرس پایه',
|
||||
'settings.maxTokens': 'حداکثر توکن',
|
||||
'settings.maxTokensHint':
|
||||
'سقف طول پاسخ. مقدار پیشفرض ۸۱۹۲. اگر یک artifact طراحی در میانهٔ استریم قطع میشود، آن را افزایش دهید (مثلاً ۳۲۷۶۸).',
|
||||
'سقف طول پاسخ. هر مدل مقدار پیشفرض تنظیمشدهٔ خود را دارد (در placeholder نمایش داده میشود)؛ برای استفاده از آن خالی بگذارید، یا برای جایگزینی، عددی وارد کنید.',
|
||||
'settings.apiHint':
|
||||
'فراخوانیها مستقیماً از این مرورگر به آدرس پایهای که تعیین کردهاید ارسال میشوند. بدون پراکسی. کلید هرگز localStorage را ترک نمیکند.',
|
||||
'settings.skipForNow': 'فعلاً رد کنید',
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ja: Dict = {
|
||||
'settings.baseUrl': 'ベース URL',
|
||||
'settings.maxTokens': '最大トークン',
|
||||
'settings.maxTokensHint':
|
||||
'応答長の上限。デフォルトは 8192。デザインの artifact がストリームの途中で切れる場合は、32768 などに引き上げてください。',
|
||||
'応答長の上限。各モデルにチューニング済みのデフォルト値があります(プレースホルダーに表示)。空のままにすればそれを使用し、数値を入力すれば上書きされます。',
|
||||
'settings.apiHint':
|
||||
'リクエストはこのブラウザから設定したベース URL に直接送信されます。プロキシなし。キーは localStorage から外に出ません。',
|
||||
'settings.skipForNow': '今はスキップ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ru: Dict = {
|
||||
'settings.baseUrl': 'Базовый URL',
|
||||
'settings.maxTokens': 'Макс. токенов',
|
||||
'settings.maxTokensHint':
|
||||
'Ограничение длины ответа. По умолчанию 8192. Увеличьте (например, до 32768), если один артефакт дизайна обрывается посреди стрима.',
|
||||
'Ограничение длины ответа. У каждой модели свой настроенный дефолт (виден в плейсхолдере); оставьте поле пустым, чтобы использовать его, или введите число, чтобы переопределить.',
|
||||
'settings.apiHint':
|
||||
'Вызовы идут напрямую из этого браузера на указанный вами базовый URL. Без прокси. Ключ никогда не покидает localStorage.',
|
||||
'settings.skipForNow': 'Пропустить сейчас',
|
||||
|
||||
@@ -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 URL’ye yönlendirilir. Vekil sunucu kullanılmaz. Anahtar hiçbir zaman localStorage’dan ayrılmaz.',
|
||||
'settings.skipForNow': 'Şimdilik atla',
|
||||
|
||||
@@ -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': '暂时跳过',
|
||||
|
||||
@@ -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': '暫時跳過',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 })),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
30
apps/web/src/state/maxTokens.ts
Normal file
30
apps/web/src/state/maxTokens.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user