From 2d5dc0f003b6b8f2af59af596e7935e04c723cce Mon Sep 17 00:00:00 2001 From: WiseGuru <42100212+WiseGuru@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:35:03 -0700 Subject: [PATCH] Expand OpenAI-compatible option --- CLAUDE.md | 2 +- README.md | 26 +++++++++++++++++++++++++- src/settings/tab.ts | 4 ++-- src/ui/setup-card.ts | 4 ++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 550385b..803f226 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `

`** 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. diff --git a/README.md b/README.md index e2ea438..31b6548 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/settings/tab.ts b/src/settings/tab.ts index 0ca8e69..80f41ec 100644 --- a/src/settings/tab.ts +++ b/src/settings/tab.ts @@ -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) => { diff --git a/src/ui/setup-card.ts b/src/ui/setup-card.ts index d6e9428..6ee8bdf 100644 --- a/src/ui/setup-card.ts +++ b/src/ui/setup-card.ts @@ -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) => {