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
63 changes: 51 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# LMterminal (`lmt`)

LMterminal (`lmt`) is a CLI tool that enables you to interact directly with OpenAI's ChatGPT models from the comfort of your terminal.
LMterminal (`lmt`) is a CLI tool that enables you to interact directly with OpenAI models from the comfort of your terminal.

![demo](https://github.com/sderev/lmt/assets/24412384/5cb2c7b2-7edd-4b24-919d-581e5cd7c5b5)

Expand All @@ -20,6 +20,7 @@ LMterminal (`lmt`) is a CLI tool that enables you to interact directly with Open
1. [Template Utilization](#template-utilization)
1. [Emoji Integration](#emoji-integration)
1. [Prompt Cost Estimation](#prompt-cost-estimation)
1. [Reasoning Effort and Request Options](#reasoning-effort-and-request-options)
1. [Reading from `stdin`](#reading-from-stdin)
1. [Append an Additional Prompt to Piped `stdin`](#append-an-additional-prompt-to-piped-stdin)
1. [Output Redirection](#output-redirection)
Expand All @@ -31,7 +32,7 @@ LMterminal (`lmt`) is a CLI tool that enables you to interact directly with Open

## Features

* **Access All ChatGPT Models**: `lmt` supports all available ChatGPT models (gpt-3.5-turbo, gpt-3.5-turbo-16k, gpt-4, gpt-4-32k), giving you the power to choose the most suitable one for your task.
* **Access Current OpenAI Models**: `lmt` supports current ChatGPT, GPT-5, Codex, and o-series models. Use `lmt models` to see the full list supported by your installed version.
* **Custom Templates**: Design your own toolbox of templates to streamline your workflow.
* **Read File**: Incorporate file content into your prompts seamlessly.
* **Output to a File**: Redirect standard output (`stdout`) to a file or another program as needed.
Expand Down Expand Up @@ -113,20 +114,18 @@ Switching between different models is a breeze with `lmt`. Use the `-m` flag fol
lmt "Explain what is a large language model" -m 4
```

Below is a table outlining available model aliases for your convenience:
Run `lmt models` to print the full list of supported model names and aliases.

A few examples:

| Alias | Corresponding Model |
| --- | --- |
| chatgpt | gpt-3.5-turbo |
| chatgpt-16k | gpt-3.5-turbo-16k |
| 3.5 | gpt-3.5-turbo |
| 3.5-16k | gpt-3.5-turbo-16k |
| 4 | gpt-4 |
| gpt4 | gpt-4 |
| 4-32k | gpt-4-32k |
| gpt4-32k | gpt-4-32k |
| `chatgpt` | `gpt-3.5-turbo` |
| `4o` | `gpt-4o` |
| `5` | `gpt-5` |
| `5.4` | `gpt-5.4` |

For instance, if you want to use the `gpt-4` model, simply include `-m 4` in your command.
For instance, if you want to use `gpt-5.4`, simply include `-m 5.4` in your command.

### Template Utilization

Expand Down Expand Up @@ -158,6 +157,46 @@ To infuse a touch of emotion into your requests, append the `--emoji` flag optio

For an estimation of your prompt's cost before sending, utilize the `--tokens` flag option.

`lmt` reports the prompt token count and the prompt-side input cost for the selected model.
For the `gpt-5.4` family, `lmt` applies short-context pricing below `272000` prompt tokens and long-context pricing at `272000` tokens and above when the model has a separate long-context rate.

### Reasoning Effort and Request Options

For models that support reasoning control, use `--reasoning-effort`:

```bash
lmt "Explain this diff" -m gpt-5.4 --reasoning-effort high
```

Accepted values are:

* `none`
* `minimal`
* `low`
* `medium`
* `high`
* `xhigh`

You can also pass additional Chat Completions request options with repeatable `-o/--option key=value` pairs:

```bash
lmt "Summarize this file" -m gpt-5.4 \
--reasoning-effort medium \
-o verbosity=low \
-o top_p=0.9 \
-o metadata.topic="cli"
```

Nested request objects can be passed with dotted keys or JSON values:

```bash
lmt "Search the web for recent releases" \
-o web_search_options.search_context_size=high \
-o response_format='{"type":"json_object"}'
```

If a request option already has a dedicated CLI flag such as `--model`, `--temperature`, `--no-stream`, or `--reasoning-effort`, use that dedicated flag instead of `-o`.

### Reading from `stdin`

`lmt` facilitates reading inputs directly from `stdin`, allowing you to pipe in the content of a file as a prompt. This feature can be particularly useful when dealing with longer or more complex prompts, or when you want to streamline your workflow by incorporating `lmt` into a larger pipeline of commands.
Expand Down
5 changes: 5 additions & 0 deletions changelog.d/20260415_235800_ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Changed
-------
* Add GPT-5.3, GPT-5.4, GPT-5 Pro, `o3-pro`, and newer Codex model names to the CLI model registry.
* Use shared model metadata for alias resolution and prompt-cost estimation, including short vs long pricing for the `gpt-5.4` family.
* Add `--reasoning-effort` and repeatable `-o/--option key=value` request passthrough for Chat Completions.
172 changes: 97 additions & 75 deletions lmterminal/cli.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,35 @@
import filecmp
import json
import shutil
import sys

import click
from click_default_group import DefaultGroup

from .lib import DEFAULT_MODEL, edit_key, prepare_and_generate_response, set_key
from .model_registry import REASONING_EFFORTS, get_valid_models, resolve_model_name
from .templates import TEMPLATES_DIR, get_default_template_file_path

VALID_MODELS = {
"gpt-3.5-turbo": (
"chatgpt",
"3.5",
),
"gpt-3.5-turbo-instruct": None,
"gpt-4": (
"4",
"gpt4",
),
"gpt-4-turbo": (
"4t",
"4-turbo",
"gpt4-turbo",
),
"gpt-4-32k": (
"4-32k",
"gpt4-32k",
),
"gpt-4o": ("4o",),
"gpt-4o-2024-05-13": None,
"gpt-4o-2024-08-06": None,
"gpt-4o-2024-11-20": None,
"gpt-4o-mini": (
"4o-mini",
"4omini",
"4om",
),
"gpt-4o-mini-2024-07-18": None,
"chatgpt-4o-latest": None,
"o1": None,
"o1-2024-12-17": None,
"o1-preview": None,
"o1-preview-2024-09-12": None,
"o1-mini": None,
"o1-mini-2024-09-12": None,
"o1-pro": None,
"o1-pro-2025-03-19": None,
"gpt-4.1": ("4.1",),
"gpt-4.1-2025-04-14": None,
"gpt-4.1-mini": ("4.1-mini",),
"gpt-4.1-mini-2025-04-14": None,
"gpt-4.1-nano": ("4.1-nano",),
"gpt-4.1-nano-2025-04-14": None,
"gpt-4.5-preview": None,
"o3": None,
"o3-2025-04-16": None,
"o3-mini": None,
"o3-mini-2025-01-31": None,
"o4-mini": None,
"o4-mini-2025-04-16": None,
"codex-mini-latest": None,
"gpt-4o-search-preview": None,
"gpt-4o-search-preview-2025-03-11": None,
"gpt-4o-mini-search-preview": None,
"gpt-4o-mini-search-preview-2025-03-11": None,
"gpt-5": (
"5",
"gpt5",
),
"gpt-5-mini": ("5-mini",),
"gpt-5-nano": ("5-nano",),
"gpt-5-chat-latest": None,
"gpt-5.1": ("5.1",),
"gpt-5.2": ("5.2",),
RESERVED_REQUEST_OPTION_KEYS = {
"messages": None,
"model": "--model",
"n": None,
"reasoning_effort": "--reasoning-effort",
"stream": "--no-stream",
"temperature": "--temperature",
}

VALID_MODELS = get_valid_models()


# The first two parameters are required by Click for a callback.
def validate_model_name(ctx, param, value):
"""
Validates the model name parameter.
"""
# This is the value that the user entered for the model name.
model_name = value.lower()

for model, aliases in VALID_MODELS.items():
if model_name == model:
return model
if aliases is not None and model_name in aliases:
return model
canonical_model_name = resolve_model_name(value)
if canonical_model_name is not None:
return canonical_model_name

error_message = (
f"{click.style('Invalid model name.', fg='red')}\n"
Expand All @@ -109,11 +50,59 @@ def validate_temperature(ctx, param, value):
raise click.BadParameter("Temperature must be between 0 and 2.")


def parse_request_option_value(raw_value):
"""Parses a request option value from the CLI."""
try:
return json.loads(raw_value)
except json.JSONDecodeError:
return raw_value


def add_request_option(options, key, value):
"""Adds a parsed request option to a nested mapping."""
key_parts = key.split(".")
if any(not key_part for key_part in key_parts):
raise click.BadParameter("Option keys cannot contain empty path segments.")

current_level = options
for key_part in key_parts[:-1]:
if key_part not in current_level:
current_level[key_part] = {}
existing_value = current_level[key_part]
if not isinstance(existing_value, dict):
raise click.BadParameter(
f"Option `{key}` conflicts with an existing non-object option."
)
current_level = existing_value

leaf_key = key_parts[-1]
if leaf_key in current_level:
raise click.BadParameter(f"Option `{key}` was provided more than once.")
current_level[leaf_key] = value


def parse_request_options(ctx, param, values):
"""Parses repeatable `key=value` request options from the CLI."""
options = {}

for raw_option in values:
if "=" not in raw_option:
raise click.BadParameter("Options must use the `key=value` form.")

key, raw_value = raw_option.split("=", 1)
if not key:
raise click.BadParameter("Option keys cannot be empty.")

add_request_option(options, key, parse_request_option_value(raw_value))

return options


@click.group(cls=DefaultGroup, default="prompt", default_if_no_args=True)
@click.version_option(package_name="lmterminal")
def lmt():
"""
Talk to ChatGPT.
Talk to OpenAI models.

Documentation: https://github.com/sderev/lmterminal
"""
Expand Down Expand Up @@ -152,6 +141,19 @@ def lmt():
help="The temperature to use for the requests.",
show_default=True,
)
@click.option(
"--reasoning-effort",
type=click.Choice(REASONING_EFFORTS),
help="Set the reasoning effort for supported models.",
)
@click.option(
"-o",
"--option",
"request_options",
multiple=True,
callback=parse_request_options,
help="Pass additional Chat Completions request options as `key=value`.",
)
@click.option(
"--tokens",
is_flag=True,
Expand Down Expand Up @@ -191,6 +193,8 @@ def prompt(
system,
emoji,
temperature,
reasoning_effort,
request_options,
tokens,
no_stream,
raw,
Expand All @@ -199,7 +203,7 @@ def prompt(
debug,
):
"""
Talk to ChatGPT.
Talk to OpenAI models.

Example: lmt prompt "Say hello" --emoji
"""
Expand Down Expand Up @@ -244,6 +248,22 @@ def prompt(
),
)

conflicting_request_keys = RESERVED_REQUEST_OPTION_KEYS.keys() & request_options.keys()
if conflicting_request_keys:
conflicting_request_key = sorted(conflicting_request_keys)[0]
dedicated_option = RESERVED_REQUEST_OPTION_KEYS[conflicting_request_key]
if dedicated_option is None:
detail = f"`-o/--option` cannot set `{conflicting_request_key}`."
else:
detail = (
f"`-o/--option` cannot override `{conflicting_request_key}`."
f" Use `{dedicated_option}` instead."
)
raise click.BadOptionUsage(
option_name="option",
message=click.style(detail, fg="red"),
)

# If *not* in an interactive shell or redirecting to a file,
# enable the `--raw` option, viz. disabling `Rich` formatting
if not sys.stdout.isatty():
Expand All @@ -264,6 +284,8 @@ def prompt(
emoji,
prompt_input,
temperature,
reasoning_effort,
request_options,
tokens,
no_stream,
raw,
Expand Down
Loading
Loading