ctrl+shift+p filters: :st2 :st3 :win :osx :linux
Browse

LLM

by tonylchang ST4 New

Chat with LLMs (Ollama, Claude, OpenAI, OpenRouter, DeepSeek, plus any OpenAI-compatible endpoint) inside Sublime Text 4. Secure external secrets storage for hosted providers.

Labels ai, llm, chat

Details

Installs

  • Total 2
  • Win 0
  • Mac 2
  • Linux 0
Jun 12 Jun 11 Jun 10 Jun 9 Jun 8 Jun 7 Jun 6 Jun 5 Jun 4 Jun 3 Jun 2 Jun 1 May 31 May 30 May 29 May 28 May 27 May 26 May 25 May 24 May 23 May 22 May 21 May 20 May 19 May 18 May 17 May 16 May 15 May 14 May 13 May 12 May 11 May 10 May 9 May 8 May 7 May 6 May 5 May 4 May 3 May 2 May 1 Apr 30 Apr 29
Windows 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Mac 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Linux 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Readme

Source
raw.​githubusercontent.​com

LLM for Sublime Text

A chat interface for LLMs (Ollama, OpenAI, Anthropic, OpenRouter, DeepSeek, plus any OpenAI-compatible endpoint) inside Sublime Text 4.

LLM streaming a response

Use it for:

  • Conversational coding help without leaving the editor.
  • Sending a code selection into the chat with one keystroke.
  • Local, private inference via Ollama (default).
  • Hosted models when you want them, with provider config and API keys stored outside the dotfiles-symlink scope.

Install

Manual install (development / pre-publish)

  1. Locate your Packages directory:
    • macOS: ~/Library/Application Support/Sublime Text/Packages/
    • Linux: ~/.config/sublime-text/Packages/
    • Windows: %APPDATA%\Sublime Text\Packages\
  2. Clone or symlink this repository into that directory as LLM:
ln -s <path-to-clone> '<packages-dir>/LLM'
  1. Restart Sublime Text.

The plugin requires Sublime Text build 4050 or newer (Python 3.8+ plugin host).

Quickstart: Ollama (works by default)

Ollama runs models locally — no API key, no network egress, and no LLM-specific configuration if you use the default llama3.2 model.

  1. Install Ollama: https://ollama.com
  2. Start the server: ollama serve (or use the desktop app).
  3. Pull a model: ollama pull llama3.2
  4. In Sublime: use command palette: LLM: Choose Model
  5. In Sublime: Tools -> LLM: Open Chat (or use the command palette: LLM: Open Chat).
  6. Type a message after the <user> prompt.
  7. Press Ctrl+Enter (macOS: Cmd+Enter) to send.

Default settings already point at local Ollama:

  • provider: "ollama"
  • base_url: "http://localhost:11434" (also honors the OLLAMA_HOST environment variable when base_url is unset)
  • model: "llama3.2"

That means a fresh install should work after ollama serve and ollama pull llama3.2. Use LLM: Choose Model or set providers.ollama.model in config.json only if you want a different local model.

If Ollama isn't running, errors surface as Ollama is not running. Start it with ollama serve.

Hosted providers

To use OpenAI, Anthropic, OpenRouter, or DeepSeek, update api_key in config.json with a key from the provider in question:

"openai": {
      "api_key": "REPLACE_ME_with_sk-...",
      "base_url": "https://api.openai.com/v1",
      "model": "gpt-4o"
    },
    "anthropic": {
      "api_key": "REPLACE_ME_with_sk-ant-...",
      "model": "claude-sonnet-4-6"
    },
    "openrouter": {
      "api_key": "REPLACE_ME_with_sk-or-...",
      "base_url": "https://openrouter.ai/api/v1",
      "model": "anthropic/claude-sonnet-4.5",
      "referer": "",
      "title": "LLM"
    },
    "deepseek": {
      "api_key": "REPLACE_ME_with_sk-...",
      "base_url": "https://api.deepseek.com",
      "model": "deepseek-v4-flash"
    },

Update model to change the default model or select “LLM: Choose Model” from the command palette to pick a specific model from the chosen provider.

External provider config and API keys

