Módulo 3: GitLab CI/CD y SDK Headless

SDK Headless: Python y TypeScript

SDK Headless: Python y TypeScript

Descripción

El SDK headless de Claude Code es la interfaz programática del agente — la misma capacidad que usas interactivamente desde el terminal, pero accesible desde código. Es lo que hace posible la portabilidad entre plataformas (la cápsula 01 del módulo lo presentó como concepto). Esta cápsula te enseña a usar el SDK desde scripts Python y TypeScript, las diferencias entre los dos, y cuándo elegir cada uno.

Al terminar, vas a poder escribir un script en Python o TypeScript que invoca al SDK con prompt y context, recibe la respuesta como datos estructurados, y la procesa programáticamente. Sin esto, las cápsulas 04-05 son ejecución a ciegas.


El SDK como Capa de Abstracción

┌──────────────────────────────────────────┐
│   USO INTERACTIVO (modo terminal)         │
│   → Tú abres Claude Code                  │
│   → Conversas natural                     │
│   → Output formateado para humanos        │
│   → Prompts conversacionales              │
└──────────────────────────────────────────┘

┌──────────────────────────────────────────┐
│   USO CON SDK HEADLESS                    │
│   → Tu script invoca la API               │
│   → Pasas prompts con estructura          │
│   → Recibes JSON con tokens y output      │
│   → Procesas programáticamente            │
│   → Reproducible, testeable, paralelizable│
└──────────────────────────────────────────┘

El SDK no es "menos" que el modo interactivo — es lo mismo accesible programáticamente. El modelo es el mismo, las capacidades son las mismas. Solo cambia la interfaz: terminal vs código.


Setup: Python

Instalar el SDK

pip install anthropic

Versión recomendada: pinear en requirements.txt:

anthropic>=0.39.0,<1.0.0

Llamada básica

"""Llamada básica al SDK de Anthropic."""
import os
from anthropic import Anthropic

# El cliente lee ANTHROPIC_API_KEY del environment
client = Anthropic()

response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Resume qué es CI/CD en 2 oraciones."}
    ],
)

print(response.content[0].text)
print(f"Tokens: {response.usage.input_tokens} in / {response.usage.output_tokens} out")

Output:

CI/CD es una práctica de software donde los cambios de código se integran (CI)
y deployean (CD) automáticamente vía pipelines. Permite detectar issues
temprano y entregar funcionalidad de forma continua.

Tokens: 32 in / 78 out

Componentes del response

response.content        # lista de bloques (típicamente 1 bloque de tipo "text")
response.content[0].text # el texto generado
response.usage.input_tokens  # tokens consumidos en input
response.usage.output_tokens # tokens generados en output
response.stop_reason    # "end_turn", "max_tokens", "stop_sequence"
response.model          # modelo usado (eco del request)
response.id             # identificador único de la llamada

Modelo: cuándo usar qué

# Económico, rápido, suficiente para análisis general
model="claude-haiku-4-5"

# Equilibrio costo/calidad, usable para code review profundo
model="claude-sonnet-5"

# Máxima calidad, para razonamiento complejo (caro)
model="claude-opus-5"

Para CI/CD genérico: Haiku. Para code review en módulos críticos (auth, payments): Sonnet. Opus rara vez se justifica en CI.


Setup: TypeScript

Instalar el SDK

npm install @anthropic-ai/sdk
# o
pnpm add @anthropic-ai/sdk
# o
yarn add @anthropic-ai/sdk

Llamada básica

// scripts/analyze.ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();  // lee ANTHROPIC_API_KEY del env

async function main() {
  const response = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    messages: [
      { role: "user", content: "Resume qué es CI/CD en 2 oraciones." }
    ],
  });

  // El primer bloque suele ser texto
  const block = response.content[0];
  if (block.type === "text") {
    console.log(block.text);
  }
  console.log(`Tokens: ${response.usage.input_tokens} in / ${response.usage.output_tokens} out`);
}

main();

Tipos de TypeScript

El SDK exporta tipos útiles:

import Anthropic, {
  type Message,
  type MessageParam,
  type ContentBlock,
  type TextBlock,
} from "@anthropic-ai/sdk";

function processResponse(response: Message): string {
  const textBlocks = response.content.filter(
    (block): block is TextBlock => block.type === "text"
  );
  return textBlocks.map((b) => b.text).join("\n");
}

