Expand OpenAI-compatible option

This commit is contained in:
WiseGuru 2026-06-14 14:35:03 -07:00
parent b0ae488f71
commit 2d5dc0f003
4 changed files with 30 additions and 6 deletions

View file

@ -293,7 +293,7 @@ Per [.editorconfig](.editorconfig): tabs (width 4), LF, UTF-8, final newline. Ma
- **`maxTokens` over a model's output cap is remapped to a friendly error.** When the requested output cap exceeds a model's max output tokens, Anthropic and OpenAI return a cryptic HTTP 400. `remapOutputLimitError` ([src/llm/index.ts](src/llm/index.ts)), applied as a `.catch()` on the `complete()` POST in [src/llm/anthropic.ts](src/llm/anthropic.ts) and [src/llm/openai.ts](src/llm/openai.ts), detects that specific 400 (body names `max_tokens`/`max_completion_tokens` + a "maximum/too large/at most/exceed" phrase) and rethrows a `ProviderError` pointing at the "Maximum note length" setting; all other errors pass through unchanged. Its `never` return preserves the awaited type. Gemini silently clamps `maxOutputTokens` instead of erroring, so it gets no remap (and can therefore truncate without warning on long notes).
- **OpenAI reasoning models need `max_completion_tokens`, not `max_tokens`.** [src/llm/openai.ts](src/llm/openai.ts) `usesCompletionTokens(id, model)` switches the param name to `max_completion_tokens` when `id === 'openai'` and the model matches `/^(o\d|gpt-5)/i` (o1/o3/o4 + gpt-5 families), which reject the legacy `max_tokens`. Scoped to the first-party `openai` id only; `openai-compatible` and `mistral` keep `max_tokens`, so a reasoning model proxied behind an openai-compatible endpoint is a known gap. This only fixes the token param; o1-mini/o1-preview also reject `system` messages, which is not handled.
- **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.
- **`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. The `openai-compatible` LLM option is also the supported route for cloud OpenAI-compatible services (DeepSeek, Kimi/Moonshot, Qwen/DashScope, Zhipu GLM) — there are no first-class provider entries for them; the README's "Cloud OpenAI-compatible LLMs" section carries the per-provider base URLs. The LLM option label is "OpenAI-compatible (cloud or local)" to reflect this.
- **`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.
- **`window.confirm` is banned** by ESLint's `no-alert`. If a future phase needs an in-vault confirmation prompt, add a small `Modal` subclass rather than reaching for `window.confirm`.
- **Sentence-case lint covers brand and acronym lists** in [eslint.config.mts](eslint.config.mts). Adding a new provider, model family, or product name means adding it to `REWRITE_BRANDS` (or `REWRITE_ACRONYMS` for things like `LLM`). Dropdown option labels also pass through the rule; in [src/settings/tab.ts](src/settings/tab.ts) and [src/ui/setup-card.ts](src/ui/setup-card.ts), labels are iterated via `opt.label` (member access) to dodge the literal-string check.

View file

