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
| Aspecto | Python | TypeScript |
|---|---|---|
| Verbosidad | Menos código, más implícito | Más explícito (tipos) |
| Errores en tiempo de compilación | No | Sí (con tipos correctos) |
| Ecosistema CI/CD | Maduro (pytest, requests, GitPython) | Maduro (octokit, jest) |
| Async/await | Disponible pero menos idiomático | Nativo, idiomático |
| Performance | Más lento (interpretado) | Más rápido (V8) |
| Curva de aprendizaje | Más baja | Media (si no conoces TS) |
| Casos de uso típicos | Data, ML, scripts, automation | Apps, 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:
- Lee
ANTHROPIC_API_KEYdel environment - Hace una llamada simple al modelo con un prompt
- Imprime el output
- Maneja errores de API con exit code 1
Ejercicio 2: JSON estructurado (Medio)
Modifica el script para:
- Pedir al modelo output en JSON con campos específicos
- Limpiar code fences si los hay
- Parsear con manejo de errores de JSON
- Acceder a campos específicos
Ejercicio 3: Script bien estructurado (Difícil)
Implementa la estructura completa (Python o TypeScript):
- ReviewConfig (clases/interfaces)
- ReviewResult
- Función
review()separada demain() - Manejo robusto de errores
- 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_KEYdel environment automáticamente — no pasar como argumento - System prompt separa el "rol" del agente de los datos
- Validar
content[0].typeantes 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
- Anthropic Python SDK — Repo oficial con docs
- Anthropic TypeScript SDK — Repo oficial
- Anthropic API Reference — Endpoint de messages completo
- Anthropic Models Documentation — Comparación de modelos
- Anthropic Pricing — Costos por modelo
- System prompts best practices — Cuándo y cómo usarlos