Skip to content

JSON-configured providers

For an HTTP LLM API, you don't need to write a Provider subclass at all — point --provider at a JSON file instead.

Example

provider.example.json:

{
  "url": "https://api.example.com/v1/chat/completions",
  "model": "my-model",
  "api_key_env": "MY_API_KEY",
  "headers": {
    "X-Org": "acme"
  },
  "timeout": 60,
  "response_paths": {
    "text": "choices.0.message.content",
    "tool_calls": "choices.0.message.tool_calls",
    "stop_reason": "choices.0.finish_reason",
    "input_tokens": "usage.prompt_tokens",
    "output_tokens": "usage.completion_tokens",
    "tool_call_id": "id",
    "tool_call_name": "function.name",
    "tool_call_arguments": "function.arguments"
  }
}
pycodeloop run "list the files here" --provider ./provider.example.json

Or as a library:

from pycodeloop.providers import get_provider

provider = get_provider("./provider.example.json")

Fields

Field Required Purpose
url Yes Endpoint that receives the chat-completions POST request
model No Sent as "model" in the request body
api_key No Literal key, sent as Authorization: Bearer <key> (or another header — see auth_header/auth_prefix)
api_key_env No Env var name to read the key from instead of a literal in the file
headers No Extra headers merged into every request
auth_header No Header name the key is sent in (default Authorization)
auth_prefix No Prefix prepended to the key's value in auth_header (default "Bearer ")
timeout No Request timeout in seconds (default 60)
response_shape No "anthropic" to parse replies from Anthropic's native content[]/usage.input_tokens shape; omit for OpenAI-shaped responses or use response_paths
response_paths No Remaps an arbitrary response shape via dot-paths — see below
request No Customizes the outgoing request body — message_shape, tool_schema, body_paths, params, params_key, extra_body (see the Anthropic example below and reference.json)

--model and the provider's own *_API_KEY env var (from --provider's resolution) still override model/api_key from the file when passed explicitly on the CLI.

response_paths

The request body always uses the OpenAI chat-completions shape. If the API's response also matches that shape (choices[0].message.content, usage.prompt_tokens, ...), omit response_paths entirely — that's the default.

If the response has a different shape, response_paths maps each field with a dot-path into the JSON response (list indices are plain numbers, e.g. choices.0.message.content):

Key Default Points at
text choices.0.message.content The reply text
tool_calls choices.0.message.tool_calls List of tool calls
stop_reason choices.0.finish_reason Why the turn ended
input_tokens usage.prompt_tokens Input token count
output_tokens usage.completion_tokens Output token count
tool_call_id id (within each tool call) its id
tool_call_name function.name (within each tool call) its name
tool_call_arguments function.arguments (within each tool call) its arguments — a JSON string or an object, either works

Example: a non-OpenAI-shaped API

{
  "url": "https://api.example.com/answer",
  "model": "my-model",
  "response_paths": {
    "text": "result.answer",
    "input_tokens": "meta.tokens_in",
    "output_tokens": "meta.tokens_out"
  }
}

A response like {"result": {"answer": "hi"}, "meta": {"tokens_in": 7, "tokens_out": 4}} resolves to text="hi", input_tokens=7, output_tokens=4 — no Python required.

For anything response_paths can't express (custom auth flow, non-JSON body, SSE with a different shape), drop down to a real custom provider instead.

Ready-made templates

templates/ has configs for common backends — point --provider straight at one, or copy it as a starting point:

Template Backend
anthropic.json Anthropic's native Messages API (not the OpenAI-compatible one)
openai.json OpenAI directly
ollama.json Local Ollama (ollama serve, no API key)
lmstudio.json Local LM Studio server (no API key)
reference.json Every field below, with its default/example value — not meant to be run as-is
pycodeloop run "..." --provider templates/ollama.json

Local ones (Ollama, LM Studio) have no api_key/api_key_envGenericProvider only sends an Authorization header when a key is actually present, so it's simply omitted.

Talking to a non-OpenAI-shaped API: Anthropic's native format

The request body defaults to the OpenAI chat-completions shape. For a backend that speaks a genuinely different shape — like Anthropic's own Messages API, which templates/anthropic.json targets — set both the outgoing request shape and the incoming response shape:

{
  "url": "https://api.anthropic.com/v1/messages",
  "model": "claude-sonnet-5",
  "api_key_env": "ANTHROPIC_API_KEY",
  "auth_header": "x-api-key",
  "auth_prefix": "",
  "headers": { "anthropic-version": "2023-06-01" },
  "request": {
    "body_paths": { "system": "system" },
    "message_shape": "anthropic",
    "tool_schema": "anthropic",
    "params": { "max_tokens": 8192 }
  },
  "response_shape": "anthropic"
}
request.* field Purpose
message_shape: "anthropic" Build each message the way Anthropic expects (content blocks, tool_use/tool_result instead of OpenAI's tool_calls/role: "tool")
tool_schema: "anthropic" Describe tools as {name, description, input_schema} instead of OpenAI's {type: "function", function: {...}}
body_paths.system Move the system prompt to its own top-level body key ("system") instead of embedding it as the first message — Anthropic requires this
params Extra static fields merged into every request body — Anthropic requires max_tokens, since it has no default

response_shape: "anthropic" parses replies from content[] blocks (type: "text"/"tool_use") instead of choices[0].message, and usage from usage.input_tokens/usage.output_tokens instead of usage.prompt_tokens/usage.completion_tokens.

auth_header/auth_prefix also differ from the OpenAI-style default (Authorization: Bearer <key>) — Anthropic wants the raw key in x-api-key with no prefix, which is why both are overridden above.