Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions docs/src/content/docs/features/prompt-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: LLM Prompt Tools
sidebar:
order: 3
lastUpdated: 2026-05-23
lastUpdated: 2026-08-03
---

InvokeAI includes two built-in tools that use local language models to help you write better prompts. Both tools appear as small buttons in the top-right corner of the positive prompt area and are only visible when you have a compatible model installed.
Expand Down Expand Up @@ -52,4 +52,6 @@ Both tools overwrite your current prompt. You can undo this change:

## Workflow Node

A **Text LLM** node is also available in the workflow editor for use in automated pipelines. It accepts a prompt string and model selection as inputs and outputs the expanded text as a string.
The workflow editor provides **Text LLM** and **Text LLM (with System Prompt Preset)** nodes for automated pipelines. Both accept a prompt, model, maximum token count, and seed, then output generated text as a string. The preset variant reads its system prompt from the System Prompts library.

Using the same seed reproduces sampling when the model, prompt, settings, hardware, and software versions remain the same. Results may differ across devices or software versions. The prompt area's **Expand Prompt** tool chooses a fresh seed for each request so repeated expansions can vary.
2 changes: 2 additions & 0 deletions invokeai/app/api/routers/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from invokeai.app.services.image_files.image_files_common import ImageFileNotFoundException
from invokeai.app.services.model_records.model_records_base import UnknownModelException
from invokeai.app.util.dynamicprompts import find_missing_wildcards
from invokeai.app.util.misc import get_random_seed
from invokeai.backend.llava_onevision_pipeline import LlavaOnevisionPipeline
from invokeai.backend.model_manager.taxonomy import ModelType
from invokeai.backend.text_llm_pipeline import DEFAULT_SYSTEM_PROMPT, ProgressCallback, TextLLMPipeline
Expand Down Expand Up @@ -166,6 +167,7 @@ def _run_expand_prompt(
prompt=prompt,
system_prompt=system_prompt or DEFAULT_SYSTEM_PROMPT,
max_new_tokens=max_tokens,
seed=get_random_seed(),
device=model_device,
dtype=TorchDevice.choose_torch_dtype(),
progress_callback=progress_callback,
Expand Down
11 changes: 9 additions & 2 deletions invokeai/app/invocations/text_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SystemPromptNotFoundError,
SystemPromptRecordDTO,
)
from invokeai.app.util.misc import SEED_MAX
from invokeai.backend.model_manager.taxonomy import ModelType
from invokeai.backend.text_llm_pipeline import DEFAULT_SYSTEM_PROMPT, TextLLMPipeline
from invokeai.backend.util.devices import TorchDevice
Expand All @@ -21,6 +22,7 @@ def _run_text_llm(
prompt: str,
system_prompt: str,
max_tokens: int,
seed: int,
) -> str:
"""Shared LLM invocation body used by every text-LLM node in this module."""
model_config = context.models.get_config(text_llm_model)
Expand All @@ -35,6 +37,7 @@ def _run_text_llm(
prompt=prompt,
system_prompt=system_prompt,
max_new_tokens=max_tokens,
seed=seed,
device=model_device,
dtype=TorchDevice.choose_torch_dtype(),
)
Expand All @@ -45,7 +48,7 @@ def _run_text_llm(
title="Text LLM",
tags=["llm", "text", "prompt"],
category="llm",
version="1.0.0",
version="1.1.0",
classification=Classification.Beta,
)
class TextLLMInvocation(BaseInvocation):
Expand All @@ -72,6 +75,7 @@ class TextLLMInvocation(BaseInvocation):
le=2048,
description="Maximum number of tokens to generate.",
)
seed: int = InputField(default=0, ge=0, le=SEED_MAX, description=FieldDescriptions.seed)

@torch.no_grad()
def invoke(self, context: InvocationContext) -> StringOutput:
Expand All @@ -81,6 +85,7 @@ def invoke(self, context: InvocationContext) -> StringOutput:
prompt=self.prompt,
system_prompt=self.system_prompt,
max_tokens=self.max_tokens,
seed=self.seed,
)
return StringOutput(value=output)

