halfoff.devDocs
Obtén una clave
API de modelos

OpenAI Chat Completions

Usa el SDK de OpenAI que ya tienes para llamar a GPT, Claude y Gemini a mitad de precio. Streaming con uso, llamadas a funciones, razonamiento y Responses API.

Cualquier código escrito para el SDK de OpenAI funciona en Half Off Dev cambiando la URL base por https://halfoff.dev/v1 y la clave por la tuya. POST /v1/chat/completions sigue la definición de OpenAI y el cuerpo se reenvía tal cual, así que la referencia de la API de OpenAI vale campo por campo. Pagas la mitad del precio de lista por cada token, y con el mismo cliente llegas también a Claude y a Gemini.

Cambia el cliente#

curl https://halfoff.dev/v1/chat/completions \
  -H "Authorization: Bearer $HALFOFF_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-6.1-sol",
    "messages": [
      {"role": "system", "content": "Eres un profesor de SQL que va al grano."},
      {"role": "user", "content": "¿Cuándo debo usar un LEFT JOIN?"}
    ]
  }'

Como los SDK leen OPENAI_BASE_URL y OPENAI_API_KEY, la mayoría de apps compatibles con el protocolo de OpenAI solo necesitan esos dos valores. En Apps compatibles con OpenAI tienes algunos ejemplos.

Qué modelos puedes llamar aquí#

Todos los modelos de texto de GET /v1/models aceptan hoy Chat Completions, Claude y Gemini incluidos. Es la vía más sencilla si quieres un único cliente para todos los fabricantes.

FamiliaIds de ejemploA tener en cuenta
GPTgpt-6.1-sol, gpt-6-astra, gpt-5.6-terra, gpt-5.6-lunaFormato nativo.
Claudeclaude-sonnet-5.5, claude-opus-5.5Para cache_control o ajustes de thinking, llámalos con Anthropic Messages.
Geminigemini-3.1-pro, gemini-3-flashPara thinkingConfig y otros campos propios de Gemini, usa la API de Gemini.

Las opciones exclusivas de un fabricante solo están garantizadas en su propio formato. Consulta formato nativo de cada familia.

Streaming, con el uso al final#

Activa stream: true. El gateway siempre añade stream_options.include_usage: true, de modo que el último fragmento antes de data: [DONE] tiene un array choices vacío y el usage de la llamada. Comprueba que choices no esté vacío antes de leer un delta.

stream = client.chat.completions.create(
    model="gpt-6.1-sol",
    messages=[{"role": "user", "content": "Enumera los planetas en orden desde el Sol."}],
    stream=True,
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:
        print("\nTokens:", chunk.usage.prompt_tokens, "de entrada,", chunk.usage.completion_tokens, "de salida")

El formato exacto en el cable y cómo aparecen los fallos a mitad de stream están en Streaming.

Llamadas a funciones#

Declara tus funciones en tools. Cuando el modelo quiere usar una, finish_reason pasa a tool_calls; ejecútala y devuelve un mensaje tool con el tool_call_id correspondiente.

import json

tools = [{
    "type": "function",
    "function": {
        "name": "check_stock",
        "description": "Devuelve cuántas unidades de un producto hay en stock.",
        "parameters": {
            "type": "object",
            "properties": {"sku": {"type": "string"}},
            "required": ["sku"],
        },
    },
}]
messages = [{"role": "user", "content": "¿Nos queda stock del SKU KB-204?"}]

completion = client.chat.completions.create(model="gpt-6.1-sol", messages=messages, tools=tools)
msg = completion.choices[0].message
if msg.tool_calls:
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = {"sku": args["sku"], "units": 17}  # tu código aquí
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
    completion = client.chat.completions.create(model="gpt-6.1-sol", messages=messages, tools=tools)
print(completion.choices[0].message.content)

tool_choice, parallel_tool_calls y response_format se reenvían sin cambios.

Esfuerzo de razonamiento#

Los modelos GPT-5 y GPT-6 piensan antes de responder. Regula cuánto con reasoning_effort y limita la salida total, razonamiento incluido, con max_completion_tokens. Los valores de esfuerzo permitidos varían según el modelo; los tienes en la guía de razonamiento de OpenAI.

completion = client.chat.completions.create(
    model="gpt-6.1-sol",
    reasoning_effort="high",
    max_completion_tokens=8000,
    messages=[{"role": "user", "content": "¿Por qué 0.1 + 0.2 != 0.3 en la mayoría de lenguajes?"}],
)
print(completion.choices[0].message.content)
print("tokens de razonamiento:", completion.usage.completion_tokens_details.reasoning_tokens)

Chat Completions no devuelve el texto del razonamiento. Sus tokens se incluyen en usage.completion_tokens y se cobran a la tarifa de salida.

La Responses API#

Para los modelos GPT, POST /v1/responses sirve la Responses API de OpenAI, que es la que habla Codex CLI. Misma URL base, misma clave; client.responses.create() no necesita cambios.

curl https://halfoff.dev/v1/responses \
  -H "Authorization: Bearer $HALFOFF_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-6.1-sol",
    "instructions": "Responde con una sola palabra.",
    "input": "¿Qué es lo contrario de una tarea limitada por la latencia?"
  }'

En streaming (stream: true) recibes los eventos tipados de la API, desde response.created pasando por response.output_text.delta hasta response.completed; el uso que trae response.completed es lo que se factura. Ten en cuenta:

  • No se guarda nada en el servidor. GET, DELETE y cancel sobre /v1/responses/{id} devuelven 404, y previous_response_id solo funciona mientras la conversación siga en el mismo canal. Envía el historial completo en input con "store": false, como hace Codex.
  • Solo GPT. Cualquier otro modelo responde 404 con not served in responses format; usa Chat Completions o su API nativa.
  • instructions en la respuesta repite lo que enviaste, o null si no enviaste nada.
  • prompt_cache_key mantiene una conversación en un solo canal, igual que en Chat Completions.

Caché de prompts#

Los modelos de OpenAI guardan en caché los prefijos largos sin que se lo pidas. La parte cacheada aparece en usage.prompt_tokens_details.cached_tokens y se cobra a la tarifa reducida de lectura de caché. Un prompt_cache_key mejora la tasa de aciertos y además le indica al gateway qué peticiones forman una conversación. Consulta Caché de prompts.

No soportado#

Chat Completions y la Responses API son los endpoints con formato de OpenAI. Estos responden 404:

  • Respuestas almacenadas: GET, DELETE o cancel sobre /v1/responses/{id}.
  • Embeddings: /v1/embeddings.
  • Audio: /v1/audio/*.
  • Completions heredado, Assistants, archivos, lotes, fine-tuning y moderación.

client.models.list() funciona contra GET /v1/models; pedir un único modelo por id, no.

Cabeceras de la petición#

CabeceraComportamiento
Authorization: BearerTu clave de Half Off Dev. Obligatoria.
openai-betaSe reenvía sin cambios.
x-halfoff-groupOpcional. Elige el grupo de canales para esta petición, salvo que la clave ya esté vinculada a uno.
x-session-idOpcional. Marca peticiones como una misma conversación para el enrutamiento según caché. prompt_cache_key o user en el cuerpo tienen el mismo efecto.

Los errores usan la forma de OpenAI, {"error": {"message": "...", "type": "...", "code": "..."}}; consulta Errores.