Ventaja TypeScript: detección de errores en tiempo de compilación. Si pasas un parámetro mal tipado, el compilador te avisa antes de ejecutar.


Comparación: Python vs TypeScript

AspectoPythonTypeScript
VerbosidadMenos código, más implícitoMás explícito (tipos)
Errores en tiempo de compilaciónNoSí (con tipos correctos)
Ecosistema CI/CDMaduro (pytest, requests, GitPython)Maduro (octokit, jest)
Async/awaitDisponible pero menos idiomáticoNativo, idiomático
PerformanceMás lento (interpretado)Más rápido (V8)
Curva de aprendizajeMás bajaMedia (si no conoces TS)
Casos de uso típicosData, ML, scripts, automationApps, frontends, herramientas modernas

Cómo elegir

Elige Python si:

  • Tu equipo ya usa Python en el proyecto
  • Vas a integrar con librerías de data (pandas, numpy)
  • Quieres script más conciso y menos ceremonia
  • El código que vas a procesar es Python

Elige TypeScript si:

  • Tu proyecto principal es TypeScript/JavaScript
  • Quieres tipos estrictos en tu script de CI
  • Vas a integrar con tooling moderno (Vite, esbuild)
  • Tu equipo prefiere async/await como patrón principal

Realidad práctica: ambos funcionan igual de bien. La elección suele depender del stack del equipo, no de capacidades del SDK.


Patrón: Script de CI Bien Estructurado

Un script de CI no debería ser un monolito. Estructura mínima recomendada:

En Python

"""scripts/code_review.py — script de code review para CI."""
from __future__ import annotations
import json
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from anthropic import Anthropic, APIError


@dataclass
class ReviewConfig:
    """Configuración del review."""
    model: str
    max_tokens: int
    conventions_file: Path
    diff_file: Path


@dataclass
class ReviewResult:
    """Resultado estructurado del review."""
    summary: str
    comments: list[dict]
    tokens_used: dict


def load_config() -> ReviewConfig:
    """Cargar config desde environment."""
    return ReviewConfig(
        model=os.environ.get("CLAUDE_MODEL", "claude-haiku-4-5"),
        max_tokens=int(os.environ.get("CLAUDE_MAX_TOKENS", "4000")),
        conventions_file=Path("CLAUDE.md"),
        diff_file=Path("pr_diff.txt"),
    )


def review(config: ReviewConfig, client: Anthropic) -> ReviewResult:
    """Ejecutar el review y retornar resultado estructurado."""
    diff = config.diff_file.read_text()
    conventions = (
        config.conventions_file.read_text()
        if config.conventions_file.exists()
        else "Sin convenciones documentadas."
    )
    
    prompt = build_prompt(diff, conventions)
    response = client.messages.create(
        model=config.model,
        max_tokens=config.max_tokens,
        messages=[{"role": "user", "content": prompt}],
    )
    
    text = response.content[0].text
    parsed = parse_review_json(text)
    
    return ReviewResult(
        summary=parsed["summary"],
        comments=parsed["comments"],
        tokens_used={
            "input": response.usage.input_tokens,
            "output": response.usage.output_tokens,
        },
    )


def build_prompt(diff: str, conventions: str) -> str:
    """Construir el prompt del review."""
    return f"""[Acá va el prompt completo con conventions y diff]"""


def parse_review_json(text: str) -> dict:
    """Parsear el JSON del response, manejando code fences."""
    text = text.strip()
    if text.startswith("```"):
        text = "\n".join(text.split("\n")[1:-1])
    return json.loads(text)


def main() -> int:
    try:
        config = load_config()
        client = Anthropic()
        result = review(config, client)
        
        # Salvar resultado para el siguiente step del workflow
        Path("review_result.json").write_text(
            json.dumps({
                "summary": result.summary,
                "comments": result.comments,
                "tokens_used": result.tokens_used,
            }, indent=2)
        )
        
        print(f"Review generado: {len(result.comments)} comments")
        return 0
    
    except APIError as e:
        print(f"ERROR de Anthropic API: {e}", file=sys.stderr)
        return 1
    except json.JSONDecodeError as e:
        print(f"ERROR parseando response JSON: {e}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"ERROR inesperado: {e}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())

Ventajas de esta estructura:

  • Funciones con responsabilidad única (testeable)
  • Configuración separada del comportamiento
  • Manejo explícito de errores con exit codes
  • Output guardado en archivo para next step del workflow
  • Type hints para futura confianza

En TypeScript

