fix(web,daemon): make max_tokens configurable (closes #29)

BYOK users on custom Anthropic-compatible providers (e.g. Xiaomi MiMo)
hit the hardcoded 8192 cap and saw artifacts truncated mid-stream.

- AppConfig.maxTokens with Settings input (EN/CN + 8 other locales)
- ProxyStreamRequest.maxTokens contract field
- anthropic, anthropic-compatible, and openai-compatible providers all
  forward cfg.maxTokens
- /api/proxy/anthropic/stream and /api/proxy/stream payloads honor it,
  defaulting to 8192 when unset so prior clients are unaffected

Original sketch by @mashu in #78 (50a9d14); rebased to the apps/web
layout and extended to the proxy paths actually used when baseUrl is
set, which is where #29's user actually traffics.
This commit is contained in:
lefarcen
2026-05-02 11:51:14 +08:00
parent 3d306cb450
commit 6a3ae5f7dc
17 changed files with 58 additions and 5 deletions

View File

@@ -2051,7 +2051,7 @@ export async function startServer({ port = 7456, returnServer = false } = {}) {
app.post('/api/proxy/anthropic/stream', async (req, res) => {
/** @type {Partial<ProxyStreamRequest>} */
const proxyBody = req.body || {};
const { baseUrl, apiKey, model, systemPrompt, messages } = proxyBody;
const { baseUrl, apiKey, model, systemPrompt, messages, maxTokens } = proxyBody;
if (!baseUrl || !apiKey || !model) {
return sendApiError(res, 400, 'BAD_REQUEST', 'baseUrl, apiKey, and model are required');
}
@@ -2067,7 +2067,7 @@ export async function startServer({ port = 7456, returnServer = false } = {}) {
const payload = {
model,
max_tokens: 8192,
max_tokens: typeof maxTokens === 'number' && maxTokens > 0 ? maxTokens : 8192,
stream: true,
system: systemPrompt || '',
messages: Array.isArray(messages)
@@ -2166,7 +2166,7 @@ export async function startServer({ port = 7456, returnServer = false } = {}) {
app.post('/api/proxy/stream', async (req, res) => {
/** @type {Partial<ProxyStreamRequest>} */
const proxyBody = req.body || {};
const { baseUrl, apiKey, model, systemPrompt, messages } = proxyBody;
const { baseUrl, apiKey, model, systemPrompt, messages, maxTokens } = proxyBody;
if (!baseUrl || !apiKey || !model) {
return sendApiError(res, 400, 'BAD_REQUEST', 'baseUrl, apiKey, and model are required');
}
@@ -2194,7 +2194,7 @@ export async function startServer({ port = 7456, returnServer = false } = {}) {
const payload = {
model,
max_tokens: 8192,
max_tokens: typeof maxTokens === 'number' && maxTokens > 0 ? maxTokens : 8192,
stream: true,
...(isMiMo ? { tool_choice: 'none', tools: [] } : {}),
messages: [

View File

@@ -445,6 +445,21 @@ export function SettingsDialog({
))}
</select>
</label>
<label className="field">
<span className="field-label">{t('settings.maxTokens')}</span>
<input
type="number"
min={1024}
max={200000}
step={1024}
value={cfg.maxTokens ?? 8192}
onChange={(e) => {
const val = parseInt(e.target.value, 10);
setCfg({ ...cfg, maxTokens: Number.isFinite(val) ? val : undefined });
}}
/>
<p className="hint">{t('settings.maxTokensHint')}</p>
</label>
<p className="hint">{t('settings.apiHint')}</p>
</section>
)}

View File

@@ -73,6 +73,9 @@ export const de: Dict = {
'settings.hide': 'Ausblenden',
'settings.model': 'Modell',
'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.',
'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

@@ -73,6 +73,9 @@ export const en: Dict = {
'settings.hide': 'Hide',
'settings.model': 'Model',
'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.',
'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

@@ -73,6 +73,9 @@ export const esES: Dict = {
'settings.hide': 'Ocultar',
'settings.model': 'Modelo',
'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.',
'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

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

View File

@@ -73,6 +73,9 @@ export const ja: Dict = {
'settings.hide': '隠す',
'settings.model': 'モデル',
'settings.baseUrl': 'ベース URL',
'settings.maxTokens': '最大トークン',
'settings.maxTokensHint':
'応答長の上限。デフォルトは 8192。デザインの artifact がストリームの途中で切れる場合は、32768 などに引き上げてください。',
'settings.apiHint':
'リクエストはこのブラウザから設定したベース URL に直接送信されます。プロキシなし。キーは localStorage から外に出ません。',
'settings.skipForNow': '今はスキップ',

View File

@@ -73,6 +73,9 @@ export const ptBR: Dict = {
'settings.hide': 'Ocultar',
'settings.model': 'Modelo',
'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.',
'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

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

View File

@@ -73,6 +73,9 @@ export const tr: Dict = {
'settings.hide': 'Gizle',
'settings.model': 'Model',
'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).',
'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

@@ -72,6 +72,9 @@ export const zhCN: Dict = {
'settings.hide': '隐藏',
'settings.model': '模型',
'settings.baseUrl': 'Base URL',
'settings.maxTokens': '最大 tokens',
'settings.maxTokensHint':
'响应长度上限,默认 8192。当单次生成的 artifact 被截断时,可以调高(例如 32768。',
'settings.apiHint':
'请求会从当前浏览器直连你设置的 Base URL无中转代理。Key 只存放在 localStorage。',
'settings.skipForNow': '暂时跳过',

View File

@@ -72,6 +72,9 @@ export const zhTW: Dict = {
'settings.hide': '隱藏',
'settings.model': '模型',
'settings.baseUrl': 'Base URL',
'settings.maxTokens': '最大 tokens',
'settings.maxTokensHint':
'回應長度上限,預設 8192。當單次生成的 artifact 被截斷時,可以調高(例如 32768。',
'settings.apiHint':
'請求會從當前瀏覽器直連你設定的 Base URL無中轉代理。Key 只存放在 localStorage。',
'settings.skipForNow': '暫時跳過',

View File

@@ -26,6 +26,7 @@ export async function streamMessageAnthropicProxy(
model: cfg.model,
systemPrompt: system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
maxTokens: cfg.maxTokens,
}),
signal,
});

View File

@@ -57,7 +57,7 @@ export async function streamMessage(
const stream = client.messages.stream(
{
model: cfg.model,
max_tokens: 8192,
max_tokens: cfg.maxTokens ?? 8192,
system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
},

View File

@@ -33,6 +33,7 @@ export async function streamMessageOpenAI(
model: cfg.model,
systemPrompt: system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
maxTokens: cfg.maxTokens,
}),
signal,
});

View File

@@ -58,6 +58,9 @@ export interface AppConfig {
// Pre-existing configs without this field fall through to the agent's
// declared default.
agentModels?: Record<string, AgentModelChoice>;
// Caps the upstream completion length in API mode. Defaults to 8192 when
// unset; raise it for providers (e.g. MiMo) that allow longer responses.
maxTokens?: number;
}
export type AgentEvent = PersistedAgentEvent;

View File

@@ -11,6 +11,9 @@ export interface ProxyStreamRequest {
model: string;
systemPrompt?: string;
messages: ProxyMessage[];
// Caps the upstream completion length. Defaults to 8192 when unset so
// pre-existing clients keep their old behavior.
maxTokens?: number;
}
export interface ProxyStreamStartPayload {