diff --git a/completions/openai.bash b/completions/openai.bash new file mode 100644 index 0000000000..20be0563dc --- /dev/null +++ b/completions/openai.bash @@ -0,0 +1,170 @@ +# Bash completion for openai CLI +# Source this file or add to /etc/bash_completion.d/ + +_openai_completions() +{ + local cur prev commands + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Main commands + commands="api tools" + + # API subcommands + api_commands="chat image audio files models completions fine_tuning" + + # Tools subcommands + tools_commands="fine_tunes" + + # Global options + global_opts="--verbose --api-base --api-key --proxy --organization --api-type --api-version --azure-endpoint --azure-ad-token --version --help" + + # Chat options + chat_opts="--model --messages --temperature --top-p --n --stream --stop --max-tokens --presence-penalty --frequency-penalty --logit-bias --user --response-format --seed --tools --tool-choice --function-call --functions" + + # Image options + image_opts="--model --prompt --size --quality --response-format --style --user --n" + + # Audio options + audio_opts="--model --voice --input --response-format --speed" + + # Files options + files_opts="--file --purpose" + + # Models options + models_opts="--list --retrieve --delete" + + # Completions options + completions_opts="--model --prompt --suffix --temperature --top-p --n --stream --stop --max-tokens --logprobs --echo --presence-penalty --frequency-penalty --best-of --logit-bias --user --seed" + + # Fine-tuning options + fine_tuning_opts="--model --training-file --hyperparameters --suffix --validation-file" + + case ${COMP_CWORD} in + 1) + COMPREPLY=( $(compgen -W "${commands} ${global_opts}" -- ${cur}) ) + return 0 + ;; + 2) + case ${prev} in + api) + COMPREPLY=( $(compgen -W "${api_commands}" -- ${cur}) ) + return 0 + ;; + tools) + COMPREPLY=( $(compgen -W "${tools_commands}" -- ${cur}) ) + return 0 + ;; + -k|--api-key) + # Don't complete API keys + return 0 + ;; + -b|--api-base) + COMPREPLY=( $(compgen -W "https://api.openai.com/v1 https://api.openai.com/v1/azure" -- ${cur}) ) + return 0 + ;; + -o|--organization) + return 0 + ;; + *) + ;; + esac + ;; + 3) + case ${COMP_WORDS[1]} in + api) + case ${prev} in + chat) + COMPREPLY=( $(compgen -W "${chat_opts}" -- ${cur}) ) + return 0 + ;; + image) + COMPREPLY=( $(compgen -W "${image_opts}" -- ${cur}) ) + return 0 + ;; + audio) + COMPREPLY=( $(compgen -W "${audio_opts}" -- ${cur}) ) + return 0 + ;; + files) + COMPREPLY=( $(compgen -W "${files_opts}" -- ${cur}) ) + return 0 + ;; + models) + COMPREPLY=( $(compgen -W "${models_opts}" -- ${cur}) ) + return 0 + ;; + completions) + COMPREPLY=( $(compgen -W "${completions_opts}" -- ${cur}) ) + return 0 + ;; + fine_tuning) + COMPREPLY=( $(compgen -W "${fine_tuning_opts}" -- ${cur}) ) + return 0 + ;; + *) + ;; + esac + ;; + *) + ;; + esac + ;; + *) + # Complete model names for --model option + if [[ ${prev} == "--model" || ${prev} == "-m" ]]; then + local models="gpt-4o gpt-4o-mini gpt-4-turbo gpt-4 gpt-3.5-turbo o1 o1-mini o1-pro o3 o3-mini o4-mini dall-e-3 dall-e-2 whisper-1 tts-1 tts-1-hd" + COMPREPLY=( $(compgen -W "${models}" -- ${cur}) ) + return 0 + fi + + # Complete voice options for --voice + if [[ ${prev} == "--voice" ]]; then + local voices="alloy echo fable onyx nova shimmer" + COMPREPLY=( $(compgen -W "${voices}" -- ${cur}) ) + return 0 + fi + + # Complete size options for --size + if [[ ${prev} == "--size" ]]; then + local sizes="256x256 512x512 1024x1024 1792x1024 1024x1792" + COMPREPLY=( $(compgen -W "${sizes}" -- ${cur}) ) + return 0 + fi + + # Complete response format options + if [[ ${prev} == "--response-format" ]]; then + local formats="json url b64 json_object text" + COMPREPLY=( $(compgen -W "${formats}" -- ${cur}) ) + return 0 + fi + + # Complete quality options + if [[ ${prev} == "--quality" ]]; then + local qualities="standard hd" + COMPREPLY=( $(compgen -W "${qualities}" -- ${cur}) ) + return 0 + fi + + # Complete style options + if [[ ${prev} == "--style" ]]; then + local styles="vivid natural" + COMPREPLY=( $(compgen -W "${styles}" -- ${cur}) ) + return 0 + fi + + # Complete purpose options for files + if [[ ${prev} == "--purpose" ]]; then + local purposes="fine-tune assistants" + COMPREPLY=( $(compgen -W "${purposes}" -- ${cur}) ) + return 0 + fi + + # Default: complete with files + COMPREPLY=( $(compgen -f -- ${cur}) ) + ;; + esac +} + +complete -F _openai_completions openai diff --git a/completions/openai.fish b/completions/openai.fish new file mode 100644 index 0000000000..fee35dc8c8 --- /dev/null +++ b/completions/openai.fish @@ -0,0 +1,124 @@ +# Fish completion for openai CLI +# Add to ~/.config/fish/completions/openai.fish + +# Global options +complete -c openai -l verbose -d "Set verbosity" -f +complete -c openai -l api-base -d "API base URL" -f +complete -c openai -l api-key -d "API key" -f +complete -c openai -l proxy -d "Proxy" -f +complete -c openai -l organization -d "Organization" -f +complete -c openai -l api-type -d "API type" -a "openai azure" -f +complete -c openai -l api-version -d "API version" -f +complete -c openai -l azure-endpoint -d "Azure endpoint" -f +complete -c openai -l azure-ad-token -d "Azure AD token" -f +complete -c openai -l version -d "Show version" -f +complete -c openai -l help -d "Show help" -f + +# Main commands +complete -c openai -n '__fish_use_subcommand' -a api -d "Direct API calls" +complete -c openai -n '__fish_use_subcommand' -a tools -d "Client side tools" + +# API subcommands +complete -c openai -n '__fish_seen_subcommand_from api' -a chat -d "Chat completions" +complete -c openai -n '__fish_seen_subcommand_from api' -a image -d "Image generation" +complete -c openai -n '__fish_seen_subcommand_from api' -a audio -d "Audio/speech" +complete -c openai -n '__fish_seen_subcommand_from api' -a files -d "File management" +complete -c openai -n '__fish_seen_subcommand_from api' -a models -d "Model management" +complete -c openai -n '__fish_seen_subcommand_from api' -a completions -d "Text completions" +complete -c openai -n '__fish_seen_subcommand_from api' -a fine_tuning -d "Fine-tuning jobs" + +# Tools subcommands +complete -c openai -n '__fish_seen_subcommand_from tools' -a fine_tunes -d "Fine-tuning tools" + +# Model names +set -l models gpt-4o gpt-4o-mini gpt-4-turbo gpt-4 gpt-3.5-turbo o1 o1-mini o1-pro o3 o3-mini o4-mini dall-e-3 dall-e-2 whisper-1 tts-1 tts-1-hd + +# Voice options +set -l voices alloy echo fable onyx nova shimmer + +# Size options +set -l sizes 256x256 512x512 1024x1024 1792x1024 1024x1792 + +# Response format options +set -l formats json url b64 json_object text + +# Quality options +set -l qualities standard hd + +# Style options +set -l styles vivid natural + +# Purpose options +set -l purposes fine-tune assistants + +# Chat options +complete -c openai -n '__fish_seen_subcommand_from chat' -l model -d "Model to use" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l messages -d "Messages JSON" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l temperature -d "Temperature" -a "0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l top-p -d "Top P" -a "0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l n -d "Number of completions" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l stream -d "Stream responses" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l stop -d "Stop sequences" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l max-tokens -d "Max tokens" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l presence-penalty -d "Presence penalty" -a "-2.0 -1.0 0.0 1.0 2.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l frequency-penalty -d "Frequency penalty" -a "-2.0 -1.0 0.0 1.0 2.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l logit-bias -d "Logit bias JSON" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l user -d "User ID" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l response-format -d "Response format" -a "$formats" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l seed -d "Seed" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l tools -d "Tools JSON" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l tool-choice -d "Tool choice" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l function-call -d "Function call" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l functions -d "Functions JSON" -f + +# Image options +complete -c openai -n '__fish_seen_subcommand_from image' -l model -d "Model" -a "dall-e-3 dall-e-2" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l prompt -d "Prompt" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l size -d "Size" -a "$sizes" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l quality -d "Quality" -a "$qualities" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l response-format -d "Format" -a "url b64" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l style -d "Style" -a "$styles" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l user -d "User ID" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l n -d "Number of images" -f + +# Audio options +complete -c openai -n '__fish_seen_subcommand_from audio' -l model -d "Model" -a "whisper-1 tts-1 tts-1-hd" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l voice -d "Voice" -a "$voices" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l input -d "Input text/file" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l response-format -d "Format" -a "mp3 opus aac flac wav pcm" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l speed -d "Speed" -a "0.25 0.5 1.0 2.0 4.0" -f + +# Files options +complete -c openai -n '__fish_seen_subcommand_from files' -l file -d "File path" -f +complete -c openai -n '__fish_seen_subcommand_from files' -l purpose -d "Purpose" -a "$purposes" -f + +# Models options +complete -c openai -n '__fish_seen_subcommand_from models' -l list -d "List models" -f +complete -c openai -n '__fish_seen_subcommand_from models' -l retrieve -d "Retrieve model" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from models' -l delete -d "Delete model" -a "$models" -f + +# Completions options +complete -c openai -n '__fish_seen_subcommand_from completions' -l model -d "Model" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l prompt -d "Prompt" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l suffix -d "Suffix" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l temperature -d "Temperature" -a "0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l top-p -d "Top P" -a "0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l n -d "Number" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l stream -d "Stream" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l stop -d "Stop" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l max-tokens -d "Max tokens" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l logprobs -d "Log probs" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l echo -d "Echo prompt" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l presence-penalty -d "Presence penalty" -a "-2.0 -1.0 0.0 1.0 2.0" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l frequency-penalty -d "Frequency penalty" -a "-2.0 -1.0 0.0 1.0 2.0" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l best-of -d "Best of" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l logit-bias -d "Logit bias JSON" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l user -d "User ID" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l seed -d "Seed" -f + +# Fine-tuning options +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l model -d "Model" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l training-file -d "Training file" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l hyperparameters -d "Hyperparameters JSON" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l suffix -d "Suffix" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l validation-file -d "Validation file" -f diff --git a/completions/openai.zsh b/completions/openai.zsh new file mode 100644 index 0000000000..ba3f0e3ec0 --- /dev/null +++ b/completions/openai.zsh @@ -0,0 +1,218 @@ +#compdef openai + +# Zsh completion for openai CLI +# Add to ~/.zshrc: autoload -Uz compinit && compinit + +_openai() { + local -a commands + local -a api_commands + local -a tools_commands + local -a models + local -a voices + local -a sizes + local -a formats + local -a qualities + local -a styles + local -a purposes + + commands=( + 'api:Direct API calls' + 'tools:Client side tools for convenience' + ) + + api_commands=( + 'chat:Chat completions API' + 'image:Image generation API' + 'audio:Audio/speech API' + 'files:File management API' + 'models:Model management API' + 'completions:Text completions API' + 'fine_tuning:Fine-tuning jobs API' + ) + + tools_commands=( + 'fine_tunes:Fine-tuning tools' + ) + + models=( + 'gpt-4o:GPT-4o - Most capable model' + 'gpt-4o-mini:GPT-4o Mini - Fast and efficient' + 'gpt-4-turbo:GPT-4 Turbo - With vision' + 'gpt-4:GPT-4 - Original GPT-4' + 'gpt-3.5-turbo:GPT-3.5 Turbo - Fast and cheap' + 'o1:O1 - Reasoning model' + 'o1-mini:O1 Mini - Fast reasoning' + 'o1-pro:O1 Pro - Enhanced reasoning' + 'o3:O3 - Latest reasoning' + 'o3-mini:O3 Mini - Fast latest reasoning' + 'o4-mini:O4 Mini - Latest fast reasoning' + 'dall-e-3:DALL-E 3 - Image generation' + 'dall-e-2:DALL-E 2 - Image generation' + 'whisper-1:Whisper - Speech to text' + 'tts-1:TTS - Text to speech' + 'tts-1-hd:TTS HD - High quality speech' + ) + + voices=(alloy echo fable onyx nova shimmer) + sizes=(256x256 512x512 1024x1024 1792x1024 1024x1792) + formats=(json url b64 json_object text) + qualities=(standard hd) + styles=(vivid natural) + purposes=(fine-tune assistants) + + _arguments -C \ + '(-h --help)'{-h,--help}'[Show help]' \ + '(-v --verbose)'{-v,--verbose}'[Set verbosity]' \ + '(-b --api-base)'{-b,--api-base}'[API base URL]:url:_urls' \ + '(-k --api-key)'{-k,--api-key}'[API key]:key:' \ + '(-p --proxy)'{-p,--proxy}'[Proxy]:proxy:' \ + '(-o --organization)'{-o,--organization}'[Organization]:org:' \ + '--api-type[API type]:type:(openai azure)' \ + '--api-version[API version]:version:' \ + '--azure-endpoint[Azure endpoint]:endpoint:_urls' \ + '--azure-ad-token[Azure AD token]:token:' \ + '(-V --version)'{-V,--version}'[Show version]' \ + '1: :->cmd' \ + '*:: :->args' + + case $state in + cmd) + _describe -t commands 'openai command' commands + ;; + args) + case $words[1] in + api) + _arguments \ + '1: :->api_cmd' \ + '*:: :->api_args' + + case $state in + api_cmd) + _describe -t commands 'api command' api_commands + ;; + api_args) + case $words[1] in + chat) + _arguments \ + '--model[Model to use]:model:->models' \ + '--messages[Messages JSON]:messages:' \ + '--temperature[Temperature]:temperature:(0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0)' \ + '--top-p[Top P]:top_p:(0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0)' \ + '--n[Number of completions]:n:' \ + '--stream[Stream responses]' \ + '--stop[Stop sequences]:stop:' \ + '--max-tokens[Max tokens]:max_tokens:' \ + '--presence-penalty[Presence penalty]:penalty:(-2.0 -1.0 0.0 1.0 2.0)' \ + '--frequency-penalty[Frequency penalty]:penalty:(-2.0 -1.0 0.0 1.0 2.0)' \ + '--logit-bias[Logit bias JSON]:bias:' \ + '--user[User ID]:user:' \ + '--response-format[Response format]:format:->formats' \ + '--seed[Seed]:seed:' \ + '--tools[Tools JSON]:tools:' \ + '--tool-choice[Tool choice]:choice:' \ + '--function-call[Function call]:call:' \ + '--functions[Functions JSON]:functions:' + ;; + image) + _arguments \ + '--model[Model]:model:(dall-e-3 dall-e-2)' \ + '--prompt[Prompt]:prompt:' \ + '--size[Size]:size:->sizes' \ + '--quality[Quality]:quality:->qualities' \ + '--response-format[Format]:format:(url b64)' \ + '--style[Style]:style:->styles' \ + '--user[User ID]:user:' \ + '--n[Number of images]:n:' + ;; + audio) + _arguments \ + '--model[Model]:model:(whisper-1 tts-1 tts-1-hd)' \ + '--voice[Voice]:voice:->voices' \ + '--input[Input text/file]:input:_files' \ + '--response-format[Format]:format:(mp3 opus aac flac wav pcm)' \ + '--speed[Speed]:speed:(0.25 0.5 1.0 2.0 4.0)' + ;; + files) + _arguments \ + '--file[File path]:file:_files' \ + '--purpose[Purpose]:purpose:->purposes' + ;; + models) + _arguments \ + '--list[List models]' \ + '--retrieve[Retrieve model]:model:->models' \ + '--delete[Delete model]:model:->models' + ;; + completions) + _arguments \ + '--model[Model]:model:->models' \ + '--prompt[Prompt]:prompt:' \ + '--suffix[Suffix]:suffix:' \ + '--temperature[Temperature]:temperature:(0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0)' \ + '--top-p[Top P]:top_p:(0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0)' \ + '--n[Number]:n:' \ + '--stream[Stream]' \ + '--stop[Stop]:stop:' \ + '--max-tokens[Max tokens]:max_tokens:' \ + '--logprobs[Log probs]:logprobs:' \ + '--echo[Echo prompt]' \ + '--presence-penalty[Presence penalty]:penalty:(-2.0 -1.0 0.0 1.0 2.0)' \ + '--frequency-penalty[Frequency penalty]:penalty:(-2.0 -1.0 0.0 1.0 2.0)' \ + '--best-of[Best of]:n:' \ + '--logit-bias[Logit bias JSON]:bias:' \ + '--user[User ID]:user:' \ + '--seed[Seed]:seed:' + ;; + fine_tuning) + _arguments \ + '--model[Model]:model:->models' \ + '--training-file[Training file]:file:_files' \ + '--hyperparameters[Hyperparameters JSON]:params:' \ + '--suffix[Suffix]:suffix:' \ + '--validation-file[Validation file]:file:_files' + ;; + esac + ;; + esac + ;; + tools) + _arguments \ + '1: :->tools_cmd' + + case $state in + tools_cmd) + _describe -t commands 'tools command' tools_commands + ;; + esac + ;; + esac + ;; + esac + + # Handle completions for specific options + case $words[-1] in + --model|-m) + _describe -t models 'model' models + ;; + --voice) + _describe -t voices 'voice' voices + ;; + --size) + _describe -t sizes 'size' sizes + ;; + --response-format) + _describe -t formats 'format' formats + ;; + --quality) + _describe -t qualities 'quality' qualities + ;; + --style) + _describe -t styles 'style' styles + ;; + --purpose) + _describe -t purposes 'purpose' purposes + ;; + esac +} + +_openai "$@" diff --git a/src/openai/cli/_completions.py b/src/openai/cli/_completions.py new file mode 100644 index 0000000000..7fc65abf91 --- /dev/null +++ b/src/openai/cli/_completions.py @@ -0,0 +1,394 @@ +"""Shell completion generation for openai CLI.""" + +from __future__ import annotations + +import sys +import argparse +from typing import Optional + + +COMPLETION_SCRIPTS = { + "bash": """\ +# Bash completion for openai CLI +# Add to ~/.bashrc: source /path/to/openai.bash +# Or copy to /etc/bash_completion.d/openai + +_openai_completions() +{ + local cur prev commands + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + commands="api tools" + api_commands="chat image audio files models completions fine_tuning" + tools_commands="fine_tunes" + + case ${COMP_CWORD} in + 1) + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + return 0 + ;; + 2) + case ${prev} in + api) + COMPREPLY=( $(compgen -W "${api_commands}" -- ${cur}) ) + return 0 + ;; + tools) + COMPREPLY=( $(compgen -W "${tools_commands}" -- ${cur}) ) + return 0 + ;; + esac + ;; + *) + # Complete model names for --model option + if [[ ${prev} == "--model" ]]; then + local models="gpt-4o gpt-4o-mini gpt-4-turbo gpt-4 gpt-3.5-turbo o1 o1-mini o1-pro o3 o3-mini o4-mini dall-e-3 dall-e-2 whisper-1 tts-1 tts-1-hd" + COMPREPLY=( $(compgen -W "${models}" -- ${cur}) ) + return 0 + fi + + # Complete voice options + if [[ ${prev} == "--voice" ]]; then + local voices="alloy echo fable onyx nova shimmer" + COMPREPLY=( $(compgen -W "${voices}" -- ${cur}) ) + return 0 + fi + + # Complete size options + if [[ ${prev} == "--size" ]]; then + local sizes="256x256 512x512 1024x1024 1792x1024 1024x1792" + COMPREPLY=( $(compgen -W "${sizes}" -- ${cur}) ) + return 0 + fi + + # Default: complete with files + COMPREPLY=( $(compgen -f -- ${cur}) ) + ;; + esac +} + +complete -F _openai_completions openai +""", + "zsh": """\ +#compdef openai + +# Zsh completion for openai CLI +# Add to ~/.zshrc: autoload -Uz compinit && compinit + +_openai() { + local -a commands api_commands tools_commands models voices sizes formats qualities styles purposes + + commands=( + 'api:Direct API calls' + 'tools:Client side tools for convenience' + ) + + api_commands=( + 'chat:Chat completions API' + 'image:Image generation API' + 'audio:Audio/speech API' + 'files:File management API' + 'models:Model management API' + 'completions:Text completions API' + 'fine_tuning:Fine-tuning jobs API' + ) + + tools_commands=( + 'fine_tunes:Fine-tuning tools' + ) + + models=( + 'gpt-4o:GPT-4o - Most capable model' + 'gpt-4o-mini:GPT-4o Mini - Fast and efficient' + 'gpt-4-turbo:GPT-4 Turbo - With vision' + 'gpt-4:GPT-4 - Original GPT-4' + 'gpt-3.5-turbo:GPT-3.5 Turbo - Fast and cheap' + 'o1:O1 - Reasoning model' + 'o1-mini:O1 Mini - Fast reasoning' + 'o1-pro:O1 Pro - Enhanced reasoning' + 'o3:O3 - Latest reasoning' + 'o3-mini:O3 Mini - Fast latest reasoning' + 'o4-mini:O4 Mini - Latest fast reasoning' + 'dall-e-3:DALL-E 3 - Image generation' + 'dall-e-2:DALL-E 2 - Image generation' + 'whisper-1:Whisper - Speech to text' + 'tts-1:TTS - Text to speech' + 'tts-1-hd:TTS HD - High quality speech' + ) + + voices=(alloy echo fable onyx nova shimmer) + sizes=(256x256 512x512 1024x1024 1792x1024 1024x1792) + formats=(json url b64 json_object text) + qualities=(standard hd) + styles=(vivid natural) + purposes=(fine-tune assistants) + + _arguments -C \\ + '(-h --help)'{-h,--help}'[Show help]' \\ + '(-v --verbose)'{-v,--verbose}'[Set verbosity]' \\ + '(-b --api-base)'{-b,--api-base}'[API base URL]:url:_urls' \\ + '(-k --api-key)'{-k,--api-key}'[API key]:key:' \\ + '(-p --proxy)'{-p,--proxy}'[Proxy]:proxy:' \\ + '(-o --organization)'{-o,--organization}'[Organization]:org:' \\ + '(-V --version)'{-V,--version}'[Show version]' \\ + '1: :->cmd' \\ + '*:: :->args' + + case $state in + cmd) + _describe -t commands 'openai command' commands + ;; + args) + case $words[1] in + api) + _arguments '1: :->api_cmd' '*:: :->api_args' + case $state in + api_cmd) + _describe -t commands 'api command' api_commands + ;; + api_args) + case $words[1] in + chat) + _arguments \\ + '--model[Model]:model:->models' \\ + '--messages[Messages JSON]:messages:' \\ + '--temperature[Temperature]:temperature:' \\ + '--top-p[Top P]:top_p:' \\ + '--n[Number]:n:' \\ + '--stream[Stream]' \\ + '--stop[Stop]:stop:' \\ + '--max-tokens[Max tokens]:max_tokens:' \\ + '--presence-penalty[Presence penalty]:penalty:' \\ + '--frequency-penalty[Frequency penalty]:penalty:' \\ + '--user[User ID]:user:' \\ + '--response-format[Format]:format:->formats' \\ + '--seed[Seed]:seed:' \\ + '--tools[Tools JSON]:tools:' \\ + '--tool-choice[Tool choice]:choice:' + ;; + image) + _arguments \\ + '--model[Model]:model:(dall-e-3 dall-e-2)' \\ + '--prompt[Prompt]:prompt:' \\ + '--size[Size]:size:->sizes' \\ + '--quality[Quality]:quality:->qualities' \\ + '--response-format[Format]:format:(url b64)' \\ + '--style[Style]:style:->styles' \\ + '--user[User ID]:user:' \\ + '--n[Number]:n:' + ;; + audio) + _arguments \\ + '--model[Model]:model:(whisper-1 tts-1 tts-1-hd)' \\ + '--voice[Voice]:voice:->voices' \\ + '--input[Input]:input:_files' \\ + '--response-format[Format]:format:(mp3 opus aac flac wav pcm)' \\ + '--speed[Speed]:speed:(0.25 0.5 1.0 2.0 4.0)' + ;; + files) + _arguments \\ + '--file[File]:file:_files' \\ + '--purpose[Purpose]:purpose:->purposes' + ;; + models) + _arguments \\ + '--list[List]' \\ + '--retrieve[Retrieve]:model:->models' \\ + '--delete[Delete]:model:->models' + ;; + completions) + _arguments \\ + '--model[Model]:model:->models' \\ + '--prompt[Prompt]:prompt:' \\ + '--temperature[Temperature]:temperature:' \\ + '--max-tokens[Max tokens]:max_tokens:' \\ + '--echo[Echo]' \\ + '--stream[Stream]' \\ + '--stop[Stop]:stop:' \\ + '--user[User ID]:user:' \\ + '--seed[Seed]:seed:' + ;; + fine_tuning) + _arguments \\ + '--model[Model]:model:->models' \\ + '--training-file[Training file]:file:_files' \\ + '--hyperparameters[Hyperparams JSON]:params:' \\ + '--suffix[Suffix]:suffix:' \\ + '--validation-file[Validation file]:file:_files' + ;; + esac + ;; + esac + ;; + tools) + _arguments '1: :->tools_cmd' + case $state in + tools_cmd) + _describe -t commands 'tools command' tools_commands + ;; + esac + ;; + esac + ;; + esac + + case $words[-1] in + --model|-m) _describe -t models 'model' models ;; + --voice) _describe -t voices 'voice' voices ;; + --size) _describe -t sizes 'size' sizes ;; + --response-format) _describe -t formats 'format' formats ;; + --quality) _describe -t qualities 'quality' qualities ;; + --style) _describe -t styles 'style' styles ;; + --purpose) _describe -t purposes 'purpose' purposes ;; + esac +} + +_openai "$@" +""", + "fish": """\ +# Fish completion for openai CLI +# Add to ~/.config/fish/completions/openai.fish + +# Global options +complete -c openai -l verbose -d "Set verbosity" -f +complete -c openai -l api-base -d "API base URL" -f +complete -c openai -l api-key -d "API key" -f +complete -c openai -l proxy -d "Proxy" -f +complete -c openai -l organization -d "Organization" -f +complete -c openai -l api-type -d "API type" -a "openai azure" -f +complete -c openai -l api-version -d "API version" -f +complete -c openai -l azure-endpoint -d "Azure endpoint" -f +complete -c openai -l azure-ad-token -d "Azure AD token" -f +complete -c openai -l version -d "Show version" -f +complete -c openai -l help -d "Show help" -f + +# Main commands +complete -c openai -n '__fish_use_subcommand' -a api -d "Direct API calls" +complete -c openai -n '__fish_use_subcommand' -a tools -d "Client side tools" + +# API subcommands +complete -c openai -n '__fish_seen_subcommand_from api' -a chat -d "Chat completions" +complete -c openai -n '__fish_seen_subcommand_from api' -a image -d "Image generation" +complete -c openai -n '__fish_seen_subcommand_from api' -a audio -d "Audio/speech" +complete -c openai -n '__fish_seen_subcommand_from api' -a files -d "File management" +complete -c openai -n '__fish_seen_subcommand_from api' -a models -d "Model management" +complete -c openai -n '__fish_seen_subcommand_from api' -a completions -d "Text completions" +complete -c openai -n '__fish_seen_subcommand_from api' -a fine_tuning -d "Fine-tuning jobs" + +# Tools subcommands +complete -c openai -n '__fish_seen_subcommand_from tools' -a fine_tunes -d "Fine-tuning tools" + +# Model names +set -l models gpt-4o gpt-4o-mini gpt-4-turbo gpt-4 gpt-3.5-turbo o1 o1-mini o1-pro o3 o3-mini o4-mini dall-e-3 dall-e-2 whisper-1 tts-1 tts-1-hd + +# Voice options +set -l voices alloy echo fable onyx nova shimmer + +# Size options +set -l sizes 256x256 512x512 1024x1024 1792x1024 1024x1792 + +# Chat options +complete -c openai -n '__fish_seen_subcommand_from chat' -l model -d "Model" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l messages -d "Messages JSON" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l temperature -d "Temperature" -a "0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l top-p -d "Top P" -a "0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l n -d "Number" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l stream -d "Stream" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l stop -d "Stop" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l max-tokens -d "Max tokens" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l presence-penalty -d "Presence penalty" -a "-2.0 -1.0 0.0 1.0 2.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l frequency-penalty -d "Frequency penalty" -a "-2.0 -1.0 0.0 1.0 2.0" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l user -d "User ID" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l response-format -d "Format" -a "json json_object" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l seed -d "Seed" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l tools -d "Tools JSON" -f +complete -c openai -n '__fish_seen_subcommand_from chat' -l tool-choice -d "Tool choice" -f + +# Image options +complete -c openai -n '__fish_seen_subcommand_from image' -l model -d "Model" -a "dall-e-3 dall-e-2" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l prompt -d "Prompt" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l size -d "Size" -a "$sizes" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l quality -d "Quality" -a "standard hd" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l response-format -d "Format" -a "url b64" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l style -d "Style" -a "vivid natural" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l user -d "User ID" -f +complete -c openai -n '__fish_seen_subcommand_from image' -l n -d "Number" -f + +# Audio options +complete -c openai -n '__fish_seen_subcommand_from audio' -l model -d "Model" -a "whisper-1 tts-1 tts-1-hd" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l voice -d "Voice" -a "$voices" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l input -d "Input" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l response-format -d "Format" -a "mp3 opus aac flac wav pcm" -f +complete -c openai -n '__fish_seen_subcommand_from audio' -l speed -d "Speed" -a "0.25 0.5 1.0 2.0 4.0" -f + +# Files options +complete -c openai -n '__fish_seen_subcommand_from files' -l file -d "File" -f +complete -c openai -n '__fish_seen_subcommand_from files' -l purpose -d "Purpose" -a "fine-tune assistants" -f + +# Models options +complete -c openai -n '__fish_seen_subcommand_from models' -l list -d "List" -f +complete -c openai -n '__fish_seen_subcommand_from models' -l retrieve -d "Retrieve" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from models' -l delete -d "Delete" -a "$models" -f + +# Completions options +complete -c openai -n '__fish_seen_subcommand_from completions' -l model -d "Model" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l prompt -d "Prompt" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l temperature -d "Temperature" -a "0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l max-tokens -d "Max tokens" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l echo -d "Echo" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l stream -d "Stream" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l stop -d "Stop" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l user -d "User ID" -f +complete -c openai -n '__fish_seen_subcommand_from completions' -l seed -d "Seed" -f + +# Fine-tuning options +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l model -d "Model" -a "$models" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l training-file -d "Training file" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l hyperparameters -d "Hyperparameters JSON" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l suffix -d "Suffix" -f +complete -c openai -n '__fish_seen_subcommand_from fine_tuning' -l validation-file -d "Validation file" -f +""", +} + + +def register(parser: argparse.ArgumentParser) -> None: + """Register the completion command.""" + sub = parser.add_parser( + "completion", + help="Generate shell completion scripts", + ) + sub.add_argument( + "shell", + choices=["bash", "zsh", "fish"], + help="Shell to generate completion for", + ) + sub.add_argument( + "--output", + "-o", + help="Output file path (default: stdout)", + ) + sub.set_defaults(func=run) + + +def run(args: argparse.Namespace) -> None: + """Generate shell completion script.""" + script = COMPLETION_SCRIPTS.get(args.shell) + if not script: + print(f"Error: Unsupported shell '{args.shell}'", file=sys.stderr) + return + + if args.output: + with open(args.output, "w") as f: + f.write(script) + print(f"Completion script written to {args.output}") + print(f"\nTo enable completions:") + if args.shell == "bash": + print(f" Add to ~/.bashrc: source {args.output}") + elif args.shell == "zsh": + print(f" Add to ~/.zshrc: source {args.output}") + elif args.shell == "fish": + print(f" Fish completions are auto-loaded from ~/.config/fish/completions/") + else: + print(script) diff --git a/tests/lib/test_completions.py b/tests/lib/test_completions.py new file mode 100644 index 0000000000..db84908d3b --- /dev/null +++ b/tests/lib/test_completions.py @@ -0,0 +1,626 @@ +"""Tests for shell completion scripts in openai.cli._completions.""" + +from __future__ import annotations + +import argparse +import pathlib + +import pytest + +from openai.cli._completions import COMPLETION_SCRIPTS, register, run + + +# --------------------------------------------------------------------------- +# Expected constants +# --------------------------------------------------------------------------- + +EXPECTED_MODELS = [ + "gpt-4o", + "gpt-4o-mini", + "gpt-4-turbo", + "gpt-4", + "gpt-3.5-turbo", + "o1", + "o1-mini", + "o1-pro", + "o3", + "o3-mini", + "o4-mini", + "dall-e-3", + "dall-e-2", + "whisper-1", + "tts-1", + "tts-1-hd", +] + +EXPECTED_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] + +EXPECTED_SIZES = [ + "256x256", + "512x512", + "1024x1024", + "1792x1024", + "1024x1792", +] + +EXPECTED_QUALITIES = ["standard", "hd"] + +EXPECTED_STYLES = ["vivid", "natural"] + +EXPECTED_FORMATS = ["json", "url", "b64", "json_object", "text"] + +EXPECTED_PURPOSES = ["fine-tune", "assistants"] + +EXPECTED_SUBCOMMANDS = { + "main": ["api", "tools"], + "api": [ + "chat", + "image", + "audio", + "files", + "models", + "completions", + "fine_tuning", + ], + "tools": ["fine_tunes"], +} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_completion_env() -> tuple[argparse.ArgumentParser, argparse._SubParsersAction]: + """Return (parent_parser, subparsers) as the real CLI builds them.""" + parser = argparse.ArgumentParser(prog="openai") + subparsers = parser.add_subparsers(dest="command") + return parser, subparsers + + +# --------------------------------------------------------------------------- +# Tests: COMPLETION_SCRIPTS dict +# --------------------------------------------------------------------------- + + +class TestCompletionScriptsDict: + """Verify the COMPLETION_SCRIPTS dictionary structure.""" + + def test_supported_shells(self) -> None: + assert set(COMPLETION_SCRIPTS.keys()) == {"bash", "zsh", "fish"} + + def test_all_scripts_are_strings(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert isinstance(script, str), f"{shell} script is not a string" + + def test_all_scripts_are_non_empty(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert len(script) > 0, f"{shell} script is empty" + + +# --------------------------------------------------------------------------- +# Tests: Bash completion script +# --------------------------------------------------------------------------- + + +class TestBashCompletion: + """Verify the bash completion script content.""" + + @pytest.fixture() + def script(self) -> str: + return COMPLETION_SCRIPTS["bash"] + + def test_contains_completion_function(self, script: str) -> None: + assert "_openai_completions()" in script + + def test_contains_complete_registration(self, script: str) -> None: + assert "complete -F _openai_completions openai" in script + + def test_contains_main_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["main"]: + assert cmd in script, f"Bash script missing main subcommand: {cmd}" + + def test_contains_api_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["api"]: + assert cmd in script, f"Bash script missing api subcommand: {cmd}" + + def test_contains_tools_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["tools"]: + assert cmd in script, f"Bash script missing tools subcommand: {cmd}" + + @pytest.mark.parametrize("model", EXPECTED_MODELS) + def test_contains_model(self, script: str, model: str) -> None: + assert model in script, f"Bash script missing model: {model}" + + @pytest.mark.parametrize("voice", EXPECTED_VOICES) + def test_contains_voice(self, script: str, voice: str) -> None: + assert voice in script, f"Bash script missing voice: {voice}" + + @pytest.mark.parametrize("size", EXPECTED_SIZES) + def test_contains_size(self, script: str, size: str) -> None: + assert size in script, f"Bash script missing size: {size}" + + def test_contains_voice_flag(self, script: str) -> None: + assert '"--voice"' in script or "--voice" in script + + def test_contains_model_flag(self, script: str) -> None: + assert "--model" in script + + def test_contains_size_flag(self, script: str) -> None: + assert "--size" in script + + def test_uses_compgen(self, script: str) -> None: + """Bash completions should use compgen.""" + assert "compgen" in script + + def test_uses_compreply(self, script: str) -> None: + assert "COMPREPLY" in script + + +# --------------------------------------------------------------------------- +# Tests: Zsh completion script +# --------------------------------------------------------------------------- + + +class TestZshCompletion: + """Verify the zsh completion script content.""" + + @pytest.fixture() + def script(self) -> str: + return COMPLETION_SCRIPTS["zsh"] + + def test_contains_compdef(self, script: str) -> None: + assert "#compdef openai" in script + + def test_contains_openai_function(self, script: str) -> None: + assert "_openai()" in script + + def test_contains_main_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["main"]: + assert cmd in script, f"Zsh script missing main subcommand: {cmd}" + + def test_contains_api_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["api"]: + assert cmd in script, f"Zsh script missing api subcommand: {cmd}" + + def test_contains_tools_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["tools"]: + assert cmd in script, f"Zsh script missing tools subcommand: {cmd}" + + @pytest.mark.parametrize("model", EXPECTED_MODELS) + def test_contains_model(self, script: str, model: str) -> None: + assert model in script, f"Zsh script missing model: {model}" + + @pytest.mark.parametrize("voice", EXPECTED_VOICES) + def test_contains_voice(self, script: str, voice: str) -> None: + assert voice in script, f"Zsh script missing voice: {voice}" + + @pytest.mark.parametrize("size", EXPECTED_SIZES) + def test_contains_size(self, script: str, size: str) -> None: + assert size in script, f"Zsh script missing size: {size}" + + @pytest.mark.parametrize("quality", EXPECTED_QUALITIES) + def test_contains_quality(self, script: str, quality: str) -> None: + assert quality in script, f"Zsh script missing quality: {quality}" + + @pytest.mark.parametrize("style", EXPECTED_STYLES) + def test_contains_style(self, script: str, style: str) -> None: + assert style in script, f"Zsh script missing style: {style}" + + @pytest.mark.parametrize("fmt", EXPECTED_FORMATS) + def test_contains_format(self, script: str, fmt: str) -> None: + assert fmt in script, f"Zsh script missing format: {fmt}" + + @pytest.mark.parametrize("purpose", EXPECTED_PURPOSES) + def test_contains_purpose(self, script: str, purpose: str) -> None: + assert purpose in script, f"Zsh script missing purpose: {purpose}" + + def test_uses_arguments(self, script: str) -> None: + assert "_arguments" in script + + def test_uses_describe(self, script: str) -> None: + assert "_describe" in script + + def test_contains_help_flag(self, script: str) -> None: + assert "--help" in script + + def test_contains_version_flag(self, script: str) -> None: + assert "--version" in script + + def test_contains_api_key_flag(self, script: str) -> None: + assert "--api-key" in script + + def test_model_descriptions_present(self, script: str) -> None: + """Zsh script should have descriptions for models.""" + assert "GPT-4o" in script + assert "DALL-E 3" in script or "dall-e-3" in script + + def test_contains_chat_options(self, script: str) -> None: + """Verify chat subcommand options are present.""" + assert "--temperature" in script + assert "--max-tokens" in script + assert "--stream" in script + assert "--messages" in script + + def test_contains_image_options(self, script: str) -> None: + """Verify image subcommand options are present.""" + assert "--prompt" in script + assert "--quality" in script + assert "--style" in script + + def test_contains_audio_options(self, script: str) -> None: + """Verify audio subcommand options are present.""" + assert "--voice" in script + assert "--input" in script + assert "--speed" in script + + +# --------------------------------------------------------------------------- +# Tests: Fish completion script +# --------------------------------------------------------------------------- + + +class TestFishCompletion: + """Verify the fish completion script content.""" + + @pytest.fixture() + def script(self) -> str: + return COMPLETION_SCRIPTS["fish"] + + def test_contains_complete_commands(self, script: str) -> None: + assert "complete -c openai" in script + + def test_contains_main_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["main"]: + assert cmd in script, f"Fish script missing main subcommand: {cmd}" + + def test_contains_api_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["api"]: + assert cmd in script, f"Fish script missing api subcommand: {cmd}" + + def test_contains_tools_subcommands(self, script: str) -> None: + for cmd in EXPECTED_SUBCOMMANDS["tools"]: + assert cmd in script, f"Fish script missing tools subcommand: {cmd}" + + @pytest.mark.parametrize("model", EXPECTED_MODELS) + def test_contains_model(self, script: str, model: str) -> None: + assert model in script, f"Fish script missing model: {model}" + + @pytest.mark.parametrize("voice", EXPECTED_VOICES) + def test_contains_voice(self, script: str, voice: str) -> None: + assert voice in script, f"Fish script missing voice: {voice}" + + @pytest.mark.parametrize("size", EXPECTED_SIZES) + def test_contains_size(self, script: str, size: str) -> None: + assert size in script, f"Fish script missing size: {size}" + + @pytest.mark.parametrize("quality", EXPECTED_QUALITIES) + def test_contains_quality(self, script: str, quality: str) -> None: + assert quality in script, f"Fish script missing quality: {quality}" + + @pytest.mark.parametrize("style", EXPECTED_STYLES) + def test_contains_style(self, script: str, style: str) -> None: + assert style in script, f"Fish script missing style: {style}" + + def test_uses_fish_subcommand_detection(self, script: str) -> None: + assert "__fish_use_subcommand" in script + + def test_uses_fish_seen_subcommand(self, script: str) -> None: + assert "__fish_seen_subcommand_from" in script + + def test_contains_global_options(self, script: str) -> None: + """Fish uses `-l` syntax for long options, not `--`.""" + assert "-l verbose" in script + assert "-l api-base" in script + assert "-l api-key" in script + assert "-l proxy" in script + assert "-l organization" in script + assert "-l version" in script + assert "-l help" in script + + def test_contains_azure_options(self, script: str) -> None: + """Fish script includes Azure-related options.""" + assert "-l api-type" in script + assert "openai azure" in script + assert "-l api-version" in script + assert "-l azure-endpoint" in script + assert "-l azure-ad-token" in script + + def test_contains_chat_options(self, script: str) -> None: + """Fish uses `-l` for long option names.""" + assert "-l model" in script + assert "-l temperature" in script + assert "-l max-tokens" in script + assert "-l stream" in script + assert "-l messages" in script + + def test_contains_image_options(self, script: str) -> None: + assert "-l prompt" in script + assert "-l quality" in script + assert "-l style" in script + + def test_contains_audio_options(self, script: str) -> None: + assert "-l voice" in script + assert "-l input" in script + assert "-l speed" in script + + def test_contains_file_options(self, script: str) -> None: + assert "-l file" in script + assert "-l purpose" in script + + def test_contains_models_subcommand_options(self, script: str) -> None: + assert "-l list" in script + assert "-l retrieve" in script + assert "-l delete" in script + + +# --------------------------------------------------------------------------- +# Tests: Script consistency across shells +# --------------------------------------------------------------------------- + + +class TestCrossShellConsistency: + """Verify that all three shell scripts expose the same models/options.""" + + def test_all_scripts_contain_same_models(self) -> None: + for model in EXPECTED_MODELS: + for shell, script in COMPLETION_SCRIPTS.items(): + assert model in script, f"{shell} script missing model: {model}" + + def test_all_scripts_contain_same_voices(self) -> None: + for voice in EXPECTED_VOICES: + for shell, script in COMPLETION_SCRIPTS.items(): + assert voice in script, f"{shell} script missing voice: {voice}" + + def test_all_scripts_contain_same_sizes(self) -> None: + for size in EXPECTED_SIZES: + for shell, script in COMPLETION_SCRIPTS.items(): + assert size in script, f"{shell} script missing size: {size}" + + def test_all_scripts_contain_main_subcommands(self) -> None: + for cmd in EXPECTED_SUBCOMMANDS["main"]: + for shell, script in COMPLETION_SCRIPTS.items(): + assert cmd in script, ( + f"{shell} script missing main subcommand: {cmd}" + ) + + +# --------------------------------------------------------------------------- +# Tests: register() and run() +# --------------------------------------------------------------------------- + + +class TestRegister: + """Test the CLI argument registration.""" + + def test_register_adds_completion_subparser(self) -> None: + parent, subparsers = _make_completion_env() + register(subparsers) + # Parse a valid completion command — should not raise + args = parent.parse_args(["completion", "bash"]) + assert args.shell == "bash" + + def test_register_accepts_all_shells(self) -> None: + parent, subparsers = _make_completion_env() + register(subparsers) + for shell in ("bash", "zsh", "fish"): + args = parent.parse_args(["completion", shell]) + assert args.shell == shell + + def test_register_rejects_invalid_shell(self) -> None: + parent, subparsers = _make_completion_env() + register(subparsers) + with pytest.raises(SystemExit): + parent.parse_args(["completion", "powershell"]) + + def test_register_accepts_output_flag(self) -> None: + parent, subparsers = _make_completion_env() + register(subparsers) + args = parent.parse_args(["completion", "bash", "--output", "/tmp/out.sh"]) + assert args.output == "/tmp/out.sh" + + def test_register_accepts_short_output_flag(self) -> None: + parent, subparsers = _make_completion_env() + register(subparsers) + args = parent.parse_args(["completion", "bash", "-o", "/tmp/out.sh"]) + assert args.output == "/tmp/out.sh" + + def test_register_sets_func_default(self) -> None: + parent, subparsers = _make_completion_env() + register(subparsers) + args = parent.parse_args(["completion", "zsh"]) + assert args.func is run + + +class TestRun: + """Test the run() function that outputs scripts.""" + + def test_run_bash_prints_to_stdout(self, capsys: pytest.CaptureFixture[str]) -> None: + args = argparse.Namespace(shell="bash", output=None) + run(args) + captured = capsys.readouterr() + assert "_openai_completions()" in captured.out + assert "complete -F _openai_completions openai" in captured.out + + def test_run_zsh_prints_to_stdout(self, capsys: pytest.CaptureFixture[str]) -> None: + args = argparse.Namespace(shell="zsh", output=None) + run(args) + captured = capsys.readouterr() + assert "#compdef openai" in captured.out + assert "_openai()" in captured.out + + def test_run_fish_prints_to_stdout(self, capsys: pytest.CaptureFixture[str]) -> None: + args = argparse.Namespace(shell="fish", output=None) + run(args) + captured = capsys.readouterr() + assert "complete -c openai" in captured.out + + def test_run_bash_writes_to_file(self, tmp_path: pathlib.Path) -> None: + out_file = tmp_path / "openai.bash" + args = argparse.Namespace(shell="bash", output=str(out_file)) + run(args) + assert out_file.exists() + content = out_file.read_text() + assert "_openai_completions()" in content + + def test_run_zsh_writes_to_file(self, tmp_path: pathlib.Path) -> None: + out_file = tmp_path / "openai.zsh" + args = argparse.Namespace(shell="zsh", output=str(out_file)) + run(args) + assert out_file.exists() + content = out_file.read_text() + assert "#compdef openai" in content + + def test_run_fish_writes_to_file(self, tmp_path: pathlib.Path) -> None: + out_file = tmp_path / "openai.fish" + args = argparse.Namespace(shell="fish", output=str(out_file)) + run(args) + assert out_file.exists() + content = out_file.read_text() + assert "complete -c openai" in content + + def test_run_file_output_prints_instructions( + self, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path + ) -> None: + out_file = tmp_path / "openai.bash" + args = argparse.Namespace(shell="bash", output=str(out_file)) + run(args) + captured = capsys.readouterr() + assert "To enable completions" in captured.out + assert "~/.bashrc" in captured.out + + def test_run_zsh_file_output_mentions_zshrc( + self, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path + ) -> None: + out_file = tmp_path / "openai.zsh" + args = argparse.Namespace(shell="zsh", output=str(out_file)) + run(args) + captured = capsys.readouterr() + assert "To enable completions" in captured.out + assert "~/.zshrc" in captured.out + + def test_run_fish_file_output_mentions_fish_completions_path( + self, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path + ) -> None: + out_file = tmp_path / "openai.fish" + args = argparse.Namespace(shell="fish", output=str(out_file)) + run(args) + captured = capsys.readouterr() + assert "fish/completions" in captured.out + + def test_run_output_file_contains_full_script(self, tmp_path: pathlib.Path) -> None: + """Written file should contain the full script content.""" + for shell in ("bash", "zsh", "fish"): + out_file = tmp_path / f"openai.{shell}" + args = argparse.Namespace(shell=shell, output=str(out_file)) + run(args) + content = out_file.read_text() + assert content == COMPLETION_SCRIPTS[shell] + + +# --------------------------------------------------------------------------- +# Tests: Model list completeness +# --------------------------------------------------------------------------- + + +class TestModelLists: + """Verify the model lists are comprehensive and up-to-date.""" + + def test_gpt4_models_present(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert "gpt-4o" in script, f"{shell}: missing gpt-4o" + assert "gpt-4o-mini" in script, f"{shell}: missing gpt-4o-mini" + assert "gpt-4-turbo" in script, f"{shell}: missing gpt-4-turbo" + assert "gpt-4" in script, f"{shell}: missing gpt-4" + + def test_reasoning_models_present(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert "o1" in script, f"{shell}: missing o1" + assert "o1-mini" in script, f"{shell}: missing o1-mini" + assert "o1-pro" in script, f"{shell}: missing o1-pro" + assert "o3" in script, f"{shell}: missing o3" + assert "o3-mini" in script, f"{shell}: missing o3-mini" + assert "o4-mini" in script, f"{shell}: missing o4-mini" + + def test_image_models_present(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert "dall-e-3" in script, f"{shell}: missing dall-e-3" + assert "dall-e-2" in script, f"{shell}: missing dall-e-2" + + def test_audio_models_present(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert "whisper-1" in script, f"{shell}: missing whisper-1" + assert "tts-1" in script, f"{shell}: missing tts-1" + assert "tts-1-hd" in script, f"{shell}: missing tts-1-hd" + + def test_gpt35_present(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert "gpt-3.5-turbo" in script + + def test_expected_model_count(self) -> None: + """Each shell script should reference all expected models.""" + for shell, script in COMPLETION_SCRIPTS.items(): + for model in EXPECTED_MODELS: + assert model in script, f"{shell}: missing model {model}" + + +# --------------------------------------------------------------------------- +# Tests: Script syntactic sanity +# --------------------------------------------------------------------------- + + +class TestScriptSyntax: + """Quick sanity checks that scripts look structurally valid.""" + + def test_bash_script_has_matching_braces(self) -> None: + script = COMPLETION_SCRIPTS["bash"] + assert script.count("{") == script.count("}") + + def test_zsh_script_has_matching_braces(self) -> None: + script = COMPLETION_SCRIPTS["zsh"] + assert script.count("{") == script.count("}") + + def test_bash_no_trailing_whitespace_on_code_lines(self) -> None: + script = COMPLETION_SCRIPTS["bash"] + for line in script.splitlines(): + assert line == line.rstrip() or line.startswith("#"), ( + f"Trailing whitespace: {line!r}" + ) + + def test_fish_comments_no_trailing_whitespace(self) -> None: + script = COMPLETION_SCRIPTS["fish"] + for line in script.splitlines(): + if line.startswith("#"): + assert line == line.rstrip(), ( + f"Trailing whitespace in comment: {line!r}" + ) + + def test_scripts_end_with_newline(self) -> None: + for shell, script in COMPLETION_SCRIPTS.items(): + assert script.endswith("\n"), f"{shell} script should end with newline" + + +# --------------------------------------------------------------------------- +# Tests: Option flag presence across shells +# --------------------------------------------------------------------------- + + +class TestOptionPresence: + """Verify key CLI options are represented in detailed (zsh/fish) scripts.""" + + def test_stream_option_in_detailed_scripts(self) -> None: + """Bash script is minimal (model/voice/size only); zsh and fish have all options.""" + for shell in ("zsh", "fish"): + assert "stream" in COMPLETION_SCRIPTS[shell], f"{shell}: missing stream option" + + def test_temperature_option_in_detailed_scripts(self) -> None: + for shell in ("zsh", "fish"): + assert "temperature" in COMPLETION_SCRIPTS[shell], f"{shell}: missing temperature option" + + def test_max_tokens_option_in_detailed_scripts(self) -> None: + for shell in ("zsh", "fish"): + assert "max-tokens" in COMPLETION_SCRIPTS[shell], f"{shell}: missing max-tokens option" + + def test_prompt_option_in_detailed_scripts(self) -> None: + for shell in ("zsh", "fish"): + assert "prompt" in COMPLETION_SCRIPTS[shell], f"{shell}: missing prompt option"