Expand All @@ -90,7 +95,7 @@ def invoke(self, context: InvocationContext) -> StringOutput:
title="Text LLM (with System Prompt Preset)",
tags=["llm", "text", "prompt", "preset", "template"],
category="llm",
version="1.0.0",
version="1.1.0",
classification=Classification.Beta,
)
class TextLLMWithPresetInvocation(BaseInvocation):
Expand Down Expand Up @@ -125,6 +130,7 @@ class TextLLMWithPresetInvocation(BaseInvocation):
le=2048,
description="Maximum number of tokens to generate.",
)
seed: int = InputField(default=0, ge=0, le=SEED_MAX, description=FieldDescriptions.seed)

def _resolve_system_prompt(self, context: InvocationContext) -> SystemPromptRecordDTO:
"""Resolve the referenced preset, enforcing the same access rules as the REST API.
Expand Down Expand Up @@ -169,5 +175,6 @@ def invoke(self, context: InvocationContext) -> StringOutput:
prompt=self.prompt,
system_prompt=record.content,
max_tokens=self.max_tokens,
seed=self.seed,
)
return StringOutput(value=output)
36 changes: 35 additions & 1 deletion invokeai/backend/text_llm_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Callable

import torch
from torch.overrides import TorchFunctionMode
from transformers import PreTrainedModel, PreTrainedTokenizerBase, TextIteratorStreamer

DEFAULT_SYSTEM_PROMPT = (
Expand All @@ -29,6 +30,37 @@
PROGRESS_EMIT_INTERVAL = 0.1


class _SeededMultinomialMode(TorchFunctionMode):
"""Use invocation-local generators for multinomial sampling in this thread."""

def __init__(self, seed: int):
self._seed = seed
self._generators: dict[torch.device, torch.Generator] = {}

def _get_generator(self, device: torch.device) -> torch.Generator:
generator_device = torch.device("cpu") if device.type == "mps" else device
if generator_device not in self._generators:
self._generators[generator_device] = torch.Generator(device=generator_device).manual_seed(self._seed)
return self._generators[generator_device]

def __torch_function__(self, func, types, args=(), kwargs=None):
kwargs = dict(kwargs or {})
if func is not torch.multinomial or kwargs.get("generator") is not None:
return func(*args, **kwargs)

probabilities = args[0] if args else kwargs["input"]
generator = self._get_generator(probabilities.device)
kwargs["generator"] = generator
if probabilities.device.type != "mps":
return func(*args, **kwargs)

if args:
args = (probabilities.cpu(), *args[1:])
else:
kwargs["input"] = probabilities.cpu()
return func(*args, **kwargs).to(probabilities.device)


class TextLLMPipeline:
"""A wrapper for a causal language model + tokenizer for text generation."""

Expand All @@ -41,6 +73,7 @@ def run(
prompt: str,
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
max_new_tokens: int = 300,
seed: int = 0,
device: torch.device = torch.device("cpu"),
dtype: torch.dtype = torch.float16,
progress_callback: ProgressCallback | None = None,
Expand Down Expand Up @@ -91,7 +124,8 @@ def run(

def _generate() -> None:
try:
self._model.generate(**generation_kwargs)
with _SeededMultinomialMode(seed):
self._model.generate(**generation_kwargs)
except BaseException as e:
generation_error.append(e)
# transformers only calls streamer.end() on the normal exit of the
Expand Down
28 changes: 26 additions & 2 deletions invokeai/frontend/web/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -81824,6 +81824,18 @@
"title": "Max Tokens",
"type": "integer"
},
"seed": {
"default": 0,
"description": "Seed for random number generation",
"field_kind": "input",
"input": "any",
"maximum": 4294967295,
"minimum": 0,
"orig_default": 0,
"orig_required": false,
"title": "Seed",
"type": "integer"
},
"type": {
"const": "text_llm",
"default": "text_llm",
Expand All @@ -81836,7 +81848,7 @@
"tags": ["llm", "text", "prompt"],
"title": "Text LLM",
"type": "object",
"version": "1.0.0",
"version": "1.1.0",
"output": {
"$ref": "#/components/schemas/StringOutput"
}
Expand Down Expand Up @@ -81927,6 +81939,18 @@
"title": "Max Tokens",
"type": "integer"
},
"seed": {
"default": 0,
"description": "Seed for random number generation",
"field_kind": "input",
"input": "any",
"maximum": 4294967295,
"minimum": 0,
"orig_default": 0,
"orig_required": false,
"title": "Seed",
"type": "integer"
},
"type": {
"const": "text_llm_with_preset",
"default": "text_llm_with_preset",
Expand All @@ -81939,7 +81963,7 @@
"tags": ["llm", "text", "prompt", "preset", "template"],
"title": "Text LLM (with System Prompt Preset)",
"type": "object",
"version": "1.0.0",
"version": "1.1.0",
"output": {
"$ref": "#/components/schemas/StringOutput"
}
Expand Down
12 changes: 12 additions & 0 deletions invokeai/frontend/web/src/services/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35633,6 +35633,12 @@ export type components = {
* @default 300
*/
max_tokens?: number;
/**
* Seed
* @description Seed for random number generation
* @default 0
*/
seed?: number;
/**
* type
* @default text_llm
Expand Down Expand Up @@ -35694,6 +35700,12 @@ export type components = {
* @default 300
*/
max_tokens?: number;
/**
* Seed
* @description Seed for random number generation
* @default 0
*/
seed?: number;
/**
* type
* @default text_llm_with_preset
Expand Down
2 changes: 2 additions & 0 deletions tests/app/invocations/test_text_llm_with_preset.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def _make_invocation(prompt_id: str = "test-id") -> TextLLMWithPresetInvocation:
system_prompt=SystemPromptField(system_prompt_id=prompt_id),
text_llm_model=ModelIdentifierField(key="dummy", hash="x", name="dummy", base="any", type="text_llm"),
max_tokens=50,
seed=123,
)


Expand Down Expand Up @@ -70,6 +71,7 @@ def test_preset_node_loads_content_from_db_and_passes_to_llm() -> None:
assert kwargs["system_prompt"] == "custom system instruction"
assert kwargs["prompt"] == "a cat"
assert kwargs["max_tokens"] == 50
assert kwargs["seed"] == 123

assert result.value == "expanded"

Expand Down
35 changes: 34 additions & 1 deletion tests/backend/text_llm/test_text_llm_api_models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
"""Tests for TextLLM API request/response models and validation."""

from unittest.mock import MagicMock, patch

import pytest
import torch
from pydantic import ValidationError

from invokeai.app.api.routers.utilities import ExpandPromptRequest, ExpandPromptResponse, ImageToPromptRequest
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.routers.utilities import (
ExpandPromptRequest,
ExpandPromptResponse,
ImageToPromptRequest,
_run_expand_prompt,
)
from invokeai.backend.model_manager.taxonomy import ModelType


class TestExpandPromptRequest:
Expand Down Expand Up @@ -52,3 +62,26 @@ def test_success_response(self):
def test_error_response(self):
resp = ExpandPromptResponse(expanded_prompt="", error="Model failed")
assert resp.error == "Model failed"


def test_expand_prompt_uses_fresh_seed() -> None:
model_config = MagicMock(type=ModelType.TextLLM, path="model")
model = MagicMock()
model.parameters.return_value = iter([torch.nn.Parameter(torch.zeros(1))])
loaded_model = MagicMock()
loaded_model.model_on_device.return_value.__enter__.return_value = (None, model)
services = MagicMock()
services.model_manager.store.get_model.return_value = model_config
services.model_manager.load.load_model.return_value = loaded_model

with (
patch.object(ApiDependencies, "invoker", MagicMock(services=services), create=True),
patch("invokeai.app.api.routers.utilities._resolve_model_path", return_value="model"),
patch("invokeai.app.api.routers.utilities.AutoTokenizer.from_pretrained"),
patch("invokeai.app.api.routers.utilities.get_random_seed", return_value=123),
patch("invokeai.app.api.routers.utilities.TextLLMPipeline") as pipeline_class,
):
pipeline_class.return_value.run.return_value = "expanded"
assert _run_expand_prompt("cat", "model", 10, None, None, "user") == "expanded"

assert pipeline_class.return_value.run.call_args.kwargs["seed"] == 123
Loading
Loading