Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
48bd90a
Initial sync dev plugin registration
ncoghlan Jul 8, 2025
63e4081
Switch plugin client to the async SDK API
ncoghlan Jul 8, 2025
200e70f
Start filling out an example plugin
ncoghlan Jul 8, 2025
e8f7a41
Initial proof-of-concept preprocessor hook
ncoghlan Jul 10, 2025
55ac0db
Make the plugin a plugin
ncoghlan Jul 10, 2025
409abed
Include the example plugin
ncoghlan Jul 10, 2025
18b3d11
Refactor for cleaner public plugin API
ncoghlan Jul 11, 2025
08ec23e
Define prefix plugin schema
ncoghlan Jul 11, 2025
1565c9a
Fill in default config values
ncoghlan Jul 11, 2025
47e3460
Add a TODO list for the initial plugin support
ncoghlan Jul 14, 2025
af5b597
Emit a status notification on prompt preprocessing
ncoghlan Jul 14, 2025
fc20646
Add an in-place status update demonstration
ncoghlan Jul 14, 2025
e139b43
Move hook details to dedicated submodules
ncoghlan Jul 14, 2025
73d340a
Expand on the controller API todo list
ncoghlan Jul 15, 2025
b41a300
Make plugin hook discovery data driven
ncoghlan Jul 15, 2025
096a00b
Add global config, make config dataclass based
ncoghlan Jul 17, 2025
920e0ea
Update TODO list
ncoghlan Jul 17, 2025
e838be6
Initial config schema test cases
ncoghlan Jul 18, 2025
25819b3
Define plugin SDK boundary
ncoghlan Jul 18, 2025
e35a5d3
Tidy up plugin execution termination
ncoghlan Jul 18, 2025
05763cf
Update TODO list
ncoghlan Jul 18, 2025
4988b76
Fix hook submodule name
ncoghlan Jul 18, 2025
9cc4e3f
Report hook invocation errors to server
ncoghlan Jul 18, 2025
46b7d6f
Update TODO notes
ncoghlan Jul 18, 2025
e7782ac
Ensure preprocessing response can be serialised
ncoghlan Jul 21, 2025
e9755f6
Prompt preprocessing is user-messages only
ncoghlan Jul 21, 2025
84d3fb3
Report plugin name in dedicated loggers
ncoghlan Jul 21, 2025
74c8535
Merge remote-tracking branch 'origin/main' into plugin-dev-support
ncoghlan Jul 21, 2025
2855b71
Improve formatting of plugin error reports
ncoghlan Jul 21, 2025
05b80b7
Minor cleanups
ncoghlan Jul 22, 2025
f4af8ec
Merge remote-tracking branch 'origin/main' into plugin-dev-support
ncoghlan Jul 22, 2025
0d9062f
Add a --debug option to the plugin runner
ncoghlan Jul 22, 2025
048fbc5
Move plugin hook management to a class
ncoghlan Jul 24, 2025
e538f2a
Add abort request processing
ncoghlan Jul 24, 2025
aa53c38
Python 3.10 type hinting compatibility
ncoghlan Jul 25, 2025
d5248ad
Clarify CI platform comment
ncoghlan Jul 25, 2025
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
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ jobs:
max-parallel: 8
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
# There's no platform specific SDK code, but explicitly check Windows anyway
# There's no platform specific SDK code, but explicitly check Windows
# to ensure there aren't any inadvertent POSIX-only assumptions
os: [ubuntu-22.04, windows-2022]

# Check https://github.com/actions/action-versions/tree/main/config/actions
Expand Down
3 changes: 3 additions & 0 deletions examples/plugins/dice-tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `lmstudio/pydice`

TODO: Example Python tools provider plugin
7 changes: 7 additions & 0 deletions examples/plugins/dice-tool/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "plugin",
"runner": "python",
"owner": "lmstudio",
"name": "py-dice-tool",
"revision": 2
}
3 changes: 3 additions & 0 deletions examples/plugins/dice-tool/src/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Example plugin that provide dice rolling tools."""

# Not yet implemented, currently used to check plugins with no hooks defined
6 changes: 6 additions & 0 deletions examples/plugins/prompt-prefix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# `lmstudio/pyprompt`

Python prompt preprocessing plugin example

Note: there's no `python` runner in LM Studio yet, so use
`python -m lmstudio.plugin --dev path/to/plugin` to run a dev instance
7 changes: 7 additions & 0 deletions examples/plugins/prompt-prefix/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "plugin",
"runner": "python",
"owner": "lmstudio",
"name": "prompt-prefix",
"revision": 1
}
76 changes: 76 additions & 0 deletions examples/plugins/prompt-prefix/src/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Example plugin that adds a prefix to all user prompts."""

import asyncio