LLM keeps provider-level settings outside Sublime's settings file. The default external config path is ~/.config/sublime-llm/config.json on macOS / Linux, or %APPDATA%\sublime-llm\config.json on Windows. A template is shipped as config.example.json.

Example:

{
  "active_provider": "ollama",
  "providers": {
    "ollama": {
      "base_url": "http://localhost:11434",
      "model": "llama3.2"
    },
    "openai": {
      "api_key": "sk-...",
      "base_url": "https://api.openai.com/v1",
      "model": "gpt-4o"
    },
    "custom": {
      "api_key": "...",
      "base_url": "http://localhost:1234/v1",
      "model": "local-model",
      "label": "LM Studio"
    }
  }
}

Provider settings are read from providers.<name> and can include base_url, model, api_key, referer, title, models, or label depending on the provider. active_provider selects the provider when provider is not set elsewhere.

API keys are resolved in this order and the first match wins:

  1. Environment variableOPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, DEEPSEEK_API_KEY, or CUSTOM_API_KEY.

On macOS, GUI apps launched from the Dock or Spotlight do not inherit environment variables from your shell rc — they're children of launchd, not of your shell. Either launch Sublime from a terminal (subl), or use the external config file instead. The same caveat applies on Linux for apps launched via desktop entry files that don't source a shell.

  1. External config fileconfig.json at the path above, with api_key under providers.<name>.api_key.

  2. Legacy key-only file — existing secrets.json installs are still read for backward compatibility. New installs should use config.json; new writes go to config.json. The old key-only shape is:

{
     "openai": "sk-...",
     "anthropic": "sk-ant-...",
     "openrouter": "sk-or-...",
     "deepseek": "sk-...",
     "custom": "..."
   }
  1. Settings file<Packages>/User/LLM.sublime-settings. Disabled by default. Set "allow_secrets_in_settings_file": true to opt in. Not recommended: this file is commonly committed to dotfiles repos and synced via cloud services. If a key is present in the settings file while the opt-in is off, the plugin logs a warning and ignores it.

On POSIX systems, the plugin checks external config and legacy key-only file permissions and warns if they're looser than 0600. When the plugin itself writes the external config file, it enforces 0600 on the file and 0700 on the parent directory. This path is outside the usual dotfiles-symlink scope and is the recommended default for most users.

Whenever a key is resolved, it is registered with the logger's redacting filter so it cannot accidentally appear in log output, even from third-party libraries.

You can verify which source a key came from with LLM: Show External Config Status. The display masks all but the last four characters of every key.

Recommended .gitignore

If you sync your Sublime user directory to git or a cloud drive, add:

# LLM local/private config
config.json
secrets.json
LLM.sublime-settings

If you absolutely must check in LLM.sublime-settings, leave allow_secrets_in_settings_file at its default (false) so that any stray key in that file is ignored at runtime.

Keybindings

Default bindings:

Key (Linux / Windows) Key (macOS) Context Effect
Ctrl+Enter Cmd+Enter inside the chat view Submit the current input region.
Ctrl+Shift+L Cmd+Shift+L any view, with a non-empty selection Send the selection to the chat view as a fenced code block.
Esc Esc chat view, while streaming Cancel the in-flight response.

All commands are also reachable via the command palette under the LLM: prefix:

Command Effect
LLM: Open Chat Open or focus the chat view (creates a side group if needed).
LLM: Submit Message Send the current input region.
LLM: Send Selection to Chat Pre-fill chat input with the current selection wrapped in a fenced code block.
LLM: Cancel Cancel an in-flight stream.
LLM: Choose Model Quick-panel model picker.
LLM: Choose Provider Quick-panel provider picker with health badges.
LLM: Show Status Diagnostic display: provider, model, health, model count.
LLM: Show External Config Status Per-provider key source, last-four-character masked.
LLM: Clear Chat History Empty the chat for the current project and remove its on-disk transcript.

LLM: Open Chat is also wired into Tools -> LLM: Open Chat. LLM: Send Selection to Chat is wired into the editor's right-click context menu when a selection is non-empty.

Settings reference

General chat settings live in LLM.sublime-settings (defaults shipped with the plugin) and can be overridden in <Packages>/User/LLM.sublime-settings or per-project in your .sublime-project file under "settings". Provider-level settings can also live in the external config file under providers.<name>.

