Update max token suggestions

This commit is contained in:
WiseGuru 2026-06-01 18:21:25 -07:00
parent 85c549136c
commit b0bc85eff0
5 changed files with 71 additions and 4 deletions

View file

@ -261,6 +261,7 @@ Per [.editorconfig](.editorconfig): tabs (width 4), LF, UTF-8, final newline. Ma
- **`safeStorage` is lazy-required inside a `Platform.isDesktop` guard** in [src/secrets.ts](src/secrets.ts). Importing `electron` at module top would crash on mobile load (it's marked `external` in esbuild). Any failure is treated as "encryption unavailable", which is also the mobile path and the Linux-without-keyring path. The settings tab surfaces the active backend via `safeStorage.getSelectedStorageBackend()` so users can see why it failed (e.g. `basic_text` is Chromium's last-resort backend and counts as unencrypted).
- **`saveManyKeys` is a silent no-op when locked.** When `mode === 'passphrase'` and `unlockedKey === null`, `saveManyKeys` does nothing. This is deliberate: unrelated `plugin.saveSettings()` calls (e.g. user changes a model dropdown) would otherwise persist empty `apiKey` values for every profile, wiping the on-disk encrypted bag. The UI prevents this by disabling key fields and gating all pipeline entry points on `encryptionStatus.locked`. `saveKey` (single-key write) still throws so callers can react.
- **Keep zxcvbn lazy and the strength API async.** [src/passphrase-strength.ts](src/passphrase-strength.ts) pulls `@zxcvbn-ts/*` via dynamic `import()` specifically so the ~1.6 MB of dictionaries are not parsed/constructed at plugin load (measured: ~9 ms startup vs ~47 ms with a static import). Do not "simplify" it back to a top-level static import or a synchronous `evaluatePassphrase` — that re-adds the cost to every Obsidian launch on every device. The async ripples (modal `updateStrength` race guard, `await isPassphraseAcceptable` in `changeEncryptionMode`) are intentional. `warmPassphraseStrength()` is fired on passphrase-modal open to hide the one-time build behind the modal animation.
- **`maxTokens` has two settings-tab views over one value.** `LLMConfig.maxTokens` (per profile, default `2560`) is the single source of truth. The normal-area "Maximum note length" dropdown (`renderNoteLength` in [src/settings/tab.ts](src/settings/tab.ts)) frames it in minutes via `NOTE_LENGTH_PRESETS` at `TOKENS_PER_MIN = 256` (≈150 wpm × ~1.3 tokens/word, padded ~20% for headings/bullets/`Speaker X:` labels; ~10 min → 2560). The Advanced "LLM max tokens" text field edits the same number raw; a value not matching a preset surfaces in the dropdown as a "Custom (N tokens, ~M min)" option. The dropdown calls `this.display()` on change so Advanced reflects the new number; the raw text field does not redraw (focus preservation), so the dropdown updates on the next full render. The Anthropic adapter's `config.maxTokens > 0 ? ... : 2560` fallback ([src/llm/anthropic.ts](src/llm/anthropic.ts)) must stay in sync with the default. The cap is on **output** tokens, so it bounds note length, not input.
- **No baked-in model defaults.** Both profiles ship with `model: ""`. The modal renders an inline setup card that blocks recording/paste until the active profile has a provider, model, key, and (for `openai-compatible`) base URL. If you add a provider, do not bake a default model string; surface it as placeholder hint text.
- **`openai-compatible` base URL asymmetry** (literal interpretation of the spec): transcription appends `/v1/audio/transcriptions` to a *root* URL (`http://localhost:8080`); LLM appends `/chat/completions` to a URL that *already includes* `/v1` (`http://localhost:11434/v1`). The settings UI hint text and setup card both guide users; do not "normalize" one to match the other.
- **`setHeading()` instead of manual `<h2>`** inside settings tabs. `obsidianmd/settings-tab/no-manual-html-headings` forbids manual headings. Same applies anywhere else inside a settings tab that needs a section header. In [src/settings/tab.ts](src/settings/tab.ts), section headings go through the `sectionHeading(parent, name, icon)` helper, which builds the `setHeading()` Setting and prepends a Lucide icon (via `setIcon`) to its `nameEl` styled by `.rewrite-heading-icon`. It returns the `Setting` so callers that attach a status badge (profile, shared core) still reach `nameEl`. Add new headings via this helper, not a bare `new Setting(...).setHeading()`, so they keep an icon.

View file

