Adaptador LLM de DeepSeek Harness: conecta cualquier modelo
Los proveedores de modelos no son solo DeepSeek; para conectar dsh a tu propio endpoint de modelo, tienes que escribir un adaptador LLM.
En este capítulo hablaremos de la clase abstracta LlmAdapter, el método stream(), el registro de rutas y la configuración de cordis.yml.
Qué es un adaptador LLM
Un adaptador LLM es una clase que hereda de LlmAdapter e implementa el método stream().
Convierte las solicitudes independientes del proveedor de Harness en llamadas API de un proveedor concreto.
Luego convierte la respuesta de vuelta a los fragmentos de Harness, es decir, StreamChunk.
Por lo tanto, agent-loop consume una interfaz unificada y no le importa qué API hay detrás.
La capa superior es agent-loop, que consume el servicio de generación de streaming independiente del proveedor.
En el medio está el registro ctx.llm, que mantiene el contrato abstracto de LlmAdapter.
En la base están los distintos adaptadores, cada uno conectándose a un formato de API diferente.
Al registrar se usactx.llm.registerAdapter(['my-provider'], adapter)para vincular rutas.
Implementación mínima
La documentación oficial proporciona un esqueleto mínimo; lo seguimos y añadimos comentarios.
stream() es el núcleo y devuelve un generador asíncrono AsyncIterable.
Los comentarios de tres pasos marcan el flujo estándar: convertir el formato de los mensajes, llamar a la API de streaming y transformar la respuesta en StreamChunk.
Ejemplo
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
// Adaptador: hereda la clase abstracta, implementa stream()
class MyAdapter extends LlmAdapter {
private apiKey: string
constructor(apiKey: string) {
super()
this.apiKey = apiKey
}
// stream() devuelve un generador asíncrono que produce StreamChunk fragmento a fragmento
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// 1. Convert options.messages to the provider format.
// 2. Call the streaming API.
// 3. Convert the response into StreamChunk values.
}
}
// Configuración del plugin: apiKey y providers son obligatorios
export interface Config {
apiKey: string
providers: string[]
}
// Esquema Schemastery con el mismo nombre, valida la configuración al cargar
export const Config: Schema<Config> = Schema.object({
apiKey: Schema.string().required(),
providers: Schema.array(Schema.string()).required(),
})
export const name = 'my-llm-adapter'
// Declara dependencia del servicio llm, garantiza que ctx.llm esté listo
export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
const adapter = new MyAdapter(config.apiKey)
// Vincula la lista de rutas del proveedor a este adaptador
ctx.llm.registerAdapter(config.providers, adapter)
}
apiKey proviene de la configuración del plugin, evitando codificar la clave directamente en el código.
El array providers declara de qué rutas de proveedores se encarga este adaptador.
En apply, primero se hace new MyAdapter(config.apiKey) y luego registerAdapter para completar el registro.
inject: ['llm'] garantiza que el servicio ctx.llm esté listo antes de ejecutar apply.
stream() devuelve un generador asíncrono que usa yield para emitir datos fragmento a fragmento.
En el próximo artículo se detallará el protocolo de fragmentación de StreamChunk.
GenerateOptions: qué recibe el adaptador
stream() recibe GenerateOptions exportado por el repositorio.
Incluye modelo, intensidad de razonamiento, historial de conversación, prompt del sistema, esquemas de herramientas, parámetros de generación, secuencias de parada y señal de cancelación.
Los campos completos se rigen por los tipos TypeScript exportados por @deepseek-ai/dsh-llm.
| Campo | Descripción |
|---|---|
| provider | Selecciona el adaptador registrado |
| model | ID del modelo que posee el adaptador, no requiere registro al inicio |
| messages | Historial de conversación |
| system prompt | Prompt del sistema |
| tools | Esquema de herramientas |
| reasoning | ID de intensidad de razonamiento que posee el adaptador (opcional) |
| signal | Señal de cancelación; se usa para detener por completo la cancelación y la liberación de recursos |
El adaptador debe mapear los campos compatibles a la API concreta.
Si algún campo no se puede soportar, se debe lanzar un LlmError con un code estable; no se debe descartar silenciosamente.
Los campos no compatibles deben reportarse explícitamente como error en lugar de ignorarse en silencio; de lo contrario, el modelo recibirá resultados incompletos.
Registrar el adaptador
En apply se llama a ctx.llm.registerAdapter para completar el registro de rutas.
Ejemplo
ctx.llm.registerAdapter(['my-provider'], adapter)
El primer parámetro es la lista de rutas de proveedores que maneja el adaptador.
GenerateOptions.provider seleccionará el adaptador registrado.
GenerateOptions.model pasa el ID del modelo que posee el adaptador y que no necesita registrarse al inicio del ciclo de vida.
Si el adaptador puede publicar opciones de modelos al selector, puede sobrescribir listModels().
Cuando se necesite devolver la identidad exacta de proveedor y modelo, se puede sobrescribir resolveModel(provider, model, signal?).
resolveModel devuelve en una sola consulta el proveedor exacto, la identidad del modelo y los metadatos opcionales de context y reasoning.
Los metadatos de reasoning describen las intensidades de razonamiento opcionales del modelo: IDs opacos ordenados, nombres para mostrar y valores predeterminados de configuración opcionales.
Conserva la lista opcional autoritativa que da el adaptador, incluido el off que devuelve la API de capacidades del upstream; no eleves estos valores a enumeraciones principales.
Las consultas asíncronas deben responder a la signal opcional para que la cancelación y la liberación de recursos se detengan por completo.
El servicio valida los resultados agregados: las intensidades de razonamiento especificadas explícitamente pero no compatibles se rechazarán antes de llamar a stream().
Omitir reasoning significa que el modelo no tiene capacidad de intensidad de razonamiento opcional.
Uso en cordis.yml
Carga el plugin del adaptador junto con agent-loop, y haz que agent-loop use el nuevo provider y model.
Ejemplo
# Carga el plugin del adaptador, apiKey se lee de la variable de entorno
- id: my-llm
name: './src/my-llm-adapter.ts'
config:
apiKey: !!js process.env.MY_API_KEY
providers:
- my-provider
# Configura agent-loop para usar el provider y model del nuevo adaptador
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents:
- id: main
provider: my-provider
model: my-model-v1
config.apiKey del plugin my-llm se lee de la variable de entorno MY_API_KEY y no se escribe en disco.
El array providers registra la ruta my-provider en MyAdapter.
agents.main de agent-loop configura provider y model, de modo que las solicitudes de generación alcanzarán al nuevo adaptador.
Referencia práctica
En el repositorio hay dos implementaciones completas listas para comparar.
llm-deepseek adapta la API de DeepSeek y usa el formato compatible con OpenAI.
llm-pi-ai adapta Pi AI, que es un formato de API diferente.
Comparando estos dos adaptadores, se puede ver cómo el mismo contrato de harness se implementa sobre diferentes SDK de proveedores.
Leyendo primero llm-deepseek y luego llm-pi-ai, es más fácil ver la idea de seam de «contrato invariable, implementaciones diversas».
Resumen y autoevaluación
Resumen en una frase: adaptador LLM = heredar LlmAdapter + implementar stream() + registro de rutas con registerAdapter + configuración en cordis.yml.
Autoevaluación 1: ¿Cuál es el tipo de valor de retorno de stream()?
Autoevaluación 2: ¿Qué representa el primer parámetro de registerAdapter?
Autoevaluación 3: ¿A través de qué opciones de configuración selecciona agent-loop el nuevo adaptador?
Otras extensiones