Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,28 @@ Bar for merging:

---

## Updating model `max_tokens` metadata

API-mode chat sends `max_tokens` to the upstream provider on every request. The web client picks that number from a three-tier lookup in [`apps/web/src/state/maxTokens.ts`](apps/web/src/state/maxTokens.ts):

1. The user's explicit override in Settings, if set.
2. Otherwise, the per-model default in [`apps/web/src/state/litellm-models.json`](apps/web/src/state/litellm-models.json) — a vendored slice of [BerriAI/litellm][litellm]'s `model_prices_and_context_window.json` (MIT). It covers ~2k chat models across Anthropic, OpenAI, DeepSeek, Groq, Together, Mistral, Gemini, Bedrock, Vertex, OpenRouter, and friends.
3. Otherwise, `FALLBACK_MAX_TOKENS = 8192`.

To pick up a newly-launched model, regenerate the vendored JSON:

```bash
node --experimental-strip-types scripts/sync-litellm-models.ts
```

The script fetches LiteLLM's catalog, filters to `mode: 'chat'` entries, projects each to its `max_output_tokens` (or `max_tokens` fallback), and writes a sorted snapshot. Commit the regenerated `litellm-models.json` alongside whatever PR triggered the refresh.

The OVERRIDES table in `maxTokens.ts` is for the rare case where LiteLLM is missing or wrong for a model id we actually use — for example, `mimo-v2.5-pro` (LiteLLM only ships MiMo via the `openrouter/xiaomi/...` and `novita/xiaomimimo/...` aliases, neither of which matches the canonical id Xiaomi's direct API uses). Keep it small; everything that LiteLLM gets right belongs upstream.

[litellm]: https://github.com/BerriAI/litellm

---

## Localization maintenance

German uses formal `Sie` because OD speaks to a mixed audience of solo creators, agencies, and engineering teams; until project feedback shows that an informal `du` voice fits better, formal German is the least surprising default. Locale PRs should translate UI chrome, core docs, and display-only gallery metadata in `apps/web/src/i18n/content.ts`, but should not translate `skills/`, `design-systems/`, or prompt bodies that agents execute. Those source prompts are maintained as workflow inputs, and keeping one source language avoids multiplying prompt QA across locales. When adding or renaming a skill, design system, or prompt template, update the German display metadata and run `pnpm --filter @open-design/web test`; `content.test.ts` fails if German display coverage drifts. Daemon errors, export filenames, and agent-generated artifact text are known limitations unless a PR explicitly scopes them.
Expand Down
22 changes: 22 additions & 0 deletions CONTRIBUTING.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,28 @@ design-systems/your-brand/

---

## 更新模型 `max_tokens` 元数据

API 模式下每次请求都会带 `max_tokens` 给上游。Web 端通过 [`apps/web/src/state/maxTokens.ts`](apps/web/src/state/maxTokens.ts) 的三层 lookup 决定这个数字:

1. 用户在 Settings 里手填的覆盖值(如果有)。
2. 否则用 [`apps/web/src/state/litellm-models.json`](apps/web/src/state/litellm-models.json) 里的 per-model 默认 —— 这是从 [BerriAI/litellm][litellm] 的 `model_prices_and_context_window.json`(MIT)摘的一份切片,覆盖约 2000 个 chat 模型,包括 Anthropic、OpenAI、DeepSeek、Groq、Together、Mistral、Gemini、Bedrock、Vertex、OpenRouter 等。
3. 都 miss 就走 `FALLBACK_MAX_TOKENS = 8192`。

新模型上线想吃到默认值,重新生成 vendored JSON:

```bash
node --experimental-strip-types scripts/sync-litellm-models.ts
```

脚本会拉 LiteLLM 的最新 catalog、过滤 `mode: 'chat'`、把每条投影到 `max_output_tokens`(缺失时 fallback 到 `max_tokens`),写成排好序的快照。把重新生成的 `litellm-models.json` 跟着触发它的 PR 一起提。

`maxTokens.ts` 里的 OVERRIDES 表只用于 LiteLLM 没收 / 收错的 model id —— 比如 `mimo-v2.5-pro`(LiteLLM 只收了 `openrouter/xiaomi/...` 和 `novita/xiaomimimo/...` 两个 alias,model id 跟小米直接 API 用的不一样)。表要保持小:凡是 LiteLLM 已经对的,**不要**抄进来。

[litellm]: https://github.com/BerriAI/litellm

---

## 代码风格

格式我们不抠(保存时跑 Prettier 就行),但有两条不能让 —— 因为它们出现在提示词栈和用户可见的 API 里:
Expand Down
8 changes: 4 additions & 4 deletions apps/daemon/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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)
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -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: [
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import {
renderModelOptions,
} from './modelOptions';
import { KNOWN_PROVIDERS } from '../state/config';
import {
MAX_MAX_TOKENS,
MIN_MAX_TOKENS,
modelMaxTokensDefault,
} from '../state/maxTokens';
import type { AgentInfo, AppConfig, AppVersionInfo, ExecMode } from '../types';
import { MEDIA_PROVIDERS } from '../media/models';
import type { MediaProvider } from '../media/models';
Expand Down Expand Up @@ -445,6 +450,27 @@ export function SettingsDialog({
))}
</select>
</label>
<label className="field">
<span className="field-label">{t('settings.maxTokens')}</span>
<input
type="number"
min={MIN_MAX_TOKENS}
max={MAX_MAX_TOKENS}
step={MIN_MAX_TOKENS}
placeholder={String(modelMaxTokensDefault(cfg.model))}
value={cfg.maxTokens ?? ''}
onChange={(e) => {
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 });
}}
/>
<p className="hint">{t('settings.maxTokensHint')}</p>
</label>
<p className="hint">{t('settings.apiHint')}</p>
</section>
)}
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const de: Dict = {
'settings.hide': 'Ausblenden',
'settings.model': 'Modell',
'settings.baseUrl': 'Base URL',
'settings.maxTokens': 'Max. Tokens (optional)',
'settings.maxTokensHint':
'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',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const en: Dict = {
'settings.hide': 'Hide',
'settings.model': 'Model',
'settings.baseUrl': 'Base URL',
'settings.maxTokens': 'Max tokens (optional)',
'settings.maxTokensHint':
'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',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const esES: Dict = {
'settings.hide': 'Ocultar',
'settings.model': 'Modelo',
'settings.baseUrl': 'URL base',
'settings.maxTokens': 'Tokens máx. (opcional)',
'settings.maxTokensHint':
'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',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const fa: Dict = {
'settings.hide': 'پنهان',
'settings.model': 'مدل',
'settings.baseUrl': 'آدرس پایه',
'settings.maxTokens': 'حداکثر توکن (اختیاری)',
'settings.maxTokensHint':
'سقف طول پاسخ. هر مدل مقدار پیش‌فرض تنظیم‌شدهٔ خود را دارد (در placeholder نمایش داده می‌شود)؛ برای استفاده از آن خالی بگذارید، یا برای جایگزینی، عددی وارد کنید.',
'settings.apiHint':
'فراخوانی‌ها مستقیماً از این مرورگر به آدرس پایه‌ای که تعیین کرده‌اید ارسال می‌شوند. بدون پراکسی. کلید هرگز localStorage را ترک نمی‌کند.',
'settings.skipForNow': 'فعلاً رد کنید',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const ja: Dict = {
'settings.hide': '隠す',
'settings.model': 'モデル',
'settings.baseUrl': 'ベース URL',
'settings.maxTokens': '最大トークン(任意)',
'settings.maxTokensHint':
'応答長の上限。各モデルにチューニング済みのデフォルト値があります(プレースホルダーに表示)。空のままにすればそれを使用し、数値を入力すれば上書きされます。',
'settings.apiHint':
'リクエストはこのブラウザから設定したベース URL に直接送信されます。プロキシなし。キーは localStorage から外に出ません。',
'settings.skipForNow': '今はスキップ',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const ptBR: Dict = {
'settings.hide': 'Ocultar',
'settings.model': 'Modelo',
'settings.baseUrl': 'URL base',
'settings.maxTokens': 'Tokens máx. (opcional)',
'settings.maxTokensHint':
'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',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const ru: Dict = {
'settings.hide': 'Скрыть',
'settings.model': 'Модель',
'settings.baseUrl': 'Базовый URL',
'settings.maxTokens': 'Макс. токенов (опционально)',
'settings.maxTokensHint':
'Ограничение длины ответа. У каждой модели свой настроенный дефолт (виден в плейсхолдере); оставьте поле пустым, чтобы использовать его, или введите число, чтобы переопределить.',
'settings.apiHint':
'Вызовы идут напрямую из этого браузера на указанный вами базовый URL. Без прокси. Ключ никогда не покидает localStorage.',
'settings.skipForNow': 'Пропустить сейчас',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const tr: Dict = {
'settings.hide': 'Gizle',
'settings.model': 'Model',
'settings.baseUrl': 'Temel URL',
'settings.maxTokens': 'Maks. token (isteğe bağlı)',
'settings.maxTokensHint':
'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',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const zhCN: Dict = {
'settings.hide': '隐藏',
'settings.model': '模型',
'settings.baseUrl': 'Base URL',
'settings.maxTokens': '最大 tokens(可选)',
'settings.maxTokensHint':
'响应长度上限。每个 model 有调优过的默认值(在 placeholder 里显示),留空即使用,输入数字则覆盖。',
'settings.apiHint':
'请求会从当前浏览器直连你设置的 Base URL,无中转代理。Key 只存放在 localStorage。',
'settings.skipForNow': '暂时跳过',
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const zhTW: Dict = {
'settings.hide': '隱藏',
'settings.model': '模型',
'settings.baseUrl': 'Base URL',
'settings.maxTokens': '最大 tokens(可選)',
'settings.maxTokensHint':
'回應長度上限。每個 model 有調過的預設值(在 placeholder 顯示),留空即使用,輸入數字則覆蓋。',
'settings.apiHint':
'請求會從當前瀏覽器直連你設定的 Base URL,無中轉代理。Key 只存放在 localStorage。',
'settings.skipForNow': '暫時跳過',
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/providers/anthropic-compatible.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { effectiveMaxTokens } from '../state/maxTokens';
import type { AppConfig, ChatMessage } from '../types';
import type { StreamHandlers } from './anthropic';
import { parseSseFrame } from './sse';
Expand Down Expand Up @@ -26,6 +27,7 @@ export async function streamMessageAnthropicProxy(
model: cfg.model,
systemPrompt: system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
maxTokens: effectiveMaxTokens(cfg),
}),
signal,
});
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -57,7 +58,7 @@ export async function streamMessage(
const stream = client.messages.stream(
{
model: cfg.model,
max_tokens: 8192,
max_tokens: effectiveMaxTokens(cfg),
system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
},
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/providers/openai-compatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -33,6 +34,7 @@ export async function streamMessageOpenAI(
model: cfg.model,
systemPrompt: system,
messages: history.map((m) => ({ role: m.role, content: m.content })),
maxTokens: effectiveMaxTokens(cfg),
}),
signal,
});
Expand Down
Loading