shell-and-makefile-only-workflow-calling-llm-with-curl · EN · 2026-10-10

Calling the Aggregator From Pure Bash and Makefiles Without Any SDK

A practical guide to calling an LLM API aggregator directly from Bash scripts and Makefiles without any SDK, covering HTTP requests, authentication, JSON handling, and Makefile integration.

Why Skip the SDK?

Sometimes you just want to call an API from a shell script or Makefile without pulling in a language-specific SDK. Maybe you're in a minimal container, writing a build step, or automating a quick task. This guide shows how to interact with an LLM API aggregator using only standard command-line tools.

Prerequisites

You'll need:

  • curl for making HTTP requests
  • jq for parsing JSON responses (optional but helpful)
  • An API key from your aggregator
  • The base URL of the aggregator's API endpoint

Most aggregators provide an OpenAI-compatible endpoint, which means you can use the same request format as OpenAI's chat completions API. Check your aggregator's documentation for the exact base URL and authentication method.

Making a Basic Request with curl

The core of any API call is an HTTP POST request. Here's a minimal example using curl:

curl https://api.aggregator.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_KEY" \
  -d '{
    "model": "claude-3-5-sonnet",
    "messages": [{"role": "user", "content": "Hello, world!"}]
  }'

Key points:

  • Replace the URL with your aggregator's actual endpoint.
  • Store your API key in an environment variable ($API_KEY) to avoid hardcoding it.
  • The -d flag sends the JSON payload.
  • The response will be JSON; you can pipe it to jq for readability.

Parsing the Response

Without jq, the raw JSON can be hard to read. Use jq to extract the assistant's reply:

response=$(curl -s https://api.aggregator.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_KEY" \
  -d '{
    "model": "claude-3-5-sonnet",
    "messages": [{"role": "user", "content": "Hello, world!"}]
  }')

echo "$response" | jq -r '.choices[0].message.content'

If jq isn't available, you can use grep/sed for simple extraction, but jq is more robust.

Building a Reusable Bash Function

Encapsulate the API call in a function for reuse:

ask() {
  local prompt="$1"
  local model="${2:-claude-3-5-sonnet}"
  curl -s https://api.aggregator.com/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $API_KEY" \
    -d "{
      \"model\": \"$model\",
      \"messages\": [{\"role\": \"user\", \"content\": \"$prompt\"}]
    }" | jq -r '.choices[0].message.content'
}

ask "What is the capital of France?"

Note: This simple function doesn't handle escaping of quotes in the prompt. For production use, use jq -n to safely build JSON.

Safer JSON Construction with jq

To avoid injection and escaping issues, build the JSON payload with jq:

payload=$(jq -n \
  --arg model "claude-3-5-sonnet" \
  --arg content "$prompt" \
  '{model: $model, messages: [{role: "user", content: $content}]}')

curl -s https://api.aggregator.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_KEY" \
  -d "$payload" | jq -r '.choices[0].message.content'

This approach handles special characters correctly.

Integrating with Makefiles

Makefiles are great for automating build steps, and you can call the API from a make target. Here's an example that generates a summary of a file:

API_KEY ?= $(shell printenv API_KEY)
ENDPOINT = https://api.aggregator.com/v1/chat/completions
MODEL = claude-3-5-sonnet

summarize:
	@payload=$$(jq -n \
		--arg model "$(MODEL)" \
		--arg content "Summarize this: $$(cat input.txt)" \
		'{model: $$model, messages: [{role: "user", content: $$content}]}'); \
	curl -s $(ENDPOINT) \
		-H "Content-Type: application/json" \
		-H "Authorization: Bearer $(API_KEY)" \
		-d "$$payload" | jq -r '.choices[0].message.content'

Key points for Makefiles:

  • Use $$ for shell variables to prevent make from expanding them.
  • Use ?= for API_KEY so it can be overridden from the environment or command line.
  • Each line in a recipe runs in its own shell, so combine commands with \ and ;.

Handling Errors and Rate Limits

Always check the HTTP status code. curl's -f flag makes it fail silently on server errors, but you can capture the status:

http_code=$(curl -s -o response.json -w "%{http_code}" ... )
if [ "$http_code" -ne 200 ]; then
  echo "Error: HTTP $http_code" >&2
  cat response.json >&2
  exit 1
fi

For rate limits (HTTP 429), implement a simple retry with backoff in your script. The aggregator may also return rate limit headers; check the documentation.

Security Considerations

  • Never hardcode API keys in scripts or Makefiles committed to version control. Use environment variables or a secrets manager.
  • Be cautious when including user input in API calls; always sanitize and use proper JSON encoding.
  • Consider setting a timeout with curl --max-time to avoid hanging scripts.

Conclusion

You don't need an SDK to call an LLM API aggregator. With curl, jq, and a bit of shell scripting, you can integrate AI capabilities into your Bash scripts and Makefiles. This approach is lightweight, portable, and works in minimal environments. Just remember to handle errors, secure your API key, and construct JSON safely.