Ever wanted to chat with any AI model directly from your terminal without leaving your workflow? I built a lightweight ZSH function that connects to any OpenAI-compatible API with minimal dependencies—just curl and jq. I use fuelix.ai as my provider, which gives me access to dozens of models from OpenAI, Anthropic, Google, and others through a single API. But the function works with any OpenAI-compatible endpoint—local models, direct provider APIs, or custom services.

The Problem

While there are many web-based and desktop AI chat applications, constantly switching contexts between your terminal and browser breaks your flow. Most CLI tools are locked you into a specific provider, or looking at you open-claw, have way to much access to the system. I wanted something that:

  • Works entirely in the terminal
  • Has minimal dependencies (just shell, curl, and jq)
  • Works with any OpenAI-compatible API
  • Maintains conversation context
  • Supports both interactive and one-off queries
  • Feels natural to use

The Solution

I created a chat() function for ZSH that provides an interactive AI chat session with full conversation history, readline editing capabilities, and a clean interface. I use it with fuelix.ai, which lets me switch between GPT-4, Claude, Gemini, and dozens of other models on the fly. But it’s provider-agnostic—works with OpenAI, Anthropic via proxies, Ollama, or any custom OpenAI-compatible endpoint.

How It Works

The function uses several ZSH features to create a polished experience:

  1. vared: ZSH’s built-in variable editor provides readline-style editing with history and cursor movement
  2. Custom Keymaps: A custom keymap handles Ctrl+D to exit gracefully
  3. jq for JSON: Message history is stored as JSON and manipulated with jq
  4. Temporary Files: Conversation history is kept in a temp file that’s automatically cleaned up
  5. ANSI Colors: print -P with ZSH color codes creates the colored prompts

The API integration uses curl to call any OpenAI-compatible endpoint, sending the full conversation history with each request. No SDKs, no pip installs, no npm packages—just standard Unix tools.

Usage Examples

Quick question:

$ chat what\'s the difference between zsh and bash in one sentence
$ Zsh is a more feature-rich, highly customizable shell with advanced completion and prompt/theming support, while Bash is the more widely defaulted, POSIX-oriented shell focused on compatibility and scripting portability.

Interactive session:

$ chat
Chat started. Type '/q' or Ctrl+D to exit, '/new' to reset.
Type '/model <name>' to change the model (current: gpt-5.2).
-----------------------------------
You: How do I find all Python files modified in the last day?
AI: Use: find . -name "*.py" -mtime 0

You: What about excluding virtual environments?
AI: Add -not -path "*/venv/*": find . -name "*.py" -mtime 0 -not -path "*/venv/*"

You: /q
$

The Complete Code

chat() {
    local history_file=$(mktemp)
    local model="gpt-5.2"
    
    # Disable syntax highlighting for vared (fixes red text)
    local ZSH_HIGHLIGHT_HIGHLIGHTERS=()
    
    # Create a custom widget to handle Ctrl+D
    _chat_exit_widget() {
        BUFFER="/q"
        zle accept-line
    }
    zle -N _chat_exit_widget
    
    # Create a custom keymap for chat
    bindkey -N chat_keymap main
    bindkey -M chat_keymap "^D" _chat_exit_widget
    
    # Trap to clean up history file on exit
    trap "rm -f $history_file" EXIT INT TERM

    # Initialize history array
    echo "[]" >| "$history_file"

    # Helper to add message to history
    _chat_add_message() {
        local role="$1"
        local content="$2"
        local tmp=$(mktemp)
        # Use jq to append to the JSON array in the file
        jq --arg role "$role" --arg content "$content" \
           '. + [{role: $role, content: $content}]' "$history_file" >| "$tmp" && mv "$tmp" "$history_file"
    }

    # Helper to make API call
    _chat_call_api() {
        local payload=$(jq -n --arg model "$model" --slurpfile messages "$history_file" '{model: $model, messages: $messages[0]}')
        
        # Call API
        local response=$(curl -s "$FUELIX_BASE_URL/chat/completions" \
            -H "Content-Type: application/json" \
            -H "Authorization: Bearer $FUELIX_API_KEY" \
            -d "$payload")
            
        # Parse response
        local content=$(printf '%s' "$response" | jq -r '.choices[0].message.content // empty')
        
        if [[ -z "$content" ]]; then
            local error=$(printf '%s' "$response" | jq -r '.error.message // "Unknown error"')
            echo "Error: $error"
            return 1
        else
            echo "$content"
            _chat_add_message "assistant" "$content"
            return 0
        fi
    }

    # If arguments provided, treat as one-off question
    if [[ -n "$@" ]]; then
        _chat_add_message "user" "$*"
        _chat_call_api
        rm -f "$history_file"
        trap - EXIT INT TERM
        return
    fi

    # Interactive mode
    clear
    echo "Chat started. Type '/q' or Ctrl+D to exit, '/new' to reset."
    echo "Type '/model <name>' to change the model (current: $model)."
    echo "-----------------------------------"

    local user_input=""
    while true; do
        # Use vared for readline-like input editing
        # -p is prompt, -c creates variable if needed
        # Use custom keymap to handle Ctrl+D
        if ! vared -M chat_keymap -p "%F{green}You:%f " -c user_input; then
            echo ""
            break
        fi
        
        # Check for exit command
        if [[ "$user_input" == "bye" || "$user_input" == "exit" || "$user_input" == "quit" || "$user_input" == "/q" ]]; then
            break
        fi

        # Check for reset command
        if [[ "$user_input" == "/new" ]]; then
            echo "[]" >| "$history_file"
            clear
            echo "Chat started. Type '/q' or Ctrl+D to exit, '/new' to reset."
            echo "Type '/model <name>' to change the model (current: $model)."
            echo "-----------------------------------"
            user_input=""
            continue
        fi

        # Check for model command
        if [[ "$user_input" == "/model"* ]]; then
            local new_model=$(echo "$user_input" | cut -d' ' -f2-)
            if [[ -n "$new_model" && "$new_model" != "/model" ]]; then
                model="$new_model"
                echo "Model changed to: $model"
            else
                echo "Current model: $model"
            fi
            user_input=""
            continue
        fi

        # Skip empty input
        if [[ -z "${user_input// }" ]]; then
            continue
        fi

        _chat_add_message "user" "$user_input"
        
        print -nP "%F{blue}AI:%f ...\r"
        
        # Capture output to print it cleanly
        local result
        result=$(_chat_call_api)
        
        # Clear the "..." line
        print -n "\r\033[K"
        print -P "%F{blue}AI:%f $result"
        echo ""
        
        # Reset input for next loop
        user_input=""
    done
    
    rm -f "$history_file"
    trap - EXIT INT TERM
}

Setup

To use this function, you’ll need:

  1. Dependencies: Install jq for JSON manipulation (brew install jq on macOS, or apt install jq on Linux)
  2. Environment Variables: Point to any OpenAI-compatible API
  3. Add to Shell: Place the function in your ~/.zshrc or source it from a separate file

Fuelix.ai:

export FUELIX_BASE_URL="https://api.fuelix.ai/v1"
export FUELIX_API_KEY="fx-..."
# Now use /model gpt-4o, /model claude-3-5-sonnet, /model gemini-pro, etc.