@ -8,7 +8,7 @@ You bring your own provider keys. Nothing is sent to a ReWrite server; the plugi
- Record audio directly in Obsidian, or paste a pre-existing transcript.
- 8 transcription providers: OpenAI Whisper, OpenAI-compatible (whisper.cpp, faster-whisper-server, etc.), Groq, AssemblyAI, Deepgram, Rev.ai, Mistral Voxtral, and a plugin-managed local whisper.cpp server (desktop).
- 5 LLM providers for cleanup: Anthropic Claude, OpenAI GPT, OpenAI-compatible (Ollama, LM Studio), Google Gemini, Mistral.
- 5 LLM providers for cleanup: Anthropic Claude, OpenAI GPT, OpenAI-compatible (Ollama, LM Studio, plus cloud services like DeepSeek, Kimi, Qwen, GLM), Google Gemini, Mistral.
- Desktop and Mobile profiles, auto-selected by environment with a manual override.
- 7 starter templates (General cleanup, Todo list, Daily note, Meeting notes, Idea capture, Lecture, Podcast), stored as editable Markdown files in your vault and ordered by filename (prefix with `01-`, `02-`, etc. to reorder).
- **Shared core**: a single vault Markdown file (`ReWrite/SharedCore.md`) holds the cleanup ground rules (don't act on the transcript, fix grammar/fillers, output only the result) that prefix every template, so you edit the baseline once. Trim it to save tokens, set `disableSharedCore: true` in a template's frontmatter to skip it there, or delete the file to turn it off entirely.
@ -49,6 +49,30 @@ ReWrite ships adapters for every provider listed below, but only some have been
| OpenAI-compatible (Ollama, LM Studio) | ✅ Tested (local Ollama) |
| OpenAI GPT | Untested |
| Google Gemini | Untested |
| DeepSeek / Kimi / Qwen / GLM (cloud OpenAI-compatible) | Untested |
## Cloud OpenAI-compatible LLMs (DeepSeek, Kimi, Qwen, GLM)
Many cloud LLM services speak the same `/chat/completions` dialect as OpenAI, so they work through the
**OpenAI-compatible** LLM provider with no extra setup. In a profile, set LLM provider to "OpenAI-compatible
(cloud or local)", paste the base URL from the table below, type a model name, and enter your API key.
| Provider | LLM base URL | Example models |
| --- | --- | --- |
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat`, `deepseek-reasoner` |
| Kimi (Moonshot) | `https://api.moonshot.ai/v1` | `kimi-k2-0905-preview`, `moonshot-v1-32k` |
| Qwen (DashScope) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen-max`, `qwen-plus` |
| Zhipu GLM | `https://open.bigmodel.cn/api/paas/v4` | `glm-4-plus` |
Notes:
- The base URL must include the version path (`/v1`, `/compatible-mode/v1`, etc.); the adapter appends
`/chat/completions` to whatever you enter.
- The OpenAI-compatible provider has no model dropdown or Refresh button, so type the model ID by hand
(the dropdowns above are just examples; consult the provider's docs for the current list).
- The URLs above are the international endpoints. China-region accounts can substitute the mainland
endpoints instead, e.g. `https://api.moonshot.cn/v1` (Kimi) and
`https://dashscope.aliyuncs.com/compatible-mode/v1` (Qwen).
## Install

View file

@ -55,7 +55,7 @@ const TRANSCRIPTION_OPTIONS: Array<{ id: TranscriptionProviderID; label: string;
const LLM_OPTIONS: Array<{ id: LLMProviderID; label: string }> = [
{ id: 'anthropic', label: 'Anthropic Claude' },
{ id: 'openai', label: 'OpenAI GPT' },
{ id: 'openai-compatible', label: 'OpenAI-compatible (local server)' },
{ id: 'openai-compatible', label: 'OpenAI-compatible (cloud or local)' },
{ id: 'gemini', label: 'Google Gemini' },
{ id: 'mistral', label: 'Mistral' },
{ id: 'none', label: 'None (skip cleanup; insert raw text)' },
@ -458,7 +458,7 @@ export class ReWriteSettingTab extends PluginSettingTab {
if (profile.llmProvider === 'openai-compatible') {
new Setting(body)
.setName('LLM base URL')
.setDesc('e.g. http://localhost:11434/v1 (Ollama) or http://localhost:1234/v1 (LM Studio)')
.setDesc('e.g. https://api.deepseek.com/v1 (cloud) or http://localhost:11434/v1 (Ollama). See README for more OpenAI-compatible providers.')
.addText((t) => {
t.setValue(profile.llmConfig.baseUrl);
t.onChange(async (v) => {

View file

@ -16,7 +16,7 @@ const TRANSCRIPTION_OPTIONS: Array<{ id: TranscriptionProviderID; label: string;
const LLM_OPTIONS: Array<{ id: LLMProviderID; label: string }> = [
{ id: 'anthropic', label: 'Anthropic Claude' },
{ id: 'openai', label: 'OpenAI GPT' },
{ id: 'openai-compatible', label: 'OpenAI-compatible (local server)' },
{ id: 'openai-compatible', label: 'OpenAI-compatible (cloud or local)' },
{ id: 'gemini', label: 'Google Gemini' },
{ id: 'mistral', label: 'Mistral' },
{ id: 'none', label: 'None (skip cleanup; insert raw text)' },
@ -138,7 +138,7 @@ export function renderSetupCard(params: SetupCardParams): void {
if (profile.llmProvider === 'openai-compatible') {
new Setting(card)
.setName('LLM base URL')
.setDesc('e.g. http://localhost:11434/v1 (Ollama) or http://localhost:1234/v1 (LM Studio)')
.setDesc('e.g. https://api.deepseek.com/v1 (cloud) or http://localhost:11434/v1 (Ollama). See README for more OpenAI-compatible providers.')
.addText((t) => {
t.setValue(profile.llmConfig.baseUrl);
t.onChange((v) => {