Key Default Purpose
provider "ollama" Active provider. One of ollama, openai, anthropic, openrouter, deepseek, custom.
model "llama3.2" Model identifier for the active provider. The default matches the Ollama quickstart; set another model via config.json, user settings, or LLM: Choose Model.
base_url "http://localhost:11434" Used by the Ollama provider. Other providers have their own defaults (see provider matrix).
temperature 0.7 Sampling temperature.
max_tokens 4096 Max completion tokens. Required for Anthropic; recommended for OpenAI.
system_prompt "" Optional system message prepended to each conversation.
allow_secrets_in_settings_file false If true, allows hosted-provider keys (*_api_key settings) to be read from this settings file. Not recommended.
openrouter_referer "" Optional HTTP-Referer header for OpenRouter analytics.
openrouter_title "" Optional X-Title header for OpenRouter analytics.
anthropic_version "2023-06-01" Value of the anthropic-version request header.
anthropic_models [] Override the model list shown by the Anthropic provider. Empty means use the plugin's built-in default list.
deepseek_models ["deepseek-v4-flash", "deepseek-v4-pro"] Fallback model list for DeepSeek when /models isn't reachable.
custom_base_url "" Required when provider is "custom". Example: http://localhost:1234/v1 for LM Studio.
custom_api_key "" Optional API key for the custom endpoint (the env var CUSTOM_API_KEY and the external config file are preferred).
custom_models [] Fallback list if the custom server doesn't expose /v1/models.
custom_label "Custom" Display label for the custom provider in error messages (e.g. "LM Studio").

Per-project overrides go in your .sublime-project:

{
  "folders": [],
  "settings": {
    "provider": "anthropic",
    "model": "claude-opus-4-7"
  }
}

Never put API keys in .sublime-project files. They're commonly committed to source control.