from lmstudio.plugin import BaseConfigSchema, PromptPreprocessorController, config_field
from lmstudio import UserMessage, UserMessageDict, TextDataDict


# Assigning ConfigSchema = SomeOtherSchemaClass also works
class ConfigSchema(BaseConfigSchema):
"""The name 'ConfigSchema' implicitly registers this as the per-chat plugin config schema."""

prefix: str = config_field(
label="Prefix to insert",
hint="This text will be inserted at the start of all user prompts",
default="And now for something completely different: ",
)


# Assigning GlobalConfigSchema = SomeOtherGlobalSchemaClass also works
class GlobalConfigSchema(BaseConfigSchema):
"""The name 'GlobalConfigSchema' implicitly registers this as the global plugin config schema."""

enable_inplace_status_demo: bool = config_field(
label="Enable in-place status demo",
hint="The plugin will run an in-place task status updating demo when invoked",
default=True,
)
inplace_status_duration: float = config_field(
label="In-place status total duration (s)",
hint="The number of seconds to spend displaying the in-place task status update",
default=5.0,
)


# Assigning preprocess_prompt = some_other_callable also works
async def preprocess_prompt(
ctl: PromptPreprocessorController[ConfigSchema, GlobalConfigSchema],
message: UserMessage,
) -> UserMessageDict | None:
"""Naming the function 'preprocess_prompt' implicitly registers it."""
if ctl.global_config.enable_inplace_status_demo:
# Run an in-place status prompt update demonstration
status_block = await ctl.notify_start("Starting task (shows a static icon).")
status_updates = (
(status_block.notify_working, "Task in progress (shows a dynamic icon)."),
(status_block.notify_waiting, "Task is blocked (shows a static icon)."),
(status_block.notify_error, "Reporting an error status."),
(status_block.notify_canceled, "Reporting cancellation."),
(
status_block.notify_done,
"In-place status update demonstration completed.",
),
)
status_duration = ctl.global_config.inplace_status_duration / len(
status_updates
)
async with status_block.notify_aborted("Task genuinely cancelled."):
for notification, status_text in status_updates:
await asyncio.sleep(status_duration)
await notification(status_text)

modified_message = message.to_dict()
# Add a prefix to all user messages
prefix_text = ctl.plugin_config.prefix
prefix: TextDataDict = {
"type": "text",
"text": prefix_text,
}
modified_message["content"] = [prefix, *modified_message["content"]]
# Demonstrate simple completion status reporting for non-blocking operations
await ctl.notify_done(f"Added prefix {prefix_text!r} to user message.")
return modified_message


print(f"{__name__} initialized from {__file__}")
12 changes: 12 additions & 0 deletions sdk-schema/sync-sdk-schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,18 @@ def _infer_schema_unions() -> None:
"LlmChannelPredictCreationParameterDict": "PredictionChannelRequestDict",
"RepositoryChannelDownloadModelCreationParameter": "DownloadModelChannelRequest",
"RepositoryChannelDownloadModelCreationParameterDict": "DownloadModelChannelRequestDict",
# Prettier plugin channel message names
"PluginsChannelSetPromptPreprocessorToClientPacketPreprocess": "PromptPreprocessingRequest",
"PluginsChannelSetPromptPreprocessorToClientPacketPreprocessDict": "PromptPreprocessingRequestDict",
"PluginsChannelSetPromptPreprocessorToServerPacketAborted": "PromptPreprocessingAborted",
"PluginsChannelSetPromptPreprocessorToServerPacketAbortedDict": "PromptPreprocessingAbortedDict",
"PluginsChannelSetPromptPreprocessorToServerPacketComplete": "PromptPreprocessingComplete",
"PluginsChannelSetPromptPreprocessorToServerPacketCompleteDict": "PromptPreprocessingCompleteDict",
"PluginsChannelSetPromptPreprocessorToServerPacketError": "PromptPreprocessingError",
"PluginsChannelSetPromptPreprocessorToServerPacketErrorDict": "PromptPreprocessingErrorDict",
# Prettier config handling type names
"LlmRpcGetLoadConfigReturns": "SerializedKVConfigSettings",
"LlmRpcGetLoadConfigReturnsDict": "SerializedKVConfigSettingsDict",
}


Expand Down
3 changes: 2 additions & 1 deletion src/lmstudio/_kv_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
LlmSplitStrategy,
LlmStructuredPredictionSetting,
LlmStructuredPredictionSettingDict,
SerializedKVConfigSettings,
)


Expand Down Expand Up @@ -324,7 +325,7 @@ def _invert_config_keymap(from_server: FromServerKeymap) -> ToServerKeymap:
)


def dict_from_kvconfig(config: KvConfig) -> DictObject:
def dict_from_kvconfig(config: KvConfig | SerializedKVConfigSettings) -> DictObject:
return {kv.key: kv.value for kv in config.fields}


Expand Down
Loading