// scripts/code-review.ts
import Anthropic from "@anthropic-ai/sdk";
import * as fs from "node:fs";
import * as path from "node:path";

interface ReviewConfig {
  model: string;
  maxTokens: number;
  conventionsFile: string;
  diffFile: string;
}

interface ReviewComment {
  path: string;
  line: number;
  severity: "critical" | "warning" | "suggestion";
  body: string;
}

interface ReviewResult {
  summary: string;
  comments: ReviewComment[];
  tokensUsed: { input: number; output: number };
}

function loadConfig(): ReviewConfig {
  return {
    model: process.env.CLAUDE_MODEL ?? "claude-haiku-4-5",
    maxTokens: parseInt(process.env.CLAUDE_MAX_TOKENS ?? "4000"),
    conventionsFile: "CLAUDE.md",
    diffFile: "pr_diff.txt",
  };
}

async function review(
  config: ReviewConfig,
  client: Anthropic,
): Promise<ReviewResult> {
  const diff = fs.readFileSync(config.diffFile, "utf-8");
  const conventions = fs.existsSync(config.conventionsFile)
    ? fs.readFileSync(config.conventionsFile, "utf-8")
    : "Sin convenciones documentadas.";
  
  const prompt = buildPrompt(diff, conventions);
  
  const response = await client.messages.create({
    model: config.model,
    max_tokens: config.maxTokens,
    messages: [{ role: "user", content: prompt }],
  });
  
  const block = response.content[0];
  if (block.type !== "text") {
    throw new Error("Expected text block in response");
  }
  
  const parsed = parseReviewJson(block.text);
  return {
    summary: parsed.summary,
    comments: parsed.comments,
    tokensUsed: {
      input: response.usage.input_tokens,
      output: response.usage.output_tokens,
    },
  };
}

function parseReviewJson(text: string): { summary: string; comments: ReviewComment[] } {
  let cleaned = text.trim();
  if (cleaned.startsWith("```")) {
    cleaned = cleaned.split("\n").slice(1, -1).join("\n");
  }
  return JSON.parse(cleaned);
}

function buildPrompt(diff: string, conventions: string): string {
  return `[Prompt completo aquí]`;
}

async function main(): Promise<number> {
  try {
    const config = loadConfig();
    const client = new Anthropic();
    const result = await review(config, client);
    
    fs.writeFileSync("review_result.json", JSON.stringify(result, null, 2));
    console.log(`Review generado: ${result.comments.length} comments`);
    return 0;
  } catch (error) {
    console.error(`ERROR: ${error instanceof Error ? error.message : error}`);
    return 1;
  }
}

main().then(process.exit);

Configuración Avanzada del SDK

System prompts (separados del user message)

response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=2000,
    system="Eres un code reviewer estricto. Output siempre en JSON.",  # ← system prompt
    messages=[{"role": "user", "content": user_prompt}],
)

Ventaja: el system prompt establece el "rol" del agente y se separa de los datos. Más limpio y reusable.

Streaming (para outputs largos)