Chat view conventions

  • The conversation is plain Markdown. Each turn starts with a prompt: <user> or <assistant> (an optional is also recognized by the syntax).
  • The input region is the text after the last <user> prompt — type freely there.
  • The chat view is a Sublime scratch buffer; it will not prompt to save on close. Per-project on-disk persistence keeps the conversation across restarts under Packages/User/sublime-llm/chats/<slug>.md.
  • Fenced code blocks (e.g. ```python) get language-specific syntax highlighting via the standard Markdown embedding mechanism. The chat syntax is ChatMarkdown.sublime-syntax, shipped with the plugin.
  • One active chat per window. Reopening it via LLM: Open Chat focuses the existing view instead of creating a new one.

Sending a selection to chat

With a non-empty selection in any view:

  • Right-click and pick LLM: Send Selection to Chat, or
  • Run LLM: Send Selection to Chat from the command palette.

This command ships without a default key binding to avoid clashing with Sublime's built-in Ctrl+Shift+L / Cmd+Shift+L (“Split selection into lines”). To bind it yourself, add to Packages/User/Default.sublime-keymap (commented examples are in the plugin's Default.sublime-keymap):

{
    "keys": ["primary+shift+l"],
    "command": "sublime_llm_send_selection",
    "context": [
        {"key": "selection_empty", "operator": "equal", "operand": false}
    ]
}

(primary resolves to ctrl on Linux/Windows and cmd on macOS.)

The chat view opens (or focuses) and the selection is appended to the input region as a fenced code block. The fence language tag is inferred from the source view's syntax — Python, JavaScript, TypeScript, TSX, JSON, YAML, HTML, CSS, Markdown, Rust, Go, Ruby, Java, C++, C, shell, and SQL are recognized; other syntaxes get an empty language tag.

If the chat input already contains text, the selection is appended below a blank line rather than replacing it.

Troubleshooting

Ollama not running. Start it with ollama serve or the desktop app. The plugin reports Ollama is not running. Start it with ollama serve. in the status bar.

**405 Method Not Allowed or similar from Ollama.** Make sure your base_url is a host plus optional port — no trailing path component. Default is http://localhost:11434; the plugin appends /api/chat and /api/tags internally. The setting also honors http://host:port without a scheme (the plugin prepends http://).

**UNREACHABLE for a hosted provider.** Check your network and corporate proxy. The plugin issues plain urllib requests; it doesn't read http_proxy/https_proxy env vars automatically (Sublime's bundled Python doesn't include urllib3 request-level proxy detection).

**MISSING_CREDENTIAL / BAD_CREDENTIAL for OpenAI, Anthropic, OpenRouter, DeepSeek.** Verify the key resolution source with LLM: Show External Config Status — it shows whether each key was resolved from env, external config, legacy file, settings, or is missing, with the last four characters masked. If env vars aren't picked up on macOS, see the launchd caveat above.

Streaming feels slow. Token batching is set to flush every 50 ms server-side; the actual cadence is dominated by the upstream model. A typing indicator is planned in the post-MVP backlog (ticket H5).

Chat view is gone. LLM: Open Chat reopens or focuses it. Chat content lives in a scratch buffer; per-project on-disk persistence keeps the conversation across restarts.

Stream looks corrupted. A STREAM_CORRUPTED message means the plugin received bytes it couldn't parse as the provider's expected wire format. Cancel and retry. If it reproduces, file an issue with the provider name and (sanitized) sample.

Security

  • No API keys are logged. The plugin's logger has a redacting filter applied to every handler. The filter masks every key the plugin has resolved this session and also catches common API-key shapes (sk-..., sk-ant-..., Bearer ...) regardless of whether they were registered explicitly.
  • No telemetry. The plugin makes HTTP requests only to the configured provider's base URL.
  • Keys are resolved per-request. Changing env vars, config.json, or a legacy key-only file takes effect on the next request without restarting Sublime.
  • The external config file gets restrictive permissions (0600 on the file, 0700 on its directory) when the plugin writes it. The plugin warns if it reads external config or a legacy key-only file with looser permissions but does not refuse to read it.
  • Settings-file storage is opt-in. Keys placed in LLM.sublime-settings are ignored unless you set "allow_secrets_in_settings_file": true. When that opt-in is on, the plugin emits a one-time warning that storing keys there is insecure.
  • See External provider config and API keys for the recommended storage approach.

If you find a security issue, please email tony@1x0.net rather than opening a public issue. See SECURITY.md for details.

Development

Tests run inside a real (headless) Sublime Text via UnitTesting's Docker runner, so test code can use the full sublime / sublime_plugin API. Requirements: Docker, plus a clone of UnitTesting.

Run tests:

UT=/path/to/UnitTesting   # git clone https://github.com/SublimeText/UnitTesting
"$UT/docker/ut-run-tests" . --package-name LLM

Run a single test file:

"$UT/docker/ut-run-tests" . --package-name LLM --file tests/test_chat_parser.py

Use --dry-run to verify the Docker/Sublime test environment without running tests. The first run builds the image and installs Sublime Text into a cache volume; later runs are fast.

Notes:

  • --package-name LLM is required: the package is mounted as Packages/LLM, and tests import the plugin as from LLM.sublime_llm import ....
  • Test classes subclass unittesting.DeferrableTestCase, which also lets a test yield to the Sublime event loop (yield a callable to poll a condition, or an int for a delay in ms) when exercising async editor behavior.

Local install loop:

  1. Symlink the repo into your Packages directory.
  2. Install Package Reloader for automatic submodule reload on save.
  3. Use the Sublime console (Ctrl+`) for view.run_command(...) and print debugging.

Architecture overview: provider abstraction lives in sublime_llm/providers/base.py; secret resolution in sublime_llm/secrets.py; chat surface in sublime_llm/chat_view.py and sublime_llm/commands.py.

Contributing

PRs welcome. Please:

  • Add unit tests for new logic in tests/. Avoid Sublime-dependent imports at module top level so the suite runs without Sublime.
  • Keep API-key handling routed through sublime_llm.secrets.resolve_key. Never log keys; call register_secret (from sublime_llm.logging_setup) on any new key material so the redaction filter catches it.
  • Match the existing code style: stdlib only, Python 3.8 target, short one-line docstrings at most, no multi-line comment blocks.

Open work and post-1.0 ideas are tracked in the GitHub issue list.

License

MIT. See LICENSE.