# OpenAI function calling: una herramienta read_url para tu modelo

> OpenAI function calling con read_url: esquema JSON y bucle en Python que convierte enlaces en Markdown limpio para el modelo. Sin KYC, paga con criptomonedas.

- URL: https://webreaderapi.com/es/docs/openai/
- Última actualización: 2026-10-22
- Editor: WebReaderAPI (https://webreaderapi.com)

Describe una función, `read_url`, y ejecútala cuando el modelo lo pida. El modelo elige qué páginas leer; tu código llama a la API y devuelve el Markdown. El mismo esquema funciona con otros modelos que admiten tool calling.

**Respuesta rápida — ¿Cómo hago que un modelo de OpenAI lea páginas web con function calling?**

Declara una función `read_url` en la lista `tools` y luego ejecuta un bucle: cuando el modelo devuelva una tool call, llama al endpoint `/read` de **WebReaderAPI** con la URL y envía el Markdown de vuelta como mensaje `tool`. Cada página cuesta 1 crédito, 2 con navegador, desde 0,07 US$/1K páginas a fecha de octubre de 2026. Sin KYC; paga con criptomonedas.

## ¿Cómo es el esquema de read_url?

Con dos parámetros basta: la `url` que se va a leer, obligatoria, y un `max_chars` opcional que permite al modelo mantener corta una página larga. La descripción es lo más importante: le dice al modelo qué devuelve la herramienta y cuándo usarla. Mantenla factual y breve.

Llamamos a la herramienta `read_url` en lugar de `web_fetch` para evitar conflictos con herramientas que algunos entornos de agentes ya ofrecen con ese nombre.

*La definición de la herramienta*

```json
{
  "type": "function",
  "function": {
    "name": "read_url",
    "description": "Read a public web page and return its main content as Markdown, with its title and source URL. Use it when the user gives a link or when a page must be read to answer.",
    "parameters": {
      "type": "object",
      "properties": {
        "url": { "type": "string", "description": "The http or https address of the page." },
        "max_chars": { "type": "integer", "description": "Most characters to return, 1000 to 500000. Default 20000." }
      },
      "required": ["url"],
      "additionalProperties": false
    }
  }
}
```

## ¿Cómo funciona el bucle de tool calling?

Envía la conversación con la lista `tools`. Si la respuesta contiene tool calls, ejecuta cada una, añade el resultado como mensaje con rol `tool` y el `tool_call_id` correspondiente, y vuelve a preguntar al modelo. Cuando llega una respuesta sin tool call, es la respuesta final.

Limita el número de rondas. Tres o cuatro son suficientes para una pregunta sobre un puñado de páginas, y el límite te protege de un modelo que sigue pidiendo más.

*Python: el bucle completo con el paquete openai*

```python
import json
import os

import requests
from openai import OpenAI

READ_URL = {
    "type": "function",
    "function": {
        "name": "read_url",
        "description": "Read a public web page and return its main content as Markdown, with its title and source URL.",
        "parameters": {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "The http or https address of the page."},
                "max_chars": {"type": "integer", "description": "Most characters to return. Default 20000."},
            },
            "required": ["url"],
            "additionalProperties": False,
        },
    },
}


def read_url(url, max_chars=20000):
    resp = requests.get(
        "https://webreaderapi.com/api/v1/read",
        headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
        params={"url": url, "max_chars": max_chars},
        timeout=75,
    )
    body = resp.json()
    if resp.status_code != 200:
        return "Could not read the page: " + body["error"]["code"]
    return "# " + (body.get("title") or url) + "\nSource: " + body["final_url"] + "\n\n" + body["markdown"]


client = OpenAI()
model = os.environ.get("OPENAI_MODEL", "gpt-4.1-mini")
messages = [{"role": "user", "content": "What does https://example.com/blog/first-post announce? Answer in two sentences."}]

for _ in range(4):
    reply = client.chat.completions.create(model=model, messages=messages, tools=[READ_URL])
    msg = reply.choices[0].message
    if not msg.tool_calls:
        print(msg.content)
        break
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        messages.append({"role": "tool", "tool_call_id": call.id, "content": read_url(**args)})

```

## ¿Cómo reduzco el uso de tokens y el coste?

Tres ajustes importan. Mantén `max_chars` moderado, en torno a 8.000 a 20.000 caracteres por página, porque cada carácter que devuelves lo lee el modelo. Mantén el `main_content=true` por defecto, para que los menús y los pies de página nunca lleguen al contexto. Y limita el bucle, como se indica arriba, para que una pregunta no pueda desencadenar decenas de lecturas.

Por nuestra parte, el coste es por página, no por carácter: 1 crédito = 1 página leída sin navegador; 2 créditos cuando la página necesita un navegador para renderizar JavaScript. Una solicitud que falla, o que el robots.txt de un sitio rechaza, no se cobra. Un asistente de soporte que lee 300 páginas al día usa unos 9.000 créditos al mes, dentro de 10.000 páginas (3,75 US$).

## ¿El esquema funciona con otros modelos?

Sí, con pequeños cambios en el wrapper. La mayoría de las APIs de tool calling aceptan un JSON Schema para los parámetros y un nombre y una descripción para la herramienta, así que `read_url` se traslada entre proveedores tal cual. La [página MCP](https://webreaderapi.com/es/docs/mcp/) empaqueta la misma función para clientes que hablan el Model Context Protocol, y la [página de LangChain](https://webreaderapi.com/es/docs/langchain/) la envuelve para agentes de LangChain y LlamaIndex.

## ¿Qué debe ver el modelo cuando se rechaza una página?

Una frase breve y clara, como la que devuelve el bucle de arriba: "Could not read the page: blocked_by_robots". Los modelos lo manejan bien y recurren a otras fuentes o se lo dicen al usuario. Respetamos robots.txt: nuestro rastreador se identifica como WebReaderAPI, sigue las reglas escritas para él o para todos los robots y no lee una página que un sitio prohíbe. Las páginas rechazadas no se cobran. Si un sitio rechaza nuestra solicitud, devolvemos su respuesta: nunca resolvemos CAPTCHAs, iniciamos sesión en cuentas ni intentamos saltar un bloqueo o un paywall. Ninguno de estos rechazos usa un crédito.

## Function calling: preguntas frecuentes

### ¿Esto necesita la Responses API o Chat Completions?

El ejemplo usa Chat Completions con el parámetro `tools`. La misma definición de función y el mismo cuerpo de `read_url` funcionan con otras interfaces de OpenAI que aceptan function tools; solo cambia el bucle que las rodea.

### ¿Puede el modelo leer varias páginas en un solo turno?

Sí. Una respuesta puede contener varias tool calls; el bucle ejecuta cada una y devuelve todos los resultados antes de preguntar de nuevo al modelo. Mantén `max_chars` más bajo cuando el modelo lee muchas páginas a la vez.

### ¿La clave API queda expuesta al modelo?

No. La clave permanece en el entorno de tu código. El modelo solo ve el nombre de la herramienta, su descripción y el texto que devuelve tu función.

### ¿Cuánto cuesta una página rechazada?

Nada. 1 crédito = 1 página leída sin navegador; 2 créditos cuando la página necesita un navegador para renderizar JavaScript. Una solicitud que falla, o que el robots.txt de un sitio rechaza, no se cobra.

---

WebReaderAPI: WebReaderAPI es una API de lectura web para agentes de IA que convierte cualquier página web pública en Markdown limpio, pagando con criptomonedas y sin subir ningún documento de identidad. Los créditos prepago desde 0,07 US$/1K páginas nunca caducan y se respeta robots.txt. Sin KYC: ni documento de identidad, ni selfie, ni documentos. Paga con criptomonedas.