@ -29,7 +29,7 @@ export function createAnthropicLLM(): LLMProvider {
if (!config.model) throw new Error('anthropic: model is not configured');
const body = {
model: config.model,
max_tokens: config.maxTokens > 0 ? config.maxTokens : 2048,
max_tokens: config.maxTokens > 0 ? config.maxTokens : 2560,
system: systemPrompt,
messages: [{ role: 'user', content: userMessage }],
};

View file

@ -20,7 +20,7 @@ const EMPTY_LLM_CONFIG: LLMConfig = {
apiKey: '',
baseUrl: '',
model: '',
maxTokens: 2048,
maxTokens: 2560,
};
const DESKTOP_DEFAULT_PROFILE: EnvironmentProfile = {

View file

@ -27,6 +27,19 @@ import { PassphraseModal } from '../ui/passphrase-modal';
// Sentinel value for the "Custom..." entry in a model dropdown; never written to config.model.
const CUSTOM_MODEL_OPTION = '__rewrite_custom__';
// Cleanup output runs ~256 tokens per minute of speech (≈150 wpm × ~1.3 tokens/word,
// padded ~20% for headings, bullets, and Speaker labels in structured/diarized notes).
// The "Maximum note length" dropdown maps these minute presets onto config.maxTokens;
// the raw token count is editable in Advanced. ~10 min → 2560 is the default.
const TOKENS_PER_MIN = 256;
const NOTE_LENGTH_PRESETS: Array<{ minutes: number; tokens: number }> = [
{ minutes: 5, tokens: 1280 },
{ minutes: 10, tokens: 2560 },
{ minutes: 20, tokens: 5120 },
{ minutes: 30, tokens: 7680 },
{ minutes: 60, tokens: 15360 },
];
const TRANSCRIPTION_OPTIONS: Array<{ id: TranscriptionProviderID; label: string; desktopOnly?: boolean }> = [
{ id: 'openai', label: 'OpenAI Whisper' },
{ id: 'openai-compatible', label: 'OpenAI-compatible (local server)' },
@ -468,11 +481,48 @@ export class ReWriteSettingTab extends PluginSettingTab {
await this.commit();
});
});
this.renderNoteLength(body, profile);
}
this.renderProfileAdvanced(body, profile);
}
// Normal-area control for cleanup output length, framed in minutes rather than raw
// tokens. Presets map onto config.maxTokens (the single source of truth); a non-preset
// value (set via the Advanced "LLM max tokens" field) surfaces as a "Custom (...)" option.
private renderNoteLength(parent: HTMLElement, profile: EnvironmentProfile): void {
const current = profile.llmConfig.maxTokens;
const matched = NOTE_LENGTH_PRESETS.some((p) => p.tokens === current);
const setting = new Setting(parent)
.setName('Maximum note length')
.setDesc(
'How long a recording the cleaned note can hold before the response is cut off. ' +
'Roughly 10 min of speech ≈ 2,560 tokens; structured or multi-speaker notes run longer. ' +
'For an exact token count, use LLM max tokens under Advanced.',
)
.addDropdown((d) => {
for (const p of NOTE_LENGTH_PRESETS) {
d.addOption(String(p.tokens), `~${p.minutes} min (${p.tokens.toLocaleString()} tokens)`);
}
if (!matched) {
const mins = Math.max(1, Math.round(current / TOKENS_PER_MIN));
d.addOption(String(current), `Custom (${current.toLocaleString()} tokens, ~${mins} min)`);
}
d.setValue(String(current));
d.onChange(async (v) => {
const n = Number.parseInt(v, 10);
if (!Number.isFinite(n) || n <= 0) return;
profile.llmConfig.maxTokens = n;
await this.commit();
// Re-render so the Advanced raw-token field reflects the new value.
this.display();
});
});
setting.settingEl.addClass('rewrite-note-length');
}
private renderProfileAdvanced(parent: HTMLElement, profile: EnvironmentProfile): void {
if (profile.transcriptionProvider === 'none' && profile.llmProvider === 'none') return;
@ -495,13 +545,16 @@ export class ReWriteSettingTab extends PluginSettingTab {
if (profile.llmProvider !== 'none') {
new Setting(details)
.setName('LLM max tokens')
.setDesc('Maximum tokens for the cleanup response. Default 2048.')
.setDesc(
'Exact token cap for the cleanup response, overriding the Maximum note length presets. ' +
'~256 tokens ≈ 1 min of cleaned speech. Default 2560.',
)
.addText((t) => {
t.inputEl.type = 'number';
t.setValue(String(profile.llmConfig.maxTokens));
t.onChange(async (v) => {
const n = Number.parseInt(v, 10);
profile.llmConfig.maxTokens = Number.isFinite(n) && n > 0 ? n : 2048;
profile.llmConfig.maxTokens = Number.isFinite(n) && n > 0 ? n : 2560;
await this.commit();
});
});

View file

@ -200,6 +200,19 @@
margin-left: -15px;
}
/* Give "Maximum note length" first-class emphasis vs the plainer fields around it. */
.rewrite-settings .rewrite-note-length {
margin-top: 8px;
padding: 6px 12px;
border-left: 3px solid var(--interactive-accent);
background-color: var(--background-secondary);
border-radius: 4px;
}
.rewrite-settings .rewrite-note-length .setting-item-name {
font-weight: var(--font-semibold, 600);
}
.rewrite-settings .rewrite-profile-active-badge {
display: inline-block;
background-color: var(--interactive-accent);