with client.messages.stream(
    model="claude-haiku-4-5",
    max_tokens=4000,
    messages=[{"role": "user", "content": prompt}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    # Después puedes acceder al final:
    final = stream.get_final_message()

Cuándo usar streaming:

  • Output esperado es largo (>2K tokens)
  • Quieres mostrar progreso al usuario
  • En CI no es común — la mayoría de veces no lo necesitas

Retry con backoff

from anthropic import Anthropic

client = Anthropic(
    max_retries=3,        # reintentos automáticos en errores transitorios
    timeout=60.0,         # timeout en segundos
)

El SDK maneja retries por defecto. Útil para CI donde errores transitorios de red son comunes.


Trampas Comunes

Error 1: Hardcodear la API key

Síntoma: Funciona local, falla en CI con "API key missing".

Por qué pasa: Pasaste la key como argumento del cliente: Anthropic(api_key="sk-ant-..."). En CI, está en el environment.

Cómo corregir: Usar el constructor sin argumentos. El SDK lee ANTHROPIC_API_KEY del environment automáticamente:

client = Anthropic()  # ← lee del env

Error 2: No manejar el caso content[0].type != "text"

Síntoma: Script falla con AttributeError o TypeError.

Por qué pasa: Asumiste que response.content[0].text siempre existe. Pero el primer bloque puede ser tool_use, image, etc.

Cómo corregir: Validar:

text_blocks = [b for b in response.content if b.type == "text"]
if not text_blocks:
    raise ValueError("No text in response")
text = text_blocks[0].text

Error 3: Pedir JSON pero no parsearlo robustamente

Síntoma: Funciona la mayoría del tiempo, pero a veces falla con JSONDecodeError.

Por qué pasa: El modelo a veces envuelve el JSON en code fences (json ... ), agrega texto al inicio o al final, o usa comillas simples.

Cómo corregir: Helper que limpia code fences antes de parsear (parse_review_json en los ejemplos arriba). Y validación: si el parse falla, log el texto original para debug.

Error 4: Ignorar max_tokens

Síntoma: Output cortado abruptamente con stop_reason="max_tokens".

Por qué pasa: Default puede ser bajo. Si pediste un review de 30 archivos con max_tokens=1024, el modelo se queda corto.

Cómo corregir: Calibrar max_tokens según el caso. Para code review, 2000-4000 suele ser apropiado. Si frecuentemente ves max_tokens como stop_reason, súbelo.

Error 5: No usar tipos en TypeScript

Síntoma: Errores en runtime que el compilador podría haber detectado.

Por qué pasa: Usar any por todos lados niega los beneficios de TypeScript.

Cómo corregir: Definir interfaces para tu data (ReviewComment, ReviewResult, etc.). Usar los tipos que exporta el SDK.


Diagnóstico

Pregunta 1: ¿Cómo lee tu script la API key?

Si dijiste "como argumento del cliente", funciona local pero falla en CI. La forma correcta es del environment.

Pregunta 2: ¿Validas que `response.content[0]` sea un bloque de texto?

Si asumes que siempre lo es, vas a tener bugs cuando el modelo use tools u otros tipos de bloques.

Pregunta 3: Si pides JSON estructurado, ¿manejas code fences?

El modelo a veces los agrega aunque le pidas "solo JSON". Limpiarlos antes de parsear evita errores intermitentes.

Pregunta 4: ¿Tu `max_tokens` está calibrado para tu uso?

Si ves frecuentemente stop_reason: "max_tokens", súbelo. Si nunca llegas cerca, puedes bajarlo para ahorrar.

Pregunta 5: ¿Tu código maneja errores con exit codes apropiados?

Sin exit codes, el workflow no sabe si el script falló. Exit 0 = success, exit 1+ = failure.


Ejercicios

Ejercicio 1: Llamada básica con manejo de errores (Fácil)

Escribe un script que:

  1. Lee ANTHROPIC_API_KEY del environment
  2. Hace una llamada simple al modelo con un prompt
  3. Imprime el output
  4. Maneja errores de API con exit code 1

Ejercicio 2: JSON estructurado (Medio)

Modifica el script para:

  1. Pedir al modelo output en JSON con campos específicos
  2. Limpiar code fences si los hay
  3. Parsear con manejo de errores de JSON
  4. Acceder a campos específicos

Ejercicio 3: Script bien estructurado (Difícil)

Implementa la estructura completa (Python o TypeScript):

  1. ReviewConfig (clases/interfaces)
  2. ReviewResult
  3. Función review() separada de main()
  4. Manejo robusto de errores
  5. Output a archivo JSON para el siguiente step

Compara con la estructura mostrada en "Patrón: Script de CI Bien Estructurado".


Resumen

  • El SDK headless es la interfaz programática a Claude Code — mismo modelo, distinta interfaz
  • Python y TypeScript tienen feature parity — la elección depende del stack del equipo
  • El cliente lee ANTHROPIC_API_KEY del environment automáticamente — no pasar como argumento
  • System prompt separa el "rol" del agente de los datos
  • Validar content[0].type antes de acceder a .text
  • Limpiar code fences antes de parsear JSON
  • Estructura del script: config + funciones puras + main con error handling

Próxima cápsula: 03 — GitLab CI/CD: stages, jobs, artifacts. Tienes el SDK funcional. Ahora aprendes el modelo de pipelines de GitLab — distinto de GitHub Actions a nivel arquitectural — para entender dónde encaja el script SDK.


Recursos Adicionales

  1. Anthropic Python SDK — Repo oficial con docs
  2. Anthropic TypeScript SDK — Repo oficial
  3. Anthropic API Reference — Endpoint de messages completo
  4. Anthropic Models Documentation — Comparación de modelos
  5. Anthropic Pricing — Costos por modelo
  6. System prompts best practices — Cuándo y cómo usarlos