A one-off bash script that pipes a prompt into curl gets you a quick win, but it falls apart the moment you need typed flags, config files, or sane error messages. This guide walks through building a python cli click gpt-5 wrapper that is maintainable, testable, and ready for production use. We’ll replace ad-hoc shell glue with a proper command-line tool that talks to an OpenAI-compatible endpoint.
Step 1: Scaffold the project and isolate dependencies
Start in a clean directory with a virtual environment. Mixing global packages with a CLI tool is how you end up with version conflicts on the deployment box.
mkdir gpt5cli && cd gpt5cli
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install click openai python-dotenv
click gives you declarative option parsing and help generation without the boilerplate of argparse. openai is the official SDK; it speaks the OpenAI-compatible HTTP contract that most gateways mirror. python-dotenv lets you load secrets from a .env file during local dev.
Create a cli.py stub so the import graph is clear:
import click
@click.command()
def main():
click.echo("placeholder")
if __name__ == "__main__":
main()
Run python cli.py to confirm the environment works before adding logic.
Step 2: Define the CLI surface with Click
A real python cli click gpt-5 tool needs more than a prompt string. You want model selection, sampling controls, and a stream flag. Click handles type coercion and --help generation for free.
import click
from openai import OpenAI
@click.command()
@click.option("--prompt", "-p", required=True, help="Prompt text to send.")
@click.option("--model", default="gpt-5", show_default=True, help="Model identifier.")
@click.option("--temperature", default=0.7, type=float, show_default=True)
@click.option("--max-tokens", default=512, type=int, show_default=True)
@click.option("--stream", is_flag=True, help="Stream tokens to stdout.")
@click.option("--api-key", envvar="OPENAI_API_KEY", help="API key (or set ENV).")
@click.option("--base-url", envvar="OPENAI_BASE_URL",
default="https://api.openai.com/v1", show_default=True)
def main(prompt, model, temperature, max_tokens, stream, api_key, base_url):
"""Minimal python cli click gpt-5 client."""
client = OpenAI(api_key=api_key, base_url=base_url)
# dispatch defined in next step
call_model(client, model, prompt, temperature, max_tokens, stream)
Two decisions matter here. First, envvar on --api-key means the key never appears in shell history. Second, --base-url is parameterized so you can target a different provider without code changes. If you point it at n4n.ai’s OpenAI-compatible endpoint, the same binary gains automatic fallback when a provider is rate-limited or degraded, plus per-token metering, without rewriting the client.
Step 3: Implement the model call
The OpenAI SDK uses chat.completions.create. For GPT-5, treat it as a chat model: a list of messages, not a raw prompt parameter.
def call_model(client, model, prompt, temperature, max_tokens, stream):
messages = [{"role": "user", "content": prompt}]
if stream:
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
stream=True,
)
for chunk in response:
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
click.echo(delta, nl=False)
click.echo()
return
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
click.echo(response.choices[0].message.content)
Non-streaming returns the full completion in response.choices[0].message.content. Streaming yields incremental deltas; we print without newline and flush at the end. This preserves token-by-token UX without corrupting output when the CLI is piped to jq or a file.
One gotcha: the SDK raises openai.APIError (or subclasses) on non-2xx. We handle that in the next step rather than letting Python dump a traceback.
Step 4: Add retries and graceful failure
Providers throttle. A CLI that crashes on the first 429 is useless in a cron job. Wrap the call with a bounded retry loop and surface a clean exit code.
import time
from openai import APIError, RateLimitError
def call_model(client, model, prompt, temperature, max_tokens, stream, retries=3):
messages = [{"role": "user", "content": prompt}]
for attempt in range(retries):
try:
if stream:
response = client.chat.completions.create(
model=model, messages=messages,
temperature=temperature, max_tokens=max_tokens, stream=True)
for chunk in response:
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
click.echo(delta, nl=False)
click.echo()
return
response = client.chat.completions.create(
model=model, messages=messages,
temperature=temperature, max_tokens=max_tokens)
click.echo(response.choices[0].message.content)
return
except RateLimitError:
if attempt == retries - 1:
raise click.ClickException("Rate limited after retries")
time.sleep(2 ** attempt)
except APIError as e:
raise click.ClickException(f"API error: {e}")
click.ClickException prints Error: ... to stderr and exits with code 1. Callers can branch on $? in scripts.
Step 5: Accept prompts from stdin and files
Hard-coding -p is fine for interactive use, but automation needs stdin. Add a --file option or fall back to stdin when --prompt is omitted.
@click.command()
@click.option("--prompt", "-p", help="Prompt text. Omit to read stdin.")
@click.option("--file", "file_path", type=click.Path(exists=True), help="Read prompt from file.")
# ... other options unchanged
def main(prompt, file_path, **kwargs):
if file_path:
with open(file_path) as f:
prompt = f.read()
elif prompt is None:
prompt = click.get_text_stream("stdin").read().strip()
if not prompt:
raise click.UsageError("No prompt provided")
# ...
Now echo "summarize this" | python cli.py works, and so does python cli.py --file brief.txt. That is the difference between a toy and a pipe-friendly utility.
Step 6: Package as an installable command
A python cli click gpt-5 tool should be pip install-able and expose a binary. Write pyproject.toml:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "gpt5cli"
version = "0.1.0"
dependencies = ["click", "openai", "python-dotenv"]
[project.scripts]
gpt5 = "cli:main"
After pip install ., the gpt5 command is on PATH. The entry point cli:main refers to the Click command object in cli.py. No if __name__ == "__main__" shim required for the installed binary, though keep it for direct execution.
Step 7: Verify end to end
Success means: correct stdout, clean exit codes, and graceful degradation. Run these checks inside the venv.
export OPENAI_API_KEY="sk-..."
python cli.py -p "What is 2+2? Reply with just the number"
Expected: prints 4 (or similar) and exits 0. Verify exit code with echo $?.
Test streaming:
python cli.py --stream -p "Count to 3 slowly" | cat
You should see tokens arrive incrementally, not buffered. If your terminal collapses them, | cat forces line-buffered output.
Test failure path:
OPENAI_API_KEY="bad" python cli.py -p "test"; echo $?
Expect Error: ... on stderr and exit code 1.
Finally, build and install:
pip install .
gpt5 -p "Hello from the packaged CLI"
If gpt5 resolves and returns a completion, the migration from bash script to python cli click gpt-5 wrapper is complete. The tool is now typed, testable, and ready to be extended with subcommands like gpt5 chat or gpt5 batch.